mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 15:52:52 +03:00
feat(api): no-thinking gateway model IDs (FCC port, Fase 8.1) (#4145)
No-thinking gateway model IDs (FCC port, Fase 8.1): synthetic claude-3-omniroute-no-thinking/<provider>/<model> catalog id that resolves to the real model with reasoning suppressed. Integrado em release/v3.8.29.
This commit is contained in:
committed by
GitHub
parent
3beba2d77f
commit
c5c612e80d
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
|
||||
"_rebaseline_2026_06_18_8_1_no_thinking_alias": "Fase 8.1 own growth: catalog.ts 1435->1440 (+5 = appendNoThinkingVariants(finalModels) call + comment at the existing finalModels chokepoint) and chat.ts 1458->1471 (+13 = applyNoThinkingAlias(body) call + comment right after body.model is read, before model resolution). All real logic lives in the new open-sse/utils/noThinkingAlias.ts (<cap); both edits are thin wiring of tested helpers at the single correct integration point in each file. Not extractable.",
|
||||
"_rebaseline_2026_06_17_4098_wafer": "PR #4098 own growth: providers.ts 3159->3169 (+10 = Wafer AI catalog entry, a single Zod-validated provider record in the providers map — pure data, standard per-provider addition; bumps catalog 227->228). Cohesive catalog growth; not extractable.",
|
||||
"_rebaseline_2026_06_17_4096_field_downgrade": "PR #4096 own growth: base.ts 1292->1334 (+42 = generic 400 field-downgrade retry at the executor fetch loop — on an upstream 400 that names an unsupported field, strip it via providerFieldStrips and retry once, plus Groq field stripping wiring). The strip table lives in the new open-sse/config/providerFieldStrips.ts (<cap). Cohesive resilience logic at the existing fetch chokepoint; not extractable.",
|
||||
"_rebaseline_2026_06_17_4071_vision_routing": "PR #4071 own growth: combo.ts 5283->5289 (+6 = vision-aware routing fix in getTargetCompatibilityFailures — image requests now require supportsVision===true, treating null/unknown as incompatible, with an explanatory comment block; plus exporting filterTargetsByRequestCompatibility for the regression test). The accompanying capability heuristic lives in src/lib/modelCapabilities.ts (419 LOC, <cap). Cohesive bug fix at the existing compatibility-filter chokepoint; not extractable.",
|
||||
@@ -109,7 +110,7 @@
|
||||
"src/app/api/providers/[id]/models/route.ts": 2512,
|
||||
"src/app/api/providers/[id]/test/route.ts": 842,
|
||||
"src/app/api/usage/analytics/route.ts": 941,
|
||||
"src/app/api/v1/models/catalog.ts": 1435,
|
||||
"src/app/api/v1/models/catalog.ts": 1440,
|
||||
"src/lib/cloudflaredTunnel.ts": 934,
|
||||
"src/lib/db/apiKeys.ts": 1661,
|
||||
"src/lib/db/core.ts": 1820,
|
||||
@@ -136,7 +137,7 @@
|
||||
"src/shared/constants/sidebarVisibility.ts": 1100,
|
||||
"src/shared/services/cliRuntime.ts": 1090,
|
||||
"src/shared/validation/schemas.ts": 2523,
|
||||
"src/sse/handlers/chat.ts": 1458,
|
||||
"src/sse/handlers/chat.ts": 1471,
|
||||
"src/sse/services/auth.ts": 2219
|
||||
},
|
||||
"_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.",
|
||||
|
||||
@@ -131,6 +131,16 @@ Authorization: Bearer your-api-key
|
||||
→ Returns all chat, embedding, and image models + combos in OpenAI format
|
||||
```
|
||||
|
||||
### No-thinking model variants
|
||||
|
||||
For thinking-capable Claude models, `/v1/models` also advertises a **no-thinking** variant whose id is prefixed with `claude-3-omniroute-no-thinking/`:
|
||||
|
||||
```
|
||||
claude-3-omniroute-no-thinking/<provider>/<model>
|
||||
```
|
||||
|
||||
Selecting this id (e.g. in a Claude Code config that always attaches a `thinking` block) resolves back to the real `<provider>/<model>` with reasoning suppressed — `thinking:{type:"disabled"}` on the `/v1/messages` path, or the `reasoning`/`reasoning_effort` fields dropped on the `/v1/chat/completions` path. The variant is only listed for Claude-family models that support thinking **and** honor `disabled` (so e.g. adaptive-only models that reject `disabled` are excluded). Operators can force the variant on or off per model via `ModelSpec.noThinkingAlias`.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility Endpoints
|
||||
|
||||
128
open-sse/utils/noThinkingAlias.ts
Normal file
128
open-sse/utils/noThinkingAlias.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* No-thinking gateway model IDs (free-claude-code port, Fase 8.1).
|
||||
*
|
||||
* Some clients — most notably Claude Code — always attach a `thinking` block to
|
||||
* certain Claude models and offer no UI to turn it off. To let an operator force a
|
||||
* thinking-capable model into a no-thinking mode purely by *model selection*, the
|
||||
* gateway exposes a synthetic catalog id:
|
||||
*
|
||||
* claude-3-omniroute-no-thinking/<provider>/<model>
|
||||
*
|
||||
* When such an id arrives on a request we strip the prefix back to the real
|
||||
* `<provider>/<model>` and suppress reasoning (`thinking:{type:"disabled"}` for the
|
||||
* Claude/Messages path; drop `reasoning`/`reasoning_effort` for the OpenAI path).
|
||||
* The existing `normalizeThinkingForModel()` still runs downstream, so models that
|
||||
* reject `disabled` are handled exactly as before.
|
||||
*
|
||||
* Catalog visibility is gated (see `shouldExposeNoThinkingAlias`): we only advertise
|
||||
* the variant for Claude-family models that actually support thinking AND honor
|
||||
* `disabled` — advertising it for a model that ignores suppression would be a lie.
|
||||
* An explicit registry override (`ModelSpec.noThinkingAlias`) wins over the default.
|
||||
*/
|
||||
import { getModelSpec } from "@/shared/constants/modelSpecs";
|
||||
|
||||
export const NO_THINKING_PREFIX = "claude-3-omniroute-no-thinking/";
|
||||
|
||||
/** True when `modelId` carries the no-thinking gateway prefix. */
|
||||
export function isNoThinkingAlias(modelId: unknown): modelId is string {
|
||||
return typeof modelId === "string" && modelId.startsWith(NO_THINKING_PREFIX);
|
||||
}
|
||||
|
||||
/** Remove the gateway prefix, returning the real `<provider>/<model>` (plain ids pass through). */
|
||||
export function stripNoThinkingAlias(modelId: string): string {
|
||||
return isNoThinkingAlias(modelId) ? modelId.slice(NO_THINKING_PREFIX.length) : modelId;
|
||||
}
|
||||
|
||||
/** Wrap a real qualified model id in the no-thinking gateway prefix. */
|
||||
export function toNoThinkingAlias(qualifiedModelId: string): string {
|
||||
return `${NO_THINKING_PREFIX}${qualifiedModelId}`;
|
||||
}
|
||||
|
||||
interface ApplyResult {
|
||||
applied: boolean;
|
||||
realModel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request-side hook: if `body.model` is a no-thinking alias, rewrite it to the real
|
||||
* model and suppress reasoning in place. No-op (and body untouched) otherwise.
|
||||
*/
|
||||
export function applyNoThinkingAlias(
|
||||
body: Record<string, unknown> | null | undefined,
|
||||
opts: { claudeFormat?: boolean } = {}
|
||||
): ApplyResult {
|
||||
if (!body || typeof body !== "object") return { applied: false };
|
||||
const model = body.model;
|
||||
if (!isNoThinkingAlias(model)) return { applied: false };
|
||||
|
||||
const realModel = stripNoThinkingAlias(model);
|
||||
if (!realModel) return { applied: false }; // malformed: nothing after the prefix
|
||||
|
||||
body.model = realModel;
|
||||
if (opts.claudeFormat === true) {
|
||||
body.thinking = { type: "disabled" };
|
||||
}
|
||||
delete body.reasoning_effort;
|
||||
delete body.reasoning;
|
||||
return { applied: true, realModel };
|
||||
}
|
||||
|
||||
interface CatalogModelEntry {
|
||||
id?: unknown;
|
||||
owned_by?: unknown;
|
||||
name?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Strip a `<provider>/` prefix to get the bare model name for spec lookup. */
|
||||
function bareModelName(id: string): string {
|
||||
const slash = id.lastIndexOf("/");
|
||||
return slash >= 0 ? id.slice(slash + 1) : id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the catalog should advertise a no-thinking variant for this entry.
|
||||
*
|
||||
* Default rule: Claude-family model that supports thinking and does NOT reject
|
||||
* `thinking:{type:"disabled"}`. An explicit `ModelSpec.noThinkingAlias` boolean
|
||||
* overrides the default in either direction (operator opt-in / opt-out).
|
||||
*/
|
||||
export function shouldExposeNoThinkingAlias(model: CatalogModelEntry): boolean {
|
||||
if (!model || typeof model !== "object") return false;
|
||||
const id = model.id;
|
||||
if (typeof id !== "string" || id.length === 0) return false;
|
||||
if (model.owned_by === "combo") return false; // combos are virtual
|
||||
if (isNoThinkingAlias(id)) return false; // never double-alias
|
||||
|
||||
const name = bareModelName(id);
|
||||
const spec = getModelSpec(name);
|
||||
if (!spec) return false;
|
||||
|
||||
if (spec.noThinkingAlias === true) return true;
|
||||
if (spec.noThinkingAlias === false) return false;
|
||||
|
||||
return (
|
||||
spec.supportsThinking === true &&
|
||||
spec.rejectsThinkingDisabled !== true &&
|
||||
/claude/i.test(name)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a no-thinking variant for every eligible model. Returns the original array
|
||||
* reference unchanged when nothing is eligible (no allocation in the common case).
|
||||
*/
|
||||
export function appendNoThinkingVariants<T extends CatalogModelEntry>(models: T[]): T[] {
|
||||
if (!Array.isArray(models)) return models;
|
||||
const variants: T[] = [];
|
||||
for (const model of models) {
|
||||
if (!shouldExposeNoThinkingAlias(model)) continue;
|
||||
const aliasId = toNoThinkingAlias(model.id as string);
|
||||
const variant: T = { ...model, id: aliasId, root: aliasId };
|
||||
if (typeof model.name === "string" && model.name) {
|
||||
variant.name = `${model.name} (no thinking)`;
|
||||
}
|
||||
variants.push(variant);
|
||||
}
|
||||
return variants.length > 0 ? [...models, ...variants] : models;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
getProviderNodes,
|
||||
getModelIsHidden,
|
||||
} from "@/lib/localDb";
|
||||
import { appendNoThinkingVariants } from "@omniroute/open-sse/utils/noThinkingAlias";
|
||||
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry";
|
||||
import { getAllImageModels } from "@omniroute/open-sse/config/imageRegistry";
|
||||
import { getAllRerankModels } from "@omniroute/open-sse/config/rerankRegistry";
|
||||
@@ -1354,6 +1355,10 @@ export async function getUnifiedModelsResponse(
|
||||
}
|
||||
}
|
||||
|
||||
// Advertise no-thinking gateway variants (Fase 8.1). Derived from the already
|
||||
// key-filtered list, so a variant only appears when its real model is permitted.
|
||||
finalModels = appendNoThinkingVariants(finalModels);
|
||||
|
||||
const getDefaultContextFallback = (model: any): number | undefined => {
|
||||
if (typeof model.context_length === "number") return undefined;
|
||||
if (model.owned_by === "combo") return undefined;
|
||||
|
||||
@@ -19,6 +19,11 @@ export interface ModelSpec {
|
||||
// (upstream returns 400). Used to normalize the request when a combo/route substitutes
|
||||
// this model after the client already chose `disabled`. See issue #3554.
|
||||
rejectsThinkingDisabled?: boolean;
|
||||
// Explicit operator override for the no-thinking gateway alias (Fase 8.1). When unset,
|
||||
// the catalog auto-advertises a `claude-3-omniroute-no-thinking/…` variant for
|
||||
// Claude-family thinking-capable models that honor `disabled`. Set `true` to force the
|
||||
// variant on for any other model, or `false` to suppress it. See open-sse/utils/noThinkingAlias.ts.
|
||||
noThinkingAlias?: boolean;
|
||||
}
|
||||
|
||||
const BEDROCK_CLAUDE_ALIASES = (...modelIds: string[]) => [
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "@omniroute/open-sse/services/accountFallback.ts";
|
||||
import { getModelInfo, getComboForModel } from "../services/model";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import { applyNoThinkingAlias } from "@omniroute/open-sse/utils/noThinkingAlias.ts";
|
||||
import { handleComboChat } from "@omniroute/open-sse/services/combo.ts";
|
||||
import { resolveComboConfig } from "@omniroute/open-sse/services/comboConfig.ts";
|
||||
import { injectHandoffIntoBody } from "@omniroute/open-sse/services/contextHandoff.ts";
|
||||
@@ -226,6 +227,18 @@ export async function handleChat(request: any, clientRawRequest: any = null) {
|
||||
|
||||
// Log request endpoint and model
|
||||
const url = new URL(request.url);
|
||||
|
||||
// No-thinking gateway alias (Fase 8.1): `claude-3-omniroute-no-thinking/<provider>/<model>`
|
||||
// resolves back to the real model with reasoning suppressed in place, before any
|
||||
// model resolution / combo routing sees it. Claude/Messages path forces
|
||||
// `thinking:{type:"disabled"}`; OpenAI path drops the reasoning fields.
|
||||
const noThinking = applyNoThinkingAlias(body, {
|
||||
claudeFormat: url.pathname.includes("/messages"),
|
||||
});
|
||||
if (noThinking.applied) {
|
||||
log.debug("NO_THINKING", `Resolved no-thinking alias → ${noThinking.realModel}`);
|
||||
}
|
||||
|
||||
let modelStr = body.model;
|
||||
|
||||
// Count messages (support both messages[] and input[] formats)
|
||||
|
||||
134
tests/unit/no-thinking-alias.test.ts
Normal file
134
tests/unit/no-thinking-alias.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import {
|
||||
NO_THINKING_PREFIX,
|
||||
isNoThinkingAlias,
|
||||
stripNoThinkingAlias,
|
||||
toNoThinkingAlias,
|
||||
shouldExposeNoThinkingAlias,
|
||||
appendNoThinkingVariants,
|
||||
applyNoThinkingAlias,
|
||||
} from "../../open-sse/utils/noThinkingAlias.ts";
|
||||
|
||||
// ── prefix predicates ────────────────────────────────────────────────────────
|
||||
|
||||
test("NO_THINKING_PREFIX is the documented gateway prefix", () => {
|
||||
assert.equal(NO_THINKING_PREFIX, "claude-3-omniroute-no-thinking/");
|
||||
});
|
||||
|
||||
test("isNoThinkingAlias detects the prefix only", () => {
|
||||
assert.equal(isNoThinkingAlias("claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5"), true);
|
||||
assert.equal(isNoThinkingAlias("anthropic/claude-opus-4-5"), false);
|
||||
assert.equal(isNoThinkingAlias("claude-opus-4-5"), false);
|
||||
// non-strings never match
|
||||
assert.equal(isNoThinkingAlias(undefined as unknown as string), false);
|
||||
assert.equal(isNoThinkingAlias(123 as unknown as string), false);
|
||||
});
|
||||
|
||||
test("stripNoThinkingAlias unwraps the prefix and passes plain ids through", () => {
|
||||
assert.equal(
|
||||
stripNoThinkingAlias("claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5"),
|
||||
"anthropic/claude-opus-4-5"
|
||||
);
|
||||
assert.equal(stripNoThinkingAlias("claude-opus-4-5"), "claude-opus-4-5");
|
||||
});
|
||||
|
||||
test("toNoThinkingAlias round-trips with stripNoThinkingAlias", () => {
|
||||
const real = "anthropic/claude-sonnet-4-6";
|
||||
const alias = toNoThinkingAlias(real);
|
||||
assert.equal(alias, "claude-3-omniroute-no-thinking/anthropic/claude-sonnet-4-6");
|
||||
assert.equal(isNoThinkingAlias(alias), true);
|
||||
assert.equal(stripNoThinkingAlias(alias), real);
|
||||
});
|
||||
|
||||
// ── request-side suppression ─────────────────────────────────────────────────
|
||||
|
||||
test("applyNoThinkingAlias rewrites the model and disables thinking (Claude format)", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
model: "claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5",
|
||||
thinking: { type: "enabled", budget_tokens: 8000 },
|
||||
reasoning_effort: "high",
|
||||
messages: [],
|
||||
};
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: true });
|
||||
assert.equal(res.applied, true);
|
||||
assert.equal(res.realModel, "anthropic/claude-opus-4-5");
|
||||
assert.equal(body.model, "anthropic/claude-opus-4-5");
|
||||
assert.deepEqual(body.thinking, { type: "disabled" });
|
||||
assert.ok(!("reasoning_effort" in body), "reasoning_effort must be stripped");
|
||||
});
|
||||
|
||||
test("applyNoThinkingAlias strips reasoning fields without a thinking block (OpenAI format)", () => {
|
||||
const body: Record<string, unknown> = {
|
||||
model: "claude-3-omniroute-no-thinking/openai/gpt-5.4",
|
||||
reasoning_effort: "high",
|
||||
reasoning: { effort: "high" },
|
||||
messages: [],
|
||||
};
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: false });
|
||||
assert.equal(res.applied, true);
|
||||
assert.equal(body.model, "openai/gpt-5.4");
|
||||
assert.ok(!("thinking" in body), "no Claude thinking block on an OpenAI body");
|
||||
assert.ok(!("reasoning_effort" in body), "reasoning_effort must be stripped");
|
||||
assert.ok(!("reasoning" in body), "reasoning must be stripped");
|
||||
});
|
||||
|
||||
test("applyNoThinkingAlias is a no-op for plain models", () => {
|
||||
const body: Record<string, unknown> = { model: "anthropic/claude-opus-4-5", thinking: { type: "enabled" } };
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: true });
|
||||
assert.equal(res.applied, false);
|
||||
assert.equal(body.model, "anthropic/claude-opus-4-5");
|
||||
assert.deepEqual(body.thinking, { type: "enabled" }, "thinking is left untouched when not an alias");
|
||||
});
|
||||
|
||||
test("applyNoThinkingAlias ignores a malformed prefix-only model", () => {
|
||||
const body: Record<string, unknown> = { model: "claude-3-omniroute-no-thinking/" };
|
||||
const res = applyNoThinkingAlias(body, { claudeFormat: true });
|
||||
assert.equal(res.applied, false);
|
||||
assert.equal(body.model, "claude-3-omniroute-no-thinking/", "left untouched when nothing follows the prefix");
|
||||
});
|
||||
|
||||
// ── catalog gating ───────────────────────────────────────────────────────────
|
||||
|
||||
const entry = (id: string, owned_by = "anthropic") => ({ id, object: "model", owned_by });
|
||||
|
||||
test("shouldExposeNoThinkingAlias accepts a Claude reasoning model that honors disabled", () => {
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("claude-opus-4-5")), true);
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("anthropic/claude-sonnet-4-6")), true);
|
||||
});
|
||||
|
||||
test("shouldExposeNoThinkingAlias rejects models where suppression is meaningless", () => {
|
||||
// gpt-4o does not support thinking
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("gpt-4o", "openai")), false);
|
||||
// fable-5 rejects thinking.type:disabled — a no-thinking variant would be a lie
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("claude-fable-5")), false);
|
||||
// combos are virtual, never aliased
|
||||
assert.equal(shouldExposeNoThinkingAlias(entry("my-combo", "combo")), false);
|
||||
// never double-alias
|
||||
assert.equal(
|
||||
shouldExposeNoThinkingAlias(entry("claude-3-omniroute-no-thinking/anthropic/claude-opus-4-5")),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("appendNoThinkingVariants adds one variant per eligible model and preserves the rest", () => {
|
||||
const models = [
|
||||
entry("claude-opus-4-5"),
|
||||
entry("gpt-4o", "openai"),
|
||||
entry("claude-fable-5"),
|
||||
];
|
||||
const out = appendNoThinkingVariants(models);
|
||||
const ids = out.map((m) => m.id);
|
||||
assert.ok(ids.includes("claude-3-omniroute-no-thinking/claude-opus-4-5"), "eligible model gets a variant");
|
||||
assert.ok(!ids.includes("claude-3-omniroute-no-thinking/gpt-4o"), "non-thinking model has no variant");
|
||||
assert.ok(!ids.includes("claude-3-omniroute-no-thinking/claude-fable-5"), "reject-disabled model has no variant");
|
||||
assert.equal(out.length, models.length + 1, "exactly one variant appended");
|
||||
// originals preserved up front
|
||||
assert.deepEqual(out.slice(0, 3), models);
|
||||
});
|
||||
|
||||
test("appendNoThinkingVariants returns the same array reference when nothing is eligible", () => {
|
||||
const models = [entry("gpt-4o", "openai")];
|
||||
assert.equal(appendNoThinkingVariants(models), models);
|
||||
});
|
||||
Reference in New Issue
Block a user