From 0d482ddb1dbe82cd0461251928f62e3eeec8017e Mon Sep 17 00:00:00 2001 From: Nur Ad-Duja Date: Mon, 27 Jul 2026 20:32:07 +0700 Subject: [PATCH] fix(codex): restore tools for kenari-routed models Codex runs the gpt-5.6 family in tool_mode="code_mode". In that mode it sends an empty top-level tools array and puts the real tool definitions in an "additional_tools" input item, which only OpenAI's own backend expands into usable tools. The Kenari gateway forwards that item as an ordinary developer message, so the model saw no tools at all and replied "I can't access a shell tool in this session". codexKenariModels cloned the native template wholesale, so every kenari-routed entry inherited tool_mode, use_responses_lite and multi_agent_version. Drop those OpenAI-private modes so Codex falls back to classic top-level function tools, which the gateway already handles. Also normalize dots to dashes in the template lookup. Cache ids use gpt-5-6-sol while native slugs use gpt-5.6-sol, so no entry ever matched its own template and everything fell back to nativeModels[0]. Verified end to end: kenari/gpt-5-6-sol and kenari/glm-5-2 both run the shell tool and return a real directory listing. --- package.json | 26 ++++++++++++++---- src/runtime/codex.js | 18 ++++++++++-- test/v2-runtime-supervisor.test.js | 44 ++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 8 deletions(-) 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 = [ {