Files
dsfree2api/internal/openai/types.go
T
nyoung ed51efd8bc fix: 网关与控制台代码审查修复批次(含回归测试)
- Chat 接口拒绝 n>1(此前静默丢弃多余 choice),返回 400 结构化错误
- Responses 接口透传 text.format(json_schema / json_object),三种接口协议对齐
- TLS 指纹统一升级到 Chrome_150(internal/httpx.DefaultProfile),与 UA 匹配
- metrics 落盘失败不再静默:Save() 返回错误并打日志,临时文件失败会清理
- GET /api/config 脱敏 admin.password 与 turnstile.api_key(控制台从不展示,
  不应随 5s 轮询下发);PUT 改走 ReplaceJSON,先校验后替换、失败回滚
- set_limits / set_turnstile 改指针部分更新:缺字段保持现状,不会把并发、
  限流或求解开关按零值清掉
- 读请求体统一 64MB 上限;配置写盘原子替换 + 0600 权限
- main.go 改为 run() 返回退出码:日志落盘、指标落盘、xray 子进程回收全部
  挂 defer,任何退出路径(含启动失败)都会执行,杜绝孤儿 xray
- proxypool / turnstile / upstream / openai / config / httpx / logbuf 等
  逐项补红绿回归测试,新增 14 个测试文件,race 下全绿
2026-10-08 15:13:23 +08:00

264 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"`
}
// 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"`
}