diff --git a/packages/core/src/__tests__/model-catalog.test.ts b/packages/core/src/__tests__/model-catalog.test.ts index eaf3d3c563..3ce417e688 100644 --- a/packages/core/src/__tests__/model-catalog.test.ts +++ b/packages/core/src/__tests__/model-catalog.test.ts @@ -62,6 +62,29 @@ test('catalog transport preserves independent limits and their unmodified defaul assert.equal(decoded.defaultInputLimit, 32000); }); +test('GPT-6 Sol and Luna use GPT labels and expose supported thinking levels', () => { + const models = [{ id: 'gpt-6-sol' }, { id: 'gpt-6-luna' }]; + for (const providerType of ['openai', 'openai-codex'] as const) { + const entries = buildModelCatalogEntries({ providerType, models, modelSource: 'fetched' }); + assert.deepEqual( + entries.map(({ id, displayName, thinkingLevels }) => ({ id, displayName, thinkingLevels })), + [ + { + id: 'gpt-6-sol', + displayName: 'GPT-6 Sol', + thinkingLevels: ['low', 'medium', 'high', 'xhigh', 'max'], + }, + { + id: 'gpt-6-luna', + displayName: 'GPT-6 Luna', + thinkingLevels: ['low', 'medium', 'high', 'xhigh', 'max'], + }, + ], + providerType, + ); + } +}); + test('a live inventory annotates a model it omits and preserves higher-priority failures', () => { const input = { providerType: 'zai-coding-plan' as const, diff --git a/packages/core/src/__tests__/model-metadata.test.ts b/packages/core/src/__tests__/model-metadata.test.ts index ec0e403128..2e8239947e 100644 --- a/packages/core/src/__tests__/model-metadata.test.ts +++ b/packages/core/src/__tests__/model-metadata.test.ts @@ -156,11 +156,13 @@ describe('model-metadata vision capability', () => { }); describe('openAiAdapterApiProtocol', () => { - it('routes a normalized gpt-5 family to the Responses wire', () => { + it('routes normalized GPT-5 and GPT-6 families to the Responses wire', () => { assert.equal(openAiAdapterApiProtocol(' GPT-5.6-sol '), 'openai-responses'); + assert.equal(openAiAdapterApiProtocol('gpt-6-sol'), 'openai-responses'); + assert.equal(openAiAdapterApiProtocol('gpt-6-luna'), 'openai-responses'); }); - it('keeps a non-gpt-5 OpenAI model on the Chat Completions wire', () => { + it('keeps an older OpenAI model on the Chat Completions wire', () => { assert.equal(openAiAdapterApiProtocol('gpt-4o'), 'openai-chat'); }); diff --git a/packages/core/src/model-metadata.ts b/packages/core/src/model-metadata.ts index 3a66da7549..2b5476c9de 100644 --- a/packages/core/src/model-metadata.ts +++ b/packages/core/src/model-metadata.ts @@ -189,7 +189,7 @@ export function openAiAdapterApiProtocol( (providerType === 'opencode-go' && id === 'muse-spark-1.2-contributor') || ((providerType === 'alibaba-token-plan-cn' || providerType === 'alibaba-token-plan') && id === 'qwen3.8-max') || - /^gpt-5/i.test(id) || + /^gpt-[56]/i.test(id) || ((providerType === 'xai' || providerType === 'xai-oauth') && id === 'grok-4.5') ? 'openai-responses' : 'openai-chat'; @@ -267,6 +267,26 @@ const GOOGLE_MODEL_OVERRIDES: Record = { }, }; +// These models are in the live models.dev OpenAI catalog but not yet in the +// bundled snapshot. The OpenAI Responses SDK accepts only these five GPT-6 +// efforts on both API and Codex OAuth paths. It discards `none` and the Codex +// model list's `ultra` for Sol, so do not offer them until the request path +// can send and handle them. +const OPENAI_GPT6_THINKING_OPTIONS: ThinkingOptions = { + efforts: ['low', 'medium', 'high', 'xhigh', 'max'], +}; + +const OPENAI_GPT6_MODEL_OVERRIDES: Record = { + 'gpt-6-sol': { + displayName: 'GPT-6 Sol', + thinkingOptions: OPENAI_GPT6_THINKING_OPTIONS, + }, + 'gpt-6-luna': { + displayName: 'GPT-6 Luna', + thinkingOptions: OPENAI_GPT6_THINKING_OPTIONS, + }, +}; + // The OAuth path pins its own context windows over whatever the public // catalog says. Base facts come from the active table, falling back to the // shipped snapshot so a model upstream stops listing keeps a display name. @@ -284,6 +304,14 @@ function withoutInputLimit(metadata: ModelMetadata | undefined): ModelMetadata | function openAiOAuthModelMetadata(active: ModelsDevMetadata): Record { return { + 'gpt-6-sol': { + ...openAiOAuthBase(active, 'gpt-6-sol'), + ...OPENAI_GPT6_MODEL_OVERRIDES['gpt-6-sol'], + }, + 'gpt-6-luna': { + ...openAiOAuthBase(active, 'gpt-6-luna'), + ...OPENAI_GPT6_MODEL_OVERRIDES['gpt-6-luna'], + }, 'gpt-5.6-sol': { ...openAiOAuthBase(active, 'gpt-5.6-sol'), contextWindow: 372_000, @@ -488,6 +516,7 @@ const COMMAND_CODE_MODEL_METADATA: Record = { function buildStaticModelMetadata(active: ModelsDevMetadata): ModelsDevMetadata { return { anthropic: ANTHROPIC_MODEL_OVERRIDES, + openai: OPENAI_GPT6_MODEL_OVERRIDES, 'claude-subscription': claudeSubscriptionModelMetadata(active), // The Command Code Provider-API plan rides the same effort table. commandcode: COMMAND_CODE_MODEL_METADATA, diff --git a/packages/runtime/src/__tests__/model-factory-thinking.test.ts b/packages/runtime/src/__tests__/model-factory-thinking.test.ts index 9370b154cf..4ce90a8994 100644 --- a/packages/runtime/src/__tests__/model-factory-thinking.test.ts +++ b/packages/runtime/src/__tests__/model-factory-thinking.test.ts @@ -214,6 +214,28 @@ describe('buildProviderOptions: thinking level', () => { }); }); + test('GPT-6 Sol and Luna send selected reasoning effort on API and Codex paths', () => { + for (const modelId of ['gpt-6-sol', 'gpt-6-luna']) { + assert.deepEqual(buildProviderOptions(conn('openai'), modelId, 'max'), { + openai: { + store: false, + reasoningSummary: 'auto', + reasoningEffort: 'max', + parallelToolCalls: true, + }, + }); + assert.deepEqual(buildProviderOptions(conn('openai-codex'), modelId, 'max'), { + openai: { + store: false, + textVerbosity: 'medium', + reasoningSummary: 'auto', + reasoningEffort: 'max', + parallelToolCalls: true, + }, + }); + } + }); + test('openai-codex (gpt-5.5) preserves store:false / textVerbosity and merges reasoningEffort', () => { assert.deepEqual(buildProviderOptions(conn('openai-codex'), 'gpt-5.5'), { openai: { diff --git a/packages/runtime/src/__tests__/responses-wire-contract.test.ts b/packages/runtime/src/__tests__/responses-wire-contract.test.ts index 3828db55fb..d6f9acd0b8 100644 --- a/packages/runtime/src/__tests__/responses-wire-contract.test.ts +++ b/packages/runtime/src/__tests__/responses-wire-contract.test.ts @@ -20,6 +20,7 @@ import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; import type { LlmConnection } from '@maka/core/llm-connections'; +import { buildModelCatalogEntries } from '@maka/core/model-catalog'; import type { RuntimeEvent } from '@maka/core/runtime-event'; import { z } from 'zod'; import { modelMetadataIdsForProvider } from '@maka/core/model-metadata'; @@ -53,6 +54,47 @@ function openAiNamespace(options: Record): Record { + test('GPT-6 catalog thinking levels reach Responses for API and Codex OAuth', async () => { + for (const providerType of ['openai', 'openai-codex'] as const) { + for (const modelId of ['gpt-6-sol', 'gpt-6-luna']) { + const [entry] = buildModelCatalogEntries({ + providerType, + models: [{ id: modelId }], + modelSource: 'fetched', + }); + assert.ok(entry); + assert.deepEqual(entry.thinkingLevels, ['low', 'medium', 'high', 'xhigh', 'max']); + + const requests: Record[] = []; + const fetch = (async (_url: string | URL | Request, init?: RequestInit) => { + requests.push(JSON.parse(String(init?.body)) as Record); + return Response.json({ + id: 'response-gpt-6', + object: 'response', + status: 'completed', + output: [], + usage: { input_tokens: 1, output_tokens: 1 }, + }); + }) as typeof globalThis.fetch; + const connection = conn(providerType); + const model = getAIModel({ connection, apiKey: 'test-token', modelId, fetch }); + + for (const level of entry.thinkingLevels) { + await model.doGenerate({ + prompt: [{ role: 'user', content: [{ type: 'text', text: 'ping' }] }], + providerOptions: buildProviderOptions(connection, modelId, level), + }); + } + + assert.deepEqual( + requests.map((body) => (body.reasoning as { effort?: string } | undefined)?.effort), + entry.thinkingLevels, + `${providerType}/${modelId}`, + ); + } + } + }); + test('does not route Maka tool_search history through OpenAI native tool_search validation', async () => { const connection = conn('openai-codex', 'codex-subscription'); connection.defaultModel = 'gpt-5.6-sol';