80 lines
3.2 KiB
TypeScript
80 lines
3.2 KiB
TypeScript
/**
|
|
* #1311: echo the client-requested model/alias name back in the response.
|
|
*
|
|
* When a request uses an alias or combo (e.g. `claude-sonnet-cx` → `cx/gpt-5.5`),
|
|
* OmniRoute forwards the upstream model name (`gpt-5.5`) in the response `model`
|
|
* field. Strict clients (e.g. Claude Desktop) validate that the response model
|
|
* matches the request and reject the mismatch with a 401. This opt-in helper
|
|
* rewrites the `model` field back to the name the client asked for.
|
|
*
|
|
* The behavior is gated by a global setting (`echoRequestedModelName`, default off),
|
|
* so the default response stays byte-for-byte unchanged.
|
|
*/
|
|
|
|
/**
|
|
* Rewrite the top-level `model` field of a parsed response object (Chat Completions
|
|
* JSON or an OpenAI SSE chunk) to `echoModel`. Mutates and returns `obj`. No-op when
|
|
* `echoModel` is falsy or `obj` has no string `model` field.
|
|
*/
|
|
export function echoModelInObject(obj: unknown, echoModel: string | null | undefined): unknown {
|
|
if (!echoModel) return obj;
|
|
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
const rec = obj as Record<string, unknown>;
|
|
if (typeof rec.model === "string") {
|
|
rec.model = echoModel;
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Rewrite the `model` field inside a single SSE line. Only `data: {json}` lines that
|
|
* carry a string `model` are rewritten; `data: [DONE]`, comments, event lines, and
|
|
* unparseable payloads pass through untouched.
|
|
*/
|
|
export function echoModelInSseLine(line: string, echoModel: string | null | undefined): string {
|
|
if (!echoModel) return line;
|
|
if (!line.startsWith("data:")) return line;
|
|
const payload = line.slice(5).trim();
|
|
if (!payload || payload === "[DONE]" || payload[0] !== "{") return line;
|
|
try {
|
|
const parsed = JSON.parse(payload) as Record<string, unknown>;
|
|
if (typeof parsed.model !== "string") return line;
|
|
parsed.model = echoModel;
|
|
return `data: ${JSON.stringify(parsed)}`;
|
|
} catch {
|
|
return line;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A TransformStream that rewrites the `model` field in every SSE `data:` chunk of a
|
|
* UTF-8 byte stream to `echoModel`. Buffers across chunk boundaries so a `data:` frame
|
|
* split across two reads is still rewritten correctly. Used as the final pipe stage of
|
|
* the streaming response when the echo setting is on.
|
|
*/
|
|
export function createModelEchoTransform(echoModel: string | null | undefined): TransformStream {
|
|
const decoder = new TextDecoder();
|
|
const encoder = new TextEncoder();
|
|
let buffer = "";
|
|
return new TransformStream({
|
|
transform(chunk, controller) {
|
|
buffer += decoder.decode(chunk, { stream: true });
|
|
// Emit complete lines; keep the trailing partial line in the buffer.
|
|
const lastNewline = buffer.lastIndexOf("\n");
|
|
if (lastNewline === -1) return;
|
|
const ready = buffer.slice(0, lastNewline + 1);
|
|
buffer = buffer.slice(lastNewline + 1);
|
|
const rewritten = ready
|
|
.split("\n")
|
|
.map((line) => echoModelInSseLine(line, echoModel))
|
|
.join("\n");
|
|
controller.enqueue(encoder.encode(rewritten));
|
|
},
|
|
flush(controller) {
|
|
const tail = buffer + decoder.decode();
|
|
if (tail) controller.enqueue(encoder.encode(echoModelInSseLine(tail, echoModel)));
|
|
},
|
|
});
|
|
}
|