spring-projects / spring-projects/spring-ai
Per-call `customHeaders` not propagated in `OpenAiAudioTranscriptionOptions`, `OpenAiAudioSpeechOptions` and `OpenAiModerationOptions`
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 9.5k
- Forks
- 2.9k
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 6
Description
Bug description
Per-call customHeaders are dropped by the remaining three affected OpenAI models: audio transcription, audio speech, and moderation.
OpenAiChatModel and OpenAiImageOptions already propagate them; embeddings are covered by #6574 and its pending fix #6575.
Root cause
Each model merges the per-call options, then builds request params without reading getCustomHeaders():
| Model | Request builder on bf122ac7f |
|---|---|
| Audio transcription | buildParams(...) |
| Audio speech | buildSpeechCreateParams(...) |
| Moderation | call(...) |
The map is otherwise consumed only while constructing an internally managed client, as shown by OpenAiModerationModel and OpenAiAudioTranscriptionModel.Builder. Per-call values therefore arrive after that read, and supplying an OpenAIClient skips it entirely.
The three buildRequestOptions(...) methods (transcription, speech, and moderation) can set only the timeout because SDK RequestOptions has no header field. The SDK instead exposes putAdditionalHeader on the transcription, speech, and moderation params builders.
Use case affected
A gateway or proxy may require a header derived from each business request, which a static client-level header cannot express on a shared model bean.
This is the same use case as #6082 and #5939.
Environment
- Spring AI:
mainatbf122ac7f(2.0.2-SNAPSHOT), and 2.0.0 - module:
spring-ai-openai - openai-java: 4.49.0, as pinned by
openai-sdk.versiononmain
Steps to reproduce
ModerationResponse resp = openAiModerationModel.call(new ModerationPrompt("hi",
OpenAiModerationOptions.builder()
.customHeaders(Map.of("x-some-header-id", "VALUE_123"))
.build()));
x-some-header-id is absent from the outgoing /v1/moderations request, and transcription and speech behave the same way.
Expected behavior
Per-call customHeaders are propagated as additionalHeaders on the params sent to the client, matching the chat and image semantics.
Minimal Complete Reproducible Example
This representative test fails on unmodified main; equivalent transcription and speech tests fail the same way:
@Test
void moderationPropagatesPerCallCustomHeaders() {
OpenAIClient mockClient = mock(OpenAIClient.class, RETURNS_DEEP_STUBS);
when(mockClient.moderations().create(any(ModerationCreateParams.class), any(RequestOptions.class)))
.thenReturn(ModerationCreateResponse.builder()
.id("TEST_ID").model("TEST_MODEL").results(List.of()).build());
OpenAiModerationModel model = OpenAiModerationModel.builder().openAiClient(mockClient).build();
model.call(new ModerationPrompt("hi",
OpenAiModerationOptions.builder()
.customHeaders(Map.of("x-some-header-id", "VALUE_123"))
.build()));
ArgumentCaptor<ModerationCreateParams> captor = ArgumentCaptor.forClass(ModerationCreateParams.class);
verify(mockClient.moderations()).create(captor.capture(), any(RequestOptions.class));
assertThat(captor.getValue()._additionalHeaders().values("x-some-header-id"))
.containsExactly("VALUE_123");
}
Suggested fix
Apply the same addition in all three request builders, matching the OpenAiImageOptions fix from #6083:
if (!CollectionUtils.isEmpty(options.getCustomHeaders())) {
options.getCustomHeaders().forEach(builder::putAdditionalHeader);
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiAudioTranscriptionModel.java, OpenAiAudioSpeechModel.java, and OpenAiModerationModel.java, focusing on their request-building methods and the provided moderation reproduction. Run or add focused tests that capture the SDK parameter builders; done means per-call customHeaders appear as additional headers for transcription, speech, and moderation requests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100