diff --git a/.changeset/sixty-suns-mix.md b/.changeset/sixty-suns-mix.md new file mode 100644 index 000000000000..e0d3643f323b --- /dev/null +++ b/.changeset/sixty-suns-mix.md @@ -0,0 +1,5 @@ +--- +"@ai-sdk/gateway": patch +--- + +fix (gateway): forward non-gateway provider options under gateway.providerOptions for fallback support diff --git a/packages/gateway/src/gateway-language-model.test.ts b/packages/gateway/src/gateway-language-model.test.ts index 679e3ddb8231..413dc5df5cd7 100644 --- a/packages/gateway/src/gateway-language-model.test.ts +++ b/packages/gateway/src/gateway-language-model.test.ts @@ -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).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).google, + ).toEqual({ + thinkingConfig: { + thinkingBudget: 128, + includeThoughts: false, + }, + }); + }); + it('should pass both zeroDataRetention and hipaaCompliant options', async () => { prepareJsonResponse({ content: { type: 'text', text: 'Test response' }, diff --git a/packages/gateway/src/gateway-language-model.ts b/packages/gateway/src/gateway-language-model.ts index 0848a5fa9b16..ca2ced6ab1ae 100644 --- a/packages/gateway/src/gateway-language-model.ts +++ b/packages/gateway/src/gateway-language-model.ts @@ -4,6 +4,7 @@ import type { LanguageModelV4StreamPart, LanguageModelV4GenerateResult, LanguageModelV4StreamResult, + JSONObject, } from '@ai-sdk/provider'; import { combineHeaders, @@ -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 = { + ...(gatewayOpts as Record), + }; + + if (Object.keys(otherProviderOpts).length > 0) { + updatedGateway['providerOptions'] = otherProviderOpts; + + optionsWithoutSignal.providerOptions = { + ...optionsWithoutSignal.providerOptions, + gateway: updatedGateway as JSONObject, + }; + } + } + } + return { args: this.maybeEncodeFileParts(optionsWithoutSignal), warnings: [], diff --git a/packages/gateway/src/gateway-provider-options.ts b/packages/gateway/src/gateway-provider-options.ts index 72ec2c6615e9..9ba3409e0c80 100644 --- a/packages/gateway/src/gateway-provider-options.ts +++ b/packages/gateway/src/gateway-provider-options.ts @@ -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>; };