| package builtin |
|
|
| import ( |
| "context" |
| "fmt" |
| "strings" |
|
|
| "ccLoad/internal/protocol" |
|
|
| "github.com/bytedance/sonic" |
| ) |
|
|
| type pendingToolCall struct { |
| id string |
| name string |
| arguments string |
| } |
|
|
| type openAIToCodexStreamState struct { |
| model string |
| usage struct { |
| promptTokens int64 |
| completionTokens int64 |
| totalTokens int64 |
| cachedTokens int64 |
| cacheCreationInputTokens int64 |
| reasoningTokens int64 |
| seen bool |
| } |
| reasoningText string |
| reasoningEncrypted string |
| toolCalls map[int]*pendingToolCall |
| } |
|
|
| type codexToOpenAIStreamState struct { |
| model string |
| usage struct { |
| inputTokens int64 |
| outputTokens int64 |
| totalTokens int64 |
| cachedTokens int64 |
| cacheCreationInputTokens int64 |
| reasoningTokens int64 |
| seen bool |
| } |
| toolCallIndex int |
| sawToolCall bool |
| toolNameMap map[string]string |
| } |
|
|
| func convertOpenAIRequestToCodex(model string, rawJSON []byte, stream bool) ([]byte, error) { |
| var req openAIChatRequest |
| if err := sonic.Unmarshal(rawJSON, &req); err != nil { |
| return nil, err |
| } |
| conv, err := normalizeOpenAIConversation(req) |
| if err != nil { |
| return nil, err |
| } |
| return encodeCodexRequest(model, conv, stream) |
| } |
|
|
| func convertCodexRequestToOpenAI(model string, rawJSON []byte, stream bool) ([]byte, error) { |
| var req codexRequest |
| if err := sonic.Unmarshal(rawJSON, &req); err != nil { |
| return nil, err |
| } |
| conv, err := normalizeCodexConversation(req) |
| if err != nil { |
| return nil, err |
| } |
| return encodeOpenAIRequest(model, conv, stream) |
| } |
|
|
| func convertOpenAIResponseToCodexNonStream(_ context.Context, model string, _, _, rawJSON []byte) ([]byte, error) { |
| var resp map[string]any |
| if err := sonic.Unmarshal(rawJSON, &resp); err != nil { |
| return nil, err |
| } |
| output, err := codexOutputItemsFromOpenAIResponse(resp) |
| if err != nil { |
| return nil, err |
| } |
| out := map[string]any{ |
| "id": "resp-proxy", |
| "object": "response", |
| "status": "completed", |
| "model": coalesceModel(model, resp["model"]), |
| "output": output, |
| } |
| if usage := openAIUsageFromMap(resp["usage"]); usage != nil { |
| out["usage"] = codexUsagePayload(&codexUsage{ |
| inputTokens: usage.promptTokens, |
| outputTokens: usage.completionTokens, |
| totalTokens: usage.totalTokens, |
| cachedTokens: usage.cachedTokens, |
| cacheCreationInputTokens: usage.cacheCreationInputTokens, |
| reasoningTokens: usage.reasoningTokens, |
| }) |
| } |
| return sonic.Marshal(out) |
| } |
|
|
| func convertCodexResponseToOpenAINonStream(_ context.Context, model string, rawReq, translatedReq, rawJSON []byte) ([]byte, error) { |
| var resp map[string]any |
| if err := sonic.Unmarshal(rawJSON, &resp); err != nil { |
| return nil, err |
| } |
| aliases := codexToolAliasesFromRequests(protocol.OpenAI, rawReq, translatedReq) |
| message, err := openAIMessageFromCodexOutput(resp["output"], aliases.restore) |
| if err != nil { |
| return nil, err |
| } |
| finishReason := "stop" |
| if rawToolCalls, ok := message["tool_calls"].([]map[string]any); ok && len(rawToolCalls) > 0 { |
| finishReason = "tool_calls" |
| } else if rawToolCalls, ok := message["tool_calls"].([]any); ok && len(rawToolCalls) > 0 { |
| finishReason = "tool_calls" |
| } |
| out := map[string]any{ |
| "id": "chatcmpl-proxy", |
| "object": "chat.completion", |
| "created": 0, |
| "model": coalesceModel(model, resp["model"]), |
| "choices": []map[string]any{{ |
| "index": 0, |
| "message": message, |
| "finish_reason": finishReason, |
| }}, |
| } |
| if usage := codexUsageFromMap(resp["usage"]); usage != nil { |
| out["usage"] = openAIUsagePayload(&openAIUsage{ |
| promptTokens: usage.inputTokens, |
| completionTokens: usage.outputTokens, |
| totalTokens: usage.totalTokens, |
| cachedTokens: usage.cachedTokens, |
| cacheCreationInputTokens: usage.cacheCreationInputTokens, |
| reasoningTokens: usage.reasoningTokens, |
| }) |
| } |
| return sonic.Marshal(out) |
| } |
|
|
| func convertOpenAIResponseToCodexStream(_ context.Context, model string, _, _, rawJSON []byte, param *any) ([][]byte, error) { |
| if param == nil { |
| var local any |
| param = &local |
| } |
| if *param == nil { |
| *param = &openAIToCodexStreamState{model: model} |
| } |
| st := (*param).(*openAIToCodexStreamState) |
| if st.model == "" { |
| st.model = model |
| } |
|
|
| line := strings.TrimSpace(string(rawJSON)) |
| if line == "" { |
| return nil, nil |
| } |
| if after, ok := strings.CutPrefix(line, "data:"); ok { |
| line = strings.TrimSpace(after) |
| } |
| if line == "[DONE]" { |
| chunks := make([][]byte, 0, 4) |
| if st.reasoningText != "" || st.reasoningEncrypted != "" { |
| item := map[string]any{ |
| "type": "response.output_item.done", |
| "item": codexReasoningItem(st.reasoningText, st.reasoningEncrypted), |
| } |
| body, err := sonic.Marshal(item) |
| if err != nil { |
| return nil, err |
| } |
| chunks = append(chunks, append([]byte("event: response.output_item.done\ndata: "), append(body, []byte("\n\n")...)...)) |
| } |
| |
| for idx := 0; ; idx++ { |
| tc, ok := st.toolCalls[idx] |
| if !ok { |
| break |
| } |
| item := map[string]any{ |
| "type": "response.output_item.done", |
| "item": map[string]any{ |
| "type": "function_call", |
| "call_id": tc.id, |
| "name": tc.name, |
| "arguments": tc.arguments, |
| }, |
| } |
| body, err := sonic.Marshal(item) |
| if err != nil { |
| return nil, err |
| } |
| chunks = append(chunks, append([]byte("event: response.output_item.done\ndata: "), append(body, []byte("\n\n")...)...)) |
| } |
| response := map[string]any{ |
| "id": "resp-proxy", |
| "object": "response", |
| "status": "completed", |
| "model": st.model, |
| } |
| if st.usage.seen { |
| response["usage"] = codexUsagePayload(&codexUsage{ |
| inputTokens: st.usage.promptTokens, |
| outputTokens: st.usage.completionTokens, |
| totalTokens: st.usage.totalTokens, |
| cachedTokens: st.usage.cachedTokens, |
| cacheCreationInputTokens: st.usage.cacheCreationInputTokens, |
| reasoningTokens: st.usage.reasoningTokens, |
| }) |
| } |
| done := map[string]any{"type": "response.completed", "response": response} |
| body, err := sonic.Marshal(done) |
| if err != nil { |
| return nil, err |
| } |
| chunks = append(chunks, append([]byte("event: response.completed\ndata: "), append(body, []byte("\n\n")...)...)) |
| return chunks, nil |
| } |
|
|
| var chunk map[string]any |
| if err := sonic.Unmarshal([]byte(line), &chunk); err != nil { |
| return nil, err |
| } |
| if chunkModel := stringValue(chunk["model"]); chunkModel != "" { |
| st.model = chunkModel |
| } |
| if usage := openAIUsageFromMap(chunk["usage"]); usage != nil { |
| st.usage.promptTokens = usage.promptTokens |
| st.usage.completionTokens = usage.completionTokens |
| st.usage.totalTokens = usage.totalTokens |
| st.usage.cachedTokens = usage.cachedTokens |
| st.usage.cacheCreationInputTokens = usage.cacheCreationInputTokens |
| st.usage.reasoningTokens = usage.reasoningTokens |
| st.usage.seen = true |
| } |
| choices, _ := chunk["choices"].([]any) |
| if len(choices) == 0 { |
| return nil, nil |
| } |
| choice, _ := choices[0].(map[string]any) |
| delta, _ := choice["delta"].(map[string]any) |
| content := stringValue(delta["content"]) |
| if reasoning := stringValue(delta["reasoning_content"]); reasoning != "" { |
| st.reasoningText += reasoning |
| return nil, nil |
| } |
| if reasoning, _ := delta["reasoning"].(map[string]any); reasoning != nil { |
| if encrypted := stringValue(reasoning["encrypted_content"]); encrypted != "" { |
| st.reasoningEncrypted = encrypted |
| } |
| return nil, nil |
| } |
| |
| if rawCalls, ok := delta["tool_calls"].([]any); ok && len(rawCalls) > 0 { |
| if st.toolCalls == nil { |
| st.toolCalls = make(map[int]*pendingToolCall) |
| } |
| for _, raw := range rawCalls { |
| tc, _ := raw.(map[string]any) |
| if tc == nil { |
| continue |
| } |
| idx := int(int64Value(tc["index"])) |
| if _, exists := st.toolCalls[idx]; !exists { |
| st.toolCalls[idx] = &pendingToolCall{} |
| } |
| p := st.toolCalls[idx] |
| if id := stringValue(tc["id"]); id != "" { |
| p.id = id |
| } |
| if fn, ok := tc["function"].(map[string]any); ok { |
| if name := stringValue(fn["name"]); name != "" { |
| p.name = name |
| } |
| if args := stringValue(fn["arguments"]); args != "" { |
| p.arguments += args |
| } |
| } |
| } |
| return nil, nil |
| } |
| if content == "" { |
| return nil, nil |
| } |
| payload := map[string]any{"type": "response.output_text.delta", "delta": content} |
| body, err := sonic.Marshal(payload) |
| if err != nil { |
| return nil, err |
| } |
| return [][]byte{append([]byte("event: response.output_text.delta\ndata: "), append(body, []byte("\n\n")...)...)}, nil |
| } |
|
|
| func convertCodexResponseToOpenAIStream(_ context.Context, model string, rawReq, translatedReq, rawJSON []byte, param *any) ([][]byte, error) { |
| if param == nil { |
| var local any |
| param = &local |
| } |
| if *param == nil { |
| *param = &codexToOpenAIStreamState{model: model} |
| } |
| st := (*param).(*codexToOpenAIStreamState) |
| if st.model == "" { |
| st.model = model |
| } |
|
|
| raw := strings.TrimSpace(string(rawJSON)) |
| if raw == "" { |
| return nil, nil |
| } |
| eventType, line := parseSSEEventBlock(raw) |
| if line == "" { |
| return nil, nil |
| } |
|
|
| var payload map[string]any |
| if err := sonic.Unmarshal([]byte(line), &payload); err != nil { |
| return nil, err |
| } |
| if response, ok := payload["response"].(map[string]any); ok { |
| if responseModel := stringValue(response["model"]); responseModel != "" { |
| st.model = responseModel |
| } |
| if usage := codexUsageFromMap(response["usage"]); usage != nil { |
| st.usage.inputTokens = usage.inputTokens |
| st.usage.outputTokens = usage.outputTokens |
| st.usage.totalTokens = usage.totalTokens |
| st.usage.cachedTokens = usage.cachedTokens |
| st.usage.cacheCreationInputTokens = usage.cacheCreationInputTokens |
| st.usage.reasoningTokens = usage.reasoningTokens |
| st.usage.seen = true |
| } |
| } |
| if eventType == "response.completed" || stringValue(payload["type"]) == "response.completed" { |
| finishReason := "stop" |
| if st.sawToolCall { |
| finishReason = "tool_calls" |
| } |
| chunk := map[string]any{ |
| "id": "chatcmpl-proxy", |
| "object": "chat.completion.chunk", |
| "created": 0, |
| "model": st.model, |
| "choices": []map[string]any{{ |
| "index": 0, |
| "delta": map[string]any{}, |
| "finish_reason": finishReason, |
| }}, |
| } |
| if st.usage.seen { |
| chunk["usage"] = openAIUsagePayload(&openAIUsage{ |
| promptTokens: st.usage.inputTokens, |
| completionTokens: st.usage.outputTokens, |
| totalTokens: st.usage.totalTokens, |
| cachedTokens: st.usage.cachedTokens, |
| cacheCreationInputTokens: st.usage.cacheCreationInputTokens, |
| reasoningTokens: st.usage.reasoningTokens, |
| }) |
| } |
| body, err := sonic.Marshal(chunk) |
| if err != nil { |
| return nil, err |
| } |
| return [][]byte{ |
| append([]byte("data: "), append(body, []byte("\n\n")...)...), |
| []byte("data: [DONE]\n\n"), |
| }, nil |
| } |
| if eventType == "response.output_text.delta" || stringValue(payload["type"]) == "response.output_text.delta" { |
| delta := stringValue(payload["delta"]) |
| if delta == "" { |
| return nil, nil |
| } |
| chunk := map[string]any{ |
| "id": "chatcmpl-proxy", |
| "object": "chat.completion.chunk", |
| "created": 0, |
| "model": st.model, |
| "choices": []map[string]any{{ |
| "index": 0, |
| "delta": map[string]any{"content": delta}, |
| }}, |
| } |
| body, err := sonic.Marshal(chunk) |
| if err != nil { |
| return nil, err |
| } |
| return [][]byte{append([]byte("data: "), append(body, []byte("\n\n")...)...)}, nil |
| } |
| if eventType == "response.output_item.done" || stringValue(payload["type"]) == "response.output_item.done" { |
| item, _ := payload["item"].(map[string]any) |
| itemType := stringValue(item["type"]) |
| switch { |
| case itemType == "function_call": |
| |
| call, err := decodeCodexToolCall(item) |
| if err != nil { |
| return nil, err |
| } |
| call.Name = st.restoreToolName(rawReq, translatedReq, call.Name) |
| |
| argsStr := "{}" |
| if len(call.Arguments) > 0 { |
| argsStr = string(call.Arguments) |
| } |
| chunk := map[string]any{ |
| "id": "chatcmpl-proxy", |
| "object": "chat.completion.chunk", |
| "created": 0, |
| "model": st.model, |
| "choices": []map[string]any{{ |
| "index": 0, |
| "delta": map[string]any{ |
| "tool_calls": []map[string]any{{ |
| "index": st.toolCallIndex, |
| "id": call.ID, |
| "type": "function", |
| "function": map[string]any{ |
| "name": call.Name, |
| "arguments": argsStr, |
| }, |
| }}, |
| }, |
| "finish_reason": nil, |
| }}, |
| } |
| body, err := sonic.Marshal(chunk) |
| if err != nil { |
| return nil, err |
| } |
| st.sawToolCall = true |
| st.toolCallIndex++ |
| return [][]byte{append([]byte("data: "), append(body, []byte("\n\n")...)...)}, nil |
| case normalizeRole(itemType) == "reasoning": |
| text := extractCodexReasoningText(item) |
| if text == "" { |
| return nil, nil |
| } |
| chunk := map[string]any{ |
| "id": "chatcmpl-proxy", |
| "object": "chat.completion.chunk", |
| "created": 0, |
| "model": st.model, |
| "choices": []map[string]any{{ |
| "index": 0, |
| "delta": map[string]any{"reasoning_content": text}, |
| }}, |
| } |
| body, err := sonic.Marshal(chunk) |
| if err != nil { |
| return nil, err |
| } |
| return [][]byte{append([]byte("data: "), append(body, []byte("\n\n")...)...)}, nil |
| default: |
| return nil, nil |
| } |
| } |
| return nil, nil |
| } |
|
|
| type openAIUsage struct { |
| promptTokens int64 |
| completionTokens int64 |
| totalTokens int64 |
| cachedTokens int64 |
| cacheCreationInputTokens int64 |
| reasoningTokens int64 |
| } |
|
|
| type codexUsage struct { |
| inputTokens int64 |
| outputTokens int64 |
| totalTokens int64 |
| cachedTokens int64 |
| cacheCreationInputTokens int64 |
| reasoningTokens int64 |
| } |
|
|
| func codexOutputItemsFromOpenAIResponse(resp map[string]any) ([]map[string]any, error) { |
| choices, _ := resp["choices"].([]any) |
| if len(choices) == 0 { |
| return nil, nil |
| } |
| choice, _ := choices[0].(map[string]any) |
| message, _ := choice["message"].(map[string]any) |
| if len(message) == 0 { |
| return nil, nil |
| } |
| items := make([]map[string]any, 0) |
| parts, err := extractOpenAIContentParts(message["content"]) |
| if err != nil { |
| return nil, err |
| } |
| textContent := make([]map[string]any, 0, len(parts)) |
| for _, part := range parts { |
| item, err := encodeCodexOutputContentPart(part) |
| if err != nil { |
| return nil, err |
| } |
| if item != nil { |
| textContent = append(textContent, item) |
| } |
| } |
| if len(textContent) > 0 { |
| items = append(items, map[string]any{"type": "message", "role": "assistant", "content": textContent}) |
| } |
| var toolCalls []openAIChatToolCall |
| if rawCalls, ok := message["tool_calls"]; ok { |
| bytes, err := sonic.Marshal(rawCalls) |
| if err != nil { |
| return nil, err |
| } |
| if err := sonic.Unmarshal(bytes, &toolCalls); err != nil { |
| return nil, err |
| } |
| } |
| for _, call := range toolCalls { |
| arguments := strings.TrimSpace(call.Function.Arguments) |
| if arguments == "" { |
| arguments = "{}" |
| } |
| items = append(items, map[string]any{ |
| "type": "function_call", |
| "call_id": call.ID, |
| "name": call.Function.Name, |
| "arguments": arguments, |
| }) |
| } |
| items = append(items, codexReasoningItemsFromOpenAIMessage(message)...) |
| return items, nil |
| } |
|
|
| func openAIMessageFromCodexOutput(output any, restore func(string) string) (map[string]any, error) { |
| if restore == nil { |
| restore = func(name string) string { return name } |
| } |
| items, _ := output.([]any) |
| contentParts := make([]map[string]any, 0) |
| toolCalls := make([]map[string]any, 0) |
| reasoning := make([]map[string]any, 0) |
| var reasoningBuilder strings.Builder |
| for i, item := range items { |
| itemMap, ok := item.(map[string]any) |
| if !ok { |
| return nil, fmt.Errorf("unsupported codex output item at index %d", i) |
| } |
| typ := normalizeRole(stringValue(itemMap["type"])) |
| switch typ { |
| case "message": |
| parts, err := extractCodexContentParts(itemMap["content"]) |
| if err != nil { |
| return nil, err |
| } |
| for _, part := range parts { |
| encoded, err := encodeOpenAIContentPart(part) |
| if err != nil { |
| return nil, err |
| } |
| contentParts = append(contentParts, encoded) |
| } |
| case "function_call": |
| call, err := decodeCodexToolCall(itemMap) |
| if err != nil { |
| return nil, err |
| } |
| call.Name = restore(call.Name) |
| encoded, err := encodeOpenAIToolCall(&call) |
| if err != nil { |
| return nil, err |
| } |
| toolCalls = append(toolCalls, encoded) |
| case "reasoning": |
| text := extractCodexReasoningText(itemMap) |
| if text != "" { |
| reasoningBuilder.WriteString(text) |
| } |
| entry := map[string]any{"type": "reasoning"} |
| if text != "" { |
| entry["text"] = text |
| } |
| if encrypted := stringValue(itemMap["encrypted_content"]); encrypted != "" { |
| entry["encrypted_content"] = encrypted |
| } |
| reasoning = append(reasoning, entry) |
| } |
| } |
| message := map[string]any{ |
| "role": "assistant", |
| "content": encodeOpenAIContentValue(contentParts), |
| } |
| if len(toolCalls) > 0 { |
| message["tool_calls"] = toolCalls |
| } |
| if reasoningBuilder.Len() > 0 { |
| message["reasoning_content"] = reasoningBuilder.String() |
| } |
| if len(reasoning) > 0 { |
| message["reasoning"] = reasoning |
| } |
| return message, nil |
| } |
|
|
| func (st *codexToOpenAIStreamState) restoreToolName(rawReq, translatedReq []byte, name string) string { |
| if st.toolNameMap == nil { |
| st.toolNameMap = codexToolAliasesFromRequests(protocol.OpenAI, rawReq, translatedReq).ShortToOriginal |
| } |
| if original := st.toolNameMap[name]; original != "" { |
| return original |
| } |
| return name |
| } |
|
|
| func encodeCodexOutputContentPart(part conversationPart) (map[string]any, error) { |
| switch part.Kind { |
| case partKindText: |
| return map[string]any{"type": "output_text", "text": part.Text}, nil |
| case partKindImage, partKindFile: |
| return nil, fmt.Errorf("unsupported non-text OpenAI response content for Codex output") |
| default: |
| return nil, nil |
| } |
| } |
|
|
| func codexReasoningItemsFromOpenAIMessage(message map[string]any) []map[string]any { |
| items := make([]map[string]any, 0) |
| if rawReasoning, ok := message["reasoning"].([]any); ok { |
| for _, item := range rawReasoning { |
| entry, ok := item.(map[string]any) |
| if !ok { |
| continue |
| } |
| text := stringValue(entry["text"]) |
| encrypted := stringValue(entry["encrypted_content"]) |
| items = append(items, codexReasoningItem(text, encrypted)) |
| } |
| } |
| if len(items) == 0 { |
| if text := stringValue(message["reasoning_content"]); text != "" { |
| items = append(items, codexReasoningItem(text, "")) |
| } |
| } |
| return items |
| } |
|
|
| func extractCodexReasoningText(item map[string]any) string { |
| for _, key := range []string{"content", "summary"} { |
| parts, _ := item[key].([]any) |
| for _, rawPart := range parts { |
| part, ok := rawPart.(map[string]any) |
| if !ok { |
| continue |
| } |
| switch normalizeRole(stringValue(part["type"])) { |
| case "reasoning_text", "summary_text": |
| if text := stringValue(part["text"]); text != "" { |
| return text |
| } |
| } |
| } |
| } |
| return "" |
| } |
|
|
| func openAIUsageFromMap(value any) *openAIUsage { |
| usageMap, ok := value.(map[string]any) |
| if !ok { |
| return nil |
| } |
| usage := &openAIUsage{ |
| promptTokens: int64Value(usageMap["prompt_tokens"]), |
| completionTokens: int64Value(usageMap["completion_tokens"]), |
| totalTokens: int64Value(usageMap["total_tokens"]), |
| cacheCreationInputTokens: int64Value(usageMap["cache_creation_input_tokens"]), |
| } |
| if details, ok := usageMap["prompt_tokens_details"].(map[string]any); ok { |
| usage.cachedTokens = int64Value(details["cached_tokens"]) |
| } |
| if details, ok := usageMap["completion_tokens_details"].(map[string]any); ok { |
| usage.reasoningTokens = int64Value(details["reasoning_tokens"]) |
| } |
| if usage.totalTokens == 0 { |
| usage.totalTokens = usage.promptTokens + usage.completionTokens |
| } |
| if usage.promptTokens == 0 && usage.completionTokens == 0 && usage.totalTokens == 0 && usage.cachedTokens == 0 && usage.cacheCreationInputTokens == 0 && usage.reasoningTokens == 0 { |
| return nil |
| } |
| return usage |
| } |
|
|
| func codexUsageFromMap(value any) *codexUsage { |
| usageMap, ok := value.(map[string]any) |
| if !ok { |
| return nil |
| } |
| usage := &codexUsage{ |
| inputTokens: int64Value(usageMap["input_tokens"]), |
| outputTokens: int64Value(usageMap["output_tokens"]), |
| totalTokens: int64Value(usageMap["total_tokens"]), |
| cacheCreationInputTokens: int64Value(usageMap["cache_creation_input_tokens"]), |
| } |
| if details, ok := usageMap["input_tokens_details"].(map[string]any); ok { |
| usage.cachedTokens = int64Value(details["cached_tokens"]) |
| } else { |
| usage.cachedTokens = int64Value(usageMap["cache_read_input_tokens"]) |
| } |
| if details, ok := usageMap["output_tokens_details"].(map[string]any); ok { |
| usage.reasoningTokens = int64Value(details["reasoning_tokens"]) |
| } |
| if usage.totalTokens == 0 { |
| usage.totalTokens = usage.inputTokens + usage.outputTokens |
| } |
| if usage.inputTokens == 0 && usage.outputTokens == 0 && usage.totalTokens == 0 && usage.cachedTokens == 0 && usage.cacheCreationInputTokens == 0 && usage.reasoningTokens == 0 { |
| return nil |
| } |
| return usage |
| } |
|
|
| func int64Value(value any) int64 { |
| switch v := value.(type) { |
| case int64: |
| return v |
| case int: |
| return int64(v) |
| case float64: |
| return int64(v) |
| case float32: |
| return int64(v) |
| default: |
| return 0 |
| } |
| } |
|
|
| func coalesceModel(model string, fallback any) string { |
| if strings.TrimSpace(model) != "" { |
| return model |
| } |
| return stringValue(fallback) |
| } |
|
|