Skip to content

Commit 67d5cab

Browse files
committed
Update version to v1.0.4
1 parent fcca386 commit 67d5cab

6 files changed

Lines changed: 508 additions & 291 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ OpenAI-compatible proxy server for the CommandCode API. It exposes `/v1/chat/com
44

55
Repository: https://github.com/dev2k6/command-code-proxy-server
66

7-
Version: `v1.0.3`
7+
Version: `v1.0.4`
88

99
## Features
1010

@@ -219,7 +219,7 @@ https://api.github.com/repos/dev2k6/command-code-proxy-server/tags
219219
If the latest GitHub tag is newer than the current app version, the version line is displayed as:
220220

221221
```text
222-
v1.0.3 (latest: v1.x.x)
222+
v1.0.4 (latest: v1.x.x)
223223
```
224224

225225
## CommandCode version header

internal/api/openai.go

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ package api
33
// OpenAI-compatible types (client-facing)
44

55
type OpenAIMessage struct {
6-
Role string `json:"role"`
7-
Content interface{} `json:"content,omitempty"`
8-
Name string `json:"name,omitempty"`
9-
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
10-
ToolCallID string `json:"tool_call_id,omitempty"`
11-
Refusal string `json:"refusal,omitempty"`
12-
Audio *MessageAudio `json:"audio,omitempty"`
6+
Role string `json:"role"`
7+
Content interface{} `json:"content,omitempty"`
8+
Name string `json:"name,omitempty"`
9+
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
10+
ToolCallID string `json:"tool_call_id,omitempty"`
11+
Refusal string `json:"refusal,omitempty"`
12+
Audio *MessageAudio `json:"audio,omitempty"`
1313
}
1414

1515
type ContentPart struct {
@@ -19,14 +19,14 @@ type ContentPart struct {
1919
}
2020

2121
type ImageURL struct {
22-
URL string `json:"url"`
23-
Detail string `json:"detail,omitempty"`
22+
URL string `json:"url"`
23+
Detail string `json:"detail,omitempty"`
2424
Modalities string `json:"modalities,omitempty"`
2525
}
2626

2727
type ToolCall struct {
28-
ID string `json:"id"`
29-
Type string `json:"type"`
28+
ID string `json:"id"`
29+
Type string `json:"type"`
3030
Function FunctionCall `json:"function"`
3131
}
3232

@@ -36,37 +36,66 @@ type FunctionCall struct {
3636
}
3737

3838
type MessageAudio struct {
39-
ID string `json:"id"`
40-
Data string `json:"data"`
39+
ID string `json:"id"`
40+
Data string `json:"data"`
4141
Duration float64 `json:"duration"`
4242
}
4343

4444
type OpenAIChatRequest struct {
45-
Model string `json:"model"`
46-
Messages []OpenAIMessage `json:"messages"`
47-
Temperature *float64 `json:"temperature,omitempty"`
48-
MaxTokens *int `json:"max_tokens,omitempty"`
49-
Stream bool `json:"stream,omitempty"`
50-
Tools []any `json:"tools,omitempty"`
45+
Model string `json:"model"`
46+
Messages []OpenAIMessage `json:"messages"`
47+
Temperature *float64 `json:"temperature,omitempty"`
48+
MaxTokens *int `json:"max_tokens,omitempty"`
49+
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
50+
Stream bool `json:"stream,omitempty"`
51+
StreamOptions any `json:"stream_options,omitempty"`
52+
Tools []any `json:"tools,omitempty"`
53+
ToolChoice any `json:"tool_choice,omitempty"`
54+
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
55+
ResponseFormat any `json:"response_format,omitempty"`
56+
Stop any `json:"stop,omitempty"`
57+
TopP *float64 `json:"top_p,omitempty"`
58+
PresencePenalty *float64 `json:"presence_penalty,omitempty"`
59+
FrequencyPenalty *float64 `json:"frequency_penalty,omitempty"`
60+
User string `json:"user,omitempty"`
61+
}
62+
63+
type OpenAIResponsesRequest struct {
64+
Model string `json:"model"`
65+
Input any `json:"input"`
66+
Instructions any `json:"instructions,omitempty"`
67+
Temperature *float64 `json:"temperature,omitempty"`
68+
MaxOutputTokens *int `json:"max_output_tokens,omitempty"`
69+
MaxTokens *int `json:"max_tokens,omitempty"`
70+
MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"`
71+
Stream bool `json:"stream,omitempty"`
72+
Tools []any `json:"tools,omitempty"`
73+
ToolChoice any `json:"tool_choice,omitempty"`
74+
ParallelToolCalls *bool `json:"parallel_tool_calls,omitempty"`
75+
ResponseFormat any `json:"response_format,omitempty"`
76+
Stop any `json:"stop,omitempty"`
77+
TopP *float64 `json:"top_p,omitempty"`
78+
User string `json:"user,omitempty"`
5179
}
5280

5381
type OpenAIChoice struct {
54-
Index int `json:"index"`
82+
Index int `json:"index"`
5583
Message *OpenAIMessage `json:"message,omitempty"`
56-
Delta *OpenAIDelta `json:"delta,omitempty"`
57-
FinishReason *string `json:"finish_reason,omitempty"`
84+
Delta *OpenAIDelta `json:"delta,omitempty"`
85+
FinishReason *string `json:"finish_reason,omitempty"`
5886
}
5987

