package builtin import ( "context" "encoding/json" "fmt" "strings" "ccLoad/internal/protocol" "ccLoad/internal/util" "github.com/bytedance/sonic" ) type codexRequest struct { Model string `json:"model"` Instructions string `json:"instructions,omitempty"` Stream util.FlexibleBool `json:"stream,omitempty"` Tools json.RawMessage `json:"tools,omitempty"` ToolChoice json.RawMessage `json:"tool_choice,omitempty"` Input []json.RawMessage `json:"input"` Reasoning *codexReasoning `json:"reasoning,omitempty"` ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"` Temperature *float64 `json:"temperature,omitempty"` TopP *float64 `json:"top_p,omitempty"` TopK *int `json:"top_k,omitempty"` MaxOutputTokens *int `json:"max_output_tokens,omitempty"` Stop json.RawMessage `json:"stop,omitempty"` Seed *int64 `json:"seed,omitempty"` FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"` PresencePenalty *float64 `json:"presence_penalty,omitempty"` User string `json:"user,omitempty"` } type codexReasoning struct { Effort string `json:"effort,omitempty"` Summary string `json:"summary,omitempty"` } type codexToGeminiStreamState struct { model string responseID string hasOutputTextDelta bool toolNameMap map[string]string } func convertCodexRequestToGemini(_ string, rawJSON []byte, _ 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 encodeGeminiRequest(conv) } func convertGeminiRequestToCodex(model string, rawJSON []byte, stream bool) ([]byte, error) { var req geminiRequestPayload if err := sonic.Unmarshal(rawJSON, &req); err != nil { return nil, err } conv, err := normalizeGeminiConversation(req) if err != nil { return nil, err } return encodeCodexRequest(model, conv, stream) } func convertGeminiResponseToCodexNonStream(_ context.Context, model string, _, _, rawJSON []byte) ([]byte, error) { var resp geminiResponse if err := sonic.Unmarshal(rawJSON, &resp); err != nil { return nil, err } output := make([]map[string]any, 0) if len(resp.Candidates) > 0 { parts, err := conversationPartsFromGeminiParts(resp.Candidates[0].Content.Parts) if err != nil { return nil, err } output, err = codexOutputItemsFromConversationParts(parts) if err != nil { return nil, err } } out := map[string]any{ "id": "resp-proxy", "object": "response", "status": "completed", "model": coalesceModel(model, resp.ModelVersion), "output": output, } if resp.UsageMetadata.PromptTokenCount != 0 || resp.UsageMetadata.CandidatesTokenCount != 0 || resp.UsageMetadata.TotalTokenCount != 0 { out["usage"] = map[string]any{ "input_tokens": resp.UsageMetadata.PromptTokenCount, "output_tokens": resp.UsageMetadata.CandidatesTokenCount, "total_tokens": resp.UsageMetadata.TotalTokenCount, } } return sonic.Marshal(out) } func convertCodexResponseToGeminiNonStream(_ 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.Gemini, rawReq, translatedReq) parts, err := geminiPartsFromCodexOutput(resp["output"], aliases.restore) if err != nil { return nil, err } var promptTokens, candidateTokens, totalTokens int64 includeUsage := false if usage := codexUsageFromMap(resp["usage"]); usage != nil { promptTokens = usage.inputTokens candidateTokens = usage.outputTokens totalTokens = usage.totalTokens includeUsage = true } return sonic.Marshal(buildGeminiPayloadFromParts(coalesceModel(model, resp["model"]), stringValue(resp["id"]), parts, "STOP", promptTokens, candidateTokens, totalTokens, includeUsage)) } type codexStreamState struct { responseID string model string pendingToolCallIDs []string nextToolCallID int usage struct { inputTokens int64 outputTokens int64 totalTokens int64 seen bool } } func convertGeminiResponseToCodexStream(_ context.Context, model string, _, _, rawJSON []byte, param *any) ([][]byte, error) { if param == nil { var local any param = &local } if *param == nil { *param = &codexStreamState{responseID: "resp-proxy", model: model, nextToolCallID: 1} } st := (*param).(*codexStreamState) if st.model == "" { st.model = model } if st.nextToolCallID == 0 { st.nextToolCallID = 1 } line := strings.TrimSpace(string(rawJSON)) if line == "" { return nil, nil } if strings.HasPrefix(line, "data:") { line = strings.TrimSpace(strings.TrimPrefix(line, "data:")) } if line == "[DONE]" { response := map[string]any{ "id": st.responseID, "object": "response", "status": "completed", } if st.model != "" { response["model"] = st.model } if st.usage.seen { response["usage"] = map[string]any{ "input_tokens": st.usage.inputTokens, "output_tokens": st.usage.outputTokens, "total_tokens": st.usage.totalTokens, } } done := map[string]any{ "type": "response.completed", "response": response, } body, err := sonic.Marshal(done) if err != nil { return nil, err } return [][]byte{append([]byte("event: response.completed\ndata: "), append(body, []byte("\n\n")...)...)}, nil } var resp geminiResponse if err := sonic.Unmarshal([]byte(line), &resp); err != nil { return nil, err } if resp.ResponseID != "" { st.responseID = resp.ResponseID } if st.model == "" { st.model = resp.ModelVersion } if resp.UsageMetadata.PromptTokenCount != 0 || resp.UsageMetadata.CandidatesTokenCount != 0 || resp.UsageMetadata.TotalTokenCount != 0 { st.usage.inputTokens = resp.UsageMetadata.PromptTokenCount st.usage.outputTokens = resp.UsageMetadata.CandidatesTokenCount st.usage.totalTokens = resp.UsageMetadata.TotalTokenCount st.usage.seen = true } if len(resp.Candidates) == 0 { return nil, nil } parts, err := extractGeminiParts(resp.Candidates[0].Content.Parts, &st.pendingToolCallIDs, &st.nextToolCallID) if err != nil { return nil, err } outputs := make([][]byte, 0, len(parts)) for _, part := range parts { switch part.Kind { case partKindText: if part.Text == "" { continue } chunk := map[string]any{ "type": "response.output_text.delta", "delta": part.Text, } body, err := sonic.Marshal(chunk) if err != nil { return nil, err } outputs = append(outputs, append([]byte("event: response.output_text.delta\ndata: "), append(body, []byte("\n\n")...)...)) case partKindToolCall: encoded, err := encodeCodexToolCall(part.ToolCall) if err != nil { return nil, err } chunk := map[string]any{ "type": "response.output_item.done", "item": encoded, } body, err := sonic.Marshal(chunk) if err != nil { return nil, err } outputs = append(outputs, append([]byte("event: response.output_item.done\ndata: "), append(body, []byte("\n\n")...)...)) default: return nil, fmt.Errorf("unsupported gemini response part kind %q", part.Kind) } } if len(outputs) == 0 { return nil, nil } return outputs, nil } func convertCodexResponseToGeminiStream(_ context.Context, model string, rawReq, translatedReq, rawJSON []byte, param *any) ([][]byte, error) { if param == nil { var local any param = &local } if *param == nil { *param = &codexToGeminiStreamState{model: model} } st := (*param).(*codexToGeminiStreamState) 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, _ := payload["response"].(map[string]any); response != nil { if responseID := stringValue(response["id"]); responseID != "" { st.responseID = responseID } if responseModel := stringValue(response["model"]); responseModel != "" { st.model = responseModel } if eventType == "response.completed" || stringValue(payload["type"]) == "response.completed" { includeUsage := false var promptTokens, candidateTokens, totalTokens int64 if usage := codexUsageFromMap(response["usage"]); usage != nil { promptTokens = usage.inputTokens candidateTokens = usage.outputTokens totalTokens = usage.totalTokens includeUsage = true } body, err := marshalDataSSE(buildGeminiPayloadFromParts(st.model, st.responseID, nil, "STOP", promptTokens, candidateTokens, totalTokens, includeUsage)) if err != nil { return nil, err } return [][]byte{body}, nil } } if eventType == "response.output_text.delta" || stringValue(payload["type"]) == "response.output_text.delta" { if content := stringValue(payload["delta"]); content != "" { st.hasOutputTextDelta = true body, err := marshalDataSSE(buildGeminiPayload(st.model, content, "", 0, 0, 0, false)) if err != nil { return nil, err } return [][]byte{body}, nil } } if eventType == "response.output_item.done" || stringValue(payload["type"]) == "response.output_item.done" { if item, _ := payload["item"].(map[string]any); item != nil { switch normalizeRole(stringValue(item["type"])) { case "function_call": call, err := decodeCodexToolCall(item) if err != nil { return nil, err } call.Name = st.restoreToolName(rawReq, translatedReq, call.Name) args, err := rawJSONToAny(call.Arguments) if err != nil { return nil, err } body, err := marshalDataSSE(buildGeminiPayloadFromParts(st.model, "", []geminiPart{{ FunctionCall: &geminiFunctionCall{Name: call.Name, Args: args}, }}, "", 0, 0, 0, false)) if err != nil { return nil, err } return [][]byte{body}, nil case "message": if st.hasOutputTextDelta { return nil, nil } parts, err := extractCodexContentParts(item["content"]) if err != nil { return nil, err } geminiParts := make([]geminiPart, 0, len(parts)) for _, part := range parts { if part.Kind != partKindText || part.Text == "" { continue } geminiParts = append(geminiParts, geminiPart{Text: part.Text}) } if len(geminiParts) == 0 { return nil, nil } st.hasOutputTextDelta = true body, err := marshalDataSSE(buildGeminiPayloadFromParts(st.model, st.responseID, geminiParts, "", 0, 0, 0, false)) if err != nil { return nil, err } return [][]byte{body}, nil case "reasoning": text := extractCodexReasoningText(item) if text == "" { return nil, nil } body, err := marshalDataSSE(buildGeminiPayload(st.model, text, "", 0, 0, 0, false)) if err != nil { return nil, err } return [][]byte{body}, nil } } } return nil, nil } func (st *codexToGeminiStreamState) restoreToolName(rawReq, translatedReq []byte, name string) string { if st.toolNameMap == nil { st.toolNameMap = codexToolAliasesFromRequests(protocol.Gemini, rawReq, translatedReq).ShortToOriginal } if original := st.toolNameMap[name]; original != "" { return original } return name }