package service import ( "encoding/json" "fmt" "strings" "oci-portal/internal/aiwire" ) // ResponsesToIR 把 Responses API 请求转换为网关 IR(OpenAI Chat 形态)。 // 有状态特性与内置工具不支持,直接报错(API 层 400)。 func ResponsesToIR(req aiwire.RespRequest) (aiwire.ChatRequest, error) { if err := respRejectUnsupported(req); err != nil { return aiwire.ChatRequest{}, err } ir := aiwire.ChatRequest{ Model: req.Model, MaxTokens: req.MaxOutputTokens, Temperature: req.Temperature, TopP: req.TopP, Stream: req.Stream, ToolChoice: respToolChoice(req.ToolChoice), } if req.Reasoning != nil { ir.ReasoningEffort = req.Reasoning.Effort } if req.Instructions != "" { ir.Messages = append(ir.Messages, aiwire.ChatMessage{Role: "system", Content: aiwire.NewTextContent(req.Instructions)}) } msgs, err := respInputToMessages(req.Input) if err != nil { return aiwire.ChatRequest{}, err } ir.Messages = append(ir.Messages, msgs...) ir.Tools = respTools(req.Tools) ir.ResponseFormat = respFormat(req.Text) return ir, nil } // respRejectUnsupported 拒绝无状态网关无法承接的请求特性。 func respRejectUnsupported(req aiwire.RespRequest) error { if req.PreviousResponseID != "" { return fmt.Errorf("previous_response_id 不支持:网关不保存历史响应,请在 input 中自带完整上下文(store:false 模式)") } if len(req.Conversation) > 0 && string(req.Conversation) != "null" { return fmt.Errorf("conversation 不支持:网关不保存对话状态") } if req.Background != nil && *req.Background { return fmt.Errorf("background 模式不支持") } for _, t := range req.Tools { if t.Type != "function" { return fmt.Errorf("不支持的工具类型 %q:仅支持 function 工具", t.Type) } } return nil } // respInputToMessages 把 input(string 或 item 数组)展开为 IR 消息序列。 func respInputToMessages(in aiwire.RespInput) ([]aiwire.ChatMessage, error) { if !in.IsArray { if strings.TrimSpace(in.Text) == "" { return nil, fmt.Errorf("input 不能为空") } return []aiwire.ChatMessage{{Role: "user", Content: aiwire.NewTextContent(in.Text)}}, nil } var msgs []aiwire.ChatMessage for _, it := range in.Items { out, err := respItemToMessages(it, msgs) if err != nil { return nil, err } msgs = out } return msgs, nil } // respItemToMessages 追加一个 input item;连续 function_call 合并进同一 assistant 消息。 func respItemToMessages(it aiwire.RespItem, msgs []aiwire.ChatMessage) ([]aiwire.ChatMessage, error) { switch it.Type { case "", "message": if it.Content.HasUnsupported() { return nil, ErrAiUnsupportedBlock } role := respRole(it.Role) return append(msgs, aiwire.ChatMessage{Role: role, Content: respContentToIR(it.Content)}), nil case "function_call": call := aiwire.ToolCall{ID: it.CallID, Type: "function", Function: aiwire.FunctionCall{Name: it.Name, Arguments: it.Arguments}} if n := len(msgs); n > 0 && msgs[n-1].Role == "assistant" && len(msgs[n-1].ToolCalls) > 0 { msgs[n-1].ToolCalls = append(msgs[n-1].ToolCalls, call) return msgs, nil } return append(msgs, aiwire.ChatMessage{Role: "assistant", ToolCalls: []aiwire.ToolCall{call}}), nil case "function_call_output": return append(msgs, aiwire.ChatMessage{Role: "tool", ToolCallID: it.CallID, Content: aiwire.NewTextContent(it.OutputText())}), nil case "reasoning": return msgs, nil // 推理块不回灌上游 default: return nil, fmt.Errorf("不支持的 input item 类型 %q", it.Type) } } // respRole 归一 item 角色;developer 视为 system,缺省 user。 func respRole(role string) string { switch role { case "system", "developer": return "system" case "assistant": return "assistant" default: return "user" } } // respContentToIR 把 Responses 内容转 IR;纯文本拍平,含图片时保留部件顺序。 func respContentToIR(c aiwire.RespContent) aiwire.Content { var parts []aiwire.ContentPart hasImage := false for _, p := range c.Parts { switch p.Type { case "input_image": parts = append(parts, aiwire.ContentPart{Type: "image_url", ImageURL: &aiwire.ImageURL{URL: p.ImageURL, Detail: p.Detail}}) hasImage = true default: if p.Text != "" { parts = append(parts, aiwire.ContentPart{Type: "text", Text: p.Text}) } } } if !c.IsArray || !hasImage { return aiwire.NewTextContent(c.JoinText()) } return aiwire.NewPartsContent(parts) } // respTools 把扁平工具定义还原为嵌套 Chat 形态;strict 无 OCI 对应,忽略。 func respTools(tools []aiwire.RespTool) []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.Parameters}}) } return out } // respToolChoice 转换 tool_choice:字符串直通;{type:function,name} 转嵌套; // allowed_tools 等对象形态降级 auto。 func respToolChoice(raw json.RawMessage) json.RawMessage { if len(raw) == 0 { return nil } t := strings.TrimSpace(string(raw)) if strings.HasPrefix(t, "\"") { return raw } var obj struct { Type string `json:"type"` Name string `json:"name"` } if err := json.Unmarshal(raw, &obj); err != nil || obj.Type != "function" || obj.Name == "" { return json.RawMessage(`"auto"`) } out, _ := json.Marshal(map[string]any{"type": "function", "function": map[string]string{"name": obj.Name}}) return out } // respFormat 把 text.format 转成 response_format;text 形态无需显式指定。 func respFormat(text *aiwire.RespText) *aiwire.ResponseFormat { if text == nil || text.Format == nil { return nil } switch text.Format.Type { case "json_object": return &aiwire.ResponseFormat{Type: "json_object"} case "json_schema": spec, _ := json.Marshal(map[string]any{ "name": text.Format.Name, "schema": json.RawMessage(text.Format.Schema), "strict": text.Format.Strict, }) return &aiwire.ResponseFormat{Type: "json_schema", JSONSchema: spec} default: return nil } } // IRRespToResponses 把 IR 非流式响应装配为 Response 对象。 func IRRespToResponses(resp *aiwire.ChatResponse, id string, created int64) aiwire.Response { out := aiwire.Response{ID: id, Object: "response", CreatedAt: created, Status: "completed", Model: resp.Model, Output: []aiwire.RespOutItem{}, Store: false} if len(resp.Choices) == 0 { return out } choice := resp.Choices[0] if text := choice.Message.Content.JoinText(); text != "" { out.Output = append(out.Output, respMessageItem(id, 0, text)) } for i, tc := range choice.Message.ToolCalls { out.Output = append(out.Output, aiwire.RespOutItem{Type: "function_call", ID: respItemID(id, "fc", len(out.Output)+i), Status: "completed", CallID: tc.ID, Name: tc.Function.Name, Arguments: respArgs(tc.Function.Arguments)}) } if choice.FinishReason == "length" { out.Status = "incomplete" out.IncompleteDetails = &aiwire.RespIncomplete{Reason: "max_output_tokens"} } out.Usage = respUsage(resp.Usage) return out } func respMessageItem(respID string, idx int, text string) aiwire.RespOutItem { return aiwire.RespOutItem{Type: "message", ID: respItemID(respID, "msg", idx), Status: "completed", Role: "assistant", Content: []aiwire.RespOutPart{{Type: "output_text", Text: text, Annotations: []any{}}}} } // respItemID 从响应 ID 派生确定性 item ID。 func respItemID(respID, kind string, idx int) string { return fmt.Sprintf("%s_%s_%d", kind, strings.TrimPrefix(respID, "resp_"), idx) } // respArgs 保证 arguments 是合法 JSON 字符串(空实参回退 {})。 func respArgs(args string) string { if strings.TrimSpace(args) == "" { return "{}" } return args } // respUsage 把 IR 用量改写为 Responses 命名口径。 func respUsage(u *aiwire.Usage) *aiwire.RespUsage { if u == nil { return nil } return &aiwire.RespUsage{InputTokens: u.PromptTokens, OutputTokens: u.CompletionTokens, TotalTokens: u.TotalTokens, InputTokensDetails: aiwire.RespInDetails{CachedTokens: u.CachedTokens()}} }