package service import ( "encoding/json" "fmt" "strings" "oci-portal/internal/aiwire" ) // ErrAiUnsupportedBlock 表示请求含网关无法承接的内容块(仅支持文本与图片)。 var ErrAiUnsupportedBlock = fmt.Errorf("暂不支持文本与图片以外的内容块") // AnthropicToIR 把 Anthropic Messages 请求转为 IR(OpenAI 线格式)。 // tool_result 块拆为独立 tool 角色消息(一拆多),顶层 system 变首条 system 消息。 func AnthropicToIR(req aiwire.MessagesRequest) (aiwire.ChatRequest, error) { ir := aiwire.ChatRequest{ Model: req.Model, Temperature: req.Temperature, TopP: req.TopP, TopK: req.TopK, Stop: req.StopSequences, Stream: req.Stream, Tools: anthTools(req.Tools), ToolChoice: anthToolChoice(req.ToolChoice), } if req.MaxTokens > 0 { mt := req.MaxTokens ir.MaxTokens = &mt } if sys := req.SystemText(); sys != "" { ir.Messages = append(ir.Messages, aiwire.ChatMessage{Role: "system", Content: aiwire.NewTextContent(sys)}) } for _, m := range req.Messages { msgs, err := anthMessageToIR(m) if err != nil { return ir, err } ir.Messages = append(ir.Messages, msgs...) } return ir, nil } // anthMessageToIR 拆解单条 Anthropic 消息;user 消息里的 tool_result 前置为独立 tool 消息, // image 块转为 IR image_url 部件。 func anthMessageToIR(m aiwire.AnthMessage) ([]aiwire.ChatMessage, error) { var out []aiwire.ChatMessage var parts []aiwire.ContentPart var toolCalls []aiwire.ToolCall hasImage := false for _, b := range m.Content.AllBlocks() { switch b.Type { case "text": parts = append(parts, aiwire.ContentPart{Type: "text", Text: b.Text}) case "image": p, err := anthImagePart(b.Source) if err != nil { return nil, err } parts, hasImage = append(parts, p), true case "tool_use": toolCalls = append(toolCalls, aiwire.ToolCall{ID: b.ID, Type: "function", Function: aiwire.FunctionCall{Name: b.Name, Arguments: string(b.Input)}}) case "tool_result": out = append(out, aiwire.ChatMessage{Role: "tool", ToolCallID: b.ToolUseID, Content: aiwire.NewTextContent(b.ResultText())}) case "thinking", "redacted_thinking": // 一期忽略 thinking 块 default: return nil, ErrAiUnsupportedBlock } } content := partsContent(parts, hasImage) if hasImage || content.JoinText() != "" || len(toolCalls) > 0 { out = append(out, aiwire.ChatMessage{Role: m.Role, Content: content, ToolCalls: toolCalls}) } return out, nil } // anthImagePart 把 Anthropic image 块转为 IR image_url 部件(base64 → data URI)。 func anthImagePart(source json.RawMessage) (aiwire.ContentPart, error) { var src struct { Type string `json:"type"` MediaType string `json:"media_type"` Data string `json:"data"` URL string `json:"url"` } if err := json.Unmarshal(source, &src); err != nil { return aiwire.ContentPart{}, fmt.Errorf("image source 解析失败: %w", err) } switch src.Type { case "base64": if src.MediaType == "" || src.Data == "" { return aiwire.ContentPart{}, fmt.Errorf("image source 缺少 media_type 或 data") } url := "data:" + src.MediaType + ";base64," + src.Data return aiwire.ContentPart{Type: "image_url", ImageURL: &aiwire.ImageURL{URL: url}}, nil case "url": if src.URL == "" { return aiwire.ContentPart{}, fmt.Errorf("image source 缺少 url") } return aiwire.ContentPart{Type: "image_url", ImageURL: &aiwire.ImageURL{URL: src.URL}}, nil } return aiwire.ContentPart{}, fmt.Errorf("不支持的 image source 类型 %q", src.Type) } // partsContent 无图片时退回字符串形态(与上游线格式习惯一致),含图片时保留块数组。 func partsContent(parts []aiwire.ContentPart, hasImage bool) aiwire.Content { if !hasImage { var sb strings.Builder for _, p := range parts { sb.WriteString(p.Text) } return aiwire.NewTextContent(sb.String()) } return aiwire.NewPartsContent(parts) } func anthTools(tools []aiwire.AnthTool) []aiwire.Tool { if len(tools) == 0 { return nil } out := make([]aiwire.Tool, 0, len(tools)) for _, t := range tools { out = append(out, aiwire.Tool{Type: "function", Function: aiwire.FunctionDef{ Name: t.Name, Description: t.Description, Parameters: t.InputSchema}}) } return out } // anthToolChoice 映射 {type:auto|any|tool,name} → OpenAI 形态。 func anthToolChoice(raw json.RawMessage) json.RawMessage { if len(raw) == 0 { return nil } var tc struct { Type string `json:"type"` Name string `json:"name"` } if json.Unmarshal(raw, &tc) != nil { return nil } switch tc.Type { case "auto": return json.RawMessage(`"auto"`) case "any": return json.RawMessage(`"required"`) case "tool": b, _ := json.Marshal(map[string]any{"type": "function", "function": map[string]string{"name": tc.Name}}) return b case "none": return json.RawMessage(`"none"`) } return nil } // IRRespToAnthropic 把 IR 非流式响应转为 Anthropic Messages 响应。 func IRRespToAnthropic(resp *aiwire.ChatResponse, id string) aiwire.MessagesResponse { out := aiwire.MessagesResponse{ID: id, Type: "message", Role: "assistant", Model: resp.Model, Content: []aiwire.AnthBlock{}} if len(resp.Choices) > 0 { choice := resp.Choices[0] if text := choice.Message.Content.JoinText(); text != "" { out.Content = append(out.Content, aiwire.AnthBlock{Type: "text", Text: text}) } for _, tc := range choice.Message.ToolCalls { out.Content = append(out.Content, aiwire.AnthBlock{Type: "tool_use", ID: tc.ID, Name: tc.Function.Name, Input: argsToJSON(tc.Function.Arguments)}) } out.StopReason = anthStopReason(choice.FinishReason) } if resp.Usage != nil { out.Usage = aiwire.AnthUsage{InputTokens: resp.Usage.PromptTokens, OutputTokens: resp.Usage.CompletionTokens, CacheReadInputTokens: resp.Usage.CachedTokens()} } return out } // argsToJSON 保证 tool_use.input 是合法 JSON 对象(模型可能产出非法片段)。 func argsToJSON(args string) json.RawMessage { trimmed := strings.TrimSpace(args) if trimmed == "" { return json.RawMessage(`{}`) } if json.Valid([]byte(trimmed)) { return json.RawMessage(trimmed) } b, _ := json.Marshal(map[string]string{"_raw": args}) return b } func anthStopReason(finish string) string { switch finish { case "length": return "max_tokens" case "tool_calls": return "tool_use" default: return "end_turn" } } // ---- Anthropic 流式状态机 ---- // AnthEvent 是一条待写出的 Anthropic SSE 事件。 type AnthEvent struct { Event string Data any } // AnthStream 把 IR chunk 流聚合为 Anthropic 事件序列: // message_start → content_block_start/delta/stop(text 与 tool_use 分块)→ message_delta → message_stop。 type AnthStream struct { id, model string started bool blockOpen bool blockIsTool bool toolID string blockIndex int stopReason string usage aiwire.AnthUsage } // NewAnthStream 构造状态机;id 为响应消息 ID。 func NewAnthStream(id, model string) *AnthStream { return &AnthStream{id: id, model: model, blockIndex: -1, stopReason: "end_turn"} } // Feed 消费一个 IR chunk,返回应立即写出的事件。 func (st *AnthStream) Feed(chunk aiwire.ChatChunk) []AnthEvent { var events []AnthEvent if !st.started { st.started = true events = append(events, st.startEvent()) } if chunk.Usage != nil { st.usage = aiwire.AnthUsage{InputTokens: chunk.Usage.PromptTokens, OutputTokens: chunk.Usage.CompletionTokens, CacheReadInputTokens: chunk.Usage.CachedTokens()} } for _, choice := range chunk.Choices { events = append(events, st.feedDelta(choice.Delta)...) if choice.FinishReason != nil && *choice.FinishReason != "" { st.stopReason = anthStopReason(*choice.FinishReason) } } return events } func (st *AnthStream) startEvent() AnthEvent { return AnthEvent{Event: "message_start", Data: map[string]any{ "type": "message_start", "message": map[string]any{ "id": st.id, "type": "message", "role": "assistant", "model": st.model, "content": []any{}, "stop_reason": nil, "usage": map[string]int{"input_tokens": 0, "output_tokens": 0}, }, }} } // feedDelta 处理文本与工具调用增量,必要时切块。 func (st *AnthStream) feedDelta(d aiwire.Delta) []AnthEvent { var events []AnthEvent if d.Content != "" { if !st.blockOpen || st.blockIsTool { events = append(events, st.openBlock(false, "", "")...) } events = append(events, AnthEvent{Event: "content_block_delta", Data: map[string]any{ "type": "content_block_delta", "index": st.blockIndex, "delta": map[string]string{"type": "text_delta", "text": d.Content}, }}) } for _, tc := range d.ToolCalls { if tc.ID != "" && (!st.blockOpen || !st.blockIsTool || st.toolID != tc.ID) { events = append(events, st.openBlock(true, tc.ID, tc.Function.Name)...) } if tc.Function.Arguments != "" && st.blockOpen && st.blockIsTool { events = append(events, AnthEvent{Event: "content_block_delta", Data: map[string]any{ "type": "content_block_delta", "index": st.blockIndex, "delta": map[string]string{"type": "input_json_delta", "partial_json": tc.Function.Arguments}, }}) } } return events } // openBlock 关闭当前块并打开新块(text 或 tool_use)。 func (st *AnthStream) openBlock(isTool bool, toolID, toolName string) []AnthEvent { var events []AnthEvent if st.blockOpen { events = append(events, st.closeBlockEvent()) } st.blockOpen, st.blockIsTool, st.toolID = true, isTool, toolID st.blockIndex++ block := map[string]any{"type": "text", "text": ""} if isTool { block = map[string]any{"type": "tool_use", "id": toolID, "name": toolName, "input": map[string]any{}} } events = append(events, AnthEvent{Event: "content_block_start", Data: map[string]any{ "type": "content_block_start", "index": st.blockIndex, "content_block": block, }}) return events } func (st *AnthStream) closeBlockEvent() AnthEvent { return AnthEvent{Event: "content_block_stop", Data: map[string]any{ "type": "content_block_stop", "index": st.blockIndex, }} } // Finish 在上游流结束后收尾:关块 → message_delta(stop_reason+usage)→ message_stop。 func (st *AnthStream) Finish() []AnthEvent { var events []AnthEvent if !st.started { events = append(events, st.startEvent()) st.started = true } if st.blockOpen { events = append(events, st.closeBlockEvent()) st.blockOpen = false } events = append(events, AnthEvent{Event: "message_delta", Data: map[string]any{ "type": "message_delta", "delta": map[string]any{"stop_reason": st.stopReason, "stop_sequence": nil}, "usage": map[string]int{"output_tokens": st.usage.OutputTokens}, }}, AnthEvent{Event: "message_stop", Data: map[string]any{"type": "message_stop"}}, ) return events } // Usage 返回聚合到的用量(供调用日志)。 func (st *AnthStream) Usage() aiwire.AnthUsage { return st.usage }