Fix/claude thinking tool call conversion (#3756)

Signed-off-by: wydream <yaodiwu618@gmail.com>
This commit is contained in:
woody
2026-05-20 11:34:07 +08:00
committed by GitHub
parent f1dfc8f3d2
commit e1e631263c
12 changed files with 1113 additions and 58 deletions

View File

@@ -25,6 +25,20 @@ var basicOpenAIConfig = func() json.RawMessage {
return data
}()
var openAIWithUpstreamErrorResponseBodyLogConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
"provider": map[string]interface{}{
"type": "openai",
"apiTokens": []string{"sk-openai-test123456789"},
"logUpstreamErrorResponseBody": true,
"modelMapping": map[string]string{
"*": "gpt-3.5-turbo",
},
},
})
return data
}()
// 测试配置OpenAI多模型配置
var openAIMultiModelConfig = func() json.RawMessage {
data, _ := json.Marshal(map[string]interface{}{
@@ -826,6 +840,83 @@ func RunOpenAIOnHttpResponseBodyTests(t *testing.T) {
require.True(t, hasResponseBodyLogs, "Should have response body processing logs")
})
t.Run("openai upstream error response body warn log disabled by default", func(t *testing.T) {
host, status := test.NewTestHost(basicOpenAIConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
requestBody := `{"model":"gpt-4o","messages":[{"role":"user","content":"test"}]}`
host.CallOnHttpRequestBody([]byte(requestBody))
require.NoError(t, host.SetProperty([]string{"response", "code_details"}, []byte("via_upstream")))
responseHeaders := [][2]string{
{":status", "400"},
{"Content-Type", "application/json"},
{"x-request-id", "upstream-req-123"},
}
action := host.CallOnHttpResponseHeaders(responseHeaders)
require.Equal(t, types.ActionContinue, action)
errorBody := `{"error":{"type":"invalid_request_error","message":"thinking is enabled but reasoning_content is missing"}}`
action = host.CallOnHttpResponseBody([]byte(errorBody))
require.Equal(t, types.ActionContinue, action)
for _, logEntry := range host.GetWarnLogs() {
require.NotContains(t, logEntry, "[upstream_error_response]")
}
})
t.Run("openai upstream error response body logs warn", func(t *testing.T) {
host, status := test.NewTestHost(openAIWithUpstreamErrorResponseBodyLogConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/chat/completions"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
requestBody := `{"model":"gpt-4o","messages":[{"role":"user","content":"test"}]}`
host.CallOnHttpRequestBody([]byte(requestBody))
require.NoError(t, host.SetProperty([]string{"response", "code_details"}, []byte("via_upstream")))
responseHeaders := [][2]string{
{":status", "400"},
{"Content-Type", "application/json"},
{"x-request-id", "upstream-req-123"},
}
action := host.CallOnHttpResponseHeaders(responseHeaders)
require.Equal(t, types.ActionContinue, action)
errorBody := `{"error":{"type":"invalid_request_error","message":"thinking is enabled but reasoning_content is missing"}}`
action = host.CallOnHttpResponseBody([]byte(errorBody))
require.Equal(t, types.ActionContinue, action)
warnLogs := host.GetWarnLogs()
hasUpstreamErrorLog := false
for _, logEntry := range warnLogs {
if strings.Contains(logEntry, "[upstream_error_response]") &&
strings.Contains(logEntry, "provider=openai") &&
strings.Contains(logEntry, "status=400") &&
strings.Contains(logEntry, "request_id=upstream-req-123") &&
strings.Contains(logEntry, "final_model=gpt-3.5-turbo") &&
strings.Contains(logEntry, "reasoning_content is missing") {
hasUpstreamErrorLog = true
break
}
}
require.True(t, hasUpstreamErrorLog, "Should log upstream 400 response body at warn level, logs: %v", warnLogs)
})
// 测试OpenAI响应体处理嵌入接口
t.Run("openai embeddings response body", func(t *testing.T) {
host, status := test.NewTestHost(basicOpenAIConfig)

View File

@@ -61,6 +61,58 @@ func RunZhipuAIClaudeAutoConversionTests(t *testing.T) {
assert.NotContains(t, bodyMap, "reasoning_effort")
})
t.Run("claude reasoning history disables zhipuai clear thinking", func(t *testing.T) {
host, status := test.NewTestHost(basicZhipuAIConfig)
defer host.Reset()
require.Equal(t, types.OnPluginStartStatusOK, status)
action := host.CallOnHttpRequestHeaders([][2]string{
{":authority", "example.com"},
{":path", "/v1/messages"},
{":method", "POST"},
{"Content-Type", "application/json"},
})
require.Equal(t, types.HeaderStopIteration, action)
requestBody := `{
"model": "glm-4.5",
"max_tokens": 1000,
"messages": [
{"role": "user", "content": "Need weather"},
{"role": "assistant", "content": [
{"type": "thinking", "thinking": "Need to call the weather tool.", "signature": "sig"},
{"type": "tool_use", "id": "toolu_1", "name": "get_weather", "input": {"city": "Paris"}}
]},
{"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_1", "content": "sunny"}
]}
],
"thinking": {"type": "enabled", "budget_tokens": 8192}
}`
action = host.CallOnHttpRequestBody([]byte(requestBody))
require.Equal(t, types.ActionContinue, action)
transformedBody := host.GetRequestBody()
require.NotNil(t, transformedBody)
var bodyMap map[string]interface{}
err := json.Unmarshal(transformedBody, &bodyMap)
require.NoError(t, err)
thinking, ok := bodyMap["thinking"].(map[string]interface{})
require.True(t, ok, "thinking field should be present")
assert.Equal(t, "enabled", thinking["type"])
assert.Equal(t, false, thinking["clear_thinking"])
messages, ok := bodyMap["messages"].([]interface{})
require.True(t, ok, "messages should be present")
require.GreaterOrEqual(t, len(messages), 2)
assistantMsg, ok := messages[1].(map[string]interface{})
require.True(t, ok, "assistant message should be an object")
assert.Equal(t, "Need to call the weather tool.", assistantMsg["reasoning_content"])
})
t.Run("claude without thinking sets thinking disabled for zhipuai", func(t *testing.T) {
host, status := test.NewTestHost(basicZhipuAIConfig)
defer host.Reset()