diff --git a/backend/internal/service/openai_gateway_passthrough.go b/backend/internal/service/openai_gateway_passthrough.go index c15a78ef48..34fb33e327 100644 --- a/backend/internal/service/openai_gateway_passthrough.go +++ b/backend/internal/service/openai_gateway_passthrough.go @@ -83,6 +83,17 @@ func needsOpenAIResponsesClientToolAdaptation(body []byte) bool { return needsAdaptation } +func shouldAdaptOpenAIResponsesClientTools(account *Account, c *gin.Context, body []byte) bool { + if account == nil || account.Type != AccountTypeAPIKey || isOpenAIResponsesCompactPath(c) || + !needsOpenAIResponsesClientToolAdaptation(body) { + return false + } + if account.Platform == PlatformOpenAI { + return true + } + return account.Platform == PlatformDeepseek && account.GetAPIProtocol() == APIProtocolResponses +} + func openAIResponsesClientToolMapping(c *gin.Context) (apicompat.ResponsesClientToolMapping, bool) { if c == nil { return apicompat.ResponsesClientToolMapping{}, false @@ -183,8 +194,7 @@ func (s *OpenAIGatewayService) forwardOpenAIPassthrough( } } - if account != nil && account.Platform == PlatformOpenAI && account.Type == AccountTypeAPIKey && - !isOpenAIResponsesCompactPath(c) && needsOpenAIResponsesClientToolAdaptation(body) { + if shouldAdaptOpenAIResponsesClientTools(account, c, body) { adaptedBody, mapping, adaptErr := adaptOpenAIResponsesClientTools(body) if adaptErr != nil { return nil, adaptErr diff --git a/backend/internal/service/openai_gateway_responses_client_tools_test.go b/backend/internal/service/openai_gateway_responses_client_tools_test.go index cfb4f6eb62..efb96ba20f 100644 --- a/backend/internal/service/openai_gateway_responses_client_tools_test.go +++ b/backend/internal/service/openai_gateway_responses_client_tools_test.go @@ -88,6 +88,74 @@ func TestClearOpenAIResponsesClientToolMappingRemovesStaleContextState(t *testin require.False(t, ok) } +func TestShouldAdaptOpenAIResponsesClientToolsForDeepSeekResponses(t *testing.T) { + gin.SetMode(gin.TestMode) + c, _ := gin.CreateTestContext(httptest.NewRecorder()) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil) + body := openAIClientToolsRequest(true) + + deepSeekResponses := &Account{ + Platform: PlatformDeepseek, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "api_protocol": APIProtocolResponses, + }, + } + require.True(t, shouldAdaptOpenAIResponsesClientTools(deepSeekResponses, c, body)) + + deepSeekChat := &Account{ + Platform: PlatformDeepseek, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "api_protocol": APIProtocolChatCompletions, + }, + } + require.False(t, shouldAdaptOpenAIResponsesClientTools(deepSeekChat, c, body)) +} + +func TestDeepSeekResponsesPassthroughRestoresClientToolsStreaming(t *testing.T) { + gin.SetMode(gin.TestMode) + body := openAIClientToolsRequest(true) + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", bytes.NewReader(body)) + + sse := strings.Join([]string{ + `data: {"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"type":"function_call","id":"i1","call_id":"c1","name":"exec","status":"in_progress"}}`, + `data: {"type":"response.function_call_arguments.done","sequence_number":1,"item_id":"i1","call_id":"c1","name":"exec","arguments":"{\"input\":\"pwd\"}"}`, + `data: {"type":"response.output_item.done","sequence_number":2,"output_index":0,"item":{"type":"function_call","id":"i1","call_id":"c1","name":"exec","arguments":"{\"input\":\"pwd\"}","status":"completed"}}`, + `data: {"type":"response.completed","sequence_number":3,"response":{"id":"resp_ds_tools","status":"completed","output":[{"type":"function_call","id":"i1","call_id":"c1","name":"exec","arguments":"{\"input\":\"pwd\"}"}],"usage":{"input_tokens":1,"output_tokens":1}}}`, + }, "\n\n") + "\n\n" + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"text/event-stream"}}, + Body: io.NopCloser(strings.NewReader(sse)), + }} + svc := openAIClientToolsTestService(upstream) + account := &Account{ + ID: 5661, + Platform: PlatformDeepseek, + Type: AccountTypeAPIKey, + Credentials: map[string]any{ + "api_key": "test-key", + "api_protocol": APIProtocolResponses, + "base_url": "https://relay.example", + }, + } + + result, err := svc.forwardOpenAIPassthrough( + context.Background(), c, account, body, body, "gpt-5.4", false, nil, true, time.Now(), + ) + + require.NoError(t, err) + require.NotNil(t, result) + assertOpenAIClientToolsLowered(t, upstream.lastBody) + output := recorder.Body.String() + require.Contains(t, output, `"type":"custom_tool_call"`) + require.Contains(t, output, `"type":"response.custom_tool_call_input.done"`) + require.Contains(t, output, `"input":"pwd"`) +} + func TestOpenAIPassthroughAPIKeyRestoresClientToolsNonStreaming(t *testing.T) { gin.SetMode(gin.TestMode) body := openAIClientToolsRequest(false)