fix(sdk): honor Codex model overrides in init progress
This commit is contained in:
5
.changeset/fix-3358-sdk-init-progress-models.md
Normal file
5
.changeset/fix-3358-sdk-init-progress-models.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3361
|
||||
---
|
||||
**SDK `resolve-model` and `init.progress` now report Codex runtime override models before applying `resolve_model_ids: "omit"`** — Codex projects using runtime-specific `model_profile_overrides` now see the resolved planner and executor model IDs in SDK query output instead of empty strings or the composed `sonnet` fallback. (#3358)
|
||||
@@ -187,6 +187,31 @@ describe('resolveModel', () => {
|
||||
expect(data).toHaveProperty('model', '');
|
||||
});
|
||||
|
||||
it('runtime codex model_profile_overrides beat resolve_model_ids omit (#3358)', async () => {
|
||||
const { resolveModel } = await import('./config-query.js');
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'codex',
|
||||
resolve_model_ids: 'omit',
|
||||
model_profile_overrides: {
|
||||
codex: {
|
||||
opus: { model: 'gpt-5.5', reasoning_effort: 'high' },
|
||||
sonnet: 'gpt-5.3-codex',
|
||||
haiku: 'gpt-5.4-mini',
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const planner = (await resolveModel(['gsd-planner'], tmpDir)).data as Record<string, unknown>;
|
||||
const executor = (await resolveModel(['gsd-executor'], tmpDir)).data as Record<string, unknown>;
|
||||
|
||||
expect(planner).toMatchObject({ model: 'gpt-5.5', profile: 'balanced' });
|
||||
expect(executor).toMatchObject({ model: 'gpt-5.3-codex', profile: 'balanced' });
|
||||
});
|
||||
|
||||
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'
|
||||
|
||||
@@ -24,7 +24,13 @@ import { planningPaths } from './helpers.js';
|
||||
import { maskIfSecret } from './secrets.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
export { MODEL_PROFILES, VALID_PROFILES, getAgentToModelMapForProfile } from '../model-catalog.js';
|
||||
import { MODEL_PROFILES, VALID_PROFILES, getAgentToModelMapForProfile } from '../model-catalog.js';
|
||||
import {
|
||||
AGENT_TO_PHASE_TYPE,
|
||||
MODEL_PROFILES,
|
||||
VALID_PROFILES,
|
||||
getAgentToModelMapForProfile,
|
||||
resolveRuntimeTierDefault,
|
||||
} from '../model-catalog.js';
|
||||
|
||||
// ─── configGet ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -114,6 +120,40 @@ export const configPath: QueryHandler = async (_args, projectDir, workstream) =>
|
||||
|
||||
// ─── resolveModel ───────────────────────────────────────────────────────────
|
||||
|
||||
type RuntimeTierName = 'opus' | 'sonnet' | 'haiku';
|
||||
|
||||
interface RuntimeTierEntry {
|
||||
model?: string;
|
||||
reasoning_effort?: string;
|
||||
}
|
||||
|
||||
function isRuntimeTierName(value: string): value is RuntimeTierName {
|
||||
return value === 'opus' || value === 'sonnet' || value === 'haiku';
|
||||
}
|
||||
|
||||
function normalizeRuntimeTierEntry(entry: unknown): RuntimeTierEntry | null {
|
||||
if (typeof entry === 'string') return { model: entry };
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
return entry as RuntimeTierEntry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveRuntimeTier(config: Record<string, unknown>, tier: string): RuntimeTierEntry | null {
|
||||
if (!isRuntimeTierName(tier)) return null;
|
||||
|
||||
const runtime = typeof config.runtime === 'string' ? config.runtime : '';
|
||||
if (!runtime || runtime === 'claude') return null;
|
||||
|
||||
const builtin = resolveRuntimeTierDefault(runtime, tier);
|
||||
const profileOverrides = config.model_profile_overrides as Record<string, unknown> | undefined;
|
||||
const runtimeOverrides = profileOverrides?.[runtime] as Record<string, unknown> | undefined;
|
||||
const userEntry = normalizeRuntimeTierEntry(runtimeOverrides?.[tier]);
|
||||
|
||||
if (!builtin && !userEntry) return null;
|
||||
return { ...(builtin ?? {}), ...(userEntry ?? {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Query handler for resolve-model command.
|
||||
*
|
||||
@@ -149,10 +189,11 @@ export const resolveModel: QueryHandler = async (args, projectDir, workstream) =
|
||||
return { data: result };
|
||||
}
|
||||
|
||||
// No project config (or explicit omit policy) -> return empty model id (CJS parity)
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
|
||||
// No project config -> return empty model id (CJS parity)
|
||||
const resolveModelIds = (config as Record<string, unknown>).resolve_model_ids;
|
||||
if (!configExists || resolveModelIds === 'omit') {
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
if (!configExists) {
|
||||
const result = agentModels
|
||||
? { model: '', profile }
|
||||
: { model: '', profile, unknown_agent: true };
|
||||
@@ -160,7 +201,6 @@ export const resolveModel: QueryHandler = async (args, projectDir, workstream) =
|
||||
}
|
||||
|
||||
// Fall back to profile lookup
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
if (!agentModels) {
|
||||
const semanticFallback =
|
||||
profile === 'quality' ? 'opus'
|
||||
@@ -175,5 +215,19 @@ export const resolveModel: QueryHandler = async (args, projectDir, workstream) =
|
||||
}
|
||||
|
||||
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
||||
const phaseType = AGENT_TO_PHASE_TYPE[agentType];
|
||||
const phaseTier = phaseType && typeof (config as Record<string, unknown>).models === 'object'
|
||||
? ((config as Record<string, unknown>).models as Record<string, unknown>)[phaseType]
|
||||
: undefined;
|
||||
const tier = typeof phaseTier === 'string' ? phaseTier : alias;
|
||||
const runtimeTier = resolveRuntimeTier(config as Record<string, unknown>, tier);
|
||||
if (runtimeTier?.model) {
|
||||
return { data: { model: runtimeTier.model, profile } };
|
||||
}
|
||||
|
||||
if (resolveModelIds === 'omit') {
|
||||
return { data: { model: '', profile } };
|
||||
}
|
||||
|
||||
return { data: { model: alias, profile } };
|
||||
};
|
||||
|
||||
@@ -173,6 +173,34 @@ describe('initProgress', () => {
|
||||
expect(typeof data.config_path).toBe('string');
|
||||
});
|
||||
|
||||
it('reports Codex runtime override models when resolve_model_ids is omit (#3358)', async () => {
|
||||
await writeFile(join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
runtime: 'codex',
|
||||
resolve_model_ids: 'omit',
|
||||
model_profile_overrides: {
|
||||
codex: {
|
||||
opus: { model: 'gpt-5.5', reasoning_effort: 'high' },
|
||||
sonnet: 'gpt-5.3-codex',
|
||||
haiku: 'gpt-5.4-mini',
|
||||
},
|
||||
},
|
||||
commit_docs: false,
|
||||
git: {
|
||||
branching_strategy: 'none',
|
||||
phase_branch_template: 'gsd/phase-{phase}-{slug}',
|
||||
milestone_branch_template: 'gsd/{milestone}-{slug}',
|
||||
quick_branch_template: null,
|
||||
},
|
||||
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true },
|
||||
}));
|
||||
|
||||
const result = await initProgress([], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
expect(data.planner_model).toBe('gpt-5.5');
|
||||
expect(data.executor_model).toBe('gpt-5.3-codex');
|
||||
});
|
||||
|
||||
// ── #2646: ROADMAP checkbox fallback when no phases/ directory ─────────
|
||||
it('derives completed_count from ROADMAP [x] checkboxes when phases/ is absent', async () => {
|
||||
// Fresh fixture: NO phases/ directory at all, checkbox-driven ROADMAP.
|
||||
|
||||
@@ -43,7 +43,7 @@ import type { QueryHandler } from './utils.js';
|
||||
async function getModelAlias(agentType: string, projectDir: string): Promise<string> {
|
||||
const result = await resolveModel([agentType], projectDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
return (data.model as string) || 'sonnet';
|
||||
return typeof data.model === 'string' ? data.model : 'sonnet';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user