diff --git a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts index d552681efa..33c29c147f 100644 --- a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts +++ b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts @@ -120,5 +120,13 @@ export function isClaudeModel(requestedModel: string) { } export function isFableModel(requestedModel: string) { - return requestedModel.includes('claude-fable'); + return isClaudeModel(requestedModel) && requestedModel.includes('fable'); +} + +export function isOpusFastModel(requestedModel: string) { + return ( + isClaudeModel(requestedModel) && + requestedModel.includes('opus') && + requestedModel.includes('fast') + ); } diff --git a/apps/web/src/lib/ai-gateway/providers/openai.ts b/apps/web/src/lib/ai-gateway/providers/openai.ts index 0439c84670..6bca5daf16 100644 --- a/apps/web/src/lib/ai-gateway/providers/openai.ts +++ b/apps/web/src/lib/ai-gateway/providers/openai.ts @@ -5,6 +5,10 @@ export function isOpenAiModel(requestedModel: string) { ); } +export function isOpenAiProModel(requestedModel: string) { + return isOpenAiModel(requestedModel) && requestedModel.includes('pro'); +} + export function isGptOssModel(requestedModel: string) { return requestedModel.includes('gpt-oss'); } diff --git a/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts b/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts index 00ad400ef4..88229d23e1 100644 --- a/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts +++ b/apps/web/src/lib/ai-gateway/providers/openrouter/types.ts @@ -4,6 +4,7 @@ import type { AnthropicProviderOptions } from '@ai-sdk/anthropic'; import type { ReasoningDetailUnion } from '@/lib/ai-gateway/custom-llm/reasoning-details'; import type { AwsCredentials } from '@/lib/ai-gateway/providers/openrouter/inference-provider-id'; import type Anthropic from '@anthropic-ai/sdk'; +import type { OpenAILanguageModelResponsesOptions } from '@ai-sdk/openai'; // Base types for OpenRouter API that don't depend on other lib files // This breaks circular dependencies with mistral.ts, minimax.ts, etc. @@ -28,6 +29,7 @@ export type VercelProviderConfig = { byok?: Record; }; anthropic?: AnthropicProviderOptions; + openai?: OpenAILanguageModelResponsesOptions; }; export function isDataCollectionExplicitlyDisallowed( diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts index e0877f1bc0..267974f4e7 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect } from '@jest/globals'; import { + getAnthropicProviderOptions, + getOpenAIProviderOptions, convertProviderOptions, - getAnthropicProviderOptionsForVercel, getVercelInferenceProvidersExcludingIgnored, hasCompatibleVercelInferenceProvider, passesVercelRoutingPercentage, @@ -9,7 +10,7 @@ import { import { getRandomNumber } from '@/lib/ai-gateway/getRandomNumber'; import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types'; -describe('getAnthropicProviderOptionsForVercel', () => { +describe('getAnthropicProviderOptions', () => { it('maps chat completion verbosity to Anthropic effort', () => { const request: GatewayRequest = { kind: 'chat_completions', @@ -20,7 +21,7 @@ describe('getAnthropicProviderOptionsForVercel', () => { }, }; - expect(getAnthropicProviderOptionsForVercel(request)).toEqual({ + expect(getAnthropicProviderOptions(request.body.model, request)).toEqual({ effort: 'high', }); }); @@ -35,7 +36,7 @@ describe('getAnthropicProviderOptionsForVercel', () => { }, }; - expect(getAnthropicProviderOptionsForVercel(request)).toEqual({ + expect(getAnthropicProviderOptions('anthropic/claude-sonnet-4.5', request)).toEqual({ effort: 'low', }); }); @@ -49,7 +50,33 @@ describe('getAnthropicProviderOptionsForVercel', () => { }, }; - expect(getAnthropicProviderOptionsForVercel(request)).toBe(undefined); + expect(getAnthropicProviderOptions(request.body.model, request)).toBe(undefined); + }); + + it('enables fast mode for fast Opus models', () => { + const request: GatewayRequest = { + kind: 'chat_completions', + body: { + model: 'anthropic/claude-opus-4.6-fast', + messages: [{ role: 'user', content: 'hello' }], + }, + }; + + expect(getAnthropicProviderOptions(request.body.model, request)).toEqual({ + speed: 'fast', + }); + }); +}); + +describe('getOpenAIProviderOptions', () => { + it('enables pro reasoning mode for GPT Pro models', () => { + expect(getOpenAIProviderOptions('openai/gpt-5.4-pro')).toEqual({ + reasoningMode: 'pro', + }); + }); + + it('returns undefined for other models', () => { + expect(getOpenAIProviderOptions('openai/gpt-5.4')).toBe(undefined); }); }); diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts index c9676d8038..650d42a883 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts @@ -27,6 +27,9 @@ import { import type { AnthropicProviderOptions } from '@ai-sdk/anthropic'; import { isDeepseekModel } from '@/lib/ai-gateway/providers/deepseek'; import type { KiloExclusiveModel } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; +import type { OpenAILanguageModelResponsesOptions } from '@ai-sdk/openai'; +import { isOpenAiProModel } from '@/lib/ai-gateway/providers/openai'; +import { isOpusFastModel } from '@/lib/ai-gateway/providers/anthropic.constants'; const getVercelRoutingPercentage = createCachedFetch( async () => { @@ -179,23 +182,44 @@ function parseAwsCredentials(input: string) { } } -export function getAnthropicProviderOptionsForVercel( +export function getAnthropicProviderOptions( + requestedModel: string, request: GatewayRequest ): AnthropicProviderOptions | undefined { - const anthropicOptions: AnthropicProviderOptions = {}; + const options: AnthropicProviderOptions = {}; if (request.kind === 'chat_completions' && request.body.verbosity) { - anthropicOptions.effort = request.body.verbosity; + options.effort = request.body.verbosity; } if (request.kind === 'responses' && request.body.text?.verbosity) { - anthropicOptions.effort = request.body.text.verbosity; + options.effort = request.body.text.verbosity; + } + + if (isOpusFastModel(requestedModel)) { + options.speed = 'fast'; } - if (Object.keys(anthropicOptions).length === 0) { + if (Object.keys(options).length === 0) { return undefined; } - return anthropicOptions; + return options; +} + +export function getOpenAIProviderOptions( + requestedModel: string +): OpenAILanguageModelResponsesOptions | undefined { + const options: OpenAILanguageModelResponsesOptions = {}; + + if (isOpenAiProModel(requestedModel)) { + options.reasoningMode = 'pro'; + } + + if (Object.keys(options).length === 0) { + return undefined; + } + + return options; } export function getVercelInferenceProviderConfigForUserByok( @@ -264,10 +288,11 @@ export async function applyVercelSettings( } if (requestToMutate.body.providerOptions) { - const anthropicOptions = getAnthropicProviderOptionsForVercel(requestToMutate); - if (anthropicOptions) { - requestToMutate.body.providerOptions.anthropic = anthropicOptions; - } + requestToMutate.body.providerOptions.anthropic = getAnthropicProviderOptions( + requestedModel, + requestToMutate + ); + requestToMutate.body.providerOptions.openai = getOpenAIProviderOptions(requestedModel); } delete requestToMutate.body.provider;