diff --git a/package.json b/package.json index 3412502..4377e54 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,26 @@ { "name": "@kenarihq/cli", - "version": "0.4.0", + "version": "0.4.1", "description": "Official kenari CLI. Route Claude Code and Codex models per process.", "license": "MIT", "type": "module", - "bin": { "kenari": "bin/kenari.js" }, - "files": ["bin", "src", "README.md", "LICENSE"], - "engines": { "node": ">=18" }, - "scripts": { "test": "node --test" }, - "repository": { "type": "git", "url": "git+https://github.com/kenarihq/cli.git" } + "bin": { + "kenari": "bin/kenari.js" + }, + "files": [ + "bin", + "src", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "test": "node --test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kenarihq/cli.git" + } } diff --git a/src/runtime/codex.js b/src/runtime/codex.js index 25207e9..10bcf33 100644 --- a/src/runtime/codex.js +++ b/src/runtime/codex.js @@ -41,6 +41,10 @@ export function loadCodexNativeModels(binary, env = process.env) { throw new KenariError('cannot read the Codex native model catalog'); } +function normalizeModelId(id) { + return String(id || '').replace(/\./g, '-'); +} + export function codexKenariModels(cache, nativeModels = []) { const fallbackTemplate = nativeModels[0]; if ((cache?.models || []).length && !fallbackTemplate) { @@ -48,11 +52,13 @@ export function codexKenariModels(cache, nativeModels = []) { } const basePriority = Math.max(0, ...nativeModels.map((model) => model.priority || 0)); return (cache?.models || []).map((model, index) => { - const template = nativeModels.find((native) => native.slug === model.id) || fallbackTemplate; + const normalizedId = normalizeModelId(model.id); + const template = nativeModels.find((native) => normalizeModelId(native.slug) === normalizedId) + || fallbackTemplate; const efforts = model.reasoning_efforts?.length ? model.reasoning_efforts : (template.supported_reasoning_levels || []).map((level) => level.effort); - return { + const entry = { ...template, slug: `kenari/${model.id}`, display_name: `Kenari ${model.id}`, @@ -69,6 +75,14 @@ export function codexKenariModels(cache, nativeModels = []) { max_context_window: model.context_limit || template.max_context_window, upgrade: null, }; + // Kenari-routed models must use classic top-level function tools. The gpt-5.6 + // templates carry OpenAI-private harness modes: in code_mode Codex sends an empty + // tools array and hides the real definitions in an "additional_tools" input item + // that only OpenAI's own backend expands, so the model ends up with no tools. + delete entry.tool_mode; + delete entry.multi_agent_version; + entry.use_responses_lite = false; + return entry; }); } diff --git a/test/v2-runtime-supervisor.test.js b/test/v2-runtime-supervisor.test.js index d798c39..a9f09dd 100644 --- a/test/v2-runtime-supervisor.test.js +++ b/test/v2-runtime-supervisor.test.js @@ -164,6 +164,50 @@ test('Codex launch injects temporary controls before original args', () => { } }); +test('codexKenariModels removes OpenAI-private mode fields and matches dotted native slugs', () => { + const native = [{ + slug: 'gpt-5.6-sol', + tool_mode: 'code_mode', + use_responses_lite: true, + multi_agent_version: 'v2', + supported_reasoning_levels: [{ effort: 'low', description: 'Low' }], + context_window: 100000, + max_context_window: 100000, + priority: 10, + }, { + slug: 'gpt-5.5', + supported_reasoning_levels: [{ effort: 'low', description: 'Low' }], + context_window: 100000, + max_context_window: 100000, + priority: 5, + }]; + const kenari = codexKenariModels({ + models: [ + { id: 'gpt-5-6-sol', context_limit: 200000, reasoning_efforts: ['medium', 'high'] }, + { id: 'glm-5-2', context_limit: 128000 }, + ], + }, native); + const sol = kenari.find((m) => m.slug === 'kenari/gpt-5-6-sol'); + assert.ok(sol, 'gpt-5-6-sol entry exists'); + assert.equal(sol.context_window, 200000); + assert.equal(sol.priority, 11); + assert.equal('tool_mode' in sol, false); + assert.equal('multi_agent_version' in sol, false); + assert.equal(sol.use_responses_lite, false); + assert.deepEqual(sol.supported_reasoning_levels, [ + { effort: 'medium', description: 'medium reasoning' }, + { effort: 'high', description: 'high reasoning' }, + ]); + assert.equal(sol.max_context_window, 200000); + + const glm = kenari.find((m) => m.slug === 'kenari/glm-5-2'); + assert.ok(glm, 'glm-5-2 entry exists'); + assert.equal(glm.context_window, 128000); + assert.equal('tool_mode' in glm, false); + assert.equal('multi_agent_version' in glm, false); + assert.equal(glm.use_responses_lite, false); +}); + test('Codex native upstream follows the active login method', () => { const cases = [ {