Skip to content
Draft
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
10 changes: 9 additions & 1 deletion apps/web/src/lib/ai-gateway/providers/anthropic.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
);
}
4 changes: 4 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/lib/ai-gateway/providers/openrouter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -28,6 +29,7 @@ export type VercelProviderConfig = {
byok?: Record<string, VercelInferenceProviderConfig[]>;
};
anthropic?: AnthropicProviderOptions;
openai?: OpenAILanguageModelResponsesOptions;
};

export function isDataCollectionExplicitlyDisallowed(
Expand Down
37 changes: 32 additions & 5 deletions apps/web/src/lib/ai-gateway/providers/vercel/index.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -18,7 +19,7 @@ describe('getAnthropicProviderOptionsForVercel', () => {
},
};

expect(getAnthropicProviderOptionsForVercel(request)).toEqual({
expect(getAnthropicProviderOptions(request.body.model, request)).toEqual({
effort: 'high',
});
});
Expand All @@ -33,7 +34,7 @@ describe('getAnthropicProviderOptionsForVercel', () => {
},
};

expect(getAnthropicProviderOptionsForVercel(request)).toEqual({
expect(getAnthropicProviderOptions('anthropic/claude-sonnet-4.5', request)).toEqual({
effort: 'low',
});
});
Expand All @@ -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);
});
});

Expand Down
45 changes: 35 additions & 10 deletions apps/web/src/lib/ai-gateway/providers/vercel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ const vercelModelIdMapping: Record<string, string | undefined> = {
'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) {
Expand Down