/** * 请求报文兼容性过滤:developer → system。 * 零 pi 依赖的纯业务模块。 * * 背景:某些 OpenAI-compatible 上游(如 OpenCode 网关背后的部分模型)不支持 `role: "developer"`, * 直接返回 400。Pi 的 OpenAICompletionsCompat.supportsDeveloperRole 可在 provider 层规避, * 但为兼容旧版本 / 未正确检测的情况,这里做一层幂等兜底:有则改、无则不动。 * * 只处理标准 OpenAI chat/completions 载荷中的 messages[].role === "developer"。 * - 若 load 里没有任何 developer → 原样返回(返回 null 表示未改动)。 * - 有 developer → 重写为 system(支持 merge 相邻 system 的选项,默认直接改)。 */ export interface CompatRule { /** true: 直接把 developer 改名为 system;false: 合并进相邻 system 消息 */ mergeSystems: boolean; } export const DEFAULT_COMPAT_RULE: CompatRule = { mergeSystems: false }; export type Payload = Record; function isArray(v: unknown): v is unknown[] { return Array.isArray(v); } function isRecord(v: unknown): v is Record { return typeof v === "object" && v !== null; } /** 检测载荷里是否存在 role === "developer" 的消息。 */ export function hasDeveloperRole(payload: Payload): boolean { const messages = payload.messages; if (!isArray(messages)) return false; return messages.some((m) => isRecord(m) && m.role === "developer"); } /** * 对 messages 执行 developer → system 兼容重写。 * 返回新 messages 数组;若无需改动返回 null(让调用方保持原样)。 */ function rewriteMessages(messages: unknown[], rule: CompatRule): unknown[] | null { let changed = false; const out: unknown[] = []; if (rule.mergeSystems) { for (const m of messages) { if (isRecord(m) && m.role === "developer") { changed = true; // 转成 system,尝试并入前一个 system const sys: Record = { ...m, role: "system" }; const prev = out[out.length - 1]; if (isRecord(prev) && prev.role === "system" && typeof prev.content === "string" && typeof sys.content === "string") { out[out.length - 1] = { ...prev, content: `${prev.content}\n\n${sys.content}` }; } else { out.push(sys); } } else { out.push(m); } } } else { for (const m of messages) { if (isRecord(m) && m.role === "developer") { changed = true; out.push({ ...m, role: "system" }); } else { out.push(m); } } } return changed ? out : null; } /** * 对 provider 载荷做兼容性重写。 * 返回新的 payload(deep-clone 顶层 messages),或 null 表示未改动(调用方可原样透传)。 */ export function applyDeveloperCompat(payload: Payload, rule: CompatRule = DEFAULT_COMPAT_RULE): Payload | null { if (!hasDeveloperRole(payload)) return null; const messages = payload.messages; if (!isArray(messages)) return null; const rewritten = rewriteMessages(messages, rule); if (rewritten === null) return null; return { ...payload, messages: rewritten }; }