Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cmd/desktop/CHANGELOG.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
[
{
"version": "v4.13.1",
"date": "2026-03-07",
"changes": [
"Fixed cc_openai2 converter transformation issue"
]
},
{
"version": "v4.13.0",
"date": "2026-01-06",
Expand Down
7 changes: 7 additions & 0 deletions cmd/desktop/CHANGELOG_CN.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
[
{
"version": "v4.13.1",
"date": "2026-03-07",
"changes": [
"修复 cc_openai2 转换器转换异常问题"
]
},
{
"version": "v4.13.0",
"date": "2026-01-06",
Expand Down
4 changes: 2 additions & 2 deletions cmd/desktop/build/darwin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
<key>CFBundleIdentifier</key>
<string>com.ccnexus.app</string>
<key>CFBundleVersion</key>
<string>4.13.0</string>
<string>4.13.1</string>
<key>CFBundleGetInfoString</key>
<string>ccNexus</string>
<key>CFBundleShortVersionString</key>
<string>4.13.0</string>
<string>4.13.1</string>
<key>CFBundleIconFile</key>
<string>iconfile</string>
<key>LSUIElement</key>
Expand Down
2 changes: 1 addition & 1 deletion cmd/desktop/build/windows/wails.exe.manifest
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity type="win32" name="ccNexus" version="4.13.0.0" processorArchitecture="*"/>
<assemblyIdentity type="win32" name="ccNexus" version="4.13.1.0" processorArchitecture="*"/>
<description>A smart API endpoint rotation proxy for Claude Code and Codex CLI</description>
<dependency>
<dependentAssembly>
Expand Down
2 changes: 1 addition & 1 deletion cmd/desktop/wails.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"info": {
"companyName": "ccNexus",
"productName": "ccNexus",
"productVersion": "4.13.0",
"productVersion": "4.13.1",
"copyright": "Copyright © 2024 Chuck",
"comments": "A smart API endpoint rotation proxy for Claude Code"
}
Expand Down
178 changes: 151 additions & 27 deletions internal/transformer/convert/claude_openai2.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,23 +28,25 @@ func ClaudeReqToOpenAI2(claudeReq []byte, model string) ([]byte, error) {
// Convert messages to input
var input []map[string]interface{}
for _, msg := range req.Messages {
item := map[string]interface{}{
"type": "message",
"role": msg.Role,
}

var contentParts []map[string]interface{}
switch content := msg.Content.(type) {
case string:
contentParts = append(contentParts, map[string]interface{}{
"type": "input_text",
"text": content,
textType := "input_text"
if msg.Role == "assistant" {
textType = "output_text"
}
input = append(input, map[string]interface{}{
"type": "message",
"role": msg.Role,
"content": []map[string]interface{}{
{
"type": textType,
"text": content,
},
},
})
case []interface{}:
contentParts = convertClaudeContentToOpenAI2(content, msg.Role)
input = append(input, convertClaudeMessageToOpenAI2Items(content, msg.Role)...)
}
item["content"] = contentParts
input = append(input, item)
}
openai2Req["input"] = input

Expand All @@ -63,11 +65,79 @@ func ClaudeReqToOpenAI2(claudeReq []byte, model string) ([]byte, error) {
})
}
openai2Req["tools"] = tools

// Preserve tool forcing semantics for Responses API backends.
if mapped := mapClaudeToolChoiceToOpenAI2(req.ToolChoice); mapped != nil {
openai2Req["tool_choice"] = mapped
} else {
// For first turn, prefer required to avoid "plan-only" responses.
// After at least one tool_result exists, switch to auto to prevent
// forced repeated tool calls in later turns.
if hasClaudeToolResult(req.Messages) {
openai2Req["tool_choice"] = "auto"
} else {
openai2Req["tool_choice"] = "required"
}
}
}

return json.Marshal(openai2Req)
}

func mapClaudeToolChoiceToOpenAI2(toolChoice interface{}) interface{} {
if toolChoice == nil {
return nil
}

switch tc := toolChoice.(type) {
case map[string]interface{}:
choiceType, _ := tc["type"].(string)
switch choiceType {
case "tool":
if name, ok := tc["name"].(string); ok && name != "" {
return map[string]interface{}{
"type": "function",
"name": name,
}
}
case "any":
return "required"
case "auto":
return "auto"
case "none":
return "none"
}
case string:
switch tc {
case "any":
return "required"
default:
return tc
}
}

return nil
}

func hasClaudeToolResult(messages []transformer.ClaudeMessage) bool {
for _, msg := range messages {
blocks, ok := msg.Content.([]interface{})
if !ok {
continue
}
for _, block := range blocks {
m, ok := block.(map[string]interface{})
if !ok {
continue
}
if t, _ := m["type"].(string); t == "tool_result" {
return true
}
}
}
return false
}

// OpenAI2ReqToClaude converts OpenAI Responses API request to Claude request
func OpenAI2ReqToClaude(openai2Req []byte, model string) ([]byte, error) {
var req transformer.OpenAI2Request
Expand Down Expand Up @@ -210,9 +280,13 @@ func OpenAI2RespToClaude(openai2Resp []byte) ([]byte, error) {
case "function_call":
var args map[string]interface{}
json.Unmarshal([]byte(item.Arguments), &args)
toolID := item.CallID
if toolID == "" {
toolID = item.ID
}
content = append(content, map[string]interface{}{
"type": "tool_use",
"id": item.CallID,
"id": toolID,
"name": item.Name,
"input": args,
})
Expand Down Expand Up @@ -429,7 +503,10 @@ func OpenAI2StreamToClaude(event []byte, ctx *transformer.StreamContext) ([]byte
result = append(result, buildClaudeEvent("content_block_stop", map[string]interface{}{"index": ctx.ToolIndex})...)
ctx.ToolBlockStarted = false
}
result = append(result, buildClaudeEvent("message_stop", map[string]interface{}{})...)
if !ctx.FinishReasonSent {
result = append(result, buildClaudeEvent("message_stop", map[string]interface{}{})...)
ctx.FinishReasonSent = true
}
return result, nil
}
return nil, nil
Expand Down Expand Up @@ -498,6 +575,9 @@ func OpenAI2StreamToClaude(event []byte, ctx *transformer.StreamContext) ([]byte
ctx.ToolBlockStarted = true
ctx.ToolIndex = ctx.ContentIndex
ctx.CurrentToolID = evt.Item.CallID
if ctx.CurrentToolID == "" {
ctx.CurrentToolID = evt.Item.ID
}
ctx.CurrentToolName = evt.Item.Name
ctx.ToolArguments = ""
result = append(result, buildClaudeEvent("content_block_start", map[string]interface{}{
Expand Down Expand Up @@ -541,45 +621,86 @@ func OpenAI2StreamToClaude(event []byte, ctx *transformer.StreamContext) ([]byte
"delta": map[string]interface{}{"stop_reason": stopReason, "stop_sequence": nil},
"usage": map[string]interface{}{"output_tokens": 0},
})...)
result = append(result, buildClaudeEvent("message_stop", map[string]interface{}{})...)
ctx.FinishReasonSent = true
}

return result, nil
}

// Helper functions

func convertClaudeContentToOpenAI2(content []interface{}, role string) []map[string]interface{} {
var parts []map[string]interface{}
contentType := "input_text"
func convertClaudeMessageToOpenAI2Items(content []interface{}, role string) []map[string]interface{} {
var items []map[string]interface{}
var messageParts []map[string]interface{}
textType := "input_text"
if role == "assistant" {
contentType = "output_text"
textType = "output_text"
}

flushMessage := func() {
if len(messageParts) == 0 {
return
}
items = append(items, map[string]interface{}{
"type": "message",
"role": role,
"content": messageParts,
})
messageParts = nil
}

for _, block := range content {
m, ok := block.(map[string]interface{})
if !ok {
continue
}
switch m["type"] {

blockType, _ := m["type"].(string)
switch blockType {
case "text":
parts = append(parts, map[string]interface{}{"type": contentType, "text": m["text"]})
text, _ := m["text"].(string)
messageParts = append(messageParts, map[string]interface{}{"type": textType, "text": text})
case "thinking":
// Skip thinking blocks - they are Claude's internal reasoning
continue
case "tool_use":
flushMessage()
callID, _ := m["id"].(string)
name, _ := m["name"].(string)
args, _ := json.Marshal(m["input"])
parts = append(parts, map[string]interface{}{
"type": "output_text",
"text": fmt.Sprintf("[Tool Call: %s(%s)]", m["name"], string(args)),
items = append(items, map[string]interface{}{
"type": "function_call",
"call_id": callID,
"name": name,
"arguments": string(args),
})
case "tool_result":
parts = append(parts, map[string]interface{}{
"type": "input_text",
"text": fmt.Sprintf("[Tool Result: %v]", m["content"]),
flushMessage()
callID, _ := m["tool_use_id"].(string)
items = append(items, map[string]interface{}{
"type": "function_call_output",
"call_id": callID,
"output": toolResultToString(m["content"]),
})
}
}
return parts
flushMessage()

return items
}

func toolResultToString(content interface{}) string {
switch v := content.(type) {
case string:
return v
default:
data, err := json.Marshal(v)
if err != nil {
return fmt.Sprint(v)
}
return string(data)
}
}

func convertOpenAI2InputToClaude(input interface{}) []map[string]interface{} {
Expand Down Expand Up @@ -619,6 +740,9 @@ func convertOpenAI2InputToClaude(input interface{}) []map[string]interface{} {
case "function_call":
// Convert to Claude tool_use
callID, _ := itemMap["call_id"].(string)
if callID == "" {
callID, _ = itemMap["id"].(string)
}
name, _ := itemMap["name"].(string)
argsStr, _ := itemMap["arguments"].(string)
var args interface{}
Expand Down
Loading
Loading