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
5 changes: 3 additions & 2 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down Expand Up @@ -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,
Expand All @@ -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.",
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
128 changes: 128 additions & 0 deletions open-sse/utils/noThinkingAlias.ts
Original file line number Diff line number Diff line change
@@ -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;
}
5 changes: 5 additions & 0 deletions src/app/api/v1/models/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/shared/constants/modelSpecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[]) => [
Expand Down
13 changes: 13 additions & 0 deletions src/sse/handlers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down
134 changes: 134 additions & 0 deletions tests/unit/no-thinking-alias.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading