diff --git a/apps/web/src/lib/ai-gateway/models.test.ts b/apps/web/src/lib/ai-gateway/models.test.ts index 6e45a5ef81..b5aa10dfed 100644 --- a/apps/web/src/lib/ai-gateway/models.test.ts +++ b/apps/web/src/lib/ai-gateway/models.test.ts @@ -18,7 +18,7 @@ import { claude_sonnet_4_6_stealth_model, claude_opus_4_6_stealth_model, } from './providers/anthropic.constants'; -import { gpt_5_6_sol_discounted_model } from './providers/openai-exclusive'; +import { gpt_5_6_sol_discounted_model, gpt_6_astra_flex_model } from './providers/openai-exclusive'; import { gemma_4_26b_a4b_it_free_model } from './providers/google'; import { isUnavailableModel } from './unavailable-models'; import { getRandomNumber } from './getRandomNumber'; @@ -152,6 +152,41 @@ describe('isFreeModel', () => { ]); }); + test('keeps the GPT-6 Astra OpenAI Flex endpoint disabled', async () => { + expect(findKiloExclusiveModel(gpt_6_astra_flex_model.public_id)).toBeNull(); + expect(gpt_6_astra_flex_model).toMatchObject({ + status: 'disabled', + internal_id: 'openai/gpt-6-astra', + gateway: 'vercel', + flags: ['reasoning', 'vision', 'flex'], + inference_provider_restriction: ['openai'], + pricing: { fallbackOnly: true }, + }); + expect( + await hasBestEffortGuessDataCollectionRequirement(gpt_6_astra_flex_model.public_id) + ).toBe(false); + expect(gpt_6_astra_flex_model.pricing?.tiers).toEqual([ + { + start_context_length: 0, + pricing: { + prompt_per_million: 5, + completion_per_million: 25, + input_cache_read_per_million: 0.5, + input_cache_write_per_million: 6.25, + }, + }, + { + start_context_length: 272_000, + pricing: { + prompt_per_million: 10, + completion_per_million: 37.5, + input_cache_read_per_million: 1, + input_cache_write_per_million: 12.5, + }, + }, + ]); + }); + test('all Kilo exclusive models should have either no pricing or valid ordered pricing tiers', () => { for (const model of kiloExclusiveModels) { if (model.pricing) { diff --git a/apps/web/src/lib/ai-gateway/models.ts b/apps/web/src/lib/ai-gateway/models.ts index 248c59ace6..1022cfdda3 100644 --- a/apps/web/src/lib/ai-gateway/models.ts +++ b/apps/web/src/lib/ai-gateway/models.ts @@ -21,7 +21,10 @@ import { stepfun_37_flash_free_model } from '@/lib/ai-gateway/providers/stepfun' import { isGrokModel } from '@/lib/ai-gateway/providers/xai'; import { isClaudeModel } from '@/lib/ai-gateway/providers/anthropic.constants'; import { GPT_CURRENT_MODEL_ID, isOpenAiModel } from '@/lib/ai-gateway/providers/openai'; -import { gpt_5_6_sol_discounted_model } from '@/lib/ai-gateway/providers/openai-exclusive'; +import { + gpt_5_6_sol_discounted_model, + gpt_6_astra_flex_model, +} from '@/lib/ai-gateway/providers/openai-exclusive'; import { GLM_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/zai'; import { type ProviderId } from '@/lib/ai-gateway/providers/types'; import type { OpenRouterReasoningConfig } from '@/lib/ai-gateway/providers/openrouter/types'; @@ -95,6 +98,7 @@ export const preferredModels = [ ...(gpt_5_6_sol_discounted_model.status === 'public' ? [gpt_5_6_sol_discounted_model.public_id] : []), + ...(gpt_6_astra_flex_model.status === 'public' ? [gpt_6_astra_flex_model.public_id] : []), GLM_CURRENT_MODEL_ID, KIMI_CURRENT_MODEL_ID, MINIMAX_CURRENT_MODEL_ID, @@ -130,6 +134,7 @@ export const kiloExclusiveModels = [ gemma_4_26b_a4b_it_free_model, qwen36_plus_stealth_model, gpt_5_6_sol_discounted_model, + gpt_6_astra_flex_model, claude_opus_4_8_stealth_model, claude_opus_4_7_stealth_model, claude_sonnet_4_6_stealth_model, diff --git a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.test.ts b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.test.ts index 7fa71cb9f0..8688ecb38a 100644 --- a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.test.ts @@ -1,10 +1,11 @@ -import { describe, expect, it } from '@jest/globals'; +import { describe, expect, it, jest } from '@jest/globals'; import { CLAUDE_OPUS_FALLBACK_MODEL_ID } from '@/lib/ai-gateway/providers/anthropic.constants'; import { applyAnthropicThinkingDefault, applyGatewayModelsFallback, applyPreferredProvider, applyReasoningDetailsTransform, + removeUnsupportedRequestServiceTier, } from '@/lib/ai-gateway/providers/apply-provider-specific-logic'; import type { GatewayRequest } from '@/lib/ai-gateway/providers/openrouter/types'; import { @@ -13,6 +14,11 @@ import { type ProviderId, } from '@/lib/ai-gateway/providers/types'; import { PERPLEXITY_KIMI_PUBLIC_ID } from '@/lib/ai-gateway/providers/partner/constants'; +import { QWEN37_MAX_MODEL_ID } from '@/lib/ai-gateway/custom-pricing'; +import { + gpt_5_6_sol_discounted_model, + gpt_6_astra_flex_model, +} from '@/lib/ai-gateway/providers/openai-exclusive'; function makeRequest(model: string, models?: string[]): GatewayRequest { return { @@ -85,6 +91,55 @@ describe('applyAnthropicThinkingDefault', () => { ); }); +describe('removeUnsupportedRequestServiceTier', () => { + it.each([ + { + model: QWEN37_MAX_MODEL_ID, + kiloExclusiveModel: null, + reason: 'non-fallback custom pricing', + }, + { + model: gpt_5_6_sol_discounted_model.public_id, + kiloExclusiveModel: gpt_5_6_sol_discounted_model, + reason: 'non-Flex Kilo-exclusive model', + }, + ])( + 'removes and logs the request-level tier for $reason', + ({ model, kiloExclusiveModel, reason }) => { + const request = makeRequest(model); + request.body.service_tier = 'priority'; + const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + removeUnsupportedRequestServiceTier(model, request, kiloExclusiveModel); + + expect(request.body.service_tier).toBeUndefined(); + expect(warn).toHaveBeenCalledWith( + '[applyProviderSpecificLogic] Removed unsupported request-level service tier', + { + model, + requestKind: 'chat_completions', + serviceTier: 'priority', + reason, + } + ); + warn.mockRestore(); + } + ); + + it.each([ + [PERPLEXITY_KIMI_PUBLIC_ID, null], + [gpt_6_astra_flex_model.public_id, gpt_6_astra_flex_model], + ['vendor/standard-model', null], + ] as const)('preserves the request-level tier for %s', (model, kiloExclusiveModel) => { + const request = makeRequest(model); + request.body.service_tier = 'priority'; + + removeUnsupportedRequestServiceTier(model, request, kiloExclusiveModel); + + expect(request.body.service_tier).toBe('priority'); + }); +}); + describe('applyReasoningDetailsTransform', () => { function makeProvider(responseTransforms: Provider['responseTransforms']): Provider { return { diff --git a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts index 2e542762f6..dfa5b49a2f 100644 --- a/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts +++ b/apps/web/src/lib/ai-gateway/providers/apply-provider-specific-logic.ts @@ -7,7 +7,10 @@ import { } from '@/lib/ai-gateway/providers/openrouter/types'; import { applyMistralModelSettings, isMistralModel } from '@/lib/ai-gateway/providers/mistral'; import { findKiloExclusiveModel } from '@/lib/ai-gateway/models'; -import { applyKiloExclusiveModelSettings } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; +import { + applyKiloExclusiveModelSettings, + type KiloExclusiveModel, +} from '@/lib/ai-gateway/providers/kilo-exclusive-model'; import { applyAnthropicModelSettings } from '@/lib/ai-gateway/providers/anthropic'; import { CLAUDE_OPUS_FALLBACK_MODEL_ID, @@ -49,6 +52,7 @@ import { isFreeModel } from '@/lib/ai-gateway/is-free-model'; import { isOpenAiModel } from '@/lib/ai-gateway/providers/openai'; import { ReasoningFormat } from '@/lib/ai-gateway/custom-llm/format'; import { ReasoningDetailType } from '@/lib/ai-gateway/custom-llm/reasoning-details'; +import { getCustomPricing } from '@/lib/ai-gateway/custom-pricing'; function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); @@ -223,6 +227,32 @@ export function applyAnthropicThinkingDefault( } } +export function removeUnsupportedRequestServiceTier( + requestedModel: string, + requestToMutate: GatewayRequest, + kiloExclusiveModel: KiloExclusiveModel | null +) { + const customPricing = getCustomPricing(requestedModel); + const reason = + customPricing && !customPricing.fallbackOnly + ? 'non-fallback custom pricing' + : kiloExclusiveModel && !kiloExclusiveModel.flags.includes('flex') + ? 'non-Flex Kilo-exclusive model' + : null; + const serviceTier = requestToMutate.body.service_tier; + if (!reason || serviceTier === undefined) { + return; + } + + console.warn('[applyProviderSpecificLogic] Removed unsupported request-level service tier', { + model: requestedModel, + requestKind: requestToMutate.kind, + serviceTier, + reason, + }); + delete requestToMutate.body.service_tier; +} + /** * Inverse of the reasoning-content response transform: folds * client-supplied `reasoning_details` back into the `reasoning_content` string @@ -288,6 +318,7 @@ export async function applyProviderSpecificLogic( enableReasoningSummaries(requestToMutate); const kiloExclusiveModel = findKiloExclusiveModel(requestedModel); + removeUnsupportedRequestServiceTier(requestedModel, requestToMutate, kiloExclusiveModel); if (kiloExclusiveModel) { applyKiloExclusiveModelSettings(requestToMutate, kiloExclusiveModel); } diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts index d7906d7d2b..61504ecc5f 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.test.ts @@ -8,7 +8,9 @@ import { type PricingTiers, } from '@/lib/ai-gateway/providers/kilo-exclusive-model'; import type { + GatewayMessagesRequest, GatewayRequest, + GatewayResponsesRequest, OpenRouterChatCompletionRequest, OpenRouterProviderConfig, } from '@/lib/ai-gateway/providers/openrouter/types'; @@ -157,6 +159,28 @@ describe('applyKiloExclusiveModelSettings', () => { expect(req.body.model).toBe('vendor/real-model'); }); + it.each([ + { + kind: 'chat_completions' as const, + body: { model: 'kilo/test-model', messages: [] } as OpenRouterChatCompletionRequest, + }, + { + kind: 'responses' as const, + body: { model: 'kilo/test-model', input: '' } as GatewayResponsesRequest, + }, + { + kind: 'messages' as const, + body: { model: 'kilo/test-model', max_tokens: 1, messages: [] } as GatewayMessagesRequest, + }, + ])('sets the Flex service tier for $kind requests', request => { + applyKiloExclusiveModelSettings( + request, + makeModel({ internal_id: 'vendor/real-model', flags: ['flex'] }) + ); + + expect(request.body.service_tier).toBe('flex'); + }); + it('leaves provider untouched when there is no restriction', () => { const req = makeRequest({ only: ['anthropic'], zdr: true }); applyKiloExclusiveModelSettings(req, makeModel({ internal_id: 'vendor/x' })); diff --git a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts index 0b51d5b0d9..e1f6962a4c 100644 --- a/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts +++ b/apps/web/src/lib/ai-gateway/providers/kilo-exclusive-model.ts @@ -11,6 +11,7 @@ import { export type KiloExclusiveModelFlag = | 'reasoning' | 'vision' + | 'flex' | 'stealth' | 'vercel-routing' | 'rate-limited' @@ -169,6 +170,9 @@ export function applyKiloExclusiveModelSettings( ) { requestToMutate.body.model = kiloExclusiveModel.internal_id; removeNonSensicalMaxTokens(requestToMutate, kiloExclusiveModel); + if (kiloExclusiveModel.flags.includes('flex')) { + requestToMutate.body.service_tier = 'flex'; + } const restriction = kiloExclusiveModel.inference_provider_restriction; if (restriction.length === 0) { return; diff --git a/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts b/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts index 4941628aef..73059a39b1 100644 --- a/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts +++ b/apps/web/src/lib/ai-gateway/providers/openai-exclusive.ts @@ -35,3 +35,40 @@ export const gpt_5_6_sol_discounted_model: KiloExclusiveModel = { }, inference_provider_restriction: ['openai'], }; + +export const gpt_6_astra_flex_model: KiloExclusiveModel = { + public_id: 'openai/gpt-6-astra-flex', + internal_id: 'openai/gpt-6-astra', + display_name: 'OpenAI: GPT-6 Astra Flex', + description: + 'GPT-6 Astra with OpenAI Flex processing, offering lower costs in exchange for slower response times and occasional resource unavailability.', + status: 'disabled', + context_length: 1_050_000, + max_completion_tokens: 128_000, + gateway: 'vercel', + flags: ['reasoning', 'vision', 'flex'], + pricing: { + fallbackOnly: true, + tiers: [ + { + start_context_length: 0, + pricing: { + prompt_per_million: 5, + completion_per_million: 25, + input_cache_read_per_million: 0.5, + input_cache_write_per_million: 6.25, + }, + }, + { + start_context_length: 272_000, + pricing: { + prompt_per_million: 10, + completion_per_million: 37.5, + input_cache_read_per_million: 1, + input_cache_write_per_million: 12.5, + }, + }, + ], + }, + inference_provider_restriction: ['openai'], +}; 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 83ed0a20c8..57679791e0 100644 --- a/apps/web/src/lib/ai-gateway/providers/vercel/index.ts +++ b/apps/web/src/lib/ai-gateway/providers/vercel/index.ts @@ -178,6 +178,7 @@ export function convertProviderOptions( ); })(); + const serviceTier = requestToMutate.body.service_tier; return { gateway: { only, @@ -186,6 +187,7 @@ export function convertProviderOptions( zeroDataRetention: provider?.zdr, disallowPromptTraining: provider?.data_collection === 'deny' || undefined, models: requestToMutate.body.models, + serviceTier: serviceTier === 'flex' || serviceTier === 'priority' ? serviceTier : undefined, }, }; } diff --git a/apps/web/src/tests/openrouter-models-config.test.ts b/apps/web/src/tests/openrouter-models-config.test.ts index 246333b228..a06baf5bb9 100644 --- a/apps/web/src/tests/openrouter-models-config.test.ts +++ b/apps/web/src/tests/openrouter-models-config.test.ts @@ -12,7 +12,10 @@ import { CLAUDE_SONNET_CURRENT_MODEL_ID, } from '@/lib/ai-gateway/providers/anthropic.constants'; import { GPT_CURRENT_MODEL_ID } from '@/lib/ai-gateway/providers/openai'; -import { gpt_5_6_sol_discounted_model } from '@/lib/ai-gateway/providers/openai-exclusive'; +import { + gpt_5_6_sol_discounted_model, + gpt_6_astra_flex_model, +} from '@/lib/ai-gateway/providers/openai-exclusive'; import { GEMMA_4_26B_A4B_IT_ID, gemma_4_26b_a4b_it_free_model, @@ -59,6 +62,12 @@ describe('OpenRouter Models Config', () => { } else { expect(preferredModels).not.toContain(gpt_5_6_sol_discounted_model.public_id); } + + if (gpt_6_astra_flex_model.status === 'public') { + expect(preferredModels).toContain(gpt_6_astra_flex_model.public_id); + } else { + expect(preferredModels).not.toContain(gpt_6_astra_flex_model.public_id); + } }); test('monitors only concrete preferred models', () => {