package service import ( "encoding/json" "fmt" "strings" "oci-portal/internal/aiwire" ) // OpenAI Chat Completions ↔ OCI OpenAI 兼容面(/actions/v1/responses)直通转换。 // 端点定位 Tier 2 兼容层(承接只会说 CC 的存量客户端),机制与 anthresponses.go // 同构。语义损失(README 披露):stop / seed / n / penalty 等无对应字段,忽略; // 上游 reasoning 输出项与增量事件丢弃;非 function 工具类型拒绝。 // ChatToResponsesBody 把 Chat Completions 请求转为直通 body(强制 store:false)。 func ChatToResponsesBody(req aiwire.ChatRequest) ([]byte, error) { input, instructions, err := chatInputItems(req.Messages) if err != nil { return nil, err } body := map[string]any{"model": req.Model, "input": input, "store": false} if instructions != "" { body["instructions"] = instructions } chatSampling(body, req) if err := chatBodyTools(body, req); err != nil { return nil, err } if tf := chatTextFormat(req.ResponseFormat); tf != nil { body["text"] = map[string]any{"format": tf} } if req.ReasoningEffort != "" { body["reasoning"] = map[string]string{"effort": strings.ToLower(req.ReasoningEffort)} } if req.Stream { body["stream"] = true } return json.Marshal(body) } // chatSampling 装配采样与输出预算;max_completion_tokens 优先于已弃用的 max_tokens。 func chatSampling(body map[string]any, req aiwire.ChatRequest) { if req.MaxCompletionTokens != nil { body["max_output_tokens"] = *req.MaxCompletionTokens } else if req.MaxTokens != nil { body["max_output_tokens"] = *req.MaxTokens } if req.Temperature != nil { body["temperature"] = *req.Temperature } if req.TopP != nil { body["top_p"] = *req.TopP } if req.ParallelToolCalls != nil { body["parallel_tool_calls"] = *req.ParallelToolCalls } } // chatBodyTools 装配工具与 tool_choice;非 function 工具类型拒绝 // (web_search 等新能力仅在 Responses / Messages 端点供给)。 func chatBodyTools(body map[string]any, req aiwire.ChatRequest) error { if len(req.Tools) == 0 { return nil } tools := make([]map[string]any, 0, len(req.Tools)) for _, t := range req.Tools { if t.Type != "function" { return fmt.Errorf("不支持的工具类型 %q:该端点仅支持 function 工具", t.Type) } tools = append(tools, map[string]any{"type": "function", "name": t.Function.Name, "description": t.Function.Description, "parameters": t.Function.Parameters}) } body["tools"] = tools if tc := chatRespToolChoice(req.ToolChoice); tc != nil { body["tool_choice"] = tc } return nil } // chatInputItems 把消息序列展开为 Responses input 项与 instructions: // system/developer 文本聚为 instructions;tool 消息为 function_call_output 项; // 其余消息经 chatMessageItems 展开,保持相对顺序。 func chatInputItems(messages []aiwire.ChatMessage) ([]any, string, error) { items := make([]any, 0, len(messages)) var sys []string for _, m := range messages { switch m.Role { case "system", "developer": if txt := m.Content.JoinText(); txt != "" { sys = append(sys, txt) } case "tool": items = append(items, map[string]any{"type": "function_call_output", "call_id": m.ToolCallID, "output": m.Content.JoinText()}) default: msgItems, err := chatMessageItems(m) if err != nil { return nil, "", err } items = append(items, msgItems...) } } return items, strings.Join(sys, "\n\n"), nil } // chatMessageItems 把 user/assistant 消息转为 message 项;assistant 的 // tool_calls 追加为独立 function_call 项(内容在前,与原语序一致)。 func chatMessageItems(m aiwire.ChatMessage) ([]any, error) { parts, err := chatContentParts(m.Role, m.Content) if err != nil { return nil, err } items := make([]any, 0, 1+len(m.ToolCalls)) if len(parts) > 0 { items = append(items, map[string]any{"role": m.Role, "content": parts}) } for _, tc := range m.ToolCalls { items = append(items, map[string]any{"type": "function_call", "call_id": tc.ID, "name": tc.Function.Name, "arguments": tc.Function.Arguments}) } return items, nil } // chatContentParts 把消息内容转为 Responses 部件(文本按角色定型,图片转 input_image)。 func chatContentParts(role string, c aiwire.Content) ([]map[string]any, error) { if !c.IsArray { if c.Text == "" { return nil, nil } return []map[string]any{respTextPart(role, c.Text)}, nil } parts := make([]map[string]any, 0, len(c.Parts)) for _, p := range c.Parts { switch p.Type { case "", "text": parts = append(parts, respTextPart(role, p.Text)) case "image_url": if p.ImageURL == nil || p.ImageURL.URL == "" { return nil, fmt.Errorf("image_url 块缺少 url") } // detail 不透传:兼容面对该字段的支持无法证实(实测期间上游视觉请求不稳, // 无从归因),它仅是质量提示,忽略合法且消除一个风险轴(README 披露) parts = append(parts, map[string]any{"type": "input_image", "image_url": p.ImageURL.URL}) default: return nil, ErrAiUnsupportedBlock } } return parts, nil } // chatRespToolChoice 映射 tool_choice:string 形态(auto/none/required)原样; // {"type":"function","function":{"name":N}} 拍平为 Responses 具名形态;其余忽略。 func chatRespToolChoice(raw json.RawMessage) any { if len(raw) == 0 { return nil } var s string if json.Unmarshal(raw, &s) == nil { switch s { case "auto", "none", "required": return s } return nil } var tc struct { Type string `json:"type"` Function struct { Name string `json:"name"` } `json:"function"` } if json.Unmarshal(raw, &tc) != nil || tc.Type != "function" || tc.Function.Name == "" { return nil } return map[string]string{"type": "function", "name": tc.Function.Name} } // chatTextFormat 把 response_format 拍平为 text.format(json_schema 提升嵌套字段)。 func chatTextFormat(rf *aiwire.ResponseFormat) map[string]any { if rf == nil || rf.Type == "" || rf.Type == "text" { return nil } format := map[string]any{"type": rf.Type} if rf.Type != "json_schema" || len(rf.JSONSchema) == 0 { return format } var js struct { Name string `json:"name"` Schema json.RawMessage `json:"schema"` Strict *bool `json:"strict"` } if json.Unmarshal(rf.JSONSchema, &js) != nil { return format } if js.Name != "" { format["name"] = js.Name } if len(js.Schema) > 0 { format["schema"] = js.Schema } if js.Strict != nil { format["strict"] = *js.Strict } return format } // ResponsesToChat 把直通非流式响应转为 Chat Completions 响应(reasoning 项丢弃)。 func ResponsesToChat(payload []byte, id string, created int64) (*aiwire.ChatResponse, error) { var resp respPayload if err := json.Unmarshal(payload, &resp); err != nil { return nil, fmt.Errorf("解析上游响应: %w", err) } msg := aiwire.ChatMessage{Role: "assistant"} var text strings.Builder for _, item := range resp.Output { switch item.Type { case "message": for _, part := range item.Content { if part.Type == "output_text" { text.WriteString(part.Text) } } case "function_call": msg.ToolCalls = append(msg.ToolCalls, aiwire.ToolCall{ID: item.CallID, Type: "function", Function: aiwire.FunctionCall{Name: item.Name, Arguments: item.Arguments}}) } } msg.Content = aiwire.NewTextContent(text.String()) choice := aiwire.Choice{Index: 0, Message: msg, FinishReason: chatFinishReason(&resp, len(msg.ToolCalls) > 0)} return &aiwire.ChatResponse{ID: id, Object: "chat.completion", Created: created, Model: resp.Model, Choices: []aiwire.Choice{choice}, Usage: usageFromResp(resp.Usage)}, nil } // chatFinishReason 映射终态:截断优先 → length;含工具调用 → tool_calls;默认 stop。 func chatFinishReason(resp *respPayload, hasTools bool) string { if respTruncated(resp) { return "length" } if hasTools { return "tool_calls" } return "stop" } // respTruncated 报告直通响应是否因 max_output_tokens 截断。 func respTruncated(resp *respPayload) bool { return resp != nil && resp.Status == "incomplete" && resp.IncompleteDetails != nil && resp.IncompleteDetails.Reason == "max_output_tokens" } // ---- Chat Completions 流式桥:直通 SSE 事件 → chat.completion.chunk 序列 ---- // ChatRespBridge 把直通 SSE 事件流桥接为 chunk 序列:首个增量补 role,文本与 // 工具增量按 OpenAI 形态输出;reasoning 系列事件丢弃;[DONE] 由传输层写出。 type ChatRespBridge struct { id, model string created int64 includeUsage bool started bool toolIndex int inTool bool finish string usage *aiwire.Usage } // NewChatRespBridge 构造桥;includeUsage 即 stream_options.include_usage。 func NewChatRespBridge(id, model string, created int64, includeUsage bool) *ChatRespBridge { return &ChatRespBridge{id: id, model: model, created: created, includeUsage: includeUsage, toolIndex: -1, finish: "stop"} } // Feed 消费一行 SSE data JSON,返回应立即写出的 chunk。 func (b *ChatRespBridge) Feed(data []byte) []aiwire.ChatChunk { var ev respStreamEvent if json.Unmarshal(data, &ev) != nil { return nil } switch ev.Type { case "response.output_text.delta": b.inTool = false return []aiwire.ChatChunk{b.chunk(b.deltaWithRole(aiwire.Delta{Content: ev.Delta}), nil)} case "response.output_item.added": return b.toolOpen(ev.Item) case "response.function_call_arguments.delta": return b.argsChunk(ev.Delta) case "response.completed", "response.incomplete", "response.failed": b.finishFrom(ev.Response) } return nil } // chunk 装配一条单 choice 增量事件。 func (b *ChatRespBridge) chunk(delta aiwire.Delta, finish *string) aiwire.ChatChunk { return aiwire.ChatChunk{ID: b.id, Object: "chat.completion.chunk", Created: b.created, Model: b.model, Choices: []aiwire.ChunkChoice{{Index: 0, Delta: delta, FinishReason: finish}}} } // deltaWithRole 给首个增量补 role(OpenAI 首块携带 role 语义)。 func (b *ChatRespBridge) deltaWithRole(d aiwire.Delta) aiwire.Delta { if !b.started { b.started = true d.Role = "assistant" } return d } // toolOpen 在新 function_call 输出项开始时发 id/name 增量并推进聚合 index; // 非工具输出项只复位增量目标。 func (b *ChatRespBridge) toolOpen(item *respOutputItem) []aiwire.ChatChunk { if item == nil || item.Type != "function_call" { b.inTool = false return nil } b.toolIndex++ b.inTool = true b.finish = "tool_calls" d := aiwire.Delta{ToolCalls: []aiwire.ToolCallDelta{{Index: b.toolIndex, ID: item.CallID, Type: "function", Function: aiwire.FunctionCallDelta{Name: item.Name}}}} return []aiwire.ChatChunk{b.chunk(b.deltaWithRole(d), nil)} } // argsChunk 输出当前工具的实参增量(仅在工具输出项进行中)。 func (b *ChatRespBridge) argsChunk(delta string) []aiwire.ChatChunk { if !b.inTool { return nil } d := aiwire.Delta{ToolCalls: []aiwire.ToolCallDelta{{Index: b.toolIndex, Function: aiwire.FunctionCallDelta{Arguments: delta}}}} return []aiwire.ChatChunk{b.chunk(d, nil)} } // finishFrom 记录终态 usage 与截断语义(max_output_tokens → length)。 func (b *ChatRespBridge) finishFrom(resp *respPayload) { if resp == nil { return } b.usage = usageFromResp(resp.Usage) if respTruncated(resp) { b.finish = "length" } } // Finish 在上游流结束后收尾:终块带 finish_reason;include_usage 时追加 usage 块 // (choices 为空数组,OpenAI 规范形态;上游未给 usage 时输出零值)。 func (b *ChatRespBridge) Finish() []aiwire.ChatChunk { chunks := []aiwire.ChatChunk{b.chunk(b.deltaWithRole(aiwire.Delta{}), &b.finish)} if !b.includeUsage { return chunks } usage := b.usage if usage == nil { usage = &aiwire.Usage{} } return append(chunks, aiwire.ChatChunk{ID: b.id, Object: "chat.completion.chunk", Created: b.created, Model: b.model, Choices: []aiwire.ChunkChoice{}, Usage: usage}) } // Usage 返回聚合到的用量(供调用日志),上游未报告时为 nil。 func (b *ChatRespBridge) Usage() *aiwire.Usage { return b.usage } // ChatResponseChunks 把完整 Chat 响应展开为 chunk 序列(内容与工具调用 → // 终块 → 可选 usage 块),供流式上游断流后的非流式降级结果推送。 func ChatResponseChunks(resp *aiwire.ChatResponse, includeUsage bool) []aiwire.ChatChunk { if len(resp.Choices) == 0 { return nil } choice := resp.Choices[0] mk := func(delta aiwire.Delta, finish *string) aiwire.ChatChunk { return aiwire.ChatChunk{ID: resp.ID, Object: "chat.completion.chunk", Created: resp.Created, Model: resp.Model, Choices: []aiwire.ChunkChoice{{Index: 0, Delta: delta, FinishReason: finish}}} } delta := aiwire.Delta{Role: "assistant", Content: choice.Message.Content.Text} for i, tc := range choice.Message.ToolCalls { delta.ToolCalls = append(delta.ToolCalls, aiwire.ToolCallDelta{Index: i, ID: tc.ID, Type: "function", Function: aiwire.FunctionCallDelta{Name: tc.Function.Name, Arguments: tc.Function.Arguments}}) } finish := choice.FinishReason chunks := []aiwire.ChatChunk{mk(delta, nil), mk(aiwire.Delta{}, &finish)} if includeUsage { usage := resp.Usage if usage == nil { usage = &aiwire.Usage{} } chunks = append(chunks, aiwire.ChatChunk{ID: resp.ID, Object: "chat.completion.chunk", Created: resp.Created, Model: resp.Model, Choices: []aiwire.ChunkChoice{}, Usage: usage}) } return chunks }