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 77c9ea9382..a53e5abfe2 100644 --- a/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts +++ b/apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts @@ -125,5 +125,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 5efa239edc..fbaf74989e 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,13 +1,14 @@ import { describe, it, expect } from '@jest/globals'; import { - getAnthropicProviderOptionsForVercel, + getAnthropicProviderOptions, + getOpenAIProviderOptions, hasCompatibleVercelInferenceProvider, passesVercelRoutingPercentage, } from '@/lib/ai-gateway/providers/vercel'; 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', @@ -18,7 +19,7 @@ describe('getAnthropicProviderOptionsForVercel', () => { }, }; - expect(getAnthropicProviderOptionsForVercel(request)).toEqual({ + expect(getAnthropicProviderOptions(request.body.model, request)).toEqual({ effort: 'high', }); }); @@ -33,7 +34,7 @@ describe('getAnthropicProviderOptionsForVercel', () => { }, }; - expect(getAnthropicProviderOptionsForVercel(request)).toEqual({ + expect(getAnthropicProviderOptions('anthropic/claude-sonnet-4.5', request)).toEqual({ effort: 'low', }); }); @@ -47,7 +48,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 9df72efcea..99fab5e12d 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 () => { @@ -133,23 +136,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( @@ -211,10 +235,11 @@ export 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; diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts index b1990fae34..c0ebc77431 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.test.ts @@ -58,6 +58,10 @@ describe('mapModelIdToVercel', () => { ['qwen/qwen3-235b-a22b', 'alibaba/qwen-3-235b'], ['qwen/qwen3-30b-a3b', 'alibaba/qwen-3-30b'], ['qwen/qwen3-32b', 'alibaba/qwen-3-32b'], + ['anthropic/claude-opus-4.8-fast', 'anthropic/claude-opus-4.8'], + ['openai/gpt-5.6-sol-pro', 'openai/gpt-5.6-sol'], + ['openai/gpt-5.6-terra-pro', 'openai/gpt-5.6-terra'], + ['openai/gpt-5.6-luna-pro', 'openai/gpt-5.6-luna'], ])('maps %s to %s', (input, expected) => { expect(mapModelIdToVercel(input)).toBe(expected); }); diff --git a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts index 10d33b0acb..4ddc1e3df0 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/mapModelIdToVercel.ts @@ -43,6 +43,12 @@ const vercelModelIdMapping: Record = { 'qwen/qwen3-235b-a22b': 'alibaba/qwen-3-235b', 'qwen/qwen3-30b-a3b': 'alibaba/qwen-3-30b', 'qwen/qwen3-32b': 'alibaba/qwen-3-32b', + + // the OpenRouter => Vercel API adapter handles setting the required parameter + 'anthropic/claude-opus-4.8-fast': 'anthropic/claude-opus-4.8', + 'openai/gpt-5.6-sol-pro': 'openai/gpt-5.6-sol', + 'openai/gpt-5.6-terra-pro': 'openai/gpt-5.6-terra', + 'openai/gpt-5.6-luna-pro': 'openai/gpt-5.6-luna', }; export function mapModelIdToVercel(modelId: string) {