package oci import ( "encoding/json" "strings" "github.com/oracle/oci-go-sdk/v65/generativeaiinference" "oci-portal/internal/aiwire" ) // irToSDK 把 IR(OpenAI 线格式)转为 OCI GENERIC 聊天请求;字段近一一镜像。 func irToSDK(ir aiwire.ChatRequest, stream bool) generativeaiinference.GenericChatRequest { req := generativeaiinference.GenericChatRequest{ Messages: irMessages(ir.Messages), MaxTokens: ir.MaxTokens, MaxCompletionTokens: ir.MaxCompletionTokens, Temperature: ir.Temperature, TopP: ir.TopP, TopK: ir.TopK, FrequencyPenalty: ir.FrequencyPenalty, PresencePenalty: ir.PresencePenalty, Seed: ir.Seed, NumGenerations: ir.N, Tools: irTools(ir.Tools), ToolChoice: irToolChoice(ir.ToolChoice), ResponseFormat: irResponseFormat(ir.ResponseFormat), } if len(ir.Stop) > 0 { req.Stop = ir.Stop } if ir.ReasoningEffort != "" { req.ReasoningEffort = generativeaiinference.GenericChatRequestReasoningEffortEnum(strings.ToUpper(ir.ReasoningEffort)) } if stream { req.IsStream = &stream includeUsage := ir.StreamOptions == nil || ir.StreamOptions.IncludeUsage req.StreamOptions = &generativeaiinference.StreamOptions{IsIncludeUsage: &includeUsage} } return req } // irMessages 按 role 拆装消息;user 消息保留图文块,其余角色只取文本。 func irMessages(msgs []aiwire.ChatMessage) []generativeaiinference.Message { out := make([]generativeaiinference.Message, 0, len(msgs)) for _, m := range msgs { switch m.Role { case "system", "developer": out = append(out, generativeaiinference.SystemMessage{Content: textContents(m.Content.JoinText())}) case "assistant": out = append(out, generativeaiinference.AssistantMessage{ Content: textContents(m.Content.JoinText()), ToolCalls: irToolCalls(m.ToolCalls), }) case "tool": tm := generativeaiinference.ToolMessage{Content: textContents(m.Content.JoinText())} if m.ToolCallID != "" { id := m.ToolCallID tm.ToolCallId = &id } out = append(out, tm) default: // user out = append(out, generativeaiinference.UserMessage{Content: chatContents(m.Content)}) } } return out } // chatContents 把 IR 内容转 SDK 内容块;文本直通,image_url 转 IMAGE 块(data URI / 公网地址)。 func chatContents(c aiwire.Content) []generativeaiinference.ChatContent { if len(c.Parts) == 0 { return textContents(c.Text) } var out []generativeaiinference.ChatContent for _, p := range c.Parts { switch { case p.Type == "image_url" && p.ImageURL != nil: img := generativeaiinference.ImageUrl{Url: &p.ImageURL.URL} if d, ok := generativeaiinference.GetMappingImageUrlDetailEnum(p.ImageURL.Detail); ok { img.Detail = d } out = append(out, generativeaiinference.ImageContent{ImageUrl: &img}) case p.Text != "": t := p.Text out = append(out, generativeaiinference.TextContent{Text: &t}) } } return out } // textContents 把纯文本包装为单元素 TextContent 数组;空文本返回 nil。 func textContents(text string) []generativeaiinference.ChatContent { if text == "" { return nil } return []generativeaiinference.ChatContent{generativeaiinference.TextContent{Text: &text}} } func irToolCalls(calls []aiwire.ToolCall) []generativeaiinference.ToolCall { if len(calls) == 0 { return nil } out := make([]generativeaiinference.ToolCall, 0, len(calls)) for _, c := range calls { id, name, args := c.ID, c.Function.Name, c.Function.Arguments out = append(out, generativeaiinference.FunctionCall{Id: &id, Name: &name, Arguments: &args}) } return out } func irTools(tools []aiwire.Tool) []generativeaiinference.ToolDefinition { if len(tools) == 0 { return nil } out := make([]generativeaiinference.ToolDefinition, 0, len(tools)) for _, t := range tools { name, desc := t.Function.Name, t.Function.Description fd := generativeaiinference.FunctionDefinition{Name: &name} if desc != "" { fd.Description = &desc } if len(t.Function.Parameters) > 0 { var params interface{} if json.Unmarshal(t.Function.Parameters, ¶ms) == nil { fd.Parameters = ¶ms } } out = append(out, fd) } return out } // irToolChoice 解析 "auto"/"none"/"required" 或 {type:function,function:{name}}。 func irToolChoice(raw json.RawMessage) generativeaiinference.ToolChoice { if len(raw) == 0 { return nil } var s string if json.Unmarshal(raw, &s) == nil { switch s { case "none": return generativeaiinference.ToolChoiceNone{} case "required": return generativeaiinference.ToolChoiceRequired{} case "auto": return generativeaiinference.ToolChoiceAuto{} } return nil } var obj struct { Function struct { Name string `json:"name"` } `json:"function"` } if json.Unmarshal(raw, &obj) == nil && obj.Function.Name != "" { return generativeaiinference.ToolChoiceFunction{Name: &obj.Function.Name} } return nil } func irResponseFormat(rf *aiwire.ResponseFormat) generativeaiinference.ResponseFormat { if rf == nil { return nil } switch rf.Type { case "json_object": return generativeaiinference.JsonObjectResponseFormat{} case "json_schema": return irJSONSchemaFormat(rf.JSONSchema) default: return nil } } // irJSONSchemaFormat 映射 OpenAI json_schema{name,description,schema,strict}。 func irJSONSchemaFormat(raw json.RawMessage) generativeaiinference.ResponseFormat { var in struct { Name string `json:"name"` Description string `json:"description"` Schema json.RawMessage `json:"schema"` Strict *bool `json:"strict"` } if json.Unmarshal(raw, &in) != nil || in.Name == "" { return generativeaiinference.JsonObjectResponseFormat{} } js := generativeaiinference.ResponseJsonSchema{Name: &in.Name, IsStrict: in.Strict} if in.Description != "" { js.Description = &in.Description } if len(in.Schema) > 0 { var schema interface{} if json.Unmarshal(in.Schema, &schema) == nil { js.Schema = &schema } } return generativeaiinference.JsonSchemaResponseFormat{JsonSchema: &js} } // sdkToIR 把非流式 GenericChatResponse 转回 IR 响应;id/created 由调用方补。 func sdkToIR(resp generativeaiinference.GenericChatResponse, model string) *aiwire.ChatResponse { out := &aiwire.ChatResponse{Object: "chat.completion", Model: model} if resp.TimeCreated != nil { out.Created = resp.TimeCreated.Unix() } for i, ch := range resp.Choices { idx := i if ch.Index != nil { idx = *ch.Index } out.Choices = append(out.Choices, aiwire.Choice{ Index: idx, Message: sdkMessageToIR(ch.Message), FinishReason: mapFinishReason(deref(ch.FinishReason)), }) } out.Usage = sdkUsageToIR(resp.Usage) return out } func sdkUsageToIR(u *generativeaiinference.Usage) *aiwire.Usage { if u == nil { return nil } out := &aiwire.Usage{} if u.PromptTokens != nil { out.PromptTokens = *u.PromptTokens } if u.CompletionTokens != nil { out.CompletionTokens = *u.CompletionTokens } if u.TotalTokens != nil { out.TotalTokens = *u.TotalTokens } if d := u.PromptTokensDetails; d != nil && d.CachedTokens != nil && *d.CachedTokens > 0 { out.PromptTokensDetails = &aiwire.PromptTokensDetails{CachedTokens: *d.CachedTokens} } return out } // sdkMessageToIR 提取助手消息文本与工具调用。 func sdkMessageToIR(m generativeaiinference.Message) aiwire.ChatMessage { out := aiwire.ChatMessage{Role: "assistant"} am, ok := m.(generativeaiinference.AssistantMessage) if !ok { return out } var sb strings.Builder for _, c := range am.Content { if tc, ok := c.(generativeaiinference.TextContent); ok && tc.Text != nil { sb.WriteString(*tc.Text) } } out.Content = aiwire.NewTextContent(sb.String()) for _, call := range am.ToolCalls { if fc, ok := call.(generativeaiinference.FunctionCall); ok { out.ToolCalls = append(out.ToolCalls, aiwire.ToolCall{ ID: deref(fc.Id), Type: "function", Function: aiwire.FunctionCall{ Name: deref(fc.Name), Arguments: deref(fc.Arguments), }, }) } } return out } // mapFinishReason 归一化结束原因;OCI 与 OpenAI 取值同名,做小写与别名兜底。 func mapFinishReason(reason string) string { switch strings.ToLower(reason) { case "", "null": return "" case "stop", "completed", "end_turn": return "stop" case "length", "max_tokens": return "length" case "tool_calls", "tool_call", "tool_use": return "tool_calls" case "complete": return "stop" case "error_toxic": return "content_filter" default: return strings.ToLower(reason) } } // genAiStreamEvent 是 GENERIC 流式事件的宽容解析结构(字段按增量 choice 形状)。 type genAiStreamEvent struct { Index *int `json:"index"` Message struct { Role string `json:"role"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` ToolCalls []struct { ID string `json:"id"` Name string `json:"name"` Arguments string `json:"arguments"` } `json:"toolCalls"` } `json:"message"` FinishReason *string `json:"finishReason"` Usage *generativeaiinference.Usage `json:"usage"` Choices []json.RawMessage `json:"choices"` // 兼容整包 choices 形态 Text string `json:"text"` // COHERE 流事件的顶层增量文本 // CohereCalls 是 COHERE 流事件顶层的工具调用(与 GENERIC 的 message.toolCalls 不同层级) CohereCalls []struct { Name string `json:"name"` Parameters interface{} `json:"parameters"` } `json:"toolCalls"` } // parseGenAiEvent 把一条 SSE 事件 JSON 解析为 IR chunk;无有效负载时 ok=false。 func parseGenAiEvent(data []byte, model string) (aiwire.ChatChunk, bool) { var ev genAiStreamEvent if err := json.Unmarshal(data, &ev); err != nil { return aiwire.ChatChunk{}, false } if len(ev.Choices) > 0 { // choices 包裹形态:取首个展开重解析 var inner genAiStreamEvent if json.Unmarshal(ev.Choices[0], &inner) == nil { inner.Usage = ev.Usage ev = inner } } chunk := aiwire.ChatChunk{Object: "chat.completion.chunk", Model: model} choice := aiwire.ChunkChoice{} if ev.Index != nil { choice.Index = *ev.Index } choice.Delta.Role = strings.ToLower(ev.Message.Role) var sb strings.Builder for _, c := range ev.Message.Content { sb.WriteString(c.Text) } if sb.Len() == 0 && ev.Text != "" { // COHERE 事件形态 sb.WriteString(ev.Text) } choice.Delta.Content = sb.String() for i, tc := range ev.Message.ToolCalls { d := aiwire.ToolCallDelta{Index: i, ID: tc.ID} if tc.ID != "" { d.Type = "function" } d.Function.Name = tc.Name d.Function.Arguments = tc.Arguments choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, d) } for i, tc := range ev.CohereCalls { // COHERE 顶层工具调用:整包出现,补派生 ID call := cohereCallToIR(tc.Name, &tc.Parameters, i) d := aiwire.ToolCallDelta{Index: len(choice.Delta.ToolCalls) + i, ID: call.ID, Type: "function"} d.Function.Name = call.Function.Name d.Function.Arguments = call.Function.Arguments choice.Delta.ToolCalls = append(choice.Delta.ToolCalls, d) } if r := mapFinishReason(deref(ev.FinishReason)); r != "" { choice.FinishReason = &r } hasPayload := choice.Delta.Content != "" || len(choice.Delta.ToolCalls) > 0 || choice.FinishReason != nil if hasPayload { chunk.Choices = []aiwire.ChunkChoice{choice} } chunk.Usage = sdkUsageToIR(ev.Usage) return chunk, hasPayload || chunk.Usage != nil }