codex-model-router 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@ The normal installation uses free mode:
10
10
 
11
11
  - The user-selected primary model handles conversation, clarification, follow-ups, final replies, and trivial work.
12
12
  - Terra/high investigates, produces implementation-ready plans, verifies results, debugs, and replans.
13
- - Luna/max performs most clear, bounded, repetitive, or independently verifiable implementation work.
13
+ - Luna/xhigh performs most clear, bounded, repetitive, or independently verifiable implementation work.
14
14
  - Sol/medium is read-only and used only when Terra still cannot resolve core logic after focused investigation and one materially revised plan, or when the user explicitly requests Sol.
15
15
  - Existing built-in and custom agents remain available and take precedence when they are a better match.
16
16
 
@@ -49,6 +49,17 @@ codex-model-router install --set-default
49
49
 
50
50
  Running a later plain `install` returns package-managed default settings to free mode. User-modified model settings are preserved.
51
51
 
52
+ ## Agent reasoning
53
+
54
+ The managed agent defaults are Terra/high, Luna/xhigh, and Sol/medium. Override all agents or individual agents during installation:
55
+
56
+ ```sh
57
+ codex-model-router install --agent-reasoning high
58
+ codex-model-router install --terra-reasoning xhigh --luna-reasoning low --sol-reasoning max
59
+ ```
60
+
61
+ Supported values are `none`, `low`, `medium`, `high`, `xhigh`, and `max`. Per-agent options override `--agent-reasoning`. Later installs preserve unchanged package-managed reasoning choices; manually edited agent files remain protected.
62
+
52
63
  ## Install
53
64
 
54
65
  Run from npm:
@@ -69,7 +80,7 @@ Installing the CLI globally only makes the command available system-wide. Use `i
69
80
  Install a specific GitHub release:
70
81
 
71
82
  ```sh
72
- npm install -g https://github.com/Honguan/codex-model-router/archive/refs/tags/v2.0.0.tar.gz
83
+ npm install -g https://github.com/Honguan/codex-model-router/archive/refs/tags/v2.1.0.tar.gz
73
84
  codex-model-router install
74
85
  ```
75
86
 
@@ -119,6 +130,8 @@ Terra verifies separately whether implementation matches the plan and whether th
119
130
 
120
131
  ```text
121
132
  codex-model-router install [--global] [--set-default] [--dry-run]
133
+ [--agent-reasoning <effort>]
134
+ [--terra-reasoning <effort>] [--luna-reasoning <effort>] [--sol-reasoning <effort>]
122
135
  codex-model-router uninstall [--global] [--dry-run]
123
136
  codex-model-router doctor [--global]
124
137
  codex-model-router --version
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { run } from "../lib/router.js";
2
+ import { runCli } from "../lib/agent-reasoning-cli.js";
3
3
 
