refactor(model): 拆 openai.go 652→216行(纯搬移零逻辑改动) — encode/response/stream 三件套对齐 anthropic 布局
- openai.go 216(适配器+Chat/ChatStream+共享助手); openai_encode.go 202(请求wire+build*); openai_response.go 79(响应wire+responseFromWire); openai_stream.go 174(scanSSE) - 字节级比对 body identical; 导出API不变(go doc diff 空); gofmt 干净 - 纯净基线(HEAD+本改)全仓 go build/vet/test 绿 + win/darwin 交叉编译绿 - 遗留: scanSSE 138行(>80, 拆分前既有,零逻辑改动未动), 另开步骤重构
This commit is contained in:
1 parent
ca2faa0f95
commit
610b330025
4 files changed
+455
-436
No files matched your search
@@ -0,0 +1,202 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// openAIChatRequest is the JSON body sent to /chat/completions. Field names
|
||||
// match the provider protocol so the struct marshals directly.
|
||||
type openAIChatRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []openAIMessage `json:"messages"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
MaxTokens int `json:"max_tokens,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
StreamOptions *openAIStreamOpts `json:"stream_options,omitempty"`
|
||||
Tools []openAITool `json:"tools,omitempty"`
|
||||
}
|
||||
|
||||
// openAIStreamOpts asks the provider to include usage in the final stream chunk.
|
||||
type openAIStreamOpts struct {
|
||||
IncludeUsage bool `json:"include_usage"`
|
||||
}
|
||||
|
||||
// openAIMessage is one request message in the wire body. Content is either a
|
||||
// plain string (text-only) or an array of {type,text|image_url} parts. Tool
|
||||
// messages carry ToolCallID (required by OpenAI); assistant messages that
|
||||
// requested functions carry ToolCalls so the following tool messages can be
|
||||
// correlated.
|
||||
type openAIMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content any `json:"content"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
ToolCalls []openAIToolCall `json:"tool_calls,omitempty"`
|
||||
}
|
||||
|
||||
// openAIToolCall is the wire form of one assistant tool_call in a request. OpenAI
|
||||
// requires each subsequent tool message to reference the id back to these calls.
|
||||
type openAIToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function openAIToolCallFn `json:"function"`
|
||||
}
|
||||
|
||||
// openAITool is the wire form of a function tool schema (M2 placeholder).
|
||||
type openAITool struct {
|
||||
Type string `json:"type"`
|
||||
Function openAIToolFn `json:"function"`
|
||||
}
|
||||
|
||||
// openAIToolFn carries the function definition.
|
||||
type openAIToolFn struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Parameters any `json:"parameters,omitempty"`
|
||||
}
|
||||
|
||||
// openAIToolCallFn is the function part of a streaming tool-call fragment.
|
||||
type openAIToolCallFn struct {
|
||||
Name string `json:"name"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
func (a *OpenAIChatAdapter) buildRequest(req Request, stream bool) ([]byte, error) {
|
||||
msgs, err := buildMessages(req.Messages)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wire := openAIChatRequest{
|
||||
Model: req.Model,
|
||||
Messages: msgs,
|
||||
Temperature: req.Temperature,
|
||||
MaxTokens: req.MaxTokens,
|
||||
Stream: stream,
|
||||
Tools: buildTools(req.Tools),
|
||||
}
|
||||
if stream {
|
||||
wire.StreamOptions = &openAIStreamOpts{IncludeUsage: true}
|
||||
}
|
||||
buf, err := json.Marshal(wire)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("model: %w: marshal request: %v", ErrBadRequest, err)
|
||||
}
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func buildMessages(msgs []Message) ([]openAIMessage, error) {
|
||||
out := make([]openAIMessage, 0, len(msgs))
|
||||
for _, m := range msgs {
|
||||
wire, err := encodeOpenAIMessage(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, wire)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// encodeOpenAIMessage renders one normalized message in OpenAI wire form. A
|
||||
// RoleTool turn becomes a tool message carrying tool_call_id (required by OpenAI,
|
||||
// and the audit's gap); an assistant turn that advertised tool calls carries the
|
||||
// corresponding tool_calls array so the provider can correlate the tool messages.
|
||||
func encodeOpenAIMessage(m Message) (openAIMessage, error) {
|
||||
if m.Role == RoleTool {
|
||||
return openAIMessage{
|
||||
Role: string(RoleTool),
|
||||
ToolCallID: toolMessageCallID(m),
|
||||
Content: messageText(m),
|
||||
}, nil
|
||||
}
|
||||
content, err := encodeContent(m.Content)
|
||||
if err != nil {
|
||||
return openAIMessage{}, err
|
||||
}
|
||||
return openAIMessage{Role: string(m.Role), Content: content, ToolCalls: encodeToolCalls(m.Content)}, nil
|
||||
}
|
||||
|
||||
// toolMessageCallID resolves the tool_call_id for a RoleTool turn: the
|
||||
// message-level id, else the id carried on the first tool-result part.
|
||||
func toolMessageCallID(m Message) string {
|
||||
if m.ToolCallID != "" {
|
||||
return m.ToolCallID
|
||||
}
|
||||
for _, p := range m.Content {
|
||||
if p.ToolCallID != "" {
|
||||
return p.ToolCallID
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// encodeToolCalls builds the assistant tool_calls array from a turn's tool-call
|
||||
// parts. Arguments are serialized to JSON because OpenAI expects a string.
|
||||
func encodeToolCalls(parts []Content) []openAIToolCall {
|
||||
calls := make([]openAIToolCall, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
if p.Type != ContentTypeToolCall {
|
||||
continue
|
||||
}
|
||||
arguments := "{}"
|
||||
if len(p.ToolArgs) > 0 {
|
||||
if b, err := json.Marshal(p.ToolArgs); err == nil {
|
||||
arguments = string(b)
|
||||
}
|
||||
}
|
||||
calls = append(calls, openAIToolCall{
|
||||
ID: p.ToolCallID,
|
||||
Type: "function",
|
||||
Function: openAIToolCallFn{Name: p.ToolCallName, Arguments: arguments},
|
||||
})
|
||||
}
|
||||
return calls
|
||||
}
|
||||
|
||||
func encodeContent(parts []Content) (any, error) {
|
||||
if len(parts) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
arr := make([]any, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
switch p.Type {
|
||||
case ContentTypeText:
|
||||
arr = append(arr, map[string]any{"type": "text", "text": p.Text})
|
||||
case ContentTypeImage:
|
||||
arr = append(arr, map[string]any{
|
||||
"type": "image_url",
|
||||
"image_url": map[string]any{"url": p.ImageURL},
|
||||
})
|
||||
case ContentTypeToolCall, ContentTypeToolResult:
|
||||
// Tool-call and tool-result parts are encoded at the message level
|
||||
// (tool_calls array / tool_call_id), never as text-image content.
|
||||
continue
|
||||
default:
|
||||
return nil, fmt.Errorf("model: %w: unsupported content type %q", ErrBadRequest, p.Type)
|
||||
}
|
||||
}
|
||||
if len(arr) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
if len(arr) == 1 {
|
||||
if blk, ok := arr[0].(map[string]any); ok && blk["type"] == "image_url" {
|
||||
return arr, nil
|
||||
}
|
||||
return arr[0].(map[string]any)["text"], nil
|
||||
}
|
||||
return arr, nil
|
||||
}
|
||||
|
||||
func buildTools(tools []ToolSpec) []openAITool {
|
||||
if len(tools) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]openAITool, 0, len(tools))
|
||||
for _, t := range tools {
|
||||
out = append(out, openAITool{Type: "function", Function: openAIToolFn{
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
Parameters: t.Parameters,
|
||||
}})
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user