- 重写工具调用解析: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 兼容性审计
269 lines
8.1 KiB
Go
269 lines
8.1 KiB
Go
package openai
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
)
|
||
|
||
type ChatMessage struct {
|
||
Role string `json:"role"`
|
||
Content MessageContent `json:"content,omitempty"`
|
||
Name string `json:"name,omitempty"`
|
||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||
}
|
||
|
||
// MessageContent accepts either a plain string or the OpenAI content-part
|
||
// array, unmarshalling into whichever shape arrives on the wire.
|
||
type MessageContent struct {
|
||
Text string
|
||
Parts []map[string]any
|
||
IsRaw bool
|
||
}
|
||
|
||
func (m *MessageContent) UnmarshalJSON(data []byte) error {
|
||
if len(data) > 0 && data[0] == '"' {
|
||
var s string
|
||
if err := json.Unmarshal(data, &s); err != nil {
|
||
return err
|
||
}
|
||
m.Text = s
|
||
m.Parts = nil
|
||
m.IsRaw = false
|
||
return nil
|
||
}
|
||
if string(data) == "null" {
|
||
m.Text = ""
|
||
m.Parts = nil
|
||
return nil
|
||
}
|
||
var parts []map[string]any
|
||
if err := json.Unmarshal(data, &parts); err != nil {
|
||
return err
|
||
}
|
||
m.Parts = parts
|
||
m.Text = ""
|
||
m.IsRaw = true
|
||
return nil
|
||
}
|
||
|
||
func (m MessageContent) MarshalJSON() ([]byte, error) {
|
||
if m.IsRaw && m.Parts != nil {
|
||
return json.Marshal(m.Parts)
|
||
}
|
||
return json.Marshal(m.Text)
|
||
}
|
||
|
||
func (m MessageContent) String() string {
|
||
if m.IsRaw || (m.Text == "" && len(m.Parts) > 0) {
|
||
return ContentToText(m.Parts)
|
||
}
|
||
return m.Text
|
||
}
|
||
|
||
type ResponseFormat struct {
|
||
Type string `json:"type,omitempty"`
|
||
JSONSchema json.RawMessage `json:"json_schema,omitempty"`
|
||
}
|
||
|
||
type StreamOptions struct {
|
||
IncludeUsage bool `json:"include_usage,omitempty"`
|
||
}
|
||
|
||
// StopSequences 兼容 OpenAI 对 stop 的两种写法:单个字符串或字符串数组。
|
||
// 直接用 []string 会让 `"stop": "END"` 这种合法请求整个解码失败(400)。
|
||
type StopSequences []string
|
||
|
||
func (s *StopSequences) UnmarshalJSON(data []byte) error {
|
||
if string(data) == "null" {
|
||
*s = nil
|
||
return nil
|
||
}
|
||
var one string
|
||
if err := json.Unmarshal(data, &one); err == nil {
|
||
if one == "" {
|
||
*s = nil
|
||
} else {
|
||
*s = StopSequences{one}
|
||
}
|
||
return nil
|
||
}
|
||
var many []string
|
||
if err := json.Unmarshal(data, &many); err != nil {
|
||
return fmt.Errorf("stop must be a string or an array of strings")
|
||
}
|
||
*s = many
|
||
return nil
|
||
}
|
||
|
||
type ChatCompletionRequest struct {
|
||
Model string `json:"model"`
|
||
Messages []ChatMessage `json:"messages"`
|
||
Stream bool `json:"stream,omitempty"`
|
||
Temperature *float64 `json:"temperature,omitempty"`
|
||
TopP *float64 `json:"top_p,omitempty"`
|
||
MaxTokens *int `json:"max_tokens,omitempty"`
|
||
MaxCompletionTok *int `json:"max_completion_tokens,omitempty"`
|
||
Tools []ToolDef `json:"tools,omitempty"`
|
||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||
User string `json:"user,omitempty"`
|
||
Stop StopSequences `json:"stop,omitempty"`
|
||
ResponseFormat *ResponseFormat `json:"response_format,omitempty"`
|
||
StreamOptions *StreamOptions `json:"stream_options,omitempty"`
|
||
N *int `json:"n,omitempty"`
|
||
Seed *int `json:"seed,omitempty"`
|
||
}
|
||
|
||
type ToolDef struct {
|
||
Type string `json:"type,omitempty"`
|
||
Function Function `json:"function"`
|
||
}
|
||
|
||
type Function struct {
|
||
Name string `json:"name"`
|
||
Description string `json:"description,omitempty"`
|
||
Parameters json.RawMessage `json:"parameters,omitempty"`
|
||
}
|
||
|
||
type ToolCall struct {
|
||
Index *int `json:"index,omitempty"`
|
||
ID string `json:"id"`
|
||
Type string `json:"type"`
|
||
Function FunctionCall `json:"function"`
|
||
}
|
||
|
||
// ToolCallDelta is one entry of a streamed tool_calls delta. Index is always
|
||
// present; the other fields are emitted only when they carry content.
|
||
type ToolCallDelta struct {
|
||
Index int `json:"index"`
|
||
ID string `json:"id,omitempty"`
|
||
Type string `json:"type,omitempty"`
|
||
Function *FunctionDelta `json:"function,omitempty"`
|
||
}
|
||
|
||
type FunctionDelta struct {
|
||
Name string `json:"name,omitempty"`
|
||
Arguments string `json:"arguments,omitempty"`
|
||
}
|
||
|
||
type FunctionCall struct {
|
||
Name string `json:"name"`
|
||
Arguments string `json:"arguments"`
|
||
}
|
||
|
||
type ResponsesRequest struct {
|
||
Model string `json:"model"`
|
||
Input json.RawMessage `json:"input"`
|
||
Stream bool `json:"stream,omitempty"`
|
||
Instructions string `json:"instructions,omitempty"`
|
||
Temperature *float64 `json:"temperature,omitempty"`
|
||
TopP *float64 `json:"top_p,omitempty"`
|
||
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
|
||
Tools []ResponsesTool `json:"tools,omitempty"`
|
||
ToolChoice json.RawMessage `json:"tool_choice,omitempty"`
|
||
// Text 是 Responses API 的输出配置块,text.format 承载输出格式约束
|
||
// (json_schema / json_object / text)。不解析它,客户端指定的
|
||
// json_schema 会被静默丢弃,模型输出的 JSON 无从校验。
|
||
Text *TextFormat `json:"text,omitempty"`
|
||
// Store defaults to true; store:false skips caching this response.
|
||
Store *bool `json:"store,omitempty"`
|
||
// PreviousResponseID continues a cached turn; its history is replayed
|
||
// before this request's input.
|
||
PreviousResponseID string `json:"previous_response_id,omitempty"`
|
||
}
|
||
|
||
// TextFormat 是 Responses API 的 text 对象,这里只消费 format。
|
||
type TextFormat struct {
|
||
Format *ResponseFormat `json:"format,omitempty"`
|
||
}
|
||
|
||
// ResponsesTool is a function tool in the flat Responses API shape; the nested
|
||
// Chat Completions shape is tolerated as well. Non-function tools (web search
|
||
// etc.) are dropped because the upstream cannot execute them.
|
||
type ResponsesTool struct {
|
||
Type string `json:"type"`
|
||
Name string `json:"name,omitempty"`
|
||
Description string `json:"description,omitempty"`
|
||
Parameters json.RawMessage `json:"parameters,omitempty"`
|
||
Strict bool `json:"strict,omitempty"`
|
||
Function *Function `json:"function,omitempty"`
|
||
}
|
||
|
||
// ToToolDef normalizes the tool onto the Chat Completions function shape.
|
||
func (t ResponsesTool) ToToolDef() (ToolDef, bool) {
|
||
if t.Function != nil && t.Function.Name != "" {
|
||
return ToolDef{Type: "function", Function: *t.Function}, true
|
||
}
|
||
if t.Name == "" || (t.Type != "" && t.Type != "function") {
|
||
return ToolDef{}, false
|
||
}
|
||
return ToolDef{Type: "function", Function: Function{
|
||
Name: t.Name, Description: t.Description, Parameters: t.Parameters,
|
||
}}, true
|
||
}
|
||
|
||
type ChoiceMessage struct {
|
||
Role string `json:"role"`
|
||
Content string `json:"content"`
|
||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||
}
|
||
|
||
type Choice struct {
|
||
Index int `json:"index"`
|
||
Message ChoiceMessage `json:"message"`
|
||
FinishReason string `json:"finish_reason"`
|
||
}
|
||
|
||
type Usage struct {
|
||
PromptTokens int `json:"prompt_tokens"`
|
||
CompletionTokens int `json:"completion_tokens"`
|
||
TotalTokens int `json:"total_tokens"`
|
||
}
|
||
|
||
type ChatCompletionResponse struct {
|
||
ID string `json:"id"`
|
||
Object string `json:"object"`
|
||
Created int64 `json:"created"`
|
||
Model string `json:"model"`
|
||
Choices []Choice `json:"choices"`
|
||
Usage Usage `json:"usage"`
|
||
}
|
||
|
||
type ChoiceDelta struct {
|
||
Role string `json:"role,omitempty"`
|
||
Content string `json:"content,omitempty"`
|
||
ToolCalls []ToolCallDelta `json:"tool_calls,omitempty"`
|
||
}
|
||
|
||
type StreamChoice struct {
|
||
Index int `json:"index"`
|
||
Delta ChoiceDelta `json:"delta"`
|
||
FinishReason *string `json:"finish_reason"`
|
||
}
|
||
|
||
type ChatCompletionChunk struct {
|
||
ID string `json:"id"`
|
||
Object string `json:"object"`
|
||
Created int64 `json:"created"`
|
||
Model string `json:"model"`
|
||
Choices []StreamChoice `json:"choices"`
|
||
Usage *Usage `json:"usage,omitempty"`
|
||
}
|
||
|
||
type ModelInfo struct {
|
||
ID string `json:"id"`
|
||
Object string `json:"object"`
|
||
Created int64 `json:"created"`
|
||
OwnedBy string `json:"owned_by"`
|
||
}
|
||
|
||
type ErrorBody struct {
|
||
Message string `json:"message"`
|
||
Type string `json:"type"`
|
||
Code any `json:"code,omitempty"`
|
||
}
|
||
|
||
type ErrorResponse struct {
|
||
Error ErrorBody `json:"error"`
|
||
}
|