4
- process.exitCode = await run(process.argv.slice(2), { output: console.log });
4
+ process.exitCode = await runCli(process.argv.slice(2), { output: console.log });
@@ -0,0 +1,160 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, readFile, realpath } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import {
6
+ AGENT_NAMES,
7
+ configureAgentReasoning,
8
+ DEFAULT_AGENT_REASONING,
9
+ REASONING_EFFORTS,
10
+ VERSION
11
+ } from "./manifest.js";
12
+ import { run as runCore } from "./router.js";
13
+
14
+ const OPTION_TO_AGENT = Object.freeze({
15
+ "--terra-reasoning": "terra",
16
+ "--luna-reasoning": "luna",
17
+ "--sol-reasoning": "sol"
18
+ });
19
+
20
+ function usage() {
21
+ return `codex-model-router ${VERSION}
22
+
23
+ Usage:
24
+ codex-model-router install [--global] [--set-default] [--dry-run]
25
+ [--agent-reasoning <effort>]
26
+ [--terra-reasoning <effort>]
27
+ [--luna-reasoning <effort>]
28
+ [--sol-reasoning <effort>]
29
+ codex-model-router uninstall [--global] [--dry-run]
30
+ codex-model-router doctor [--global]
31
+ codex-model-router --version
32
+
33
+ Reasoning efforts: ${REASONING_EFFORTS.join(", ")}
34
+ Defaults: Terra=${DEFAULT_AGENT_REASONING.terra}, Luna=${DEFAULT_AGENT_REASONING.luna}, Sol=${DEFAULT_AGENT_REASONING.sol}`;
35
+ }
36
+
37
+ function optionValue(argv, index, option) {
38
+ const token = argv[index];
39
+ if (token.startsWith(`${option}=`)) return { value: token.slice(option.length + 1), consumed: 1 };
40
+ const value = argv[index + 1];
41
+ if (!value || value.startsWith("--")) return { error: `missing value for ${option}` };
42
+ return { value, consumed: 2 };
43
+ }
44
+
45
+ function parseReasoningOptions(argv) {
46
+ const command = argv[0];
47
+ const cleaned = command ? [command] : [];
48
+ const overrides = {};
49
+ let all;
50
+
51
+ for (let index = 1; index < argv.length;) {
52
+ const token = argv[index];
53
+ const option = token.split("=", 1)[0];
54
+ const agent = OPTION_TO_AGENT[option];
55
+ if (option !== "--agent-reasoning" && !agent) {
56
+ cleaned.push(token);
57
+ index += 1;
58
+ continue;
59
+ }
60
+ if (command !== "install") return { error: `${option} is supported only for install` };
61
+ const parsed = optionValue(argv, index, option);
62
+ if (parsed.error) return parsed;
63
+ const reasoning = parsed.value.toLowerCase();
64
+ if (!REASONING_EFFORTS.includes(reasoning)) {
65
+ return { error: `unsupported reasoning effort: ${parsed.value}` };
66
+ }
67
+ if (agent) overrides[agent] = reasoning;
68
+ else all = reasoning;
69
+ index += parsed.consumed;
70
+ }
71
+ return { cleaned, overrides, all };
72
+ }
73
+
74
+ function digest(content) {
75
+ return createHash("sha256").update(content).digest("hex");
76
+ }
77
+
78
+ async function readRegularText(path) {
79
+ try {
80
+ const info = await lstat(path);
81
+ if (!info.isFile() || info.isSymbolicLink()) return null;
82
+ return await readFile(path, "utf8");
83
+ } catch (error) {
84
+ if (error?.code === "ENOENT") return null;
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ function activeReasoning(content) {
90
+ const match = content.match(/^\s*model_reasoning_effort\s*=\s*"([^"\r\n]+)"\s*(?:#.*)?$/m);
91
+ return match && REASONING_EFFORTS.includes(match[1]) ? match[1] : null;
92
+ }
93
+
94
+ function versionAtLeast(version, minimum) {
95
+ const parse = (value) => String(value || "0").split(".").slice(0, 3).map((part) => Number.parseInt(part, 10) || 0);
96
+ const left = parse(version);
97
+ const right = parse(minimum);
98
+ for (let index = 0; index < 3; index += 1) {
99
+ if (left[index] !== right[index]) return left[index] > right[index];
100
+ }
101
+ return true;
102
+ }
103
+
104
+ async function installedReasoning(argv, options) {
105
+ const global = argv.includes("--global");
106
+ const home = resolve(options.home ?? homedir());
107
+ const cwd = resolve(options.cwd ?? process.cwd());
108
+ const env = options.env ?? process.env;
109
+ const codexHome = global
110
+ ? resolve(env.CODEX_HOME || join(home, ".codex"))
111
+ : join(cwd, ".codex");
112
+ const stateText = await readRegularText(join(codexHome, "model-router-state.json"));
113
+ if (!stateText) return { reasoning: {}, content: {}, packageVersion: null };
114
+
115
+ let state;
116
+ try {
117
+ state = JSON.parse(stateText);
118
+ } catch {
119
+ return { reasoning: {}, content: {}, packageVersion: null };
120
+ }
121
+
122
+ const physicalCodexHome = await realpath(codexHome);
123
+ const reasoning = {};
124
+ const content = {};
125
+ for (const name of AGENT_NAMES) {
126
+ const path = join(physicalCodexHome, "agents", `${name}.toml`);
127
+ const tracked = state.files?.[name];
128
+ const current = await readRegularText(path);
129
+ if (!tracked || !current || resolve(tracked.path) !== resolve(path) || digest(current) !== tracked.hash) continue;
130
+ const effort = activeReasoning(current);
131
+ if (!effort) continue;
132
+ reasoning[name] = effort;
133
+ content[name] = current;
134
+ }
135
+ return { reasoning, content, packageVersion: state.packageVersion };
136
+ }
137
+
138
+ export async function runCli(argv, options = {}) {
139
+ const output = options.output ?? console.log;
140
+ if (!argv.length || argv.includes("--help") || argv.includes("-h")) {
141
+ output(usage());
142
+ return 0;
143
+ }
144
+
145
+ const parsed = parseReasoningOptions(argv);
146
+ if (parsed.error) {
147
+ output(`${parsed.error}\n\n${usage()}`);
148
+ return 1;
149
+ }
150
+
151
+ const installed = await installedReasoning(parsed.cleaned, options);
152
+ const selected = { ...DEFAULT_AGENT_REASONING };
153
+ if (versionAtLeast(installed.packageVersion, "2.1.0")) Object.assign(selected, installed.reasoning);
154
+ if (parsed.all) {
155
+ for (const name of AGENT_NAMES) selected[name] = parsed.all;
156
+ }
157
+ Object.assign(selected, parsed.overrides);
158
+ configureAgentReasoning(selected, installed.content);
159
+ return runCore(parsed.cleaned, options);
160
+ }
package/lib/manifest.js CHANGED
@@ -87,7 +87,7 @@ Return only the artifact required by the current stage.
87
87
  const luna = `name = "luna"
88
88
  description = "Implements clear, bounded work from an approved plan."
89
89
  model = "gpt-5.6-luna"
90
- model_reasoning_effort = "max"
90
+ model_reasoning_effort = "xhigh"
91
91
  sandbox_mode = "workspace-write"
92
92
  developer_instructions = """
93
93
  Implement the current approved plan with the smallest coherent change.
@@ -155,45 +155,91 @@ Omit irrelevant fields and vague instructions. Preserve verified work, revise on
155
155
  Verification answers separately: did implementation match the plan, and did the plan plus implementation satisfy the request? Use PASS, IMPLEMENTATION_FIX, PLAN_REVISION, EVIDENCE_GAP, or REQUIREMENT_CLARIFICATION.
156
156
  `;
157
157
 
158
- export const TEMPLATES = Object.freeze({
159
- terra: Object.freeze({ relative: ["agents", "terra.toml"], content: terra }),
160
- luna: Object.freeze({ relative: ["agents", "luna.toml"], content: luna }),
161
- sol: Object.freeze({ relative: ["agents", "sol.toml"], content: sol }),
162
- skill: Object.freeze({ relative: ["model-router", "SKILL.md"], content: skill }),
163
- planning: Object.freeze({ relative: ["implementation-planning", "SKILL.md"], content: planning })
158
+ export const REASONING_EFFORTS = Object.freeze([
159
+ "none",
160
+ "low",
161
+ "medium",
162
+ "high",
163
+ "xhigh",
164
+ "max"
165
+ ]);
166
+
167
+ export const DEFAULT_AGENT_REASONING = Object.freeze({
168
+ terra: "high",
169
+ luna: "xhigh",
170
+ sol: "medium"
164
171
  });
165
172
 
166
- export const MANAGED_FILE_NAMES = Object.freeze(["terra", "luna", "sol", "skill", "planning"]);
167
173
  export const AGENT_NAMES = Object.freeze(["terra", "luna", "sol"]);
168
174
 
169
- export const LEGACY_TEMPLATES = Object.freeze({
170
- terra: Object.freeze([]),
171
- luna: Object.freeze([legacyLunaV12]),
172
- sol: Object.freeze([legacySolWorkspaceWrite, legacySolReadOnly]),
173
- skill: Object.freeze([legacySkillWorkspaceWrite, legacySkillReadOnly]),
174
- planning: Object.freeze([])
175
- });
175
+ const AGENT_BASE_TEMPLATES = Object.freeze({ terra, luna, sol });
176
+
177
+ function withReasoning(content, reasoning) {
178
+ const pattern = /^model_reasoning_effort = "[^"]+"$/m;
179
+ if (!pattern.test(content)) throw new Error("agent template is missing model_reasoning_effort");
180
+ return content.replace(pattern, `model_reasoning_effort = "${reasoning}"`);
181
+ }
182
+
183
+ export const TEMPLATES = {
184
+ terra: { relative: Object.freeze(["agents", "terra.toml"]), content: terra },
185
+ luna: { relative: Object.freeze(["agents", "luna.toml"]), content: luna },
186
+ sol: { relative: Object.freeze(["agents", "sol.toml"]), content: sol },
187
+ skill: { relative: Object.freeze(["model-router", "SKILL.md"]), content: skill },
188
+ planning: { relative: Object.freeze(["implementation-planning", "SKILL.md"]), content: planning }
189
+ };
190
+
191
+ export const MANAGED_FILE_NAMES = Object.freeze(["terra", "luna", "sol", "skill", "planning"]);
176
192
 
177
- export const AGENT_EXPECTATIONS = Object.freeze({
178
- terra: Object.freeze({
193
+ export const LEGACY_TEMPLATES = {
194
+ terra: [],
195
+ luna: [legacyLunaV12, withReasoning(luna, "max")],
196
+ sol: [legacySolWorkspaceWrite, legacySolReadOnly],
197
+ skill: [legacySkillWorkspaceWrite, legacySkillReadOnly],
198
+ planning: []
199
+ };
200
+
201
+ export const AGENT_EXPECTATIONS = {
202
+ terra: {
179
203
  name: "terra",
180
204
  model: "gpt-5.6-terra",
181
205
  model_reasoning_effort: "high",
182
206
  sandbox_mode: "read-only"
183
- }),
184
- luna: Object.freeze({
207
+ },
208
+ luna: {
185
209
  name: "luna",
186
210
  model: "gpt-5.6-luna",
187
- model_reasoning_effort: "max",
211
+ model_reasoning_effort: "xhigh",
188
212
  sandbox_mode: "workspace-write"
189
- }),
190
- sol: Object.freeze({
213
+ },
214
+ sol: {
191
215
  name: "sol",
192
216
  model: "gpt-5.6-sol",
193
217
  model_reasoning_effort: "medium",
194
218
  sandbox_mode: "read-only"
195
- })
196
- });
219
+ }
220
+ };
221
+
222
+ export function configureAgentReasoning(overrides = {}, migrateFrom = {}) {
223
+ const selected = { ...DEFAULT_AGENT_REASONING, ...overrides };
224
+ for (const [name, reasoning] of Object.entries(selected)) {
225
+ if (!AGENT_NAMES.includes(name)) throw new Error(`unknown agent: ${name}`);
226
+ if (!REASONING_EFFORTS.includes(reasoning)) {
227
+ throw new Error(`unsupported reasoning effort for ${name}: ${reasoning}`);
228
+ }
229
+ }
230
+
231
+ for (const name of AGENT_NAMES) {
232
+ const previous = migrateFrom[name];
233
+ if (typeof previous === "string" && !LEGACY_TEMPLATES[name].includes(previous)) {
234
+ LEGACY_TEMPLATES[name].push(previous);
235
+ }
236
+ TEMPLATES[name].content = withReasoning(AGENT_BASE_TEMPLATES[name], selected[name]);
237
+ AGENT_EXPECTATIONS[name].model_reasoning_effort = selected[name];
238
+ }
239
+ return Object.freeze({ ...selected });
240
+ }
241
+
242
+ configureAgentReasoning();
197
243
 
198
244
  export const SKILL_EXPECTATIONS = Object.freeze({
199
245
  skill: Object.freeze({
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "codex-model-router",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Safely install adaptive free-mode Codex routing for Terra, Luna, and Sol.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "codex-model-router": "./bin/codex-model-router.js"
8
8
  },
9
9
  "scripts": {
10
- "check": "node --check bin/codex-model-router.js && node --check lib/manifest.js && node --check lib/router.js && node --check lib/router-core.js && node --check lib/toml.js && node --check scripts/release-metadata.js && node --check scripts/verify-published.js",
10
+ "check": "node --check bin/codex-model-router.js && node --check lib/agent-reasoning-cli.js && node --check lib/manifest.js && node --check lib/router.js && node --check lib/router-core.js && node --check lib/toml.js && node --check scripts/release-metadata.js && node --check scripts/verify-published.js",
11
11
  "test": "node --test",
12
12
  "test:package": "node scripts/package-smoke.js"
13
13
  },