mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 05:12:16 +03:00
fix(routing): preserve reasoning overrides across transports and fallbacks (#13556)
Merged. The failure mode was concrete — a matched reasoning rule dropped on native Responses/Anthropic paths, model-suffix/account defaults, or fallback preparation, and `_omnirouteReasoningRule` leaking upstream as `Unsupported parameter` — and the fix is carried in the request-local credential context through dispatch, refreshed credentials and fallbacks, with forced effort winning over defaults and client-forged markers dropped at ingress. The 11-case integration suite exercises the real routing/translation modules. Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904. Thank you — and for keeping this a runtime-only change with the editor and service-tier work in their own PRs.
This commit is contained in:
1
changelog.d/fixes/13556-reasoning-rule-propagation.md
Normal file
1
changelog.d/fixes/13556-reasoning-rule-propagation.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(routing):** Preserve forced reasoning effort across native requests, account defaults and combo fallbacks while keeping internal routing directives out of upstream payloads. ([#13556](https://github.com/diegosouzapw/OmniRoute/pull/13556)) — thanks @JxnLexn
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
import { getAccessToken } from "../services/tokenRefresh.ts";
|
||||
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
|
||||
import { applyReasoningInputPolicy } from "../services/reasoningInputPolicy.ts";
|
||||
import { getForcedReasoningEffort } from "../utils/reasoningRuleContext.ts";
|
||||
import { normalizeCodexVerbosity } from "../services/codexVerbosity.ts";
|
||||
import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
|
||||
import { CORS_HEADERS } from "../utils/cors.ts";
|
||||
@@ -819,6 +820,22 @@ export class CodexExecutor extends BaseExecutor {
|
||||
requestInput.body
|
||||
);
|
||||
const nextInput = { ...requestInput, credentials };
|
||||
const forcedEffort = getForcedReasoningEffort(credentials);
|
||||
if (forcedEffort) {
|
||||
const nextBody =
|
||||
nextInput.body && typeof nextInput.body === "object"
|
||||
? (nextInput.body as Record<string, unknown>)
|
||||
: {};
|
||||
nextInput.body = {
|
||||
...nextBody,
|
||||
reasoning: {
|
||||
...(nextBody.reasoning && typeof nextBody.reasoning === "object"
|
||||
? nextBody.reasoning
|
||||
: {}),
|
||||
effort: forcedEffort,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (isCodexAppServerRequired(nextInput.credentials)) {
|
||||
if (!this.appServer) {
|
||||
@@ -1380,8 +1397,13 @@ export class CodexExecutor extends BaseExecutor {
|
||||
// Issue #2331: model suffix aliases (for example gpt-5.5-xhigh) represent an
|
||||
// explicit model selection, so they must override client-injected defaults such
|
||||
// as OpenCode's automatic reasoning.effort=medium for GPT-5-family requests.
|
||||
// A server-selected force rule is stronger than either source.
|
||||
const rawEffort =
|
||||
modelEffort || explicitReasoning || requestReasoningEffort || fallbackReasoningEffort;
|
||||
getForcedReasoningEffort(credentials) ||
|
||||
modelEffort ||
|
||||
explicitReasoning ||
|
||||
requestReasoningEffort ||
|
||||
fallbackReasoningEffort;
|
||||
|
||||
if (rawEffort) {
|
||||
const clampedEffort = clampEffort(cleanModel, rawEffort);
|
||||
|
||||
@@ -125,6 +125,8 @@ import { stripStore, usesClaudeBridge } from "./chatCore/agentRouterProtocol.ts"
|
||||
import { normalizeClaudeToolsForDispatch } from "./chatCore/claudeToolDefaults.ts";
|
||||
import { injectSystemPrompt, injectCustomSystemPrompt } from "../services/systemPrompt.ts";
|
||||
import { translateRequest, needsTranslation } from "../translator/index.ts";
|
||||
import { applyReasoningRuleDirective } from "@/lib/reasoningRouting/policy";
|
||||
import { withReasoningRuleContext } from "../utils/reasoningRuleContext.ts";
|
||||
import { FORMATS } from "../translator/formats.ts";
|
||||
import { collectCustomToolNamesForSourceFormat } from "../translator/request/openai-responses/additionalTools.ts";
|
||||
import { sanitizeKiroTools } from "../utils/kiroSanitizer.ts";
|
||||
@@ -499,6 +501,9 @@ export async function handleChatCore({
|
||||
fallbackAttempts = undefined,
|
||||
}) {
|
||||
let { provider, model, extendedContext } = modelInfo;
|
||||
// Keep the selected rule across format conversion, retries and refreshed credentials.
|
||||
// Each combo leg gets its own execution context; nothing is written to shared accounts.
|
||||
const reasoningRuleDirective = body?._omnirouteReasoningRule;
|
||||
// #12150 P1b: true iff the video-bridge guardrail rendered >=1 transcript
|
||||
// cue into a replaced part of this request. Gates both request- and
|
||||
// response-derived Memory extraction
|
||||
@@ -1214,6 +1219,22 @@ export async function handleChatCore({
|
||||
|
||||
log?.debug?.("FORMAT", `${sourceFormat} → ${targetFormat} | stream=${stream}`);
|
||||
|
||||
if (reasoningRuleDirective) {
|
||||
// Cache identity must use the effective effort, not the overridden client value.
|
||||
// Retain the directive for the translation step, where general thinking defaults run.
|
||||
body = {
|
||||
...(applyReasoningRuleDirective(
|
||||
body,
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES
|
||||
? "openai-responses"
|
||||
: sourceFormat === FORMATS.CLAUDE
|
||||
? "claude"
|
||||
: undefined
|
||||
) as Record<string, unknown>),
|
||||
_omnirouteReasoningRule: reasoningRuleDirective,
|
||||
};
|
||||
}
|
||||
|
||||
// Preserve original body for cache signature — the body variable is mutated
|
||||
// multiple times below (sanitization, memory/skills injection) before the
|
||||
// cache store path runs at Phase 9.1 (non-streaming) / Phase 9.2 (streaming).
|
||||
@@ -2267,7 +2288,7 @@ export async function handleChatCore({
|
||||
try {
|
||||
if (nativeResponsesPassthrough) {
|
||||
translatedBody = stampNativeResponsesPassthroughBody(
|
||||
body,
|
||||
applyReasoningRuleDirective(body, "openai-responses") as Record<string, unknown>,
|
||||
nativeCodexPassthrough
|
||||
? "codex"
|
||||
: nativeXaiResponsesPassthrough
|
||||
@@ -2288,6 +2309,10 @@ export async function handleChatCore({
|
||||
// Claude Code-compatible providers expect Anthropic Messages-shaped payloads,
|
||||
// but we extract only role/text/max_tokens/effort from an OpenAI-like view first.
|
||||
if (sourceFormat === FORMATS.CLAUDE && isClaudeCodeSemanticPassthrough) {
|
||||
normalizedForCc = applyReasoningRuleDirective(
|
||||
normalizedForCc,
|
||||
"claude"
|
||||
) as typeof normalizedForCc;
|
||||
log?.debug?.("FORMAT", "claude-code semantic passthrough enabled for compatible bridge");
|
||||
} else if (sourceFormat !== FORMATS.OPENAI) {
|
||||
const normalizeToolCallId = getModelNormalizeToolCallId(
|
||||
@@ -2323,6 +2348,10 @@ export async function handleChatCore({
|
||||
const ccRequestDefaults = getClaudeCodeCompatibleRequestDefaults(
|
||||
credentials?.providerSpecificData
|
||||
);
|
||||
// OpenAI-shaped bridge requests skip translateRequest too.
|
||||
if (sourceFormat === FORMATS.OPENAI) {
|
||||
normalizedForCc = applyReasoningRuleDirective(normalizedForCc) as typeof normalizedForCc;
|
||||
}
|
||||
translatedBody = buildClaudeCodeCompatibleRequest({
|
||||
sourceBody: body,
|
||||
normalizedBody: normalizedForCc,
|
||||
@@ -2352,7 +2381,7 @@ export async function handleChatCore({
|
||||
// payloads at high context (150+ msgs, 100+ tools). Fix: #1359.
|
||||
// Claude Code sends well-formed Messages API payloads — trust them
|
||||
// regardless of combo strategy or cache_control settings.
|
||||
translatedBody = { ...body };
|
||||
translatedBody = applyReasoningRuleDirective({ ...body }, "claude");
|
||||
translatedBody._disableToolPrefix = true;
|
||||
|
||||
// Sanitize historical thinking-block signatures for Anthropic-native Claude OAuth.
|
||||
@@ -2996,15 +3025,18 @@ export async function handleChatCore({
|
||||
// Get executor for this provider (with optional upstream proxy routing)
|
||||
const executor = await resolveExecutorWithProxy(provider);
|
||||
const getExecutionCredentials = () =>
|
||||
resolveExecutionCredentialsFor({
|
||||
credentials,
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
endpointPath,
|
||||
targetFormat,
|
||||
provider,
|
||||
ccSessionId,
|
||||
modelInfo,
|
||||
});
|
||||
withReasoningRuleContext(
|
||||
resolveExecutionCredentialsFor({
|
||||
credentials,
|
||||
nativeCodexPassthrough: nativeResponsesPassthrough,
|
||||
endpointPath,
|
||||
targetFormat,
|
||||
provider,
|
||||
ccSessionId,
|
||||
modelInfo,
|
||||
}),
|
||||
reasoningRuleDirective
|
||||
);
|
||||
|
||||
let onPipelineStreamError: streamFailure.PipelineStreamErrorHandler | null = null;
|
||||
let onClientDisconnectFinalize:
|
||||
|
||||
28
open-sse/utils/reasoningRuleContext.ts
Normal file
28
open-sse/utils/reasoningRuleContext.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Request-local execution metadata, not a provider credential or a wire field.
|
||||
// Symbol keys survive credential object spreads but cannot be supplied through JSON.
|
||||
const FORCED_EFFORT = Symbol.for("omniroute.forcedReasoningEffort");
|
||||
const EFFORTS = new Set(["none", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
||||
|
||||
export function withReasoningRuleContext<T>(credentials: T, directive: unknown): T {
|
||||
if (
|
||||
!credentials ||
|
||||
typeof credentials !== "object" ||
|
||||
!directive ||
|
||||
typeof directive !== "object"
|
||||
)
|
||||
return credentials;
|
||||
const rule = directive as Record<string, unknown>;
|
||||
if (
|
||||
!rule.id ||
|
||||
rule.effortMode !== "force" ||
|
||||
typeof rule.targetEffort !== "string" ||
|
||||
!EFFORTS.has(rule.targetEffort)
|
||||
)
|
||||
return credentials;
|
||||
return { ...credentials, [FORCED_EFFORT]: rule.targetEffort };
|
||||
}
|
||||
|
||||
export function getForcedReasoningEffort(credentials: unknown): string | undefined {
|
||||
if (!credentials || typeof credentials !== "object") return undefined;
|
||||
return (credentials as { [FORCED_EFFORT]?: string })[FORCED_EFFORT];
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { logger } from "@omniroute/open-sse/utils/logger.ts";
|
||||
import { resolveProxy } from "@omniroute/open-sse/utils/networkProxy.ts";
|
||||
import { withCodexFingerprintCredentials } from "@omniroute/open-sse/config/codexIdentity.ts";
|
||||
import { proxyConfigToUrl } from "@omniroute/open-sse/utils/proxyDispatcher.ts";
|
||||
import { withReasoningRuleContext } from "@omniroute/open-sse/utils/reasoningRuleContext.ts";
|
||||
import {
|
||||
attachReasoningRuleDirective,
|
||||
applyReasoningRuleDirective,
|
||||
@@ -544,12 +545,17 @@ async function prepare(body: JsonRecord) {
|
||||
|
||||
let responseBodyWithMemory = await maybeInjectResponsesWsMemory(responseBody, metadata);
|
||||
let reasoningRouting: JsonRecord | null = null;
|
||||
let reasoningRuleDirective: unknown;
|
||||
if (reasoningDecision) {
|
||||
const withDirective = attachReasoningRuleDirective(responseBodyWithMemory, reasoningDecision);
|
||||
reasoningRuleDirective = withDirective._omnirouteReasoningRule;
|
||||
reasoningRouting = isRecord(withDirective._omnirouteReasoningRouteTrace)
|
||||
? withDirective._omnirouteReasoningRouteTrace
|
||||
: null;
|
||||
responseBodyWithMemory = applyReasoningRuleDirective(withDirective) as JsonRecord;
|
||||
responseBodyWithMemory = applyReasoningRuleDirective(
|
||||
withDirective,
|
||||
"openai-responses"
|
||||
) as JsonRecord;
|
||||
delete responseBodyWithMemory._omnirouteReasoningRouteTrace;
|
||||
}
|
||||
// #8052: the WS bridge previously skipped the whole prompt-compression pipeline that the
|
||||
@@ -561,7 +567,7 @@ async function prepare(body: JsonRecord) {
|
||||
requestId: randomUUID(),
|
||||
});
|
||||
const credentialsWithFingerprint = withCodexFingerprintCredentials(
|
||||
refreshedCredentials,
|
||||
withReasoningRuleContext(refreshedCredentials, reasoningRuleDirective),
|
||||
context.clientHeaders,
|
||||
responseBodyWithMemory
|
||||
);
|
||||
|
||||
@@ -531,7 +531,10 @@ export function attachReasoningRuleDirective(
|
||||
return body;
|
||||
}
|
||||
|
||||
export function applyReasoningRuleDirective(bodyInput: unknown): unknown {
|
||||
export function applyReasoningRuleDirective(
|
||||
bodyInput: unknown,
|
||||
targetFormat?: "openai-responses" | "claude"
|
||||
): unknown {
|
||||
const source = asRecord(bodyInput);
|
||||
const directive = asRecord(source._omnirouteReasoningRule);
|
||||
if (!directive.id) return bodyInput;
|
||||
@@ -542,9 +545,11 @@ export function applyReasoningRuleDirective(bodyInput: unknown): unknown {
|
||||
if (effortMode === "force" && targetEffort === "none") clearReasoning(body);
|
||||
else if ((effortMode === "force" || effortMode === "default") && targetEffort) {
|
||||
if (effortMode === "force") clearDiscreteReasoning(body);
|
||||
body.reasoning_effort = targetEffort;
|
||||
body.reasoning = { ...asRecord(body.reasoning), effort: targetEffort };
|
||||
body.output_config = { ...asRecord(body.output_config), effort: targetEffort };
|
||||
if (!targetFormat) body.reasoning_effort = targetEffort;
|
||||
if (targetFormat !== "claude")
|
||||
body.reasoning = { ...asRecord(body.reasoning), effort: targetEffort };
|
||||
if (targetFormat !== "openai-responses")
|
||||
body.output_config = { ...asRecord(body.output_config), effort: targetEffort };
|
||||
}
|
||||
applyBudget(
|
||||
body,
|
||||
|
||||
@@ -448,6 +448,14 @@ async function handleChatImplementation(
|
||||
return errorResponse(HTTP_STATUS.BAD_REQUEST, "Invalid JSON body");
|
||||
}
|
||||
|
||||
// Only the server's policy resolver may attach execution directives or route traces.
|
||||
// Discard lookalike JSON fields supplied by callers before evaluating any rule.
|
||||
if (body && typeof body === "object") {
|
||||
body = { ...body };
|
||||
delete body._omnirouteReasoningRule;
|
||||
delete body._omnirouteReasoningRouteTrace;
|
||||
}
|
||||
|
||||
// Feature #6241: fold the canonical `effort` / `thinking` request params onto the
|
||||
// per-provider reasoning fields (reasoning_effort / reasoning.effort / thinking) that the
|
||||
// existing translators already consume. Done here — right after the body is first
|
||||
|
||||
389
tests/integration/reasoning-routing-reliability.test.ts
Normal file
389
tests/integration/reasoning-routing-reliability.test.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { createChatPipelineHarness } from "./_chatPipelineHarness.ts";
|
||||
|
||||
const h = await createChatPipelineHarness("reasoning-reliability");
|
||||
const providers = await import("../../src/lib/db/providers.ts");
|
||||
const { flushProxyLogsSync } = await import("../../src/lib/proxyLogger.ts");
|
||||
type RecordedBody = Record<string, unknown> & { reasoning?: { effort?: string } };
|
||||
let calls: RecordedBody[] = [];
|
||||
let firstConnectionId: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await h.resetStorage();
|
||||
h.BaseExecutor.RETRY_CONFIG.delayMs = 0;
|
||||
calls = [];
|
||||
const connection = await providers.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
name: "Reasoning fixture",
|
||||
accessToken: "fixture-access-token",
|
||||
refreshToken: "fixture-refresh-token",
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
providerSpecificData: { requestDefaults: { reasoningEffort: "high" } },
|
||||
});
|
||||
firstConnectionId = String(connection.id);
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
assert.match(String(url), /chatgpt\.com\/backend-api\/codex\/responses/);
|
||||
calls.push(JSON.parse(String(init.body)) as RecordedBody);
|
||||
return new Response(
|
||||
"data: " +
|
||||
JSON.stringify({
|
||||
type: "response.completed",
|
||||
response: {
|
||||
id: "resp_fixture",
|
||||
object: "response",
|
||||
status: "completed",
|
||||
model: "gpt-5.6-luna",
|
||||
output: [
|
||||
{
|
||||
id: "msg_fixture",
|
||||
type: "message",
|
||||
role: "assistant",
|
||||
content: [{ type: "output_text", text: "ok", annotations: [] }],
|
||||
},
|
||||
],
|
||||
usage: { input_tokens: 1, output_tokens: 1, total_tokens: 2 },
|
||||
},
|
||||
}) +
|
||||
"\n\ndata: [DONE]\n\n",
|
||||
{ headers: { "Content-Type": "text/event-stream" } }
|
||||
);
|
||||
};
|
||||
});
|
||||
test.afterEach(async () => {
|
||||
flushProxyLogsSync();
|
||||
h.BaseExecutor.RETRY_CONFIG.delayMs = h.originalRetryDelayMs;
|
||||
await h.resetStorage();
|
||||
});
|
||||
test.after(() => {
|
||||
flushProxyLogsSync();
|
||||
return h.cleanup();
|
||||
});
|
||||
|
||||
async function forceLow(modelPattern: string, apiKeyId: string) {
|
||||
return h.reasoningRulesDb.createReasoningRoutingRule({
|
||||
name: "Force low",
|
||||
description: "",
|
||||
scope: "apiKey",
|
||||
apiKeyId,
|
||||
comboId: null,
|
||||
connectionId: null,
|
||||
modelPattern,
|
||||
sourceEffort: "any",
|
||||
requestTags: [],
|
||||
tagMatchMode: "any",
|
||||
effortMode: "force",
|
||||
targetEffort: "low",
|
||||
targetKind: "keep",
|
||||
targetModel: null,
|
||||
targetComboId: null,
|
||||
budgetAction: "preserve",
|
||||
budgetTokens: null,
|
||||
priority: 0,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
test("reliable reasoning: forced effort wins over explicit effort and adaptive header opt-in", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
await forceLow("codex/gpt-5.6-luna", key.id);
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: key.key,
|
||||
headers: { "x-omniroute-effort": "auto" },
|
||||
body: {
|
||||
model: "codex/gpt-5.6-luna",
|
||||
stream: false,
|
||||
reasoning_effort: "high",
|
||||
messages: [
|
||||
{ role: "user", content: "Prove the theorem step by step and verify every edge case." },
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls.at(-1)?.reasoning?.effort, "low");
|
||||
});
|
||||
|
||||
test("reliable reasoning: explicit client effort survives adaptive header opt-in", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
await providers.updateProviderConnection(firstConnectionId, { providerSpecificData: {} });
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: key.key,
|
||||
headers: { "x-omniroute-effort": "auto" },
|
||||
body: {
|
||||
model: "codex/gpt-5.6-luna",
|
||||
stream: false,
|
||||
reasoning_effort: "medium",
|
||||
messages: [{ role: "user", content: "Reply ok." }],
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls.at(-1)?.reasoning?.effort, "medium");
|
||||
});
|
||||
|
||||
test("reliable reasoning: native Responses honors key force without leaking cross-format fields", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
await forceLow("codex/gpt-5.6-luna", key.id);
|
||||
const input = [
|
||||
{ type: "message", role: "user", content: [{ type: "input_text", text: "Reply ok" }] },
|
||||
];
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
url: "http://localhost/v1/responses",
|
||||
authKey: key.key,
|
||||
body: { model: "codex/gpt-5.6-luna", input, stream: false, reasoning: { effort: "high" } },
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].reasoning?.effort, "low");
|
||||
assert.deepEqual(calls[0].input, input);
|
||||
for (const name of [
|
||||
"_omnirouteReasoningRule",
|
||||
"_omnirouteReasoningRouteTrace",
|
||||
"output_config",
|
||||
"reasoning_effort",
|
||||
]) {
|
||||
assert.equal(calls[0][name], undefined, name);
|
||||
}
|
||||
});
|
||||
|
||||
test("reliable reasoning: combo member effort suffix cannot override the selected key rule", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
await h.combosDb.createCombo({
|
||||
name: "luna-combo",
|
||||
models: ["codex/gpt-5.6-luna-high"],
|
||||
strategy: "priority",
|
||||
});
|
||||
await forceLow("luna-combo", key.id);
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: key.key,
|
||||
body: {
|
||||
model: "luna-combo",
|
||||
stream: false,
|
||||
reasoning_effort: "high",
|
||||
messages: [{ role: "user", content: "Reply ok" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].model, "gpt-5.6-luna");
|
||||
assert.equal(calls[0].reasoning?.effort, "low");
|
||||
});
|
||||
|
||||
test("reliable reasoning: client metadata cannot impersonate an administrator rule", async () => {
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
body: {
|
||||
model: "codex/gpt-5.6-luna",
|
||||
stream: false,
|
||||
reasoning_effort: "high",
|
||||
messages: [{ role: "user", content: "Reply ok" }],
|
||||
_omnirouteReasoningRule: { id: "forged", effortMode: "force", targetEffort: "low" },
|
||||
_omnirouteReasoningRouteTrace: { ruleId: "forged" },
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls[0].reasoning?.effort, "high");
|
||||
assert.equal(calls[0]._omnirouteReasoningRule, undefined);
|
||||
});
|
||||
|
||||
test("reliable reasoning: fallback attempts retain the forced effort", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
const second = await providers.createProviderConnection({
|
||||
provider: "codex",
|
||||
authType: "oauth",
|
||||
name: "Fallback fixture",
|
||||
accessToken: "fixture-fallback-token",
|
||||
isActive: true,
|
||||
testStatus: "active",
|
||||
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
providerSpecificData: {},
|
||||
});
|
||||
await h.combosDb.createCombo({
|
||||
name: "fallback-combo",
|
||||
models: [
|
||||
{ model: "codex/gpt-5.6-luna-high", connectionId: firstConnectionId },
|
||||
{ model: "codex/gpt-5.5-xhigh", connectionId: String(second.id) },
|
||||
],
|
||||
strategy: "priority",
|
||||
});
|
||||
await forceLow("fallback-combo", key.id);
|
||||
const successFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url, init: RequestInit = {}) => {
|
||||
const body = JSON.parse(String(init.body)) as RecordedBody;
|
||||
if (body.model === "gpt-5.6-luna") {
|
||||
calls.push(body);
|
||||
return new Response(JSON.stringify({ error: { message: "Fixture unavailable" } }), {
|
||||
status: 503,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return successFetch(url, init);
|
||||
};
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: key.key,
|
||||
body: {
|
||||
model: "fallback-combo",
|
||||
stream: false,
|
||||
reasoning_effort: "high",
|
||||
messages: [{ role: "user", content: "Reply ok" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(calls.length >= 2);
|
||||
assert.equal(calls.at(-1)?.model, "gpt-5.5");
|
||||
assert.ok(calls.every((call) => call.reasoning?.effort === "low"));
|
||||
});
|
||||
|
||||
test("reliable reasoning: force is request-local and does not affect another API key", async () => {
|
||||
const restricted = await h.seedApiKey({ name: "Forced key" });
|
||||
const ordinary = await h.seedApiKey({ name: "Ordinary key" });
|
||||
await forceLow("codex/gpt-5.6-luna", restricted.id);
|
||||
for (const key of [restricted, ordinary]) {
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: key.key,
|
||||
body: {
|
||||
model: "codex/gpt-5.6-luna",
|
||||
stream: false,
|
||||
reasoning_effort: "high",
|
||||
messages: [{ role: "user", content: "Reply " + key.id }],
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.reasoning?.effort),
|
||||
["low", "high"]
|
||||
);
|
||||
});
|
||||
|
||||
test("reliable reasoning: force none survives Codex account defaults", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
const rule = await forceLow("codex/gpt-5.6-luna", key.id);
|
||||
await h.reasoningRulesDb.updateReasoningRoutingRule(rule.id, { targetEffort: "none" });
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
authKey: key.key,
|
||||
url: "http://localhost/v1/responses",
|
||||
body: {
|
||||
model: "codex/gpt-5.6-luna",
|
||||
input: "Reply ok",
|
||||
stream: false,
|
||||
reasoning: { effort: "high" },
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(calls[0].reasoning?.effort, "none");
|
||||
});
|
||||
|
||||
test("reliable reasoning: native default applies only when client effort is missing", async () => {
|
||||
const key = await h.seedApiKey();
|
||||
const rule = await forceLow("codex/gpt-5.6-luna", key.id);
|
||||
await h.reasoningRulesDb.updateReasoningRoutingRule(rule.id, { effortMode: "default" });
|
||||
for (const effort of [undefined, "high"]) {
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
url: "http://localhost/v1/responses",
|
||||
authKey: key.key,
|
||||
body: {
|
||||
model: "codex/gpt-5.6-luna",
|
||||
input: "Default " + effort,
|
||||
stream: false,
|
||||
...(effort ? { reasoning: { effort } } : {}),
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
assert.deepEqual(
|
||||
calls.map((call) => call.reasoning?.effort),
|
||||
["low", "high"]
|
||||
);
|
||||
assert.ok(calls.every((call) => call.output_config === undefined));
|
||||
});
|
||||
|
||||
test("reliable reasoning: WebSocket prepare preserves force in the final Codex payload", async () => {
|
||||
process.env.OMNIROUTE_WS_BRIDGE_SECRET = "fixture-bridge-secret";
|
||||
const { POST } = await import("../../src/app/api/internal/codex-responses-ws/route.ts");
|
||||
const key = await h.seedApiKey();
|
||||
await forceLow("codex/gpt-5.6-luna", key.id);
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/internal/codex-responses-ws", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-omniroute-ws-bridge-secret": "fixture-bridge-secret",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: "prepare",
|
||||
requestUrl: "http://localhost/v1/responses",
|
||||
headers: { authorization: "Bearer " + key.key },
|
||||
response: {
|
||||
model: "codex/gpt-5.6-luna-high",
|
||||
input: "Reply ok",
|
||||
reasoning: { effort: "high" },
|
||||
},
|
||||
}),
|
||||
})
|
||||
);
|
||||
const data = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(data.response.reasoning.effort, "low");
|
||||
assert.equal(data.response.output_config, undefined);
|
||||
assert.equal(data.response._omnirouteReasoningRule, undefined);
|
||||
assert.equal(calls.length, 0);
|
||||
});
|
||||
|
||||
test("reliable reasoning: native Anthropic applies effort without OpenAI wire fields", async () => {
|
||||
await h.seedConnection("anthropic");
|
||||
const key = await h.seedApiKey();
|
||||
await forceLow("anthropic/claude-sonnet-4-6", key.id);
|
||||
globalThis.fetch = async (_url, init: RequestInit = {}) => {
|
||||
calls.push(JSON.parse(String(init.body)));
|
||||
return h.buildClaudeResponse("ok");
|
||||
};
|
||||
const response = await h.handleChat(
|
||||
h.buildRequest({
|
||||
url: "http://localhost/v1/messages",
|
||||
authKey: key.key,
|
||||
body: {
|
||||
model: "anthropic/claude-sonnet-4-6",
|
||||
max_tokens: 128,
|
||||
stream: false,
|
||||
thinking: { type: "adaptive" },
|
||||
output_config: { effort: "high" },
|
||||
messages: [{ role: "user", content: "Reply ok" }],
|
||||
},
|
||||
})
|
||||
);
|
||||
await response.text();
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal((calls[0].output_config as { effort?: string })?.effort, "low");
|
||||
assert.equal(calls[0].reasoning, undefined);
|
||||
assert.equal(calls[0].reasoning_effort, undefined);
|
||||
assert.equal(calls[0]._omnirouteReasoningRule, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user