Skip to content
Merged
63 changes: 63 additions & 0 deletions src/agents/plugins/__tests__/opencode-gpt55-routing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,66 @@ describe('GPT-5.6 → Responses API routing', () => {
expect(OPENCODE_MODEL_CONFIGS['gpt-5.6-sol-2026-07-09']!.limit.context).toBe(1050000);
});
});

describe('GPT-6 → Responses API routing', () => {
let convertApiModelToOpenCodeConfig: typeof import('../opencode/opencode-dynamic-models.js').convertApiModelToOpenCodeConfig;
let OPENCODE_MODEL_CONFIGS: typeof import('../opencode/opencode-model-configs.js').OPENCODE_MODEL_CONFIGS;

beforeEach(async () => {
vi.resetModules();
({ convertApiModelToOpenCodeConfig } = await import('../opencode/opencode-dynamic-models.js'));
({ OPENCODE_MODEL_CONFIGS } = await import('../opencode/opencode-model-configs.js'));
});

// ── Dynamic path (live catalogue) ──────────────────────────────────────────

it('routes gpt-6-luna to Responses API via dynamic model conversion', () => {
const config = convertApiModelToOpenCodeConfig(makeLlmModel('gpt-6-luna'));
expect(config.use_responses_api).toBe(true);
});

it('routes openai.gpt-6-luna (vendor-prefixed) to Responses API via dynamic conversion', () => {
const config = convertApiModelToOpenCodeConfig(makeLlmModel('openai.gpt-6-luna'));
expect(config.use_responses_api).toBe(true);
});

it('routes gpt-6-sol-2026-09-22 (dated) to Responses API via dynamic conversion', () => {
const config = convertApiModelToOpenCodeConfig(makeLlmModel('gpt-6-sol-2026-09-22'));
expect(config.use_responses_api).toBe(true);
});

it('detects the gpt-6 family for vendor-prefixed ids', () => {
const config = convertApiModelToOpenCodeConfig(makeLlmModel('openai.gpt-6-sol'));
expect(config.family).toBe('gpt-6');
});

it('dynamic gpt-6-luna reports context limit of 1050000', () => {
const config = convertApiModelToOpenCodeConfig(makeLlmModel('gpt-6-luna'));
expect(config.limit.context).toBe(1050000);
expect(config.limit.output).toBe(128000);
});

it('dynamic openai.gpt-6-luna reports context limit of 1050000', () => {
const config = convertApiModelToOpenCodeConfig(makeLlmModel('openai.gpt-6-luna'));
expect(config.limit.context).toBe(1050000);
});

// ── Static fallback path (OPENCODE_MODEL_CONFIGS) ──────────────────────────

it('static config has gpt-6-luna with use_responses_api: true', () => {
expect(OPENCODE_MODEL_CONFIGS['gpt-6-luna']).toBeDefined();
expect(OPENCODE_MODEL_CONFIGS['gpt-6-luna']!.use_responses_api).toBe(true);
});

it('static config gpt-6-sol supports tool_call', () => {
expect(OPENCODE_MODEL_CONFIGS['gpt-6-sol']!.tool_call).toBe(true);
});

it('static config gpt-6-luna reports context limit of 1050000', () => {
expect(OPENCODE_MODEL_CONFIGS['gpt-6-luna']!.limit.context).toBe(1050000);
});

it('static config gpt-6-sol reports context limit of 1050000', () => {
expect(OPENCODE_MODEL_CONFIGS['gpt-6-sol']!.limit.context).toBe(1050000);
});
});
6 changes: 5 additions & 1 deletion src/agents/plugins/opencode/opencode-dynamic-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import { logger } from '../../../utils/logger.js';
//
// Naming conventions observed in CodeMie deployments:
// Responses API → gpt-5-2-*, gpt-5.2-*, gpt-5.x-codex-*, gpt-5-x-codex-*,
// gpt-5.4-*, gpt-5-4-*, gpt-5.5-*, gpt-5-5-*, gpt-5.6-*, gpt-5-6-*
// gpt-5.4-*, gpt-5-4-*, gpt-5.5-*, gpt-5-5-*, gpt-5.6-*, gpt-5-6-*,
// gpt-6-*, gpt-6.* (e.g. gpt-6-luna, gpt-6.1-sol, openai.gpt-6-luna, bare openai.gpt-6)
// Chat Completions → gpt-4*, gpt-5-<year>-*, o1/o3/o4*, gemini-*, claude-*, …
//
// Update this list whenever new Responses-API-only models are deployed.
Expand All @@ -45,6 +46,7 @@ const RESPONSES_API_MODEL_PATTERNS: RegExp[] = [
/^gpt-5\.5-/, // gpt-5.5-2026-04-24 — same Azure restriction as gpt-5.4
/^gpt-5-5-/, // hyphenated variant of gpt-5.5-*
/gpt-5[.-]6/, // gpt-5.6-* (e.g. openai.gpt-5.6-luna) — same Azure restriction (tools + reasoning_effort)
/gpt-6(?!\d)/, // gpt-6-* / gpt-6.* (e.g. gpt-6-luna, gpt-6.1-sol, openai.gpt-6-luna, chatgpt-6.1-sol) — Responses API for built-in tools; (?!\d) keeps gpt-60-style ids out while staying ready for 6.2+
];

