mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
feat(translator): add token counting functionality for Gemini, Claude, and CLI - Introduced `TokenCount` handling across various Codex translators (Gemini, Claude, CLI) with respective implementations. - Added utility methods for token counting and formatting responses. - Integrated `tiktoken-go/tokenizer` library for tokenization. - Updated CodexExecutor with token counting logic to support multiple models including GPT-5 variants. - Refined go.mod and go.sum to include new dependencies. feat(runtime): add token counting functionality across executors - Implemented token counting in OpenAICompatExecutor, QwenExecutor, and IFlowExecutor. - Added utilities for token counting and response formatting using `tiktoken-go/tokenizer`. - Integrated token counting into translators for Gemini, Claude, and Gemini CLI. - Enhanced multiple model support, including GPT-5 variants, for token counting. docs: update environment variable instructions for multi-model support - Added details for setting `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, and `ANTHROPIC_DEFAULT_HAIKU_MODEL` for version 2.x.x. - Clarified usage of `ANTHROPIC_MODEL` and `ANTHROPIC_SMALL_FAST_MODEL` for version 1.x.x. - Expanded examples for setting environment variables across different models including Gemini, GPT-5, Claude, and Qwen3.
361 lines
14 KiB
Go
361 lines
14 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"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"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 {
|
|
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, _ []byte, rawJSON []byte, _ *any) string {
|
|
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
|
|
|
rootResult := gjson.ParseBytes(rawJSON)
|
|
if rootResult.Get("type").String() != "response.completed" {
|
|
return ""
|
|
}
|
|
|
|
responseData := rootResult.Get("response")
|
|
if !responseData.Exists() {
|
|
return ""
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"id": responseData.Get("id").String(),
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": responseData.Get("model").String(),
|
|
"content": []interface{}{},
|
|
"stop_reason": nil,
|
|
"stop_sequence": nil,
|
|
"usage": map[string]interface{}{
|
|
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
|
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
|
},
|
|
}
|
|
|
|
var contentBlocks []interface{}
|
|
hasToolCall := false
|
|
|
|
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
|
|
output.ForEach(func(_, item gjson.Result) bool {
|
|
switch item.Get("type").String() {
|
|
case "reasoning":
|
|
thinkingBuilder := strings.Builder{}
|
|
if summary := item.Get("summary"); summary.Exists() {
|
|
if summary.IsArray() {
|
|
summary.ForEach(func(_, part gjson.Result) bool {
|
|
if txt := part.Get("text"); txt.Exists() {
|
|
thinkingBuilder.WriteString(txt.String())
|
|
} else {
|
|
thinkingBuilder.WriteString(part.String())
|
|
}
|
|
return true
|
|
})
|
|
} else {
|
|
thinkingBuilder.WriteString(summary.String())
|
|
}
|
|
}
|
|
if thinkingBuilder.Len() == 0 {
|
|
if content := item.Get("content"); content.Exists() {
|
|
if content.IsArray() {
|
|
content.ForEach(func(_, part gjson.Result) bool {
|
|
if txt := part.Get("text"); txt.Exists() {
|
|
thinkingBuilder.WriteString(txt.String())
|
|
} else {
|
|
thinkingBuilder.WriteString(part.String())
|
|
}
|
|
return true
|
|
})
|
|
} else {
|
|
thinkingBuilder.WriteString(content.String())
|
|
}
|
|
}
|
|
}
|
|
if thinkingBuilder.Len() > 0 {
|
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
"type": "thinking",
|
|
"thinking": thinkingBuilder.String(),
|
|
})
|
|
}
|
|
case "message":
|
|
if content := item.Get("content"); content.Exists() {
|
|
if content.IsArray() {
|
|
content.ForEach(func(_, part gjson.Result) bool {
|
|
if part.Get("type").String() == "output_text" {
|
|
text := part.Get("text").String()
|
|
if text != "" {
|
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
"type": "text",
|
|
"text": text,
|
|
})
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
} else {
|
|
text := content.String()
|
|
if text != "" {
|
|
contentBlocks = append(contentBlocks, map[string]interface{}{
|
|
"type": "text",
|
|
"text": text,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case "function_call":
|
|
hasToolCall = true
|
|
name := item.Get("name").String()
|
|
if original, ok := revNames[name]; ok {
|
|
name = original
|
|
}
|
|
|
|
toolBlock := map[string]interface{}{
|
|
"type": "tool_use",
|
|
"id": item.Get("call_id").String(),
|
|
"name": name,
|
|
"input": map[string]interface{}{},
|
|
}
|
|
|
|
if argsStr := item.Get("arguments").String(); argsStr != "" {
|
|
var args interface{}
|
|
if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
|
|
toolBlock["input"] = args
|
|
}
|
|
}
|
|
|
|
contentBlocks = append(contentBlocks, toolBlock)
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
if len(contentBlocks) > 0 {
|
|
response["content"] = contentBlocks
|
|
}
|
|
|
|
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
|
response["stop_reason"] = stopReason.String()
|
|
} else if hasToolCall {
|
|
response["stop_reason"] = "tool_use"
|
|
} else {
|
|
response["stop_reason"] = "end_turn"
|
|
}
|
|
|
|
if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
|
|
response["stop_sequence"] = stopSequence.Value()
|
|
}
|
|
|
|
if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
|
|
response["usage"] = map[string]interface{}{
|
|
"input_tokens": responseData.Get("usage.input_tokens").Int(),
|
|
"output_tokens": responseData.Get("usage.output_tokens").Int(),
|
|
}
|
|
}
|
|
|
|
responseJSON, err := json.Marshal(response)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return string(responseJSON)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
|
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
|
}
|