/** * OpenAI's hosted Responses API is lenient about item shapes; vLLM validates * them against the official pydantic schemas with zero tolerance. Coding * agents in the wild send the lenient dialect — chat-style `"text"` content * parts, assistant history messages carrying `output_text` parts without the * full output-item wrapper, chat-nested tool definitions — and vLLM 400s the * whole request. This is the one place the proxy translates: rewrite those * lax shapes into the strict equivalents vLLM accepts, leave everything that * already conforms untouched. */ type Rec = Record; const isRec = (v: unknown): v is Rec => typeof v === "object" && v !== null && !Array.isArray(v); const INPUT_ROLES = new Set(["user", "system", "developer"]); let syntheticId = 0; const nextMessageId = (): string => `msg_lax_${++syntheticId}`; /** Chat-style `text` part → `input_text` (user/system/developer messages). */ const normalizeInputPart = (part: unknown): unknown => { if (!isRec(part)) return part; if (part["type"] === "text") return { ...part, type: "input_text" }; return part; }; /** `text`/`input_text` part → `output_text` with required `annotations` (assistant). */ const normalizeOutputPart = (part: unknown): unknown => { if (!isRec(part)) return part; const type = part["type"]; if (type === "text" || type === "input_text") { return { type: "output_text", text: part["text"], annotations: part["annotations"] ?? [] }; } if (type === "output_text" && !("annotations" in part)) { return { ...part, annotations: [] }; } return part; }; const normalizeInputItem = (item: unknown): unknown => { if (!isRec(item)) return item; // Only message-like items need repair; function_call, function_call_output, // reasoning, item_reference, etc. pass vLLM's validation as sent. const type = item["type"]; if (type !== undefined && type !== "message") return item; const role = item["role"]; const content = item["content"]; if (typeof role === "string" && INPUT_ROLES.has(role)) { if (!Array.isArray(content)) return item; return { ...item, content: content.map(normalizeInputPart) }; } if (role === "assistant") { // Plain-string content is valid as an easy input message; arrays are only // valid inside a full output message item (type/id/status all required). if (!Array.isArray(content)) return item; return { type: "message", role: "assistant", id: typeof item["id"] === "string" ? item["id"] : nextMessageId(), status: typeof item["status"] === "string" ? item["status"] : "completed", content: content.map(normalizeOutputPart), }; } return item; }; /** Chat-nested `{type:"function", function:{...}}` tool → flat Responses form. */ const normalizeTool = (tool: unknown): unknown => { if (!isRec(tool)) return tool; const toolFunction = tool["function"]; if (tool["type"] === "function" && isRec(toolFunction) && !("name" in tool)) { return { type: "function", name: toolFunction["name"], description: toolFunction["description"] ?? null, parameters: toolFunction["parameters"] ?? null, strict: toolFunction["strict"] ?? false, }; } return tool; }; /** Mutates a parsed /v1/responses body so lax OpenAI shapes validate on vLLM. */ export const normalizeResponsesBody = (body: Rec): void => { if (Array.isArray(body["input"])) { body["input"] = body["input"].map(normalizeInputItem); } if (Array.isArray(body["tools"])) { body["tools"] = body["tools"].map(normalizeTool); } };