* fix(#2832): gsd-sdk auto detects Codex runtime correctly Two-part fix for #2832 (gsd-sdk auto silently routing non-Claude runtime projects through the Claude Agent SDK): 1. Runtime gate at the `auto` entry point. New `runtime-gate.ts` exports `assertRuntimeSupportsAutoMode(config)` which throws an actionable error when `GSD_RUNTIME` / `config.runtime` resolves to a non-Claude runtime (codex, gemini, opencode, etc.). The autonomous orchestrator only knows how to drive `@anthropic-ai/claude-agent-sdk` today; failing fast with a clear pointer at the in-session slash commands beats the previous instant `[FAILED] $0.00 0.1s` flake. Wired into `cli.ts` before the GSD/InitRunner construction. 2. Runtime-aware `resolveModel()` in `session-runner.ts`. The profile -> id map (`balanced -> claude-sonnet-4-6`, etc.) was applied unconditionally, so even with `runtime: codex` and `resolve_model_ids: omit` the SDK forced a Claude id into `query()`. Now the profile map only fires when the runtime is Claude and the explicit `resolve_model_ids: "omit"` knob short-circuits to undefined, mirroring `query/config-query.ts`. Tests (vitest, sdk/src): - runtime-gate.test.ts (8 cases): claude / unset / unknown pass; codex, gemini, opencode throw; GSD_RUNTIME wins over config.runtime; error message references #2832 and the slash-command workaround. - session-runner.test.ts (4 new cases under "resolveModel runtime awareness (#2832)"): codex runtime + balanced profile -> no model injected; resolve_model_ids: omit -> no model; claude runtime still resolves to claude-sonnet-4-6 (no regression); explicit options.model wins on any runtime. * fix(#2832): address CR — env-precedence in resolveModel + accurate source attribution Two CodeRabbit findings on PR #2844: 1. session-runner.ts:resolveModel() (Major) — read runtime via detectRuntime() so GSD_RUNTIME env precedence is honored. Without this, a Codex run with a Claude-shaped config still fell into the Claude-only profile-id branch. 2. runtime-gate.ts:assertRuntimeSupportsAutoMode() (Minor) — when GSD_RUNTIME holds an unsupported value, detectRuntime() falls through to config but the source label still reported the discarded env value. Fix: validate env against SUPPORTED_RUNTIMES before attributing the source. Tests added for both: env-precedence in session-runner, source attribution in runtime-gate. 17/17 pass.
This commit is contained in:
@@ -95,4 +95,70 @@ describe('runPhaseStepSession', () => {
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.sessionId).toBe('test-session');
|
||||
});
|
||||
|
||||
// ─── #2832: runtime-aware model resolution ─────────────────────────────────
|
||||
// Issue: with `runtime: codex` and `model_profile: balanced`, resolveModel
|
||||
// mapped balanced -> 'claude-sonnet-4-6' and forced the Codex run through
|
||||
// a Claude model. For non-Claude runtimes the SDK must NOT inject a Claude
|
||||
// model id; it should leave model unset (or honor explicit overrides) and
|
||||
// let the runtime use its configured default.
|
||||
describe('resolveModel runtime awareness (#2832)', () => {
|
||||
it('does NOT pass a Claude profile model id when runtime is codex', async () => {
|
||||
await runPhaseStepSession(
|
||||
'prompt',
|
||||
PhaseStepType.Execute,
|
||||
makeConfig({ runtime: 'codex', model_profile: 'balanced' } as Partial<GSDConfig>),
|
||||
);
|
||||
const opts = mockQueryCalls[0].options as { model?: string };
|
||||
expect(opts.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does NOT pass a Claude profile model id when resolve_model_ids is "omit"', async () => {
|
||||
await runPhaseStepSession(
|
||||
'prompt',
|
||||
PhaseStepType.Execute,
|
||||
makeConfig({ resolve_model_ids: 'omit', model_profile: 'balanced' } as Partial<GSDConfig>),
|
||||
);
|
||||
const opts = mockQueryCalls[0].options as { model?: string };
|
||||
expect(opts.model).toBeUndefined();
|
||||
});
|
||||
|
||||
it('still maps profile -> Claude id when runtime is claude (no regression)', async () => {
|
||||
await runPhaseStepSession(
|
||||
'prompt',
|
||||
PhaseStepType.Execute,
|
||||
makeConfig({ runtime: 'claude', model_profile: 'balanced' } as Partial<GSDConfig>),
|
||||
);
|
||||
const opts = mockQueryCalls[0].options as { model?: string };
|
||||
expect(opts.model).toBe('claude-sonnet-4-6');
|
||||
});
|
||||
|
||||
it('respects GSD_RUNTIME env precedence over config (no Claude id when env=codex)', async () => {
|
||||
const prev = process.env.GSD_RUNTIME;
|
||||
process.env.GSD_RUNTIME = 'codex';
|
||||
try {
|
||||
await runPhaseStepSession(
|
||||
'prompt',
|
||||
PhaseStepType.Execute,
|
||||
makeConfig({ model_profile: 'balanced' } as Partial<GSDConfig>),
|
||||
);
|
||||
const opts = mockQueryCalls[0].options as { model?: string };
|
||||
expect(opts.model).toBeUndefined();
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.GSD_RUNTIME;
|
||||
else process.env.GSD_RUNTIME = prev;
|
||||
}
|
||||
});
|
||||
|
||||
it('explicit options.model wins for any runtime', async () => {
|
||||
await runPhaseStepSession(
|
||||
'prompt',
|
||||
PhaseStepType.Execute,
|
||||
makeConfig({ runtime: 'codex' } as Partial<GSDConfig>),
|
||||
{ model: 'gpt-5.3-codex' },
|
||||
);
|
||||
const opts = mockQueryCalls[0].options as { model?: string };
|
||||
expect(opts.model).toBe('gpt-5.3-codex');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user