diff --git a/src/integrations/generated/integrationArtifacts.generated.ts b/src/integrations/generated/integrationArtifacts.generated.ts index ee24bd6065..4f11a1f79d 100644 --- a/src/integrations/generated/integrationArtifacts.generated.ts +++ b/src/integrations/generated/integrationArtifacts.generated.ts @@ -77,6 +77,7 @@ import modelFireworksMerged from '../models/fireworks-merged.js' import modelGemini from '../models/gemini.js' import modelGlm from '../models/glm.js' import modelGpt from '../models/gpt.js' +import modelGpt6 from '../models/gpt6.js' import modelKimi from '../models/kimi.js' import modelLing from '../models/ling.js' import modelLlama from '../models/llama.js' @@ -97,7 +98,7 @@ export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[] export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[] export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[] -export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[] +export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelGpt6, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[] export const MODEL_DESCRIPTORS = MODEL_DESCRIPTOR_GROUPS.flat() satisfies readonly ModelDescriptor[] export { PROVIDER_PRESET_MANIFEST, ORDERED_PROVIDER_PRESETS } from './integrationManifest.generated.js' diff --git a/src/integrations/gpt6Catalog.test.ts b/src/integrations/gpt6Catalog.test.ts new file mode 100644 index 0000000000..c57f37f858 --- /dev/null +++ b/src/integrations/gpt6Catalog.test.ts @@ -0,0 +1,43 @@ +import { expect, test } from 'bun:test' + +import gpt6Models from './models/gpt6.js' +import openaiVendor from './vendors/openai.js' + +const GPT6_FLAGSHIPS = ['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol'] as const +const GPT6_IDS = [...GPT6_FLAGSHIPS, 'gpt-6-luna'] as const + +test('gpt-6 model descriptors carry the full API spec (no Codex cap published)', () => { + for (const id of GPT6_IDS) { + const descriptor = gpt6Models.find(model => model.id === id) + expect(descriptor).toBeDefined() + // Descriptor and catalog intentionally AGREE here — unlike the 5.6 + // family, there is no published Codex-route cap for GPT-6 (the 272k pin + // is verified ONLY for 5.6; see the gpt6.ts descriptor comment). The + // full 1.05M spec ships with a dated comment + first-500 monitoring + // flag instead of an invented cap. + expect(descriptor?.contextWindow).toBe(1_050_000) + expect(descriptor?.maxOutputTokens).toBe(128_000) + } +}) + +test('openai vendor gpt-6 catalog entries carry limits and reasoning metadata', () => { + for (const id of GPT6_IDS) { + const entry = openaiVendor.catalog?.models?.find(model => model.id === id) + expect(entry).toBeDefined() + expect(entry?.contextWindow).toBe(1_050_000) + expect(entry?.maxOutputTokens).toBe(128_000) + expect(entry?.reasoning?.wireFormat).toBe('reasoning_effort') + expect(entry?.reasoning?.levels).toContain('xhigh') + } +}) + +test('openai vendor gpt-6 flagships expose the Ultra tier as max; luna tops out at xhigh', () => { + for (const id of GPT6_FLAGSHIPS) { + const entry = openaiVendor.catalog?.models?.find(model => model.id === id) + expect(entry?.reasoning?.levels).toContain('max') + expect(entry?.reasoning?.defaultLevel).toBe('high') + } + const luna = openaiVendor.catalog?.models?.find(model => model.id === 'gpt-6-luna') + expect(luna?.reasoning?.levels).not.toContain('max') + expect(luna?.reasoning?.defaultLevel).toBe('medium') +}) diff --git a/src/integrations/models/gpt6.ts b/src/integrations/models/gpt6.ts new file mode 100644 index 0000000000..68879e4457 --- /dev/null +++ b/src/integrations/models/gpt6.ts @@ -0,0 +1,56 @@ +import { defineModel } from '../define.js' + +const gpt6Capabilities = { + supportsVision: true, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: true, + supportsReasoning: false, + supportsPreciseTokenCount: true, +} + +function gpt6Model( + id: string, + label: string, + contextWindow: number, + maxOutputTokens: number, +) { + return defineModel({ + id, + label, + brandId: 'gpt', + vendorId: 'openai', + classification: ['chat', 'vision', 'coding'], + defaultModel: id, + capabilities: gpt6Capabilities, + contextWindow, + maxOutputTokens, + }) +} + +export default [ + // GPT-6 family (October 2026). Live API spec, two agreeing sources + // (2026-10-13): OpenRouter (`openai/gpt-6-astra`: `context_length` + // 1,050,000, `top_provider.max_completion_tokens` 128,000) and models.dev + // (`limit.context` 1,050,000, `limit.input` 922,000, `limit.output` + // 128,000). The same 1,050,000 / 128,000 spec is verified for gpt-6.1-sol, + // gpt-6-sol, and gpt-6-luna (both sources). + // + // Unlike the 5.6 family, there is NO published Codex-route cap for these + // models: OpenAI's Codex bundled metadata pin (openai/codex#33972) covers + // the 5.6 family only, and no Astra/Sol/Luna Codex number (or pricing-tier + // boundary rule) could be found as of 2026-10-13 — in-session research came + // back empty (no web tooling available; zero Astra references in-tree). + // So the descriptors below carry the full API spec instead of a + // Codex-route pin. Do NOT copy the 5.6 272k cap here: that number is + // verified ONLY for the 5.6 family. FIRST-500 MONITORING: watch early + // production Codex traffic for 500 "input exceeds the context window" + // failures (issue #1118 precedent); if the Codex backend enforces a lower + // effective cap, pin these descriptors to it with provenance. + // The 1.05M API spec lives in the openai-vendor catalog entries, same as + // the 5.6 family. Last cross-checked 2026-10-13. + gpt6Model('gpt-6-astra', 'GPT-6 Astra', 1_050_000, 128_000), + gpt6Model('gpt-6.1-sol', 'GPT-6.1 Sol', 1_050_000, 128_000), + gpt6Model('gpt-6-sol', 'GPT-6 Sol', 1_050_000, 128_000), + gpt6Model('gpt-6-luna', 'GPT-6 Luna', 1_050_000, 128_000), +] diff --git a/src/integrations/vendors/openai.ts b/src/integrations/vendors/openai.ts index 156ce59fbe..97ef0fec78 100644 --- a/src/integrations/vendors/openai.ts +++ b/src/integrations/vendors/openai.ts @@ -63,6 +63,21 @@ export default defineVendor({ { id: 'gpt-5.6-terra', apiName: 'gpt-5.6-terra', label: 'GPT-5.6 Terra', modelDescriptorId: 'gpt-5.6-terra', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, { id: 'gpt-5.6-luna', apiName: 'gpt-5.6-luna', label: 'GPT-5.6 Luna', modelDescriptorId: 'gpt-5.6-luna', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, { id: 'gpt-5.5', apiName: 'gpt-5.5', label: 'GPT-5.5', modelDescriptorId: 'gpt-5.5', contextWindow: 272_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, + // GPT-6 family (October 2026): same /v1/responses + nested + // reasoning.effort wire shape as the 5.6 family (modelRequiresResponsesApi + // routes them there; verified 5.6 behavior, inherited pending live + // GPT-6 wire confirmation). Effort ladder per models.dev + // `reasoning_options` (low, medium, high, xhigh, max) plus the + // operator-confirmed native-Codex Ultra tier on Sol/Astra model types + // only (2026-10-13): flagships (astra/sol/6.1-sol) expose `max`, Luna + // tops out at `xhigh`. The Ultra→max wire mapping is the working + // hypothesis, not a live-verified row — see the gpt6.ts descriptor + // comment. Defaults inherit the family pattern (high flagship, + // medium luna) pending Codex-default evidence. + { id: 'gpt-6-astra', apiName: 'gpt-6-astra', label: 'GPT-6 Astra', modelDescriptorId: 'gpt-6-astra', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh', 'max'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, + { id: 'gpt-6.1-sol', apiName: 'gpt-6.1-sol', label: 'GPT-6.1 Sol', modelDescriptorId: 'gpt-6.1-sol', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh', 'max'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, + { id: 'gpt-6-sol', apiName: 'gpt-6-sol', label: 'GPT-6 Sol', modelDescriptorId: 'gpt-6-sol', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh', 'max'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, + { id: 'gpt-6-luna', apiName: 'gpt-6-luna', label: 'GPT-6 Luna', modelDescriptorId: 'gpt-6-luna', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], defaultLevel: 'medium', wireFormat: 'reasoning_effort' } }, { id: 'gpt-5.4', apiName: 'gpt-5.4', label: 'GPT-5.4', modelDescriptorId: 'gpt-5.4', contextWindow: 1_050_000, maxOutputTokens: 128_000, capabilities: { supportsReasoning: true }, reasoning: { mode: 'levels', levels: ['low', 'medium', 'high', 'xhigh'], defaultLevel: 'high', wireFormat: 'reasoning_effort' } }, { id: 'gpt-5-mini', apiName: 'gpt-5-mini', label: 'GPT-5 Mini' }, { id: 'gpt-4o', apiName: 'gpt-4o', label: 'GPT-4o' }, diff --git a/src/services/api/providerConfig.local.test.ts b/src/services/api/providerConfig.local.test.ts index fd644f84c8..0f636c2c0a 100644 --- a/src/services/api/providerConfig.local.test.ts +++ b/src/services/api/providerConfig.local.test.ts @@ -380,6 +380,13 @@ test('modelRequiresResponsesApi matches gpt-5.4/5.5/5.6 (excl. mini/nano) only', 'gpt-5.6-terra', 'gpt-5.6-luna', 'GPT-5.6-SOL', + // GPT-6 family inherits the 5.6 responses routing + 'gpt-6', + 'gpt-6-astra', + 'gpt-6.1-sol', + 'gpt-6-sol', + 'gpt-6-luna', + 'GPT-6-ASTRA', // patch releases of a verified family stay routed 'gpt-5.4.1', ]) { @@ -402,6 +409,9 @@ test('modelRequiresResponsesApi matches gpt-5.4/5.5/5.6 (excl. mini/nano) only', 'gpt-5.7', 'gpt-5.8-preview', 'gpt-5.9', + // gpt-60-style ids are not the gpt-6 family + 'gpt-60', + 'gpt-6-mini', 'o3', 'claude-opus-4-8', ]) { @@ -434,6 +444,20 @@ test('auto-routes gpt-5.6 to responses on the default OpenAI base', () => { }) }) +test('auto-routes gpt-6-astra to responses on the default OpenAI base', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_MODEL = 'gpt-6-astra' + delete process.env.OPENAI_API_FORMAT + + expect(resolveProviderRequest()).toMatchObject({ + transport: 'responses', + resolvedModel: 'gpt-6-astra', + baseUrl: 'https://api.openai.com/v1', + reasoning: { effort: 'high' }, + }) +}) + test('auto-routes gpt-5.6 to responses on regional OpenAI subdomains', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' process.env.OPENAI_BASE_URL = 'https://eu.api.openai.com/v1' diff --git a/src/services/api/providerConfig.protoAlias.test.ts b/src/services/api/providerConfig.protoAlias.test.ts index db85d4f2bb..6cd511d94d 100644 --- a/src/services/api/providerConfig.protoAlias.test.ts +++ b/src/services/api/providerConfig.protoAlias.test.ts @@ -71,6 +71,16 @@ describe('providerConfig — Codex alias lookup is prototype-safe', () => { expect(getReasoningEffortForModel('gpt-5.3-codex-spark')).toBeUndefined() }) + test('getReasoningEffortForModel resolves GPT-6 family defaults', () => { + expect(getReasoningEffortForModel('gpt-6')).toBe('high') + expect(getReasoningEffortForModel('gpt-6-astra')).toBe('high') + expect(getReasoningEffortForModel('gpt-6.1-sol')).toBe('high') + expect(getReasoningEffortForModel('gpt-6-sol')).toBe('high') + expect(getReasoningEffortForModel('gpt-6-luna')).toBe('medium') + expect(supportsCodexReasoningEffort('gpt-6-astra')).toBe(true) + expect(supportsCodexReasoningEffort('gpt-6-luna')).toBe(true) + }) + // The descriptor parsing path (parseModelDescriptor, via resolveProviderRequest) // is guarded too — a proto-name model must resolve to itself, not to the // inherited Object constructor's (nonexistent) `.model`, which produced an diff --git a/src/services/api/providerConfig.ts b/src/services/api/providerConfig.ts index c783c790f4..4fcc1ad4ad 100644 --- a/src/services/api/providerConfig.ts +++ b/src/services/api/providerConfig.ts @@ -103,6 +103,33 @@ const CODEX_ALIAS_MODELS: Record< model: 'gpt-5.6-luna', reasoningEffort: 'medium', }, + // GPT-6 family (October 2026). Served on the Codex backend (native picker: + // GPT-6.1 Sol, Astra, Sol, Luna) and direct OpenAI /v1/responses. Bare + // `gpt-6` follows the Codex CLI convention of resolving to the flagship + // tier (Sol). Defaults inherit the family pattern (high flagship, medium + // luna) pending Codex-default evidence — see the gpt6.ts descriptor + // comment. gpt-5.5 stays pinned: it retires from Codex sign-in 2026-10-14 + // and codexplan already resolves to gpt-5.6-sol above. + 'gpt-6': { + model: 'gpt-6-sol', + reasoningEffort: 'high', + }, + 'gpt-6-astra': { + model: 'gpt-6-astra', + reasoningEffort: 'high', + }, + 'gpt-6.1-sol': { + model: 'gpt-6.1-sol', + reasoningEffort: 'high', + }, + 'gpt-6-sol': { + model: 'gpt-6-sol', + reasoningEffort: 'high', + }, + 'gpt-6-luna': { + model: 'gpt-6-luna', + reasoningEffort: 'medium', + }, 'gpt-5.5': { model: 'gpt-5.5', reasoningEffort: 'high', @@ -487,7 +514,13 @@ function shouldUseGithubResponsesApi(model: string): boolean { // gpt-5-mini, gpt-4.x, o-series, and claude-* stay on chat/completions. export function modelRequiresResponsesApi(model: string): boolean { const normalized = model.trim().toLowerCase().split('?', 1)[0] ?? '' - return /^gpt-5\.[4-6](?!\d)/.test(normalized) && + const isGpt56Responses = /^gpt-5\.[4-6](?!\d)/.test(normalized) + // GPT-6 family inherits the 5.6 responses routing: same reasoning-model + // shape (function tools + reasoning_effort rejected on chat/completions). + // Verified 5.6 behavior, inherited pending live GPT-6 wire confirmation — + // see the gpt6.ts descriptor comment. + const isGpt6Responses = /^gpt-6(?:[.-]|$)/.test(normalized) + return (isGpt56Responses || isGpt6Responses) && !GPT5_MINI_NANO_RE.test(normalized) } @@ -495,15 +528,19 @@ export function modelRequiresResponsesApi(model: string): boolean { // gpt-50-style ids. Shared by supportsCodexReasoningEffort and the Codex // profile model gate so the family shape lives in one place. const GPT5_FAMILY_RE = /^gpt-5(?:[.-]|$)/ +// GPT-6 family (October 2026): same Codex-backend eligibility and effort +// support as gpt-5. Separate pattern so the 5.x boundary stays legible. +const GPT6_FAMILY_RE = /^gpt-6(?:[.-]|$)/ const GPT5_MINI_NANO_RE = /(?:^|[-.])(?:mini|nano)(?:[-.]|$)/ // gpt-5 family models the ChatGPT Codex backend can serve. The -mini/-nano // tiers are API-only (never exposed through the Codex transport), so a // stale gpt-5-mini pick saved under a direct-OpenAI profile must fall back // to the Codex profile's default rather than be sent to the backend and 400. +// Covers gpt-6 the same way (native Codex picker serves the GPT-6 family). export function isCodexEligibleGpt5Model(model: string): boolean { const base = model.trim().toLowerCase().split('?', 1)[0] ?? '' - return GPT5_FAMILY_RE.test(base) && !GPT5_MINI_NANO_RE.test(base) + return (GPT5_FAMILY_RE.test(base) || GPT6_FAMILY_RE.test(base)) && !GPT5_MINI_NANO_RE.test(base) } // The responses auto-route only fires for the OpenAI first-party surface @@ -1543,5 +1580,5 @@ export function supportsCodexReasoningEffort(model: string): boolean { return true } - return GPT5_FAMILY_RE.test(base) + return GPT5_FAMILY_RE.test(base) || GPT6_FAMILY_RE.test(base) } diff --git a/src/utils/context.test.ts b/src/utils/context.test.ts index b764d31cc9..22bd1df902 100644 --- a/src/utils/context.test.ts +++ b/src/utils/context.test.ts @@ -458,6 +458,39 @@ test('gpt-5.6 family keeps the full window on the direct-OpenAI route', () => { } }) +test('gpt-6 family ships the full API spec on the Codex route (no Codex cap published)', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_BASE_URL = 'https://chatgpt.com/backend-api/codex' + delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS + delete process.env.OPENAI_MODEL + + // Unlike the 5.6 family (pinned to the verified ~272k Codex effective + // input boundary), GPT-6 has no published Codex-route cap — the 272k pin + // is verified ONLY for 5.6 (openai/codex#33972). The gpt6.ts descriptors + // carry the full 1.05M spec with a dated comment + first-500 monitoring + // flag; if the Codex backend enforces a lower cap, pin them then. + for (const model of ['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna']) { + expect(getContextWindowForModel(model)).toBe(1_050_000) + expect(getModelMaxOutputTokens(model)).toEqual({ + default: 128_000, + upperLimit: 128_000, + }) + } +}) + +test('gpt-6 family keeps the full window on the direct-OpenAI route', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + delete process.env.OPENAI_BASE_URL + delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS + delete process.env.OPENAI_MODEL + + // The openai-route catalog entries preserve the true 1.05M window, + // same as the 5.6 family. + for (const model of ['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol', 'gpt-6-luna']) { + expect(getContextWindowForModel(model)).toBe(1_050_000) + } +}) + test('gpt-5.4 family uses provider-specific context and output caps', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS diff --git a/src/utils/effort.codex.test.ts b/src/utils/effort.codex.test.ts index 6e47a7856d..cc87e81e7f 100644 --- a/src/utils/effort.codex.test.ts +++ b/src/utils/effort.codex.test.ts @@ -1730,3 +1730,91 @@ test('explicit compat metadata wire formats are controllable and feed the reques expect(modelSupportsEffort('custom-deepseek-low-only')).toBe(false) expect(modelSupportsWireEffort('custom-deepseek-low-only')).toBe(false) }) + +test('GPT-6 flagships on the ChatGPT Codex backend expose the Ultra tier as max', async () => { + // The Codex transport resolves to a catalog-less route, so this runs the + // legacy path: the standard OpenAI ladder plus `max` on the flagship + // tiers only (operator-confirmed Ultra on Sol/Astra, 2026-10-13). + const { getAvailableEffortLevels, modelSupportsEffort, resolveAppliedEffort } = + await importFreshEffortModule({ + provider: 'codex', + supportsCodexReasoningEffort: true, + }) + + for (const model of ['gpt-6-astra', 'gpt-6-sol', 'gpt-6.1-sol']) { + expect(modelSupportsEffort(model)).toBe(true) + expect(getAvailableEffortLevels(model)).toEqual([ + 'low', + 'medium', + 'high', + 'xhigh', + 'max', + ]) + expect(resolveAppliedEffort(model, 'max')).toBe('max') + } + + // Luna tops out at Extra High — no Ultra tier. + expect(modelSupportsEffort('gpt-6-luna')).toBe(true) + expect(getAvailableEffortLevels('gpt-6-luna')).toEqual([ + 'low', + 'medium', + 'high', + 'xhigh', + ]) +}) + +test('openai vendor GPT-6 catalog entries expose verified reasoning controls', async () => { + const openaiVendor = (await import('../integrations/vendors/openai.js')).default + const { + getAvailableEffortLevels, + getDefaultEffortForModel, + modelSupportsEffort, + modelSupportsWireEffort, + resolveAppliedEffort, + resolveModelReasoningControl, + } = await importFreshEffortModule({ + provider: 'openai', + supportsCodexReasoningEffort: false, + routeId: 'openai', + catalogEntries: openaiVendor.catalog?.models ?? [], + }) + + for (const model of ['gpt-6-astra', 'gpt-6.1-sol', 'gpt-6-sol']) { + expect(resolveModelReasoningControl(model)).toMatchObject({ + supportsReasoning: true, + controllable: true, + source: 'metadata', + levels: ['low', 'medium', 'high', 'xhigh', 'max'], + defaultLevel: 'high', + wireFormat: 'reasoning_effort', + }) + expect(modelSupportsEffort(model)).toBe(true) + expect(modelSupportsWireEffort(model)).toBe(true) + expect(getAvailableEffortLevels(model)).toEqual([ + 'low', + 'medium', + 'high', + 'xhigh', + 'max', + ]) + expect(getDefaultEffortForModel(model)).toBe('high') + expect(resolveAppliedEffort(model, undefined)).toBe('high') + expect(resolveAppliedEffort(model, 'max')).toBe('max') + } + + expect(resolveModelReasoningControl('gpt-6-luna')).toMatchObject({ + supportsReasoning: true, + controllable: true, + source: 'metadata', + levels: ['low', 'medium', 'high', 'xhigh'], + defaultLevel: 'medium', + wireFormat: 'reasoning_effort', + }) + expect(getAvailableEffortLevels('gpt-6-luna')).toEqual([ + 'low', + 'medium', + 'high', + 'xhigh', + ]) + expect(getDefaultEffortForModel('gpt-6-luna')).toBe('medium') +}) diff --git a/src/utils/effort.ts b/src/utils/effort.ts index 22c5d3db6e..ce91e14192 100644 --- a/src/utils/effort.ts +++ b/src/utils/effort.ts @@ -845,6 +845,18 @@ function legacyModelSupportsMaxEffort( if (model.toLowerCase().includes('opus-4-6') || model.toLowerCase().includes('opus-4-7') || model.toLowerCase().includes('opus-4-8')) { return true } + // GPT-6 flagship tiers (astra/sol/6.1-sol) serve the native-Codex Ultra + // tier, which maps to wire `max` (models.dev `reasoning_options` plus + // operator-confirmed Ultra on Sol/Astra model types, 2026-10-13; the + // Ultra→max mapping is a hypothesis pending live wire confirmation — see + // the gpt6.ts descriptor comment). Luna tops out at xhigh. + const gpt6Base = model.trim().toLowerCase().split('?', 1)[0] ?? '' + if ( + gpt6Base === 'gpt-6-astra' || gpt6Base === 'gpt-6-sol' || + gpt6Base === 'gpt-6.1-sol' || gpt6Base === 'gpt-6' + ) { + return true + } if (process.env.USER_TYPE === 'ant' && resolveAntModel(model)) { return true } @@ -921,7 +933,15 @@ function getLegacyAvailableEffortLevels( m.includes('gemini-3') ) && getReasoningApiProvider(context) === 'openai' if (modelUsesOpenAIEffort(model, context) && !isOpenCodeNativeFormat) { - return [...OPENAI_EFFORT_LEVELS] as EffortLevel[] + const levels = [...OPENAI_EFFORT_LEVELS] as EffortLevel[] + // GPT-6 flagships append the native-Codex Ultra tier (wire `max`) to the + // standard OpenAI ladder; every other OpenAI-effort model keeps the + // 4-level ladder. No current model took this branch before GPT-6 (Opus + // max models resolve through the native-format path above). + if (legacyModelSupportsMaxEffort(model, context)) { + levels.push('max') + } + return levels } const levels: EffortLevel[] = ['low', 'medium', 'high'] if (legacyModelSupportsXHighEffort(model, context)) { diff --git a/src/utils/model/model.ts b/src/utils/model/model.ts index 2c13e5902f..e35832f7c2 100644 --- a/src/utils/model/model.ts +++ b/src/utils/model/model.ts @@ -698,6 +698,10 @@ export function getPublicModelDisplayName(model: ModelName): string | null { ) { // Return display names for known GitHub Copilot models const copilotModelNames: Record = { + 'gpt-6-astra': 'GPT-6 Astra', + 'gpt-6.1-sol': 'GPT-6.1 Sol', + 'gpt-6-sol': 'GPT-6 Sol', + 'gpt-6-luna': 'GPT-6 Luna', 'gpt-5.6-sol': 'GPT-5.6 Sol', 'gpt-5.6-terra': 'GPT-5.6 Terra', 'gpt-5.6-luna': 'GPT-5.6 Luna', @@ -733,6 +737,14 @@ export function getPublicModelDisplayName(model: ModelName): string | null { return null } switch (model) { + case 'gpt-6-astra': + return 'GPT-6 Astra' + case 'gpt-6.1-sol': + return 'GPT-6.1 Sol' + case 'gpt-6-sol': + return 'GPT-6 Sol' + case 'gpt-6-luna': + return 'GPT-6 Luna' case 'gpt-5.6-sol': return 'GPT-5.6 Sol' case 'gpt-5.6-terra': @@ -924,6 +936,12 @@ export function parseUserSpecifiedModel( const query = modelString.slice('gpt-5.6'.length) return 'gpt-5.6-sol' + query + (has1mTag ? '[1m]' : '') } + // Bare gpt-6 resolves to the flagship tier (Sol), like gpt-5.6 above and + // the Codex CLI convention. Same trailing-query/[1m] handling. + if (modelString === 'gpt-6' || modelString.startsWith('gpt-6?')) { + const query = modelString.slice('gpt-6'.length) + return 'gpt-6-sol' + query + (has1mTag ? '[1m]' : '') + } // Opus 4/4.1 are no longer available on the first-party API (same as // Claude.ai) — silently remap to the current Opus default. The 'opus' diff --git a/src/utils/model/modelOptions.ts b/src/utils/model/modelOptions.ts index 1385b3d783..2db4ea22dd 100644 --- a/src/utils/model/modelOptions.ts +++ b/src/utils/model/modelOptions.ts @@ -423,6 +423,26 @@ function getCodexSparkOption(): ModelOption { function getCodexModelOptions(): ModelOption[] { return [ + { + value: 'gpt-6.1-sol', + label: 'gpt-6.1-sol', + description: 'GPT-6.1 Sol · Flagship, Light to Ultra reasoning', + }, + { + value: 'gpt-6-astra', + label: 'gpt-6-astra', + description: 'GPT-6 Astra · Flagship, Light to Ultra reasoning', + }, + { + value: 'gpt-6-sol', + label: 'gpt-6-sol', + description: 'GPT-6 Sol · Flagship, Light to Ultra reasoning', + }, + { + value: 'gpt-6-luna', + label: 'gpt-6-luna', + description: 'GPT-6 Luna · Fast and cost-effective, Light to Extra High reasoning', + }, { value: 'gpt-5.6-sol', label: 'gpt-5.6-sol', diff --git a/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts b/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts index 152b631ae4..120e2926a6 100644 --- a/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts +++ b/src/utils/model/parseUserSpecifiedModel.codexTag.test.ts @@ -113,3 +113,24 @@ describe('parseUserSpecifiedModel — bare gpt-5.6 resolves to Sol', () => { ) }) }) + +// Bare gpt-6 resolves to the flagship tier (Sol), mirroring gpt-5.6 above. +describe('parseUserSpecifiedModel — bare gpt-6 resolves to Sol', () => { + test('gpt-6 maps to gpt-6-sol and preserves the [1m] tag', () => { + expect(parseUserSpecifiedModel('gpt-6')).toBe('gpt-6-sol') + expect(parseUserSpecifiedModel('gpt-6[1m]')).toBe('gpt-6-sol[1m]') + }) + + test('explicit GPT-6 tier ids pass through unchanged', () => { + expect(parseUserSpecifiedModel('gpt-6-astra')).toBe('gpt-6-astra') + expect(parseUserSpecifiedModel('gpt-6.1-sol')).toBe('gpt-6.1-sol') + expect(parseUserSpecifiedModel('gpt-6-sol')).toBe('gpt-6-sol') + expect(parseUserSpecifiedModel('gpt-6-luna')).toBe('gpt-6-luna') + }) + + test('a ?reasoning= query suffix does not defeat the rewrite', () => { + expect(parseUserSpecifiedModel('gpt-6?reasoning=medium')).toBe( + 'gpt-6-sol?reasoning=medium', + ) + }) +})