Files
OmniRoute/tests/unit/claude-effort-variants.test.ts
Mrinal Joshi d82ca30253 feat(models): advertise Claude reasoning-effort variants in /v1/models (#7497)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168)

* fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179)

* fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216)

* fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220)

* fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225)

* test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341)

main's copy of this test still does git I/O inside a unit test:

    const baseSrc = git(['show', 'origin/main:' + FILE]);

Runners check out a shallow single ref, so origin/main does not resolve and the
test dies with 'fatal: invalid object name origin/main'. Every PR into main
fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336
and #7337, six PRs red on a defect none of them introduced. #7313 has no other
red at all.

release/v3.8.49 already carries a fix (2e42b8efc, #7174: try/catch, fetch
origin/main on demand, t.skip() when unreachable), but it only reaches main at
release time — so main stays broken for the whole cycle. Cherry-picking it would
also import a new problem: PR Test Policy classifies t.skip() as a silenced
assertion, which we watched it correctly catch on #7300 today.

This is the hermetic version instead (ported from #7327, which does the same for
the release branch): read the file straight off disk, compare against an empty
base so baseTaut/baseExtTaut are 0 — the strictest possible comparison point —
and call evaluateMasking() directly. No git ref, no fetch, no skip, nothing the
runner's checkout depth can break.

The #6634 regression stays covered: the guard's logic lives in
SELF_TEST_FIXTURE_RE (check-test-masking.mjs:337), not in the test. Proven both
ways on main before committing — neutralise SELF_TEST_FIXTURE_RE to /$^/ and
the test FAILS; restore it and it passes 2/2, with check-test-masking.mjs left
byte-identical.

Co-authored-by: growab <nekron@icloud.com>

* chore(quality): tighten main's coverage baseline to the CI's real numbers (#7347)

main's ratchet had been failing --require-tighten on every PR: 11 metrics
improved but the baseline was never tightened. Same class as the #6634
selfref guard — an infra fix that lands only on the release branch leaves
main red for the whole cycle, and every PR into main pays for it.

Values are the merged-coverage numbers from a run on main itself (a local
run measures ~68% vs CI's ~80%; the baseline's own note warns about that
gap). Only the 11 coverage values change — gitleaks and semgrepFindings
keep main's own state.

No changelog fragment: #7326 carries it on release/v3.8.49, and a second
one here would double the entry at release time.

* feat(models): advertise Claude reasoning-effort variants in /v1/models

Effort-capable Claude models (Fable 5, Opus 4.8, Sonnet 5, ...) steer
reasoning via reasoning_effort, and the gateway already routes suffixed ids
like claude/claude-fable-5-high back to the base model + reasoning_effort
(applyClaudeEffortVariant / splitClaudeEffortSuffix). Rich clients such as
VS Code render this as a reasoningEffort config slider, but catalog-only
clients (OpenCode, plain OpenAI-SDK pickers) can only choose a model by id,
so an effort level was unreachable: they saw claude/claude-fable-5 but never
its Low/Medium/High/XHigh options.

Synthesize those variants in the catalog the same way no-thinking variants
are exposed (appendNoThinkingVariants): appendClaudeEffortVariants derives
claude/<model>-{low,medium,high[,xhigh]} from the already key-filtered list,
so a variant only appears when its real model is permitted. Levels come from
the single source of truth (supportsXHighEffort): xhigh only for models that
support it (not Opus 4.6/4.5 or Haiku). Purely additive to catalog
visibility; routing is unchanged.

- open-sse/utils/claudeEffortVariants.ts: new capability-gated synthesizer
- src/app/api/v1/models/catalog.ts: wire it in before the no-thinking pass
- tests/unit/claude-effort-variants.test.ts: 12 cases (gating, levels,
  prefix normalization, no variants-of-variants)

* refactor(models): make shouldExposeClaudeEffortVariants a type guard

Address PR review feedback (gemini-code-assist): turn the predicate into a
type guard `model is CatalogModelEntry & { id: string }` so TypeScript narrows
`model.id` to string after the check, removing the explicit `as string` cast
in appendClaudeEffortVariants. No behavior change; 12/12 unit tests still pass.

* fix(catalog): keep effort-variant root unprefixed (provider-scoped models route serves root verbatim)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: growab <nekron@icloud.com>
Co-authored-by: Mrinal Joshi <mri-jo@users.noreply.github.com>
2026-07-18 21:19:27 -03:00

136 lines
5.9 KiB
TypeScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
CLAUDE_EFFORT_VARIANT_LEVELS,
CLAUDE_XHIGH_EFFORT_LEVEL,
formatClaudeEffortLabel,
shouldExposeClaudeEffortVariants,
claudeEffortLevelsFor,
appendClaudeEffortVariants,
} from "../../open-sse/utils/claudeEffortVariants.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);
});
// ── 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, []);
});