Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/resources/extensions/gsd/complexity-classifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ const UNIT_TYPE_TIERS: Record<string, ComplexityTier> = {
// Tier 1 — Light: compact verification turns
"run-uat": "light",

// Subagent recon/mapping types: read-only filesystem scans that require no
// substantive reasoning. Only add entries here for verifiably read-only scout
// agents. Do NOT add security-auditor, code-reviewer, or analysis agents here
// -- those belong at standard or heavy (#subagent-tier-policy).
"subagent/codebase-mapper": "light", // read-only FS scan; no deep reasoning required
"subagent/scout": "light", // broad recon; analogous to codebase-mapper

// Tier 2 — Standard: research, routine discussion, slice completion
// complete-slice can carry large inlined context; avoid routing it to the
// cheapest "light" model by default (#4520).
Expand Down
3 changes: 3 additions & 0 deletions src/resources/extensions/gsd/model-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export const MODEL_CAPABILITY_TIER: Record<string, ComplexityTier> = {
"claude-sonnet-4-6": "standard",
"claude-sonnet-4-5-20250514": "standard",
"claude-3-5-sonnet-latest": "standard",
"claude-sonnet-5": "standard", // GA on GitHub Copilot, Anthropic, Vertex, Bedrock
"gpt-4o": "standard",
"gpt-4.1": "standard",
"gpt-5.1-codex-max": "standard",
Expand Down Expand Up @@ -130,6 +131,7 @@ const MODEL_COST_PER_1K_INPUT: Record<string, number> = {
"claude-3-5-haiku-latest": 0.0008,
"claude-sonnet-4-6": 0.003,
"claude-sonnet-4-5-20250514": 0.003,
"claude-sonnet-5": 0.003, // $3.00/M input; matches Sonnet 4.x pricing
"claude-opus-4-6": 0.005,
"claude-opus-4-7": 0.005,
"claude-opus-4-8": 0.005,
Expand Down Expand Up @@ -176,6 +178,7 @@ export const MODEL_CAPABILITY_PROFILES: Record<string, ModelCapabilities> = {
"claude-fable-5": { coding: 97, debugging: 92, research: 87, reasoning: 97, speed: 30, longContext: 85, instruction: 92 },
"claude-sonnet-4-6": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
"claude-sonnet-4-5-20250514": { coding: 85, debugging: 80, research: 75, reasoning: 80, speed: 60, longContext: 75, instruction: 85 },
"claude-sonnet-5": { coding: 90, debugging: 85, research: 80, reasoning: 87, speed: 55, longContext: 80, instruction: 88 },
"claude-3-5-sonnet-latest": { coding: 82, debugging: 78, research: 72, reasoning: 78, speed: 62, longContext: 70, instruction: 82 },
"claude-haiku-4-5": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
"claude-3-5-haiku-latest": { coding: 60, debugging: 50, research: 45, reasoning: 50, speed: 95, longContext: 50, instruction: 75 },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,24 @@ describe("ClassificationResult taskMetadata", () => {
assert.equal(typeof extractTaskMetadata, "function", "extractTaskMetadata should be a callable function");
});
});

// --- subagent recon tier regression ---
// Before this fix, all subagent/* unit types fell through to the
// `UNIT_TYPE_TIERS[unitType] ?? "standard"` default, causing codebase-mapper and
// scout to dispatch to claude-sonnet instead of claude-haiku.

test("subagent/codebase-mapper classifies as light", () => {
const result = classifyUnitComplexity("subagent/codebase-mapper", "M001", "/tmp/fake");
assert.equal(result.tier, "light", "codebase-mapper is read-only FS scanning - should be light tier");
});

test("subagent/scout classifies as light", () => {
const result = classifyUnitComplexity("subagent/scout", "M001", "/tmp/fake");
assert.equal(result.tier, "light", "scout is read-only recon - should be light tier");
});

test("subagent/security-auditor is NOT light (stays at standard default)", () => {
// Deliberate: analysis agents must not be silently downgraded to haiku.
const result = classifyUnitComplexity("subagent/security-auditor", "M001", "/tmp/fake");
assert.notEqual(result.tier, "light", "security-auditor requires reasoning - must not be light");
});
51 changes: 51 additions & 0 deletions src/resources/extensions/gsd/tests/model-router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
scoreEligibleModels,
getEligibleModels,
MODEL_CAPABILITY_PROFILES,
MODEL_CAPABILITY_TIER,
} from "../model-router.js";
import type { DynamicRoutingConfig, RoutingDecision, ModelCapabilities } from "../model-router.js";
import type { ClassificationResult } from "../complexity-classifier.js";
Expand Down Expand Up @@ -1324,3 +1325,53 @@ describe("getModelTier unknown default", () => {
assert.equal(heavyModels.length, 0, "Unknown model should NOT be in heavy tier");
});
});

// --- claude-sonnet-5 catalog regression (v1.12.0 gap) ---
// Discovered during Edelman Studio M010 model-routing config: claude-sonnet-5 was
// absent from MODEL_CAPABILITY_TIER, causing isKnownModel() to return false and
// the #2192 routing-bypass path to activate for any phase configured with sonnet-5.

test("claude-sonnet-5 is classified as standard tier in MODEL_CAPABILITY_TIER", () => {
assert.equal(
MODEL_CAPABILITY_TIER["claude-sonnet-5"],
"standard",
"claude-sonnet-5 must be in the tier map so isKnownModel() returns true",
);
});

test("claude-sonnet-5 as ceiling: standard task is NOT bypassed - routing applies normally", () => {
// With sonnet-5 now a known model, resolveModelForComplexity must NOT hit the
// #2192 bail-out. A standard task with a sonnet-5 ceiling should stay on sonnet-5.
const config = {
...defaultRoutingConfig(),
enabled: true,
tier_models: { light: "claude-haiku-4-5", standard: "claude-sonnet-5", heavy: "claude-opus-4-8" },
};
const result = resolveModelForComplexity(
{ tier: "standard", reason: "test", downgraded: false },
{ primary: "claude-sonnet-5", fallbacks: [] },
config,
["claude-haiku-4-5", "claude-sonnet-5", "claude-opus-4-8"],
);
assert.equal(result.modelId, "claude-sonnet-5", "standard task with sonnet-5 ceiling should use sonnet-5");
assert.equal(result.wasDowngraded, false, "should not be downgraded when task tier matches ceiling");
assert.ok(!result.reason?.includes("not in the known tier map"), "must not hit #2192 bypass reason");
});

test("claude-sonnet-5 as ceiling: light task IS downgraded to haiku - routing not bypassed", () => {
// Confirms that with sonnet-5 as a known model, dynamic routing correctly
// downgrades light tasks to tier_models.light (haiku) instead of bypassing.
const config = {
...defaultRoutingConfig(),
enabled: true,
tier_models: { light: "claude-haiku-4-5", standard: "claude-sonnet-5", heavy: "claude-opus-4-8" },
};
const result = resolveModelForComplexity(
{ tier: "light", reason: "test", downgraded: false },
{ primary: "claude-sonnet-5", fallbacks: [] },
config,
["claude-haiku-4-5", "claude-sonnet-5", "claude-opus-4-8"],
);
assert.equal(result.modelId, "claude-haiku-4-5", "light task with sonnet-5 ceiling must downgrade to haiku");
assert.equal(result.wasDowngraded, true);
});
Loading