- 重写工具调用解析: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 兼容性审计
101 lines
2.6 KiB
Go
101 lines
2.6 KiB
Go
package openai
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
)
|
|
|
|
// StoredTurn is the minimal state kept to reconstruct context for
|
|
// previous_response_id and to serve GET /v1/responses/{id}. It mirrors the
|
|
// reference responses_adapter::StoredTurn.
|
|
type StoredTurn struct {
|
|
// InputText is the user input of the turn (rebuilt into a user message).
|
|
InputText string
|
|
// Output is the model's output item array, replayed item by item.
|
|
Output []map[string]any
|
|
// Response is the full Response object snapshot.
|
|
Response map[string]any
|
|
}
|
|
|
|
// HistoryMessages replays a stored turn as chat messages: the prior user input
|
|
// first, then assistant text / function_call items (consecutive function calls
|
|
// merge into one assistant tool_calls message).
|
|
func HistoryMessages(turn StoredTurn) []ChatMessage {
|
|
var out []ChatMessage
|
|
if strings.TrimSpace(turn.InputText) != "" {
|
|
out = append(out, ChatMessage{Role: "user", Content: MessageContent{Text: turn.InputText}})
|
|
}
|
|
for _, item := range turn.Output {
|
|
switch stringFieldAny(item, "type") {
|
|
case "message":
|
|
text := outputItemText(item)
|
|
if text == "" {
|
|
continue
|
|
}
|
|
out = append(out, ChatMessage{Role: "assistant", Content: MessageContent{Text: text}})
|
|
case "function_call":
|
|
call := ToolCall{
|
|
ID: stringFieldAny(item, "call_id", "id"),
|
|
Type: "function",
|
|
Function: FunctionCall{
|
|
Name: stringFieldAny(item, "name"),
|
|
Arguments: rawAnyText(item["arguments"], "{}"),
|
|
},
|
|
}
|
|
if call.ID == "" {
|
|
call.ID = "call_" + randomHex(8)
|
|
}
|
|
if n := len(out); n > 0 && out[n-1].Role == "assistant" && len(out[n-1].ToolCalls) > 0 {
|
|
out[n-1].ToolCalls = append(out[n-1].ToolCalls, call)
|
|
} else {
|
|
out = append(out, ChatMessage{Role: "assistant", ToolCalls: []ToolCall{call}})
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func outputItemText(item map[string]any) string {
|
|
parts, _ := item["content"].([]any)
|
|
var b strings.Builder
|
|
for _, p := range parts {
|
|
m, ok := p.(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if s, ok := m["text"].(string); ok {
|
|
b.WriteString(s)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func stringFieldAny(m map[string]any, keys ...string) string {
|
|
for _, k := range keys {
|
|
if s, ok := m[k].(string); ok && s != "" {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// rawAnyText renders a stored field that may be a plain string or a bare JSON
|
|
// value into text.
|
|
func rawAnyText(v any, def string) string {
|
|
switch t := v.(type) {
|
|
case nil:
|
|
return def
|
|
case string:
|
|
if strings.TrimSpace(t) == "" {
|
|
return def
|
|
}
|
|
return t
|
|
default:
|
|
raw, err := json.Marshal(t)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return string(raw)
|
|
}
|
|
}
|