Files
OmniRoute/tests/unit/claude-effort-variants.test.ts
Will Gordon 4795825513 fix(sse): make Claude effort/no-think catalog variants dispatchable on every provider (#9006)
* fix(executors): route Claude-via-Vertex through native rawPredict with real streaming

Claude models on Vertex AI were being sent through the generic OpenAI-
compatible partner endpoint, which 404s/errors for Claude on at least
some projects. Route them through Vertex's native Anthropic Messages
API (publishers/anthropic/.../rawPredict) instead, stripping the
body-level model field rawPredict rejects and injecting the required
anthropic_version field.

rawPredict only ever returns a complete JSON body, never real SSE
framing, so streaming requests now get a genuine Anthropic-format SSE
stream synthesized from that JSON (message_start/content_block_*/
message_delta/message_stop), which the existing claude-to-openai
response translator already knows how to parse.

Also fixes two response-format resolution bugs that silently dropped
a custom model's DB-stored targetFormat override whenever the model
id also existed in the static provider registry (as claude-sonnet-4-6
and claude-opus-4-7 do under vertex): resolveModelOrError had its own
ad-hoc resolution that never consulted the override, and even once
fixed, executeChatWithBreaker discarded the correctly-resolved format
before handleChatCore's own resolution ran a second time.

* docs: add changelog fragment for #8909

* refactor(sse): extract shared Claude effort-model predicate

* fix(sse): strip Claude effort-suffix ids for any provider serving a real Claude model

* fix(sse): keep no-think and CC-discovery catalog variant roots unprefixed

* fix(dashboard): re-qualify no-think playground model ids correctly

* fix(sse): scope Vertex 404s to a per-model lockout via passthroughModels

* docs: add changelog fragment for the Claude catalog/dispatch fix

* fix(sse): align regex naming and changelog formatting

* fix(sse): clarify effort-variant strip comment and add cross-module drift guard

* fix(sse): disambiguate Vertex connection-wide vs per-model 403s

* docs: document Vertex 403 disambiguation in changelog fragment

* fix(sse): correlate reason and resource within the same ErrorInfo detail

* fix(sse): extract Vertex error classifier and rebaseline frozen file sizes

* test: register vertex-passthrough-model-lockout in stryker tap.testFiles

* fix(sse): reconciles rebase-onto-tip drift for 9006

Two categories of inherited base-branch breakage surfaced when
rebasing onto release/v3.8.50's latest tip, both confirmed unrelated
to this PR's own diff:

- check:file-size: base.ts and chat.ts drifted further past their
  frozen caps via already-merged commits (7163081f5 and others) that
  didn't rebaseline after growing them. Documented and bumped in
  file-size-baseline.json.
- chat-helpers.test.ts: two gpt-5.5 routing assertions predate #9275
  (fix(routing): bare model ids route to codex first), which
  deliberately made gpt-5.5 route to codex unconditionally, regardless
  of which other providers are active. Confirmed via #9275's own
  commit message and code comments this is intentional, not a
  regression; verified reproducible on the raw base tip alone, with
  no changes from this PR involved. Updated both assertions and their
  names to match the new, intentional default.

* ci: re-trigger checks after GitHub Actions incident (2026-08-07, resolved)

* ci: re-trigger checks (previous push event was dropped)

* fix(quality): rebaseline combo-routing-engine.test.ts own-comment growth

The ALL_ACCOUNTS_INACTIVE->ALL_TARGETS_SKIPPED fix (a32aed738) added explanatory comments (+7 lines), pushing the file past its frozen 3457 cap. CI's PR-mode check:file-size caught it; local check-file-size.mjs was not re-run after that specific commit.
2026-08-11 10:02:48 -03:00

247 lines
11 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
CLAUDE_EFFORT_VARIANT_LEVELS,
CLAUDE_XHIGH_EFFORT_LEVEL,
formatClaudeEffortLabel,
shouldExposeClaudeEffortVariants,
isKnownClaudeEffortBaseModel,
claudeEffortLevelsFor,
appendClaudeEffortVariants,
} from "../../open-sse/utils/claudeEffortVariants.ts";
import { shouldExposeNoThinkingAlias } from "../../open-sse/utils/noThinkingAlias.ts";
import { appendCcDiscoveryAliases } from "../../open-sse/utils/ccDiscoveryAliases.ts";
const mk = (id: string, extra: Record<string, unknown> = {}) => ({
id,
owned_by: id.split("/")[0],
name: id.split("/").pop(),
...extra,
});
// ── constants / labels ───────────────────────────────────────────────────────
test("advertises Low/Medium/High as the base effort levels", () => {
assert.deepEqual([...CLAUDE_EFFORT_VARIANT_LEVELS], ["low", "medium", "high"]);
assert.equal(CLAUDE_XHIGH_EFFORT_LEVEL, "xhigh");
});
test("formatClaudeEffortLabel matches the VS Code catalog casing", () => {
assert.equal(formatClaudeEffortLabel("low"), "Low");
assert.equal(formatClaudeEffortLabel("medium"), "Medium");
assert.equal(formatClaudeEffortLabel("high"), "High");
assert.equal(formatClaudeEffortLabel("xhigh"), "XHigh");
});
// ── shouldExposeClaudeEffortVariants ─────────────────────────────────────────
test("exposes variants for thinking-capable Claude base models", () => {
assert.equal(shouldExposeClaudeEffortVariants(mk("claude/claude-fable-5")), true);
assert.equal(shouldExposeClaudeEffortVariants(mk("claude/claude-opus-4-8")), true);
assert.equal(shouldExposeClaudeEffortVariants(mk("cc/claude-fable-5")), true);
});
test("adaptive-only models (Fable 5) still get effort variants despite rejecting disabled", () => {
// Regression guard: the no-thinking gate excludes rejectsThinkingDisabled models,
// but effort variants must NOT — Fable 5 is adaptive-only yet takes an effort.
assert.equal(shouldExposeClaudeEffortVariants(mk("claude/claude-fable-5")), true);
});
test("does not expose variants for non-Claude, combos, or non-thinking models", () => {
assert.equal(shouldExposeClaudeEffortVariants(mk("codex/gpt-5.5")), false);
assert.equal(shouldExposeClaudeEffortVariants({ id: "x", owned_by: "combo" }), false);
assert.equal(shouldExposeClaudeEffortVariants(mk("gemini-cli/gemini-3.1-pro-preview")), false);
});
test("never double-synthesizes: already-suffixed or no-think ids are skipped", () => {
assert.equal(shouldExposeClaudeEffortVariants(mk("claude/claude-fable-5-high")), false);
assert.equal(shouldExposeClaudeEffortVariants(mk("claude/claude-fable-5-xhigh")), false);
assert.equal(shouldExposeClaudeEffortVariants(mk("no-think/claude/claude-fable-5")), false);
});
test("non-string / empty / non-object ids never match", () => {
assert.equal(shouldExposeClaudeEffortVariants(undefined as never), false);
assert.equal(shouldExposeClaudeEffortVariants({ id: "" }), false);
assert.equal(shouldExposeClaudeEffortVariants({ id: 42 as never }), false);
});
// ── isKnownClaudeEffortBaseModel ─────────────────────────────────────────────
test("isKnownClaudeEffortBaseModel returns true for a real effort-capable Claude model", () => {
assert.equal(isKnownClaudeEffortBaseModel("claude-fable-5"), true);
});
test("isKnownClaudeEffortBaseModel returns false for a non-Claude model", () => {
assert.equal(isKnownClaudeEffortBaseModel("gpt-4o"), false);
});
test("isKnownClaudeEffortBaseModel returns false for an unregistered model id", () => {
assert.equal(isKnownClaudeEffortBaseModel("totally-unregistered-model-xyz"), false);
});
test("isKnownClaudeEffortBaseModel returns false for a non-Claude model that also supports thinking (SC-1)", () => {
// gpt-5.5 has supportsThinking:true in MODEL_SPECS (like 36+ other non-Claude models) —
// the /claude/i name check is the only thing excluding it, not the thinking flag alone.
assert.equal(isKnownClaudeEffortBaseModel("gpt-5.5"), false);
});
// ── claudeEffortLevelsFor ────────────────────────────────────────────────────
test("xHigh is added only for models that support it", () => {
assert.deepEqual(claudeEffortLevelsFor("claude", "claude-fable-5"), [
"low",
"medium",
"high",
"xhigh",
]);
assert.deepEqual(claudeEffortLevelsFor("claude", "claude-opus-4-8"), [
"low",
"medium",
"high",
"xhigh",
]);
// Opus 4.6 and Haiku 4.5 are flagged supportsXHighEffort:false in the registry.
assert.deepEqual(claudeEffortLevelsFor("claude", "claude-opus-4-6"), ["low", "medium", "high"]);
assert.deepEqual(claudeEffortLevelsFor("claude", "claude-haiku-4-5-20251001"), [
"low",
"medium",
"high",
]);
});
// ── appendClaudeEffortVariants ───────────────────────────────────────────────
test("appends effort variant ids + names for eligible models only", () => {
const out = appendClaudeEffortVariants([mk("claude/claude-fable-5"), mk("codex/gpt-5.5")]);
const ids = out.map((m) => m.id);
assert.deepEqual(ids, [
"claude/claude-fable-5",
"codex/gpt-5.5",
"claude/claude-fable-5-low",
"claude/claude-fable-5-medium",
"claude/claude-fable-5-high",
"claude/claude-fable-5-xhigh",
]);
const high = out.find((m) => m.id === "claude/claude-fable-5-high");
assert.equal(high?.name, "claude-fable-5 (High)");
// root stays unprefixed — the provider-scoped models route serves it verbatim.
assert.equal(high?.root, "claude-fable-5-high");
});
test("normalizes the provider prefix (cc → claude) when a canonical map is given", () => {
const out = appendClaudeEffortVariants([mk("cc/claude-fable-5")], { cc: "claude" });
const variantIds = out.map((m) => m.id).filter((id) => /-(low|medium|high|xhigh)$/.test(id));
assert.deepEqual(variantIds, [
"claude/claude-fable-5-low",
"claude/claude-fable-5-medium",
"claude/claude-fable-5-high",
"claude/claude-fable-5-xhigh",
]);
});
test("returns the original array reference when nothing is eligible", () => {
const input = [mk("codex/gpt-5.5"), mk("gemini-cli/gemini-3.1-pro-preview")];
const out = appendClaudeEffortVariants(input);
assert.equal(out, input);
});
test("never generates variants-of-variants when the list already contains effort ids", () => {
// The catalog calls this once, but even if suffixed ids are already present they
// must be skipped — no `claude/claude-fable-5-high-high` etc.
const withVariants = appendClaudeEffortVariants([mk("claude/claude-fable-5")]);
const again = appendClaudeEffortVariants(withVariants);
const doubleSuffixed = again
.map((m) => m.id)
.filter((id) => /-(low|medium|high|xhigh)-(low|medium|high|xhigh)$/.test(id));
assert.deepEqual(doubleSuffixed, []);
});
// ── cross-module drift guard: CLAUDE_EFFORT_SUFFIX_RE parity ────────────────
//
// `CLAUDE_EFFORT_SUFFIX_RE` (`/-(?:xhigh|high|medium|low)$/i`) is intentionally
// duplicated as a local, non-exported constant in THREE sibling modules: this
// file's module (claudeEffortVariants.ts), noThinkingAlias.ts, and
// ccDiscoveryAliases.ts. A cross-import consolidation of that constant was
// already proposed and explicitly reverted earlier in this project's review
// cycle — the plan deliberately kept local duplication for these three
// sibling modules (accepted by the Reduction Analyst). This test does NOT
// argue for reversing that decision and must NOT be read as one. Its only
// purpose is a behavioral drift guard: if a future edit changes the effort
// levels recognized by one copy (e.g. adds a new level, or narrows/widens the
// suffix pattern) without updating the other two, this test fails instead of
// the three modules silently disagreeing about which ids carry an
// effort-level suffix.
test("CLAUDE_EFFORT_SUFFIX_RE stays in sync across claudeEffortVariants/noThinkingAlias/ccDiscoveryAliases (drift guard — do not consolidate, see comment above)", () => {
// Real, registered, thinking-capable Claude model that does NOT reject
// `thinking:{type:"disabled"}` — satisfies every module's registry-lookup
// gate identically, so any behavioral difference below is attributable only
// to the effort-suffix regex, not to some other per-module gating rule.
const BASE = "claude-opus-4-5";
const EFFORT_SUFFIXES = ["-low", "-medium", "-high", "-xhigh", "-XHIGH"];
// Trailing tokens that look suffix-like but must NOT match the regex
// (anchored to exactly low/medium/high/xhigh at end-of-string).
const NON_MATCHING_SUFFIXES = ["-max", "-highest"];
for (const suffix of EFFORT_SUFFIXES) {
const qualifiedId = `claude/${BASE}${suffix}`;
assert.equal(
shouldExposeClaudeEffortVariants(mk(qualifiedId)),
false,
`claudeEffortVariants must exclude ${qualifiedId}`
);
assert.equal(
shouldExposeNoThinkingAlias(mk(qualifiedId)),
false,
`noThinkingAlias must exclude ${qualifiedId}`
);
const mirrored = appendCcDiscoveryAliases(
[{ id: `cc/${BASE}${suffix}`, owned_by: "cc" }],
() => true
);
assert.equal(
mirrored.length,
1,
`ccDiscoveryAliases must never mirror an effort-suffixed id (${suffix})`
);
}
// Control: the identical base model WITHOUT a suffix must pass all three
// gates — proves the suffix itself (not something else about the id) is
// what excluded the cases above.
assert.equal(shouldExposeClaudeEffortVariants(mk(`claude/${BASE}`)), true);
assert.equal(shouldExposeNoThinkingAlias(mk(`claude/${BASE}`)), true);
const baseMirror = appendCcDiscoveryAliases([{ id: `cc/${BASE}`, owned_by: "cc" }], () => true);
assert.equal(baseMirror.length, 2, "unsuffixed id must still be mirrored");
// Suffix-like-but-non-matching trailing tokens must NOT be excluded by the
// regex. This isolates the regex's specificity (exactly xhigh/high/medium/low)
// from the models-registry prefix-matching gate: `getCanonicalModelSpecId`
// resolves "claude-opus-4-5-max" back to the "claude-opus-4-5" spec via its
// prefix-match fallback, so `shouldExposeClaudeEffortVariants` /
// `shouldExposeNoThinkingAlias` still pass their registry-lookup gate here —
// any exclusion left could only come from the suffix regex, and there is none.
for (const suffix of NON_MATCHING_SUFFIXES) {
const qualifiedId = `claude/${BASE}${suffix}`;
assert.equal(
shouldExposeClaudeEffortVariants(mk(qualifiedId)),
true,
`claudeEffortVariants must not treat "${suffix}" as an effort suffix`
);
assert.equal(
shouldExposeNoThinkingAlias(mk(qualifiedId)),
true,
`noThinkingAlias must not treat "${suffix}" as an effort suffix`
);
const mirrored = appendCcDiscoveryAliases(
[{ id: `cc/${BASE}${suffix}`, owned_by: "cc" }],
() => true
);
assert.equal(
mirrored.length,
2,
`ccDiscoveryAliases must still mirror a non-effort-suffix-looking id ("${suffix}")`
);
}
});