function isResponsesApiModel(id: string): boolean {
Expand All @@ -58,6 +60,7 @@ function detectFamily(id: string): string {
if (id.startsWith('gemini')) return 'gemini-2';
if (id.startsWith('gpt-4')) return 'gpt-4';
if (id.startsWith('gpt-5') || /gpt-5[.-]6/.test(id)) return 'gpt-5';
if (/gpt-6(?!\d)/.test(id)) return 'gpt-6';
if (/^o[134]-/.test(id) || id === 'o1') return 'openai-reasoning';
if (id.startsWith('qwen')) return 'qwen3';
if (id.startsWith('deepseek')) return 'deepseek';
Expand All @@ -77,6 +80,7 @@ function detectLimits(id: string, family: string): { context: number; output: nu
if (id.startsWith('gpt-4o')) return { context: 128000, output: 16384 };
if (id.startsWith('gpt-5.5') || id.startsWith('gpt-5-5')) return { context: 1050000, output: 128000 }; // Azure-published window for gpt-5.5
if (/gpt-5[.-]6/.test(id)) return { context: 1050000, output: 128000 }; // Azure-published window for gpt-5.6
if (/gpt-6(?!\d)/.test(id)) return { context: 1050000, output: 128000 }; // Vendor-published window for gpt-6 (1,050,000 context, 128k output; covers 6.1-sol, future 6.2+)
if (id.startsWith('gpt-5')) return { context: 400000, output: 128000 };
if (/^o[134]-/.test(id) || id === 'o1') return { context: 200000, output: 100000 };
if (id.startsWith('qwen') || id.startsWith('moonshotai') || id.startsWith('kimi')) return { context: 262144, output: 131072 };
Expand Down
126 changes: 126 additions & 0 deletions src/agents/plugins/opencode/opencode-model-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,124 @@ export const OPENCODE_MODEL_CONFIGS: Record<string, OpenCodeModelConfig> = {
}
},

// ── GPT-6 Models (vendor-published: 1,050,000 context, 128k output, Responses API) ──
'gpt-6-luna': {
id: 'gpt-6-luna',
name: 'GPT-6 Luna',
displayName: 'GPT-6 Luna',
family: 'gpt-6',
tool_call: true,
reasoning: true,
attachment: true,
temperature: false,
structured_output: true,
use_responses_api: true,
modalities: {
input: ['text', 'image'],
output: ['text']
},
knowledge: '2026-05-18',
release_date: '2026-09-22',
last_updated: '2026-09-22',
open_weights: false,
cost: {
input: 0.10,
output: 0.50,
cache_read: 0.01
},
limit: {
context: 1050000,
output: 128000
}
},
'gpt-6-sol': {
id: 'gpt-6-sol',
name: 'GPT-6 Sol',
displayName: 'GPT-6 Sol',
family: 'gpt-6',
tool_call: true,
reasoning: true,
attachment: true,
temperature: false,
structured_output: true,
use_responses_api: true,
modalities: {
input: ['text', 'image'],
output: ['text']
},
knowledge: '2026-05-18',
release_date: '2026-09-22',
last_updated: '2026-09-22',
open_weights: false,
cost: {
input: 2,
output: 10,
cache_read: 0.20
},
limit: {
context: 1050000,
output: 128000
}
},
'gpt-6-astra': {
id: 'gpt-6-astra',
name: 'GPT-6 Astra',
displayName: 'GPT-6 Astra',
family: 'gpt-6',
tool_call: true,
reasoning: true,
attachment: true,
temperature: false,
structured_output: true,
use_responses_api: true,
modalities: {
input: ['text', 'image'],
output: ['text']
},
knowledge: '2026-05-18',
release_date: '2026-09-22',
last_updated: '2026-09-22',
open_weights: false,
cost: {
input: 10,
output: 50,
cache_read: 1
},
limit: {
context: 1050000,
output: 128000
}
},
'gpt-6.1-sol': {
id: 'gpt-6.1-sol',
name: 'GPT-6.1 Sol',
displayName: 'GPT-6.1 Sol',
family: 'gpt-6',
tool_call: true,
reasoning: true,
attachment: true,
temperature: false,
structured_output: true,
use_responses_api: true,
modalities: {
input: ['text', 'image'],
output: ['text']
},
knowledge: '2026-05-18',
release_date: '2026-09-22',
last_updated: '2026-09-22',
open_weights: false,
cost: {
input: 2,
output: 10,
cache_read: 0.1
},
limit: {
context: 1050000,
output: 128000
}
},

// ── Claude Models ──────────────────────────────────────────────────
'claude-4-5-sonnet': {
id: 'claude-4-5-sonnet',
Expand Down Expand Up @@ -647,6 +765,14 @@ const MODEL_FAMILY_DEFAULTS: Record<string, Partial<OpenCodeModelConfig>> = {
modalities: { input: ['text', 'image', 'audio', 'video'], output: ['text'] },
limit: { context: 1048576, output: 65536 }
},
'gpt-6': {
family: 'gpt-6',
reasoning: true,
attachment: true,
temperature: false,
modalities: { input: ['text', 'image'], output: ['text'] },
limit: { context: 1050000, output: 128000 }
},
'gpt': {
family: 'gpt-5',
reasoning: true,
Expand Down
32 changes: 32 additions & 0 deletions src/agents/plugins/pi/__tests__/pi.models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,35 @@ describe('convertLlmModelToPiEntry — cost', () => {
expect(entry.compat).toEqual({ forceAdaptiveThinking: true });
});
});

describe('convertLlmModelToPiEntry — GPT-6 limits and routing', () => {
beforeEach(() => {
vi.mocked(lookupPrice).mockReset();
vi.mocked(lookupPrice).mockReturnValue(null);
});

function gpt6Model(id: string): LlmModel {
return llmModel({ base_name: id, deployment_name: id, label: id });
}

it('should report a 1050000-token window for gpt-6-luna', () => {
const entry = convertLlmModelToPiEntry(gpt6Model('gpt-6-luna'));

expect(entry.contextWindow).toBe(1050000);
expect(entry.maxTokens).toBe(128000);
});

it('should report a 1050000-token window for a vendor-prefixed id', () => {
const entry = convertLlmModelToPiEntry(gpt6Model('openai.gpt-6-sol'));

expect(entry.contextWindow).toBe(1050000);
expect(entry.maxTokens).toBe(128000);
});

it('should route gpt-6-luna via openai-responses with reasoning', () => {
const entry = convertLlmModelToPiEntry(gpt6Model('gpt-6-luna'));

expect(entry.api).toBe('openai-responses');
expect(entry.reasoning).toBe(true);
});
});
5 changes: 5 additions & 0 deletions src/agents/plugins/pi/pi.models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const RESPONSES_API_PATTERNS: RegExp[] = [
/^gpt-5\.5-/,
/^gpt-5-5-/,
/gpt-5[.-]6/,
// gpt-6-*/gpt-6.* (e.g. gpt-6-luna, gpt-6.1-sol, openai.gpt-6-luna, bare openai.gpt-6).
// (?!\d) keeps gpt-60-style ids out while staying ready for 6.2+.
/gpt-6(?!\d)/,
];

export function classifyPiModel(modelId: string): PiModelClassification {
Expand Down Expand Up @@ -69,6 +72,7 @@ function detectLimits(id: string): { contextWindow: number; maxTokens: number }
if (id.startsWith('gpt-4.1')) return { contextWindow: 1048576, maxTokens: 32768 };
if (/^gpt-5\.5-/.test(id) || /^gpt-5-5-/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 };
if (/gpt-5[.-]6/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 };
if (/gpt-6(?!\d)/.test(id)) return { contextWindow: 1050000, maxTokens: 128000 };
if (id.startsWith('gpt-5')) return { contextWindow: 400000, maxTokens: 128000 };
if (/^o[134]-/.test(id) || id === 'o1') return { contextWindow: 200000, maxTokens: 100000 };
if (id.startsWith('qwen') || id.startsWith('moonshotai') || id.startsWith('kimi')) {
Expand Down Expand Up @@ -96,6 +100,7 @@ function isReasoningModel(id: string): boolean {
id.startsWith('gemini') ||
id.startsWith('gpt-5') ||
/gpt-5[.-]6/.test(id) ||
/gpt-6(?!\d)/.test(id) ||
/^o[134]-/.test(id) ||
id === 'o1' ||
id.startsWith('deepseek') ||
Expand Down
13 changes: 11 additions & 2 deletions src/cli/commands/proxy/connectors/__tests__/vscode-models.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@ describe('findVsCodeCapabilityEntry', () => {
expect(findVsCodeCapabilityEntry('openai.gpt-5.6-luna')?.family).toBe('gpt-5.6-luna');
});

it('returns undefined for a model with no capability-table family', () => {
expect(findVsCodeCapabilityEntry('gpt-6-sol')).toBeUndefined();
it('returns the table entry for a GPT-6 model', () => {
expect(findVsCodeCapabilityEntry('gpt-6-sol')).toMatchObject({
family: 'gpt-6-sol',
apiType: 'responses',
maxInputTokens: 922000,
maxOutputTokens: 128000,
});
});

it('resolves a vendor-prefixed GPT-6 tenant id to its family entry', () => {
expect(findVsCodeCapabilityEntry('openai.gpt-6-luna')?.family).toBe('gpt-6-luna');
});
});

Expand Down
Loading
Loading