Files
nyoungo 86492460b0 feat: 工具调用解析、Responses 状态缓存、幂等重放与模型别名
- 重写工具调用解析:XML、DSML 未加引号键、fence 内跳过、参数外溢等形状,并按请求携带的 tools 过滤工具名
- 新增 GET /v1/responses/{id} 与 previous_response_id 有界状态缓存(进程内、TTL、store=false 不写入)
- /v1/chat/completions 支持 Idempotency-Key 幂等回放(1 MiB 上限、24h TTL、并发去重)
- 模型别名与 default_model 降级;错误体统一输出 param/code
- 补 e2e 修复场景与 docs/compat-audit.md 兼容性审计
2026-10-09 00:50:01 +08:00

6.1 KiB
Raw Permalink Blame History

OpenAI 兼容性审计(Chat Completions / Responses / Messages)

审计基准:openai-openapi 的 CreateChatCompletionRequest / CreateChatCompletionStreamResponse / CreateResponse / ResponseStreamEvent schema,以及 openai-python 的运行时解析逻辑。 审计对象:本仓库(github.com/nyoungo/dsfree2api)。

本文档记录已核对的差异与修复结论。每条结论标注规范来源,便于回归对照。


1. 工具调用修复(tool-call repair lineage)

模型在受限通道下会产出各种畸形工具调用。网关在 internal/openai 内做 宽容解析,避免把原始文本透传给客户端。

规范:Chat Completions 的 tool_calls 条目必须含 type="function" 与 function.name(function.arguments 为 JSON 字符串);Responses 的 function_call 条目必须含 call_id / name / arguments。

已实现的修复(internal/openai/toolcalls.go):

  • JSON 外壳:{"tool_calls":[...]}、裸数组、裸对象、json / 围栏;
  • 未加引号的键({name: "f"} → {"name": "f"});
  • 非法反斜杠(C:\Users → C:\\Users);
  • 字段别名:function/params/parameters/args/input;
  • name 与 arguments 互换(name 为对象、arguments 为字符串);
  • 参数外溢到顶层({"name":"f","city":"北京"});
  • XML 方言:<tool_call> / <tool_calls> / <invoke name> + <parameter>, 以及 <function><name>…<arguments>…;
  • 特殊标记 <|tool▁calls▁begin|> / <|tool▁calls▁end|>,并对 |(U+FF5C)、 ▁(U+2581) 做模糊匹配;
  • 代码围栏内的 XML 示例会被跳过(isInsideCodeFence),但围栏内的 JSON 仍按既有行为解析(由 TestTryParseToolCallsMarkdownJSONBlock 固定)。

约束:function.name 缺失的条目会被整条候选丢弃,绝不返回 name 为空的 调用(否则错误会被推迟到执行阶段才暴露)。

已知差距:流式路径(internal/openai/argstream.go)只对流式化的 {"tool_calls":…} 形状增量下发;上面这些新形状会在 Finalize 时作为完整 tool_calls 重发(resend),行为正确但不增量。可接受。

回归:internal/openai/repair_test.go 覆盖 10 个场景主题 (01–04 XML、05 字段别名、06 arguments 字符串、07/08 括号、09 互换、 10 参数外溢)。


2. 服务param/code字段缺失(OpenAI Error schema)

规范:OpenAI Error 的 required 为 ["type", "message", "param", "code"]; SDK 常按 code 区分 model_not_found 等语义。

修复:internal/api/server.go 的 writeError 现在总是输出 message / type / param / code 四个字段(不适用时为 null),并按状态码 推导:

  • 401 → code="invalid_api_key"
  • 404 且 message 以 model not found 开头 → param="model", code="model_not_found"
  • 429 → code="rate_limit_exceeded"
  • 413 → code="request_too_large"
  • 502/504 → code="upstream_error" / upstream_timeout"
  • idempotency_error → code="idempotency_error"

Anthropic 信封(writeAnthropicError)保持 {"type":"error","error":{type,message}} 不变。


3. Responses API 状态(previous_response_id / store)

规范:CreateResponse 支持 store(默认 true)与 previous_response_id; GET /v1/responses/{id} 返回响应快照。

修复:

  • openai.ResponsesRequest 新增 store *bool 与 previous_response_id string;
  • 进程内、有界、带 TTL 的缓存(internal/api/responsestore.go), 容量默认 256、TTL 默认 1h,可由 [responses] 配置;
  • 非流式与流式完成路径都会写入(store:false 跳过);
  • previous_response_id 命中时用 openai.HistoryMessages 重建上下文, 拼接到本轮 input 之前(system/developer 指令保持在最前);
  • 过期 / 未知 previous_response_id → 400 invalid_request_error; GET 未知或过期 → 404。

取舍:绝不落盘(响应可能含用户内容),重启即失效,与上游无关的 无状态网关定位一致。


4. Idempotency-Key 语义

规范:Stripe / OpenAI 约定。

实现(internal/api/idempotency.go),作用于 POST /v1/chat/completions、/v1/responses、/v1/messages:

  • 作用域 = (API key, method+path, Idempotency-Key);
  • 指纹 = sha256(scope|key|body);
  • 同键 + 不同请求体 → 400 idempotency_error;
  • 首次执行期间再次到达 → 409 idempotency_error;
  • 已完成 → 逐字节回放(状态码 / Content-Type / 响应体), 并带 idempotent-replayed: true;
  • 单条响应体上限 1 MiB、总量 64 MiB、容量 1024、TTL 24h; 超限标记为不可回放(重试 409);写入失败或处理中途 panic 则撤销占位, 让重试可以真正重跑;
  • 超过 255 字符的 key → 400。

5. 模型别名

config.ModelAliases / DefaultModel + Config.ResolveModel:请求模型名按 精确 id → 大小写不敏感 id → model_aliases(大小写不敏感)→ default_model 解析,供 Codex CLI / Claude Code 发送的任意 gpt-* / claude-* 名字使用。 /v1/models 也会列出别名。见 internal/config/alias_test.go、 internal/api/alias_test.go。


6. 工具类型过滤与推理字段

Responses 请求里的非 function 工具(web_search_preview 等)会被 ResponsesTool.ToToolDef 丢弃(上游无法执行)。已由 internal/openai/prompt_test.go:144 固定。

reasoning / reasoning_effort 字段同样不会被转发(ResponsesRequest 不解析, encoding/json 忽略未知字段),即对上游强制关闭,避免请求体被拒绝。


7. 待观察 / 风险

  • hif-leim 风险标记:仅记录,暂不改变行为。若上游开始返回该标记, 需要单独评估对工具调用解析的影响。
  • 流式工具增量覆盖:见 §1「已知差距」。
  • 状态缓存进程内:多实例部署时 previous_response_id 与 Idempotency-Key 不共享;如需跨实例,需要外部存储(Redis)后续评估。