- 重写工具调用解析: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 兼容性审计
68 lines
2.2 KiB
Go
68 lines
2.2 KiB
Go
package openai
|
|
|
|
// BackfillToolIDs enforces the non-empty tool id invariant on an inbound
|
|
// transcript. Upstream rejects any tool message whose tool_call_id is empty
|
|
// ("tool messages must include a non-empty string tool_call_id"), and strict
|
|
// clients echo back exactly what an earlier response carried — so both sides
|
|
// of the tool round-trip must be repaired here:
|
|
//
|
|
// 1. every assistant tool_calls entry gets an id (and type) when missing;
|
|
// 2. every tool result without a tool_call_id is paired with the nearest
|
|
// preceding unanswered assistant tool call (walking backwards, taking
|
|
// calls in order); when nothing is left to pair with, a placeholder id
|
|
// is generated so the transcript stays well-formed either way.
|
|
//
|
|
// It mutates msgs in place and is idempotent: a transcript that already
|
|
// satisfies the invariant comes back unchanged.
|
|
func BackfillToolIDs(msgs []ChatMessage) {
|
|
// Pass 1: assistant side — a tool call without an id is unusable by any
|
|
// client, and an empty type breaks strict response validation.
|
|
for i := range msgs {
|
|
if msgs[i].Role != "assistant" {
|
|
continue
|
|
}
|
|
for j := range msgs[i].ToolCalls {
|
|
tc := &msgs[i].ToolCalls[j]
|
|
if tc.ID == "" {
|
|
tc.ID = "call_" + randomHex(8)
|
|
}
|
|
if tc.Type == "" {
|
|
tc.Type = "function"
|
|
}
|
|
}
|
|
}
|
|
// answered collects every id a tool result already references, plus the
|
|
// ids claimed while pairing empty results below.
|
|
answered := make(map[string]bool, len(msgs))
|
|
for _, m := range msgs {
|
|
if m.Role == "tool" && m.ToolCallID != "" {
|
|
answered[m.ToolCallID] = true
|
|
}
|
|
}
|
|
// Pass 2: tool side — empty ids pair with the closest preceding call that
|
|
// has not been answered yet, so results land on the calls of their own
|
|
// turn even when older turns left calls unanswered.
|
|
for i := range msgs {
|
|
if msgs[i].Role != "tool" || msgs[i].ToolCallID != "" {
|
|
continue
|
|
}
|
|
id := ""
|
|
for j := i - 1; j >= 0 && id == ""; j-- {
|
|
if msgs[j].Role != "assistant" {
|
|
continue
|
|
}
|
|
for _, tc := range msgs[j].ToolCalls {
|
|
if tc.ID != "" && !answered[tc.ID] {
|
|
id = tc.ID
|
|
answered[id] = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if id == "" {
|
|
id = "call_" + randomHex(8)
|
|
}
|
|
msgs[i].ToolCallID = id
|
|
}
|
|
}
|