mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 04:20:50 +08:00
209 lines
9.2 KiB
Go
209 lines
9.2 KiB
Go
// Package claude provides response translation functionality for Codex to Claude Code API compatibility.
|
|
// This package handles the conversion of Codex API responses into Claude Code-compatible
|
|
// Server-Sent Events (SSE) format, implementing a sophisticated state machine that manages
|
|
// different response types including text content, thinking processes, and function calls.
|
|
// The translation ensures proper sequencing of SSE events and maintains state across
|
|
// multiple response chunks to provide a seamless streaming experience.
|
|
package claude
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
var (
|
|
dataTag = []byte("data:")
|
|
)
|
|
|
|
// ConvertCodexResponseToClaude performs sophisticated streaming response format conversion.
|
|
// This function implements a complex state machine that translates Codex API responses
|
|
// into Claude Code-compatible Server-Sent Events (SSE) format. It manages different response types
|
|
// and handles state transitions between content blocks, thinking processes, and function calls.
|
|
//
|
|
// Response type states: 0=none, 1=content, 2=thinking, 3=function
|
|
// The function maintains state across multiple calls to ensure proper SSE event sequencing.
|
|
//
|
|
// Parameters:
|
|
// - ctx: The context for the request, used for cancellation and timeout handling
|
|
// - modelName: The name of the model being used for the response (unused in current implementation)
|
|
// - rawJSON: The raw JSON response from the Codex API
|
|
// - param: A pointer to a parameter object for maintaining state between calls
|
|
//
|
|
// Returns:
|
|
// - []string: A slice of strings, each containing a Claude Code-compatible JSON response
|
|
func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
|
log.Debug("ConvertCodexResponseToClaude")
|
|
if *param == nil {
|
|
hasToolCall := false
|
|
*param = &hasToolCall
|
|
}
|
|
|
|
// log.Debugf("rawJSON: %s", string(rawJSON))
|
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
|
return []string{}
|
|
}
|
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
|
|
|
output := ""
|
|
rootResult := gjson.ParseBytes(rawJSON)
|
|
typeResult := rootResult.Get("type")
|
|
typeStr := typeResult.String()
|
|
template := ""
|
|
if typeStr == "response.created" {
|
|
template = `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"claude-opus-4-1-20250805","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0},"content":[],"stop_reason":null}}`
|
|
template, _ = sjson.Set(template, "message.model", rootResult.Get("response.model").String())
|
|
template, _ = sjson.Set(template, "message.id", rootResult.Get("response.id").String())
|
|
|
|
output = "event: message_start\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.reasoning_summary_part.added" {
|
|
template = `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
|
|
output = "event: content_block_start\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.reasoning_summary_text.delta" {
|
|
template = `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
template, _ = sjson.Set(template, "delta.thinking", rootResult.Get("delta").String())
|
|
|
|
output = "event: content_block_delta\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.reasoning_summary_part.done" {
|
|
template = `{"type":"content_block_stop","index":0}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
|
|
output = "event: content_block_stop\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.content_part.added" {
|
|
template = `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
|
|
output = "event: content_block_start\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.output_text.delta" {
|
|
template = `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
template, _ = sjson.Set(template, "delta.text", rootResult.Get("delta").String())
|
|
|
|
output = "event: content_block_delta\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.content_part.done" {
|
|
template = `{"type":"content_block_stop","index":0}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
|
|
output = "event: content_block_stop\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
} else if typeStr == "response.completed" {
|
|
template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
|
p := (*param).(*bool)
|
|
if *p {
|
|
template, _ = sjson.Set(template, "delta.stop_reason", "tool_use")
|
|
} else {
|
|
template, _ = sjson.Set(template, "delta.stop_reason", "end_turn")
|
|
}
|
|
template, _ = sjson.Set(template, "usage.input_tokens", rootResult.Get("response.usage.input_tokens").Int())
|
|
template, _ = sjson.Set(template, "usage.output_tokens", rootResult.Get("response.usage.output_tokens").Int())
|
|
|
|
output = "event: message_delta\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
output += "event: message_stop\n"
|
|
output += `data: {"type":"message_stop"}`
|
|
output += "\n\n"
|
|
} else if typeStr == "response.output_item.added" {
|
|
itemResult := rootResult.Get("item")
|
|
itemType := itemResult.Get("type").String()
|
|
if itemType == "function_call" {
|
|
p := true
|
|
*param = &p
|
|
template = `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
template, _ = sjson.Set(template, "content_block.id", itemResult.Get("call_id").String())
|
|
{
|
|
// Restore original tool name if shortened
|
|
name := itemResult.Get("name").String()
|
|
rev := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
|
if orig, ok := rev[name]; ok {
|
|
name = orig
|
|
}
|
|
template, _ = sjson.Set(template, "content_block.name", name)
|
|
}
|
|
|
|
output = "event: content_block_start\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
|
|
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
|
|
output += "event: content_block_delta\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
}
|
|
} else if typeStr == "response.output_item.done" {
|
|
itemResult := rootResult.Get("item")
|
|
itemType := itemResult.Get("type").String()
|
|
if itemType == "function_call" {
|
|
template = `{"type":"content_block_stop","index":0}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
|
|
output = "event: content_block_stop\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
}
|
|
} else if typeStr == "response.function_call_arguments.delta" {
|
|
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
|
template, _ = sjson.Set(template, "index", rootResult.Get("output_index").Int())
|
|
template, _ = sjson.Set(template, "delta.partial_json", rootResult.Get("delta").String())
|
|
|
|
output += "event: content_block_delta\n"
|
|
output += fmt.Sprintf("data: %s\n\n", template)
|
|
}
|
|
|
|
return []string{output}
|
|
}
|
|
|
|
// ConvertCodexResponseToClaudeNonStream converts a non-streaming Codex response to a non-streaming Claude Code response.
|
|
// This function processes the complete Codex response and transforms it into a single Claude Code-compatible
|
|
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
|
// the information into a single response that matches the Claude Code API format.
|
|
//
|
|
// Parameters:
|
|
// - ctx: The context for the request, used for cancellation and timeout handling
|
|
// - modelName: The name of the model being used for the response (unused in current implementation)
|
|
// - rawJSON: The raw JSON response from the Codex API
|
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
|
//
|
|
// Returns:
|
|
// - string: A Claude Code-compatible JSON response containing all message content and metadata
|
|
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, _ []byte, _ *any) string {
|
|
log.Debug("ConvertCodexResponseToClaudeNonStream")
|
|
return ""
|
|
}
|
|
|
|
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
|
|
func buildReverseMapFromClaudeOriginalShortToOriginal(original []byte) map[string]string {
|
|
tools := gjson.GetBytes(original, "tools")
|
|
rev := map[string]string{}
|
|
if !tools.IsArray() {
|
|
return rev
|
|
}
|
|
var names []string
|
|
arr := tools.Array()
|
|
for i := 0; i < len(arr); i++ {
|
|
n := arr[i].Get("name").String()
|
|
if n != "" {
|
|
names = append(names, n)
|
|
}
|
|
}
|
|
if len(names) > 0 {
|
|
m := buildShortNameMap(names)
|
|
for orig, short := range m {
|
|
rev[short] = orig
|
|
}
|
|
}
|
|
return rev
|
|
}
|