fix(sdk): gate reasoning effort by runtime allowlist
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
type: Added
|
||||
type: Changed
|
||||
pr: 3474
|
||||
---
|
||||
**Agent launch model resolution now transports reasoning effort** — `resolve-model` JSON output includes supported runtime `reasoning_effort`, and runtime adapter guidance passes it to child-agent launches only when supported.
|
||||
@@ -2360,7 +2360,7 @@ Direct mapping:
|
||||
GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
|
||||
at install time so \`model_overrides\` from \`.planning/config.json\` and
|
||||
\`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
|
||||
- Resolved \`reasoning_effort="low|medium|high|xhigh"\` → pass \`reasoning_effort\`
|
||||
- Resolved \`reasoning_effort="low|medium|high|xhigh"\` (\`xhigh\` is a GSD/Codex tier, not a generic runtime enum) → pass \`reasoning_effort\`
|
||||
to \`spawn_agent\` when the runtime/tool supports it. Omit missing, empty,
|
||||
inherited, or unsupported values; do not invent one-off effort literals in
|
||||
workflow prose.
|
||||
|
||||
@@ -980,7 +980,7 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p
|
||||
|
||||
When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtime-native model IDs instead of Claude aliases. This lets a single shared `.planning/config.json` work cleanly across Claude and Codex.
|
||||
|
||||
`resolve-model` JSON output includes `reasoning_effort` when it is defined by the same runtime tier that selected the model. Runtime adapters may pass that value to child-agent launch calls that support it; runtimes without explicit support omit it.
|
||||
`resolve-model` JSON output includes `reasoning_effort` when the runtime tier resolved for the agent (after phase-type overrides) defines a `reasoning_effort`. Runtime adapters may pass that value to child-agent launch calls that support it; runtimes without explicit support omit it.
|
||||
|
||||
**Built-in tier maps:**
|
||||
|
||||
|
||||
@@ -214,6 +214,8 @@ describe('resolveModel', () => {
|
||||
|
||||
it('returns runtime reasoning_effort from the same phase-tier source as model', async () => {
|
||||
const { resolveModel } = await import('./config-query.js');
|
||||
const { resolveRuntimeTierDefault } = await import('../model-catalog.js');
|
||||
const opusCodexTier = resolveRuntimeTierDefault('codex', 'opus');
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({
|
||||
@@ -228,10 +230,35 @@ describe('resolveModel', () => {
|
||||
expect(executor).toMatchObject({
|
||||
model: 'gpt-5.4',
|
||||
profile: 'budget',
|
||||
reasoning_effort: 'xhigh',
|
||||
reasoning_effort: opusCodexTier?.reasoning_effort,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not leak reasoning_effort from overrides for unsupported runtimes', async () => {
|
||||
const { resolveModel } = await import('./config-query.js');
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'opencode',
|
||||
models: { planning: 'opus' },
|
||||
model_profile_overrides: {
|
||||
opencode: {
|
||||
opus: { model: 'openrouter/openai/gpt-5.5', reasoning_effort: 'high' },
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const planner = (await resolveModel(['gsd-planner'], tmpDir)).data as Record<string, unknown>;
|
||||
|
||||
expect(planner).toMatchObject({
|
||||
model: 'openrouter/openai/gpt-5.5',
|
||||
profile: 'balanced',
|
||||
});
|
||||
expect(planner).not.toHaveProperty('reasoning_effort');
|
||||
});
|
||||
|
||||
it('resolveModel uses workstream config when --ws is specified', async () => {
|
||||
const { resolveModel } = await import('./config-query.js');
|
||||
// Root config: balanced profile → gsd-executor resolves to 'sonnet'
|
||||
|
||||
@@ -30,8 +30,11 @@ import {
|
||||
VALID_PROFILES,
|
||||
getAgentToModelMapForProfile,
|
||||
resolveRuntimeTierDefault,
|
||||
runtimesWithReasoningEffort,
|
||||
} from '../model-catalog.js';
|
||||
|
||||
const RUNTIMES_WITH_REASONING_EFFORT = runtimesWithReasoningEffort();
|
||||
|
||||
// ─── configGet ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -151,7 +154,11 @@ function resolveRuntimeTier(config: Record<string, unknown>, tier: string): Runt
|
||||
const userEntry = normalizeRuntimeTierEntry(runtimeOverrides?.[tier]);
|
||||
|
||||
if (!builtin && !userEntry) return null;
|
||||
return { ...(builtin ?? {}), ...(userEntry ?? {}) };
|
||||
const merged = { ...(builtin ?? {}), ...(userEntry ?? {}) };
|
||||
if (!RUNTIMES_WITH_REASONING_EFFORT.has(runtime)) {
|
||||
delete merged.reasoning_effort;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -133,7 +133,11 @@ describe('getCodexSkillAdapterHeader', () => {
|
||||
const result = getCodexSkillAdapterHeader('gsd-execute-phase');
|
||||
assert.ok(result.includes('spawn_agent'), 'maps to spawn_agent');
|
||||
assert.ok(result.includes('agent_type'), 'maps subagent_type to agent_type');
|
||||
assert.ok(result.includes('reasoning_effort'), 'documents reasoning_effort transport');
|
||||
assert.match(
|
||||
result,
|
||||
/Resolved `reasoning_effort="low\|medium\|high\|xhigh"` \(`xhigh` is a GSD\/Codex tier, not a generic runtime enum\) → pass `reasoning_effort`\s+to `spawn_agent` when the runtime\/tool supports it/,
|
||||
'documents reasoning_effort transport',
|
||||
);
|
||||
assert.ok(result.includes('do not invent one-off effort literals'), 'keeps effort policy centralized');
|
||||
assert.ok(result.includes('fork_context'), 'documents fork_context default');
|
||||
assert.ok(result.includes('wait(ids)'), 'documents parallel wait pattern');
|
||||
|
||||
@@ -1132,6 +1132,42 @@ describe('resolve-model command', () => {
|
||||
assert.strictEqual(output.reasoning_effort, 'xhigh');
|
||||
});
|
||||
|
||||
test('does not include reasoning_effort for unsupported runtime overrides', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'opencode',
|
||||
models: { planning: 'opus' },
|
||||
model_profile_overrides: {
|
||||
opencode: {
|
||||
opus: { model: 'openrouter/openai/gpt-5.5', reasoning_effort: 'high' },
|
||||
},
|
||||
},
|
||||
}));
|
||||
const result = runGsdTools('resolve-model gsd-planner', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.model, 'openrouter/openai/gpt-5.5');
|
||||
assert.strictEqual(output.profile, 'balanced');
|
||||
assert.ok(!Object.prototype.hasOwnProperty.call(output, 'reasoning_effort'));
|
||||
});
|
||||
|
||||
test('does not include reasoning_effort for per-agent model_overrides', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'codex',
|
||||
models: { planning: 'opus' },
|
||||
model_overrides: { 'gsd-planner': 'gpt-5.5' },
|
||||
}));
|
||||
const result = runGsdTools('resolve-model gsd-planner', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.model, 'gpt-5.5');
|
||||
assert.strictEqual(output.profile, 'balanced');
|
||||
assert.ok(!Object.prototype.hasOwnProperty.call(output, 'reasoning_effort'));
|
||||
});
|
||||
|
||||
test('fails when no agent-type provided', () => {
|
||||
const result = runGsdTools('resolve-model', tmpDir);
|
||||
assert.ok(!result.success, 'should fail without agent-type');
|
||||
|
||||
Reference in New Issue
Block a user