Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-suns-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ai-sdk/gateway": patch
---

fix (gateway): forward non-gateway provider options under gateway.providerOptions for fallback support
136 changes: 136 additions & 0 deletions packages/gateway/src/gateway-language-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1652,6 +1652,142 @@ describe('GatewayLanguageModel', () => {
});
});

it('should nest non-gateway provider options under gateway.providerOptions for doGenerate', async () => {
prepareJsonResponse({
content: { type: 'text', text: 'Test response' },
});

await createTestModel().doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 128,
includeThoughts: false,
},
},
gateway: {
models: ['google/gemini-2.5-flash', 'openai/gpt-5-mini'],
},
},
});

const requestBody = await server.calls[0].requestBodyJson;
expect(requestBody.providerOptions).toEqual({
google: {
thinkingConfig: {
thinkingBudget: 128,
includeThoughts: false,
},
},
gateway: {
models: ['google/gemini-2.5-flash', 'openai/gpt-5-mini'],
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 128,
includeThoughts: false,
},
},
},
},
});
});

it('should nest non-gateway provider options under gateway.providerOptions for doStream', async () => {
prepareStreamResponse({
content: ['Hello', ' world'],
});

const { stream } = await createTestModel().doStream({
prompt: TEST_PROMPT,
providerOptions: {
anthropic: {
cacheControl: { type: 'ephemeral' },
},
gateway: {
order: ['bedrock', 'anthropic'],
},
},
});

await convertReadableStreamToArray(stream);

const requestBody = await server.calls[0].requestBodyJson;
expect(requestBody.providerOptions).toEqual({
anthropic: {
cacheControl: { type: 'ephemeral' },
},
gateway: {
order: ['bedrock', 'anthropic'],
providerOptions: {
anthropic: {
cacheControl: { type: 'ephemeral' },
},
},
},
});
});

it('should not modify providerOptions when no non-gateway options exist', async () => {
prepareJsonResponse({
content: { type: 'text', text: 'Test response' },
});

await createTestModel().doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
gateway: {
order: ['openai'],
zeroDataRetention: true,
},
},
});

const requestBody = await server.calls[0].requestBodyJson;
expect(requestBody.providerOptions).toEqual({
gateway: {
order: ['openai'],
zeroDataRetention: true,
},
});
expect(
(requestBody.providerOptions as Record<string, unknown>).gateway,
).not.toHaveProperty('providerOptions');
});

it('should preserve original top-level non-gateway options for backward compatibility', async () => {
prepareJsonResponse({
content: { type: 'text', text: 'Test response' },
});

await createTestModel().doGenerate({
prompt: TEST_PROMPT,
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 128,
includeThoughts: false,
},
},
gateway: {
models: ['google/gemini-2.5-flash'],
},
},
});

const requestBody = await server.calls[0].requestBodyJson;

expect(
(requestBody.providerOptions as Record<string, unknown>).google,
).toEqual({
thinkingConfig: {
thinkingBudget: 128,
includeThoughts: false,
},
});
});

it('should pass both zeroDataRetention and hipaaCompliant options', async () => {
prepareJsonResponse({
content: { type: 'text', text: 'Test response' },
Expand Down
25 changes: 25 additions & 0 deletions packages/gateway/src/gateway-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
LanguageModelV4StreamPart,
LanguageModelV4GenerateResult,
LanguageModelV4StreamResult,
JSONObject,
} from '@ai-sdk/provider';
import {
combineHeaders,
Expand Down Expand Up @@ -59,6 +60,30 @@ export class GatewayLanguageModel implements LanguageModelV4 {
private async getArgs(options: LanguageModelV4CallOptions) {
const { abortSignal: _abortSignal, ...optionsWithoutSignal } = options;

if (optionsWithoutSignal.providerOptions) {
const { gateway: gatewayOpts, ...otherProviderOpts } =
optionsWithoutSignal.providerOptions;

if (
gatewayOpts != null &&
typeof gatewayOpts === 'object' &&
!Array.isArray(gatewayOpts)
) {
const updatedGateway: Record<string, unknown> = {
...(gatewayOpts as Record<string, unknown>),
};

if (Object.keys(otherProviderOpts).length > 0) {
updatedGateway['providerOptions'] = otherProviderOpts;

optionsWithoutSignal.providerOptions = {
...optionsWithoutSignal.providerOptions,
gateway: updatedGateway as JSONObject,
};
}
}
}

return {
args: this.maybeEncodeFileParts(optionsWithoutSignal),
warnings: [],
Expand Down
3 changes: 3 additions & 0 deletions packages/gateway/src/gateway-provider-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,7 @@ export type GatewayProviderOptions = {

/** Filter to providers with zero data retention agreements. */
zeroDataRetention?: boolean;

/** Provider options keyed by provider name, used for fallback model support. */
providerOptions?: Record<string, Record<string, unknown>>;
};