6088
type OpenAIDelta struct {
61-
Role string `json:"role,omitempty"`
62-
Content string `json:"content,omitempty"`
89+
Role string `json:"role,omitempty"`
90+
Content string `json:"content,omitempty"`
6391
ToolCalls []OpenAIDeltaToolCall `json:"tool_calls,omitempty"`
92+
Refusal string `json:"refusal,omitempty"`
6493
}
6594

6695
type OpenAIDeltaToolCall struct {
67-
Index int `json:"index"`
68-
ID string `json:"id,omitempty"`
69-
Type string `json:"type,omitempty"`
96+
Index int `json:"index"`
97+
ID string `json:"id,omitempty"`
98+
Type string `json:"type,omitempty"`
7099
Function *OpenAIDeltaFunction `json:"function,omitempty"`
71100
}
72101

@@ -90,6 +119,17 @@ type OpenAIChatResponse struct {
90119
Usage *OpenAIUsage `json:"usage,omitempty"`
91120
}
92121

122+
type OpenAIErrorResponse struct {
123+
Error OpenAIError `json:"error"`
124+
}
125+
126+
type OpenAIError struct {
127+
Message string `json:"message"`
128+
Type string `json:"type"`
129+
Param any `json:"param"`
130+
Code any `json:"code"`
131+
}
132+
93133
type OpenAIModel struct {
94134
ID string `json:"id"`
95135
Object string `json:"object"`

internal/proxy/convert.go

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proxy
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"strings"
67

78
"github.com/dev2k6/command-code-proxy-server/internal/api"
@@ -10,24 +11,34 @@ import (
1011
// Convert OpenAI messages to CommandCode format
1112
func ConvertMessages(openAIMsgs []api.OpenAIMessage) []api.CCMessage {
1213
var ccMsgs []api.CCMessage
14+
toolNames := map[string]string{}
15+
1316
for _, m := range openAIMsgs {
14-
// Convert tool role to user with tool-result type
17+
for _, tc := range m.ToolCalls {
18+
if tc.ID != "" && tc.Function.Name != "" {
19+
toolNames[tc.ID] = tc.Function.Name
20+
}
21+
}
22+
1523
if m.Role == "tool" {
24+
toolName := m.Name
25+
if toolName == "" {
26+
toolName = toolNames[m.ToolCallID]
27+
}
1628
ccMsgs = append(ccMsgs, api.CCMessage{
1729
Role: "user",
1830
Content: []api.CCContentPart{{
1931
Type: "tool-result",
2032
ToolCallID: strPtr(m.ToolCallID),
21-
ToolName: strPtr(m.Name),
33+
ToolName: strPtr(toolName),
2234
Text: strPtr(contentToString(m.Content)),
2335
}},
2436
})
2537
continue
2638
}
2739

28-
// Convert assistant with tool_calls
2940
if m.Role == "assistant" && len(m.ToolCalls) > 0 {
30-
contentParts := parseContent(m.Content)
41+
contentParts := parseContent(m.Content, toolNames)
3142
for _, tc := range m.ToolCalls {
3243
contentParts = append(contentParts, api.CCContentPart{
3344
Type: "tool_use",
@@ -36,18 +47,11 @@ func ConvertMessages(openAIMsgs []api.OpenAIMessage) []api.CCMessage {
3647
Input: parseToolInput(tc.Function.Arguments),
3748
})
3849
}
39-
ccMsgs = append(ccMsgs, api.CCMessage{
40-
Role: m.Role,
41-
Content: contentParts,
42-
})
50+
ccMsgs = append(ccMsgs, api.CCMessage{Role: m.Role, Content: contentParts})
4351
continue
4452
}
4553

46-
contentParts := parseContent(m.Content)
47-
ccMsgs = append(ccMsgs, api.CCMessage{
48-
Role: m.Role,
49-
Content: contentParts,
50-
})
54+
ccMsgs = append(ccMsgs, api.CCMessage{Role: m.Role, Content: parseContent(m.Content, toolNames)})
5155
}
5256
return ccMsgs
5357
}
@@ -85,10 +89,7 @@ func ConvertTools(openAITools []any) []any {
8589
inputSchema = map[string]any{"type": "object", "properties": map[string]any{}}
8690
}
8791

88-
ccTool := map[string]any{
89-
"name": name,
90-
"input_schema": inputSchema,
91-
}
92+
ccTool := map[string]any{"name": name, "input_schema": inputSchema}
9293
if description, ok := fn["description"].(string); ok && description != "" {
9394
ccTool["description"] = description
9495
}
@@ -118,45 +119,72 @@ func strPtr(s string) *string {
118119

119120
func contentToString(content interface{}) string {
120121
switch v := content.(type) {
122+
case nil:
123+
return ""
121124
case string:
122125
return v
123126
case []any:
127+
var b strings.Builder
124128
for _, part := range v {
125129
if partMap, ok := part.(map[string]any); ok {
126-
if text, ok := partMap["text"].(string); ok {
127-
return text
130+
text := contentPartToString(partMap)
131+
if text != "" {
132+
b.WriteString(text)
128133
}
129134
}
130135
}
136+
return b.String()
137+
default:
138+
data, err := json.Marshal(v)
139+
if err != nil {
140+
return ""
141+
}
142+
return string(data)
131143
}
132-
return ""
133144
}
134145

135146
func contentPartToString(content any) string {
136147
switch v := content.(type) {
148+
case nil:
149+
return ""
137150
case string:
138151
return v
139152
case []any:
140153
var b strings.Builder
141154
for _, item := range v {
142155
if m, ok := item.(map[string]any); ok {
143-
if text, ok := m["text"].(string); ok {
144-
b.WriteString(text)
145-
}
156+
b.WriteString(contentPartToString(m))
146157
}
147158
}
148159
return b.String()
149-
default:
160+
case map[string]any:
161+
for _, key := range []string{"text", "content", "output_text", "input_text", "refusal", "thinking", "redacted_thinking"} {
162+
if text, ok := v[key].(string); ok {
163+
return text
164+
}
165+
}
166+
if imgURL, ok := v["image_url"].(map[string]any); ok {
167+
if url, ok := imgURL["url"].(string); ok {
168+
return "[Image URL: " + url + "]"
169+
}
170+
}
171+
if url, ok := v["image_url"].(string); ok {
172+
return "[Image URL: " + url + "]"
173+
}
150174
data, err := json.Marshal(v)
151175
if err != nil {
152176
return ""
153177
}
154178
return string(data)
179+
default:
180+
return fmt.Sprint(v)
155181
}
156182
}
157183

158-
func parseContent(content interface{}) []api.CCContentPart {
184+
func parseContent(content interface{}, toolNames map[string]string) []api.CCContentPart {
159185
switch v := content.(type) {
186+
case nil:
187+
return nil
160188
case string:
161189
if v == "" {
162190
return nil
@@ -171,24 +199,27 @@ func parseContent(content interface{}) []api.CCContentPart {
171199
}
172200
typ, _ := partMap["type"].(string)
173201
switch typ {
174-
case "text":
175-
if text, ok := partMap["text"].(string); ok && text != "" {
202+
case "text", "input_text", "output_text", "refusal", "thinking", "redacted_thinking", "reasoning", "document", "search_result":
203+
if text := contentPartToString(partMap); text != "" {
176204
parts = append(parts, api.CCContentPart{Type: "text", Text: strPtr(text)})
177205
}
178-
case "image_url":
179-
if imgURL, ok := partMap["image_url"].(map[string]any); ok {
180-
if url, ok := imgURL["url"].(string); ok && url != "" {
181-
parts = append(parts, api.CCContentPart{Type: "text", Text: strPtr("[Image URL: " + url + "]")})
182-
}
206+
case "image_url", "input_image", "image":
207+
if text := contentPartToString(partMap); text != "" {
208+
parts = append(parts, api.CCContentPart{Type: "text", Text: strPtr(text)})
183209
}
184210
case "tool_use":
185211
id, _ := partMap["id"].(string)
186212
name, _ := partMap["name"].(string)
187-
input := partMap["input"]
188-
parts = append(parts, api.CCContentPart{Type: "tool_use", ID: strPtr(id), Name: strPtr(name), Input: input})
213+
if id != "" && name != "" {
214+
toolNames[id] = name
215+
}
216+
parts = append(parts, api.CCContentPart{Type: "tool_use", ID: strPtr(id), Name: strPtr(name), Input: partMap["input"]})
189217
case "tool-call":
190218
id, _ := partMap["id"].(string)
191219
name, _ := partMap["name"].(string)
220+
if id != "" && name != "" {
221+
toolNames[id] = name
222+
}
192223
input := partMap["input"]
193224
if input == nil {
194225
input = partMap["arguments"]
@@ -200,12 +231,15 @@ func parseContent(content interface{}) []api.CCContentPart {
200231
toolID, _ = partMap["toolCallId"].(string)
201232
}
202233
toolName, _ := partMap["toolName"].(string)
234+
if toolName == "" {
235+
toolName = toolNames[toolID]
236+
}
203237
parts = append(parts, api.CCContentPart{Type: "tool-result", ToolCallID: strPtr(toolID), ToolName: strPtr(toolName), Text: strPtr(contentPartToString(partMap["content"]))})
204238
}
205239
}
206240
return parts
207241
default:
208-
return nil
242+
return []api.CCContentPart{{Type: "text", Text: strPtr(contentToString(v))}}
209243
}
210244
}
211245

@@ -218,9 +252,7 @@ func ExtractSystem(msgs []api.OpenAIMessage) (string, []api.OpenAIMessage) {
218252
if system.Len() > 0 {
219253
system.WriteString("\n")
220254
}
221-
if contentStr, ok := m.Content.(string); ok {
222-
system.WriteString(contentStr)
223-
}
255+
system.WriteString(contentToString(m.Content))
224256
} else {
225257
rest = append(rest, m)
226258
}

0 commit comments

Comments
 (0)