/** * Extracts text from OpenAI Responses content when it is a string or an array of text-bearing items. * Unsupported shapes return an empty string rather than throwing. * Array items without string `text` are skipped, so non-text payload parts are silently dropped. */ export function extractOpenAIResponsesText(content: unknown): string { if (typeof content === "string") return content; if (!Array.isArray(content)) return ""; return content .map((item: any) => (typeof item?.text === "string" ? item.text : "")) .filter(Boolean) .join("\n"); } /** * Rewrites Pi-style Responses input by lifting developer/system messages into `instructions` when needed. * Non-Responses payloads are returned unchanged; patched payloads are shallow copies with `max_output_tokens` removed and reasoning summaries stripped. * System/developer messages are removed from `input` only when this helper creates missing instructions. */ export function patchOpenAIResponsesPayload(payload: any, fallbackInstructions = "You are a helpful assistant."): any { if (!payload?.input || !Array.isArray(payload.input) || payload.messages) { return payload; } const patched = { ...payload }; if (!patched.instructions) { const instructions: string[] = []; const filteredInput: any[] = []; for (const message of patched.input) { if (message?.role === "developer" || message?.role === "system") { const text = extractOpenAIResponsesText(message.content); if (text) instructions.push(text); continue; } filteredInput.push(message); } patched.instructions = instructions.join("\n\n") || fallbackInstructions; patched.input = filteredInput; } delete patched.max_output_tokens; if (patched.reasoning?.summary) { patched.reasoning = { effort: patched.reasoning.effort }; } return patched; } /** * Detects OpenAI Responses models routed through a non-OpenAI base URL. * Returns `true` only for `api: "openai-responses"` with a non-empty third-party base URL. * This is a heuristic gate and does not validate provider capabilities or mutate model config. */ export function shouldPatchProxyOpenAIResponsesModel(model: unknown): boolean { const api = String((model as any)?.api ?? ""); if (api !== "openai-responses") return false; const baseUrl = String((model as any)?.baseUrl ?? "").toLowerCase(); if (!baseUrl) return false; return !baseUrl.includes("api.openai.com"); } /** * Converts `developer` role messages to `system` in an openai-completions payload. * Payloads without a messages array are returned unchanged, and changed payloads are shallow copies. * Message objects are copied only when their role changes, preserving all other fields for provider compatibility. */ export function patchDeveloperToSystem(payload: any): any { if (!payload?.messages || !Array.isArray(payload.messages)) return payload; let changed = false; const messages = payload.messages.map((msg: any) => { if (msg?.role === "developer") { changed = true; return { ...msg, role: "system" }; } return msg; }); return changed ? { ...payload, messages } : payload; } /** * Detects openai-completions models likely to reject `developer` role messages. * Returns `true` only for non-OpenAI base URLs using `api: "openai-completions"`. * The heuristic is for direct `completeSimple()` calls that bypass proxy-compat's config-driven request pipeline. */ export function needsDeveloperToSystemPatch(model: unknown): boolean { const api = String((model as any)?.api ?? ""); if (api !== "openai-completions") return false; const baseUrl = String((model as any)?.baseUrl ?? "").toLowerCase(); if (!baseUrl) return false; // OpenAI itself supports `developer` role; third-party providers often don't return !baseUrl.includes("api.openai.com"); }