368 lines
11 KiB
Go
368 lines
11 KiB
Go
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
|
|
}
|