在 ai-electron 应用内实现本地路由层(Responses ⇄ Chat Completions 协议转换):模型管理探测到「只会 Chat」的端点(DeepSeek、dashscope compatible-mode 等)时自动启用,codex 零改动照常工作。应用内自动桥接,不做独立服务与多上游切换 UI。
实现约束:不从 git 历史恢复任何旧桥接代码,全部重写。 唯一规格依据是 codex rust-v0.155.1 线协议契约(已从 E:\workspace\ai\codex 源码逐字段钉死),行为参照 cc-switch / LiteLLM / claude-code-router。
codex app-server ──POST /v1/responses (SSE)──▶ ChatRouterService(127.0.0.1:随机端口)
wire_api="responses"(不变) │ 纯函数翻译:Responses ⇄ Chat
buildArgs 零改动 ▼
真实上游 POST {upstream}/chat/completions(流式转发)
responses → 直连(现状不变);探测 chat-only → 起路由层。electron/service/codex/chatRouter/translate.ts(翻译层,纯函数 + SSE 状态机,零 IO)请求方向 responsesToChatRequest(req)(白名单式构建,未列字段一律不发):
instructions → {role:"developer", content};model 直传input → messages:message → 对应 role(content 数组里 input_text/output_text→text,input_image→image_url,其余丢弃留诊断);function_call/custom_tool_call → assistant + tool_calls[{id: call_id, type:"function", function:{name, arguments}}];function_call_output/custom_tool_call_output → {role:"tool", tool_call_id: call_id, content: output};reasoning/其他类型 → 丢弃 + 诊断计数tools:仅 type:"function",扁平 {name, description, parameters} → 嵌套 {type:"function", function:{…}};tool_choice:字符串直传,{type:"function",name} → 嵌套;max_output_tokens→max_tokens;temperature/top_p/parallel_tool_calls 直传stream:true → stream:true + stream_options:{include_usage:true}(不支持的端点忽略;usage 全零兜底)store/include/reasoning/prompt_cache_key/text/client_metadata/metadata/service_tier/previous_response_id响应方向 class ChatSseTranslator(吃上游 chat SSE 分片,吐 codex SSE 事件帧):
begin():不等上游首字节先发 response.created + response.in_progress——先建 SSE 头,防单槽端点慢 prefill 被 codex 空闲窗掐线重发response.created → response.output_item.added(message) → response.output_text.delta* → response.output_item.done(完整 message item) → response.completed;delta.reasoning_content(DeepSeek R1)→ response.reasoning_summary_text.delta(包在 reasoning item added/done 里,先开先闭);delta.tool_calls[] 按 index 归拢增量,finish_reason:"tool_calls"/stop 时逐个补发完整 output_item.done({type:"function_call", call_id, name, arguments}) —— 参数必须整体出现在 done(0.155.1 忽略 function_call_arguments.delta)response.completed:response.id 必填(生成 resp_<uuid>);finish_reason:"length" → status:"incomplete" + incomplete_details;chunk 级 usage → input_tokens/output_tokens/total_tokens(+details 全零),缺省省略 usagedata:[DONE]、跨分包行缓冲、坏 JSON 跳过不杀流、finish() 冲刷幂等、fail(message) 补 response.failedelectron/service/codex/chatRouter/routerService.ts(HTTP 外壳)node:http listen(0,'127.0.0.1');只接 POST /v1/responses,其余 404;非 JSON body 400Authorization 原样透传 + 模型配置 headersJson + 强制 accept-encoding: identity(防上游 gzip 切碎 SSE)begin() → fetch(upstream + /chat/completions) → body.getReader() 边收边翻边写 → finish();客户端断连 → AbortController 取消上游(防单槽占死)stream:false,防御性保留):缓冲整包 → JSON 翻译;上游非 2xx:流式 fail()、非流式透传状态码;fetch 异常 502start(upstreamBaseUrl, extraHeaders) / stop() / url;诊断钩子(序号/耗时/上游字节/事件数)接装配层 pushDiagnosticchatRouter/translate.test.ts:请求映射(items/tools/tool_choice/剔除字段/流参数)、纯文本流事件序列 golden、跨分包、并行 tool_calls 按 index 归拢、reasoning_content 先开先闭、length→incomplete、[DONE]/坏 JSON/finish 幂等chatRouter/routerService.test.ts:非流式、流式(首事件 created、末事件 completed、usage)、上游 500 透传、GET 404 与端口释放、客户端挂断 1 秒内 abort 上游、慢 prefill 1.5 秒内首事件到达chatRouter/router.itest.ts(纳入 smoke:codex):真二进制 → 路由 → 假 chat 上游跑两轮 turn,断言第二轮上游 messages 含第一轮提问+回复+第二轮提问(全量历史语义)providerService.ts:probeEndpoint chat-only 分支放行(detail 改「将通过内置路由层桥接对接」,probe 逻辑不动);applyProvider:responses 直连不变,chat-only 时起路由(upstream=真实地址)→ spec.baseUrl=路由 URL → writeModelCatalog(slug 不受影响)→ runtime.applyProvider → 失败回滚停路由;applied.bridged=true 落盘且 applied.baseUrl 存上游真实地址(与已修的 modelApplied 三重比对天然兼容);clearProvider 停路由types.ts:AppliedProviderInfo.bridged?: boolean;CodexStatusResult.router?: { running; upstream } | nullindex.ts:路由挂进 CodexContainer;disposeCodex 兜底 stop;toStatusResult 填 routercodex/api/codexApi.ts(类型与注释)、ai/views/aiPlugin/index.vue(chat-only 从红色 error 改 success/info「将通过内置路由层桥接对接」+ 运行时面板展示路由状态);codexChatStore.ts 无需改(确认)providerService.test.ts(chat-only 拒绝→放行+baseUrl 断言)、codexRuntime.itest.ts、codexCtl.itest.ts(锁「拒绝」的旧用例)npm test、npm run smoke:codex、tsc --noEmit、vue-tsc(src/codex 0 报错)、build-electronstream_options.include_usage → usage 全零(token 统计缺失,回合正常)unsupported(两种路由都没有)维持拒绝