mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 05:02:15 +03:00
* 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 (7163081f5and 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.
110 lines
4.7 KiB
TypeScript
110 lines
4.7 KiB
TypeScript
/**
|
|
* Claude Code discovery aliases (`claude/<id>` mirror entries).
|
|
*
|
|
* Claude Code's gateway model discovery only lists models whose id begins with
|
|
* `claude` or `anthropic` — any other provider prefix (`kimi/…`, `gemini-cli/…`,
|
|
* combo names, etc.) is invisible to it even when the underlying model is fully
|
|
* routable through OmniRoute. To make every enabled model reachable from Claude
|
|
* Code without renaming anything in the real catalog, this module synthesizes a
|
|
* mirror entry for each eligible model:
|
|
*
|
|
* claude/<original-id> e.g. claude/kimi/kimi-k2.6
|
|
* claude/combo/<combo-name> for owned_by:"combo" entries
|
|
*
|
|
* The mirror entry keeps every field from the original (translated by the
|
|
* existing model-id handling once the request lands, same as `no-think/…` and
|
|
* the effort-variant aliases), only overriding `id`, `root` (back-pointer to the
|
|
* real id), and `display_name`. This mirrors the structure of
|
|
* `claudeEffortVariants.ts` and `noThinkingAlias.ts`: pure synthesis over the
|
|
* already key-filtered catalog list, no I/O, no mutation of the input array.
|
|
*
|
|
* Never aliased:
|
|
* - ids that already start with `claude` or `anthropic` (with or without a
|
|
* following `/`, case-insensitive) — would double-prefix or shadow the base id.
|
|
* - `no-think/…` aliases and reasoning-effort variants (`-low`/`-medium`/`-high`/
|
|
* `-xhigh` suffix) — v1 only mirrors base ids; effort/no-think discovery is a
|
|
* separate concern.
|
|
* - entries the caller's `isEnabled` predicate rejects.
|
|
*/
|
|
|
|
export const CC_DISCOVERY_PREFIX = "claude/";
|
|
export const CC_DISCOVERY_COMBO_PREFIX = "claude/combo/";
|
|
|
|
// Ids that already live under the claude/anthropic namespace — never re-mirror them.
|
|
const ALREADY_CLAUDE_RE = /^(?:claude|anthropic)(?:\/|$)/i;
|
|
// Ids that already carry a reasoning-effort suffix — v1 only mirrors base ids.
|
|
const CLAUDE_EFFORT_SUFFIX_RE = /-(?:xhigh|high|medium|low)$/i;
|
|
const NO_THINKING_PREFIX = "no-think/";
|
|
// Built-in `auto`/`auto/*` combos are synthesized by createBuiltinAutoCombo, NOT
|
|
// stored in the DB combos table — the request-path resolver (getComboByName) can't
|
|
// find them, so mirroring them would advertise a `claude/combo/auto/*` id that the
|
|
// request path rejects. Skip them; DB-defined combos (arbitrary names) still mirror.
|
|
const BUILTIN_AUTO_COMBO_RE = /^auto(?:\/|$)/;
|
|
|
|
interface CcDiscoveryCatalogEntry {
|
|
id?: unknown;
|
|
owned_by?: unknown;
|
|
name?: unknown;
|
|
root?: unknown;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* Append `claude/<id>` (or `claude/combo/<id>` for combos) discovery-mirror
|
|
* entries for every eligible model. Returns the original array reference
|
|
* unchanged when nothing is eligible (no allocation in the common case).
|
|
*
|
|
* `isEnabled` is caller-supplied so this module stays free of feature-flag /
|
|
* gate lookups — it only knows how to synthesize the mirror shape.
|
|
*/
|
|
/**
|
|
* Ids the mirror must never cover: already claude/anthropic (would double-prefix
|
|
* or shadow the base id), `no-think/…` aliases, and reasoning-effort variants —
|
|
* v1 mirrors base ids only.
|
|
*/
|
|
function isMirrorableId(id: string): boolean {
|
|
if (id.length === 0) return false;
|
|
if (ALREADY_CLAUDE_RE.test(id)) return false;
|
|
if (id.startsWith(NO_THINKING_PREFIX)) return false;
|
|
return !CLAUDE_EFFORT_SUFFIX_RE.test(id);
|
|
}
|
|
|
|
/** Strip a `<provider>/` prefix to get the bare model name, matching the convention in
|
|
* claudeEffortVariants.ts / noThinkingAlias.ts. */
|
|
function bareModelName(id: string): string {
|
|
const slash = id.lastIndexOf("/");
|
|
return slash >= 0 ? id.slice(slash + 1) : id;
|
|
}
|
|
|
|
export function appendCcDiscoveryAliases<T extends CcDiscoveryCatalogEntry>(
|
|
models: T[],
|
|
isEnabled: (entry: T) => boolean
|
|
): T[] {
|
|
if (!Array.isArray(models)) return models;
|
|
|
|
const aliases: T[] = [];
|
|
for (const model of models) {
|
|
const id = model.id;
|
|
if (typeof id !== "string" || !isMirrorableId(id)) continue;
|
|
if (!isEnabled(model)) continue;
|
|
|
|
const isCombo = model.owned_by === "combo";
|
|
// Skip built-in auto combos — advertised-but-unroutable (see the regex above).
|
|
if (isCombo && BUILTIN_AUTO_COMBO_RE.test(id)) continue;
|
|
const aliasId = isCombo ? `${CC_DISCOVERY_COMBO_PREFIX}${id}` : `${CC_DISCOVERY_PREFIX}${id}`;
|
|
const label = typeof model.name === "string" && model.name ? model.name : id;
|
|
|
|
aliases.push({
|
|
...model,
|
|
id: aliasId,
|
|
// Combo names may legally contain "/" (comboNameSchema allows it), so a combo's
|
|
// root must stay the full name verbatim — only real provider-qualified ids get
|
|
// the "/" stripped down to the bare model name.
|
|
root: isCombo ? id : bareModelName(id),
|
|
display_name: `${label} (OmniRoute)`,
|
|
} as T);
|
|
}
|
|
|
|
return aliases.length > 0 ? [...models, ...aliases] : models;
|
|
}
|