| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- //go:build unit
- package service
- import (
- "context"
- "fmt"
- "io"
- "net/http"
- "strings"
- "testing"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- // 国产供应商原生 Anthropic 直通路径(api_protocol=anthropic)的 reasoning_effort 记录。
- // 回归背景:该路径此前从不提取 Claude 协议的 output_config.effort,也不做
- // thinking-enabled 兜底,导致 kimi/zhipu/deepseek 平台分组的 /v1/messages 请求
- // usage_log.reasoning_effort 恒为 NULL。
- func nativeAnthropicTestAccount() *Account {
- return &Account{
- ID: 702,
- Name: "kimi-native",
- Platform: PlatformKimi,
- Type: AccountTypeAPIKey,
- Concurrency: 1,
- Credentials: map[string]any{
- "api_key": "sk-test",
- "api_protocol": APIProtocolAnthropic,
- "api_base_urls": map[string]any{
- APIProtocolAnthropic: "http://anthropic.example",
- },
- },
- }
- }
- func nativeAnthropicGLMTestAccount() *Account {
- account := nativeAnthropicTestAccount()
- account.Name = "zhipu-native"
- account.Platform = PlatformZhipu
- return account
- }
- func nativeAnthropicBufferedResponse() *http.Response {
- return &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"application/json"}},
- Body: io.NopCloser(strings.NewReader(
- `{"id":"msg_1","type":"message","role":"assistant","model":"k3",` +
- `"content":[{"type":"text","text":"pong"}],"stop_reason":"end_turn",` +
- `"usage":{"input_tokens":93,"output_tokens":16}}`,
- )),
- }
- }
- func nativeAnthropicStreamResponse() *http.Response {
- sse := `event: message_start
- data: {"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","model":"k3","content":[],"stop_reason":null,"usage":{"input_tokens":93,"output_tokens":1}}}
- event: content_block_start
- data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
- event: content_block_delta
- data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"pong"}}
- event: content_block_stop
- data: {"type":"content_block_stop","index":0}
- event: message_delta
- data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":16}}
- event: message_stop
- data: {"type":"message_stop"}
- `
- return &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{"Content-Type": []string{"text/event-stream"}},
- Body: io.NopCloser(strings.NewReader(sse)),
- }
- }
- func TestNativeAnthropicPassthroughRecordsOutputConfigEffort(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"k3","max_tokens":32,"stream":false,` +
- `"output_config":{"effort":"low"},` +
- `"messages":[{"role":"user","content":"hi"}]}`)
- upstream := &httpUpstreamRecorder{resp: nativeAnthropicBufferedResponse()}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- result, err := svc.ForwardAsAnthropic(context.Background(),
- adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicTestAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.NotNil(t, result.ReasoningEffort)
- require.Equal(t, "low", *result.ReasoningEffort)
- }
- func TestNativeAnthropicPassthroughThinkingEnabledFallback(t *testing.T) {
- gin.SetMode(gin.TestMode)
- // 未显式传 effort,但 thinking 已启用:k3 属于 passback-required 白名单,应兜底记为 high。
- body := []byte(`{"model":"k3","max_tokens":32,"stream":false,` +
- `"thinking":{"type":"enabled","budget_tokens":1024},` +
- `"messages":[{"role":"user","content":"hi"}]}`)
- upstream := &httpUpstreamRecorder{resp: nativeAnthropicBufferedResponse()}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- result, err := svc.ForwardAsAnthropic(context.Background(),
- adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicTestAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.NotNil(t, result.ReasoningEffort)
- require.Equal(t, "high", *result.ReasoningEffort)
- }
- func TestNativeAnthropicPassthroughStreamRecordsEffort(t *testing.T) {
- gin.SetMode(gin.TestMode)
- body := []byte(`{"model":"k3","max_tokens":32,"stream":true,` +
- `"output_config":{"effort":"max"},` +
- `"messages":[{"role":"user","content":"hi"}]}`)
- upstream := &httpUpstreamRecorder{resp: nativeAnthropicStreamResponse()}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- result, err := svc.ForwardAsAnthropic(context.Background(),
- adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicTestAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.NotNil(t, result.ReasoningEffort)
- require.Equal(t, "max", *result.ReasoningEffort)
- }
- func TestNativeAnthropicPassthroughNoEffortStaysNil(t *testing.T) {
- gin.SetMode(gin.TestMode)
- // 既无 output_config.effort 也未启用 thinking:保持 nil,不做语义注入。
- body := []byte(`{"model":"k3","max_tokens":32,"stream":false,` +
- `"messages":[{"role":"user","content":"hi"}]}`)
- upstream := &httpUpstreamRecorder{resp: nativeAnthropicBufferedResponse()}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- result, err := svc.ForwardAsAnthropic(context.Background(),
- adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicTestAccount(), body, "", "")
- require.NoError(t, err)
- require.NotNil(t, result)
- require.Nil(t, result.ReasoningEffort)
- }
- func TestNativeAnthropicPassthroughNormalizesGLM53Thinking(t *testing.T) {
- gin.SetMode(gin.TestMode)
- tests := []struct {
- name string
- stream bool
- preference string
- wantEffort string
- }{
- {name: "disabled buffered", preference: `"thinking":{"type":"disabled"},`, wantEffort: "low"},
- {name: "off buffered", preference: `"thinking":{"type":"off"},`, wantEffort: "low"},
- {name: "none buffered", preference: `"thinking":{"type":"none"},`, wantEffort: "low"},
- {name: "enabled buffered", preference: `"thinking":{"type":"enabled"},`, wantEffort: "high"},
- {name: "enabled streaming", stream: true, preference: `"thinking":{"type":"enabled"},`, wantEffort: "high"},
- {name: "adaptive buffered", preference: `"thinking":{"type":"adaptive"},`, wantEffort: "high"},
- {name: "adaptive streaming", stream: true, preference: `"thinking":{"type":"adaptive"},`, wantEffort: "high"},
- {name: "minimal buffered", preference: `"output_config":{"effort":"minimal"},`, wantEffort: "low"},
- {name: "low buffered", preference: `"output_config":{"effort":"low"},`, wantEffort: "low"},
- {name: "medium streaming", stream: true, preference: `"output_config":{"effort":"medium"},`, wantEffort: "high"},
- {name: "high buffered", preference: `"output_config":{"effort":"high"},`, wantEffort: "high"},
- {name: "xhigh buffered", preference: `"output_config":{"effort":"xhigh"},`, wantEffort: "max"},
- {name: "max buffered", preference: `"output_config":{"effort":"max"},`, wantEffort: "max"},
- {name: "ultra buffered", preference: `"output_config":{"effort":"ultra"},`, wantEffort: "max"},
- {name: "output effort wins over thinking", preference: `"thinking":{"type":"adaptive"},"output_config":{"effort":"low"},`, wantEffort: "low"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- body := []byte(fmt.Sprintf(`{"model":"glm-5.3","max_tokens":32,"stream":%t,%s"messages":[{"role":"user","content":"hi"}]}`, tt.stream, tt.preference))
- response := nativeAnthropicBufferedResponse()
- if tt.stream {
- response = nativeAnthropicStreamResponse()
- }
- upstream := &httpUpstreamRecorder{resp: response}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- _, err := svc.ForwardAsAnthropic(context.Background(),
- adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicGLMTestAccount(), body, "", "")
- require.NoError(t, err)
- require.Equal(t, "enabled", gjson.GetBytes(upstream.lastBody, "thinking.type").String())
- require.Equal(t, tt.wantEffort, gjson.GetBytes(upstream.lastBody, "output_config.effort").String())
- })
- }
- }
- func TestNativeAnthropicPassthroughLeavesOtherThinkingUntouched(t *testing.T) {
- gin.SetMode(gin.TestMode)
- tests := []struct {
- name string
- body string
- }{
- {name: "glm 5.3 unspecified", body: `{"model":"glm-5.3","max_tokens":32,"stream":false,"messages":[]}`},
- {name: "glm 5.2 disabled", body: `{"model":"glm-5.2","max_tokens":32,"stream":false,"thinking":{"type":"disabled"},"messages":[]}`},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- body := []byte(tt.body)
- upstream := &httpUpstreamRecorder{resp: nativeAnthropicBufferedResponse()}
- svc := &OpenAIGatewayService{cfg: rawChatCompletionsTestConfig(), httpUpstream: upstream}
- _, err := svc.ForwardAsAnthropic(context.Background(),
- adaptiveProtocolTestContext("/v1/messages", body), nativeAnthropicGLMTestAccount(), body, "", "")
- require.NoError(t, err)
- require.JSONEq(t, tt.body, string(upstream.lastBody))
- })
- }
- }
|