- 重写工具调用解析: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 兼容性审计
6.1 KiB
OpenAI 兼容性审计(Chat Completions / Responses / Messages)
审计基准:
openai-openapi的CreateChatCompletionRequest/CreateChatCompletionStreamResponse/CreateResponse/ResponseStreamEventschema,以及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)后续评估。