Skip to content

Commit 4e24e67

Browse files
committed
v1.0.4-preview.1
1 parent 3abcc4f commit 4e24e67

12 files changed

Lines changed: 215 additions & 64 deletions

model/log.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ func logOtherRetryMarkers(other string) (errorRetry bool, emptyRetry bool) {
247247
return false, false
248248
}
249249
if marker, ok := otherMap["empty_retry"].(bool); ok && marker {
250-
return false, true
250+
emptyRetry = true
251251
}
252252
if retryLog, ok := otherMap["retry_log"].(bool); ok && retryLog {
253253
errorRetry = true

model/log_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ func TestGetAllLogsRetrySubtypeFilters(t *testing.T) {
8484

8585
errorLogs, total, err := GetAllLogs(LogTypeUnknown, LogFilterErrorRetry, 0, 0, "", "", "", 0, 10, 0, "", "", "")
8686
require.NoError(t, err)
87-
require.EqualValues(t, 3, total)
88-
require.Len(t, errorLogs, 3)
89-
require.ElementsMatch(t, []int{logs[0].Id, logs[4].Id, logs[5].Id}, []int{errorLogs[0].Id, errorLogs[1].Id, errorLogs[2].Id})
87+
require.EqualValues(t, 4, total)
88+
require.Len(t, errorLogs, 4)
89+
require.ElementsMatch(t, []int{logs[0].Id, logs[4].Id, logs[5].Id, logs[6].Id}, []int{errorLogs[0].Id, errorLogs[1].Id, errorLogs[2].Id, errorLogs[3].Id})
9090

9191
emptyLogs, total, err := GetAllLogs(LogTypeUnknown, LogFilterEmptyRetry, 0, 0, "", "", "", 0, 10, 0, "", "", "")
9292
require.NoError(t, err)
@@ -105,9 +105,9 @@ func TestSumUsedQuotaRetrySubtypeFilters(t *testing.T) {
105105

106106
errorStat, err := SumUsedQuota(LogTypeUnknown, LogFilterErrorRetry, 0, 0, "", "", "", 0, "")
107107
require.NoError(t, err)
108-
require.Equal(t, 1000, errorStat.Quota)
109-
require.Equal(t, 3, errorStat.Rpm)
110-
require.Equal(t, 56, errorStat.Tpm)
108+
require.Equal(t, 1350, errorStat.Quota)
109+
require.Equal(t, 4, errorStat.Rpm)
110+
require.Equal(t, 81, errorStat.Tpm)
111111

112112
emptyStat, err := SumUsedQuota(LogTypeUnknown, LogFilterEmptyRetry, 0, 0, "", "", "", 0, "")
113113
require.NoError(t, err)

relay/channel/gemini/adaptor_responses.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gemini
22

33
import (
4+
"fmt"
45
"strings"
56

67
"github.com/MAX-API-Next/MAX-API/common"
@@ -41,8 +42,9 @@ func filterGeminiResponsesTools(raw []byte) ([]byte, error) {
4142

4243
filtered := make([]map[string]any, 0, len(tools))
4344
for _, tool := range tools {
44-
if strings.TrimSpace(common.Interface2String(tool["type"])) != "function" {
45-
continue
45+
toolType := strings.TrimSpace(common.Interface2String(tool["type"]))
46+
if toolType != "function" {
47+
return nil, fmt.Errorf("gemini responses conversion does not support tool type %q", toolType)
4648
}
4749
filtered = append(filtered, tool)
4850
}

relay/channel/gemini/adaptor_responses_test.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package gemini
22

33
import (
4+
"fmt"
45
"testing"
56

67
"github.com/MAX-API-Next/MAX-API/common"
@@ -58,12 +59,18 @@ func TestConvertOpenAIResponsesRequestToGeminiSkipsCustomToolCalls(t *testing.T)
5859
},
5960
}),
6061
Tools: mustGeminiRawMessage(t, []map[string]any{
61-
{"type": "custom", "name": "apply_patch"},
62-
{"type": "unknown", "name": "unknown"},
62+
{
63+
"type": "function",
64+
"name": "lookup",
65+
"description": "Lookup data",
66+
"parameters": map[string]any{
67+
"type": "object",
68+
},
69+
},
6370
}),
6471
})
6572

66-
assert.Empty(t, got.GetTools())
73+
require.Len(t, got.GetTools(), 1)
6774
require.Len(t, got.Contents, 2)
6875
assert.Equal(t, "model", got.Contents[0].Role)
6976
require.Len(t, got.Contents[0].Parts, 1)
@@ -76,19 +83,43 @@ func TestConvertOpenAIResponsesRequestToGeminiSkipsCustomToolCalls(t *testing.T)
7683
assert.Nil(t, got.Contents[1].Parts[0].FunctionResponse)
7784
}
7885

86+
func TestConvertOpenAIResponsesRequestToGeminiRejectsUnsupportedTools(t *testing.T) {
87+
got, err := convertResponsesToGemini(dto.OpenAIResponsesRequest{
88+
Model: "gemini-test",
89+
Input: mustGeminiRawMessage(t, "hello"),
90+
Tools: mustGeminiRawMessage(t, []map[string]any{
91+
{"type": "web_search_preview"},
92+
}),
93+
})
94+
95+
require.Error(t, err)
96+
require.Nil(t, got)
97+
assert.Contains(t, err.Error(), `tool type "web_search_preview"`)
98+
}
99+
79100
func mustConvertResponsesToGemini(t *testing.T, req dto.OpenAIResponsesRequest) *dto.GeminiChatRequest {
80101
t.Helper()
102+
got, err := convertResponsesToGemini(req)
103+
require.NoError(t, err)
104+
return got
105+
}
106+
107+
func convertResponsesToGemini(req dto.OpenAIResponsesRequest) (*dto.GeminiChatRequest, error) {
81108
info := &relaycommon.RelayInfo{
82109
OriginModelName: req.Model,
83110
ChannelMeta: &relaycommon.ChannelMeta{
84111
UpstreamModelName: req.Model,
85112
},
86113
}
87114
got, err := (&Adaptor{}).ConvertOpenAIResponsesRequest(nil, info, req)
88-
require.NoError(t, err)
115+
if err != nil {
116+
return nil, err
117+
}
89118
geminiReq, ok := got.(*dto.GeminiChatRequest)
90-
require.True(t, ok)
91-
return geminiReq
119+
if !ok {
120+
return nil, fmt.Errorf("unexpected converted request type %T", got)
121+
}
122+
return geminiReq, nil
92123
}
93124

94125
func mustGeminiRawMessage(t *testing.T, value any) []byte {

relay/channel/task/doubao/adaptor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ type requestPayload struct {
5555
} `json:"tools,omitempty"`
5656
SafetyIdentifier *string `json:"safety_identifier,omitempty"`
5757
Priority *dto.IntValue `json:"priority,omitempty"`
58-
Resolution string `json:"resolution,omitempty"`
59-
Ratio string `json:"ratio,omitempty"`
58+
Resolution *string `json:"resolution,omitempty"`
59+
Ratio *string `json:"ratio,omitempty"`
6060
Duration *dto.IntValue `json:"duration,omitempty"`
6161
Frames *dto.IntValue `json:"frames,omitempty"`
6262
Seed *dto.IntValue `json:"seed,omitempty"`
@@ -173,8 +173,8 @@ func (a *TaskAdaptor) resolveSeedanceBillingInputs(c *gin.Context, req *relaycom
173173
if !a.useSeedanceMediaProtocol() {
174174
payload, err := a.convertToRequestPayload(req)
175175
if err == nil && payload != nil {
176-
if payload.Resolution != "" {
177-
resolution = payload.Resolution
176+
if payload.Resolution != nil && *payload.Resolution != "" {
177+
resolution = *payload.Resolution
178178
}
179179
hasVideo = hasVideo || hasVideoInContent(payload.Content)
180180
}

relay/channel/task/doubao/adaptor_media_protocol_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,5 +267,32 @@ func TestConvertToRequestPayloadPreservesSeedanceFields(t *testing.T) {
267267
assert.Equal(t, "safety-123", *payload.SafetyIdentifier)
268268
require.NotNil(t, payload.Priority)
269269
assert.Equal(t, 0, int(*payload.Priority))
270-
assert.Equal(t, "4k", payload.Resolution)
270+
require.NotNil(t, payload.Resolution)
271+
assert.Equal(t, "4k", *payload.Resolution)
272+
}
273+
274+
func TestConvertToRequestPayloadPreservesExplicitEmptyOptionalStrings(t *testing.T) {
275+
var req relaycommon.TaskSubmitReq
276+
err := common.Unmarshal([]byte(`{
277+
"model": "doubao-seedance-2-0-260128",
278+
"prompt": "test",
279+
"metadata": {
280+
"resolution": "",
281+
"ratio": ""
282+
}
283+
}`), &req)
284+
require.NoError(t, err)
285+
286+
payload, err := (&TaskAdaptor{}).convertToRequestPayload(&req)
287+
require.NoError(t, err)
288+
require.NotNil(t, payload)
289+
require.NotNil(t, payload.Resolution)
290+
assert.Equal(t, "", *payload.Resolution)
291+
require.NotNil(t, payload.Ratio)
292+
assert.Equal(t, "", *payload.Ratio)
293+
294+
data, err := common.Marshal(payload)
295+
require.NoError(t, err)
296+
assert.Contains(t, string(data), `"resolution":""`)
297+
assert.Contains(t, string(data), `"ratio":""`)
271298
}

service/convert.go

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -626,12 +626,7 @@ func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relayco
626626
claudeContent.Type = "tool_use"
627627
claudeContent.Id = toolUse.ID
628628
claudeContent.Name = toolUse.Function.Name
629-
var mapParams map[string]interface{}
630-
if err := common.Unmarshal([]byte(toolUse.Function.Arguments), &mapParams); err == nil {
631-
claudeContent.Input = mapParams
632-
} else {
633-
claudeContent.Input = map[string]interface{}{"arguments": toolUse.Function.Arguments}
634-
}
629+
claudeContent.Input = parseToolCallArguments(toolUse.Function.Arguments)
635630
contents = append(contents, claudeContent)
636631
}
637632
}
@@ -642,6 +637,17 @@ func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relayco
642637
return claudeResponse
643638
}
644639

640+
func parseToolCallArguments(rawArgs string) map[string]interface{} {
641+
if rawArgs == "" {
642+
return make(map[string]interface{})
643+
}
644+
var args map[string]interface{}
645+
if err := common.Unmarshal([]byte(rawArgs), &args); err != nil {
646+
return map[string]interface{}{"arguments": rawArgs}
647+
}
648+
return args
649+
}
650+
645651
func stopReasonOpenAI2Claude(reason string) string {
646652
return reasonmap.OpenAIFinishReasonToClaudeStopReason(reason)
647653
}
@@ -873,19 +879,10 @@ func ResponseOpenAI2Gemini(openAIResponse *dto.OpenAITextResponse, info *relayco
873879
if len(toolCalls) > 0 {
874880
for _, toolCall := range toolCalls {
875881
// 解析参数
876-
var args map[string]interface{}
877-
if toolCall.Function.Arguments != "" {
878-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
879-
args = map[string]interface{}{"arguments": toolCall.Function.Arguments}
880-
}
881-
} else {
882-
args = make(map[string]interface{})
883-
}
884-
885882
part := dto.GeminiPart{
886883
FunctionCall: &dto.FunctionCall{
887884
FunctionName: toolCall.Function.Name,
888-
Arguments: args,
885+
Arguments: parseToolCallArguments(toolCall.Function.Arguments),
889886
},
890887
}
891888
content.Parts = append(content.Parts, part)
@@ -967,19 +964,10 @@ func StreamResponseOpenAI2Gemini(openAIResponse *dto.ChatCompletionsStreamRespon
967964
if choice.Delta.ToolCalls != nil {
968965
for _, toolCall := range choice.Delta.ToolCalls {
969966
// 解析参数
970-
var args map[string]interface{}
971-
if toolCall.Function.Arguments != "" {
972-
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
973-
args = map[string]interface{}{"arguments": toolCall.Function.Arguments}
974-
}
975-
} else {
976-
args = make(map[string]interface{})
977-
}
978-
979967
part := dto.GeminiPart{
980968
FunctionCall: &dto.FunctionCall{
981969
FunctionName: toolCall.Function.Name,
982-
Arguments: args,
970+
Arguments: parseToolCallArguments(toolCall.Function.Arguments),
983971
},
984972
}
985973
content.Parts = append(content.Parts, part)

service/convert_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ func TestResponseOpenAI2GeminiPreservesTextWithToolCalls(t *testing.T) {
5050
assert.Equal(t, map[string]interface{}{"q": "max-api"}, parts[1].FunctionCall.Arguments)
5151
}
5252

53+
func TestResponseOpenAI2GeminiWrapsMalformedToolArguments(t *testing.T) {
54+
resp := openAIResponseWithToolCallArguments(t, "{")
55+
resp.Choices[0].Message.Content = ""
56+
got := ResponseOpenAI2Gemini(resp, &relaycommon.RelayInfo{})
57+
58+
require.NotNil(t, got)
59+
require.Len(t, got.Candidates, 1)
60+
parts := got.Candidates[0].Content.Parts
61+
require.Len(t, parts, 1)
62+
require.NotNil(t, parts[0].FunctionCall)
63+
assert.Equal(t, map[string]interface{}{"arguments": "{"}, parts[0].FunctionCall.Arguments)
64+
}
65+
5366
func openAIResponseWithTextAndToolCall(t *testing.T) *dto.OpenAITextResponse {
5467
return openAIResponseWithToolCallArguments(t, `{"q":"max-api"}`)
5568
}

service/openaicompat/chat_to_responses_response.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,7 @@ func ChatCompletionsResponseToResponsesResponse(resp *dto.OpenAITextResponse, id
109109
})
110110
}
111111
for i, toolCall := range choice.Message.ParseToolCalls() {
112-
toolOutput, err := chatToolCallToResponsesOutput(toolCall, id, i, status)
113-
if err != nil {
114-
return nil, nil, err
115-
}
116-
out.Output = append(out.Output, toolOutput)
112+
out.Output = append(out.Output, chatToolCallToResponsesOutput(toolCall, id, i, status))
117113
}
118114

119115
return out, usage, nil
@@ -532,7 +528,7 @@ func responseOutputStatus(resp *dto.OpenAIResponsesResponse) string {
532528
return "incomplete"
533529
}
534530

535-
func chatToolCallToResponsesOutput(toolCall dto.ToolCallRequest, responseID string, index int, status string) (dto.ResponsesOutput, error) {
531+
func chatToolCallToResponsesOutput(toolCall dto.ToolCallRequest, responseID string, index int, status string) dto.ResponsesOutput {
536532
callID := strings.TrimSpace(toolCall.ID)
537533
if callID == "" {
538534
callID = fmt.Sprintf("%s_call_%d", responseID, index)
@@ -545,15 +541,15 @@ func chatToolCallToResponsesOutput(toolCall dto.ToolCallRequest, responseID stri
545541
CallId: callID,
546542
Name: toolCall.Function.Name,
547543
Arguments: chatArgumentsRawMessage(toolCall.Function.Arguments),
548-
}, nil
544+
}
549545
}
550546
return dto.ResponsesOutput{
551547
Type: toolCall.Type,
552548
ID: callID,
553549
Status: status,
554550
CallId: callID,
555551
Arguments: toolCall.Custom,
556-
}, nil
552+
}
557553
}
558554

559555
func chatArgumentsRawMessage(arguments string) []byte {

0 commit comments

Comments
 (0)