fix(sdk): gate reasoning effort by runtime allowlist

This commit is contained in:
radioflyer28
2026-05-13 22:27:15 -04:00
parent c20f714d68
commit 810778e137
7 changed files with 80 additions and 6 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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:**

View File

@@ -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'

View File

@@ -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;
}
/**

View File

@@ -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');

View File

@@ -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');