Files
OmniRoute/open-sse/services/targetRequestSanitizer.ts
Jan Leon b2b568b08e Fix routed target request parameters (#7323)
* fix routed target request parameters

* chore: rerun CI

* test(chatcore): align PR #7323 Codex-routing test with #7533 verbosity gating

The "Codex Responses routing keeps reasoning effort while dropping GPT-only
verbosity" test translated a Responses-shape request with credentials=null
and only the positional `provider` arg set to "opencode-go". #7533's
verbosity carry-over (Responses `text.verbosity` -> Chat `verbosity`) reads
the destination from `credentials.provider`, which the source->openai
translation step never threads from the positional provider arg — so
`translated.verbosity` came back undefined instead of "low", failing before
prepareUpstreamBody's sanitizer was even reached.

The test's intent (per its own name/comment) is a combo/fallback reroute:
translate while still addressed at Codex (an #7533-allowlisted OpenAI-param
destination, so verbosity legitimately survives translateRequest), then
resolve the final upstream target to opencode-go/GLM so prepareUpstreamBody's
sanitizeRequestForResolvedTarget (#7050/#7533) strips the GPT-only verbosity
for that concrete target while preserving reasoning_effort. Fixed by passing
`credentials: { provider: "codex" }` to the first translateRequest call to
match how production actually carries the destination provider, instead of
relying on the provider positional argument. No production code changed —
#7050 and #7533's sanitization are intentional and protected; only the test's
setup was misaligned with them.

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

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-18 21:18:34 -03:00

91 lines
3.0 KiB
TypeScript

/**
* Final request sanitation against the resolved upstream target.
*
* Clients legitimately send controls for the model they selected. Routing rules,
* combos and fallbacks may replace that model with a different family after the
* request has already been parsed and translated. This boundary removes controls
* that belong to the source model but are invalid for the actual target.
*/
import { stripUnsupportedParams } from "../translator/paramSupport.ts";
import { sanitizeReasoningEffortForProvider } from "../executors/base/reasoningEffort.ts";
type JsonRecord = Record<string, unknown>;
type LoggerLike =
| {
debug?: (tag: string, message: string) => void;
info?: (tag: string, message: string) => void;
}
| null
| undefined;
function isRecord(value: unknown): value is JsonRecord {
return value !== null && typeof value === "object" && !Array.isArray(value);
}
/** GPT-5 Chat/Responses models are the only family that owns `verbosity`. */
export function targetSupportsVerbosity(model: string | null | undefined): boolean {
return typeof model === "string" && /(?:^|\/)gpt-5(?:[._-]|$)/i.test(model.trim());
}
function stripVerbosityForTarget(body: JsonRecord, model: string): string[] {
if (targetSupportsVerbosity(model)) return [];
const stripped: string[] = [];
if (Object.hasOwn(body, "verbosity")) {
delete body.verbosity;
stripped.push("verbosity");
}
if (isRecord(body.text) && Object.hasOwn(body.text, "verbosity")) {
const text = { ...body.text };
delete text.verbosity;
if (Object.keys(text).length === 0) delete body.text;
else body.text = text;
stripped.push("text.verbosity");
}
return stripped;
}
/**
* Sanitize a translated request using the concrete provider/model selected by
* routing. Returns a fresh top-level object and never mutates the caller body.
*/
export function sanitizeRequestForResolvedTarget<T extends JsonRecord>(
body: T,
options: {
provider: string | null | undefined;
model: string;
log?: LoggerLike;
}
): T {
let next = { ...body } as T;
const stripped = stripVerbosityForTarget(next, options.model);
// Keep reasoning intent, but normalize its effort vocabulary for the
// concrete provider/model selected by routing (for example xhigh → high on
// explicit opt-outs, or xhigh → max for native DeepSeek). The request-format
// translators have already mapped the shape itself: Responses
// reasoning.effort → Chat reasoning_effort, or → Claude thinking.
next = sanitizeReasoningEffortForProvider(
next,
options.provider || "",
options.model,
options.log
) as T;
// Apply operator-configured provider/model filters at the common dispatch
// boundary so custom executors cannot accidentally bypass them.
stripUnsupportedParams(options.provider, options.model, next);
if (stripped.length > 0) {
options.log?.debug?.(
"TARGET_PARAMS",
`Stripped ${stripped.join(", ")} for resolved target ${options.provider || "unknown"}/${options.model}`
);
}
return next;
}