| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- //go:build unit
- package service
- import (
- "context"
- "net/http"
- "net/http/httptest"
- "strconv"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- "github.com/tidwall/gjson"
- )
- // Anthropic Messages 出站(三个 builder)的 Ollama Cloud DeepSeek max_tokens clamp
- // 接线验证。实测背景:POST ollama.com/v1/messages(Bearer)max_tokens=256000 被上游
- // 以 "max_tokens (256000) exceeds model's maximum output tokens (65536)" 400 拒绝。
- // 用例直接调用各 builder 实际生成 *http.Request,同时检查出站 URL 与 wire body。
- func messagesClampTestConfig() *config.Config {
- cfg := rawChatCompletionsTestConfig()
- cfg.Gateway = config.GatewayConfig{MaxLineSize: defaultMaxLineSize}
- return cfg
- }
- func newMessagesClampTestContext(t *testing.T) *gin.Context {
- t.Helper()
- gin.SetMode(gin.TestMode)
- rec := httptest.NewRecorder()
- c, _ := gin.CreateTestContext(rec)
- c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
- return c
- }
- // messagesClampOllamaAccount 构造挂在实际 ollama.com 上游的 APIKey 账号。
- // builder A / B 的 Messages base 取 GetBaseURL()(凭证 base_url),builder C 的
- // Anthropic 协议 base 取 GetAnthropicProtocolBaseURL()(anthropic 协议时同为
- // 凭证 base_url)。
- func messagesClampOllamaAccount(id int64, platform string) *Account {
- return &Account{
- ID: id,
- Name: "ollama-cloud-messages",
- Platform: platform,
- Type: AccountTypeAPIKey,
- Credentials: map[string]any{
- "api_key": "sk-test",
- "base_url": "https://ollama.com",
- },
- Extra: map[string]any{},
- }
- }
- func messagesClampBody(model string, maxTokens int) []byte {
- return []byte(`{"model":"` + model + `","max_tokens":` + strconv.Itoa(maxTokens) +
- `,"stream":false,"messages":[{"role":"user","content":"hi"}]}`)
- }
- // TestBuildUpstreamRequest_ClampsOllamaCloudDeepSeekMaxTokens 覆盖 GatewayService
- // .buildUpstreamRequest(Anthropic 平台原生 Messages + 共享重试路径)。
- func TestBuildUpstreamRequest_ClampsOllamaCloudDeepSeekMaxTokens(t *testing.T) {
- svc := &GatewayService{cfg: messagesClampTestConfig()}
- c := newMessagesClampTestContext(t)
- body := messagesClampBody("deepseek-v4-flash", 256000)
- account := messagesClampOllamaAccount(401, PlatformAnthropic)
- req, wireBody, err := svc.buildUpstreamRequest(
- context.Background(), c, account, body, "sk-test", "api_key",
- "deepseek-v4-flash", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages?beta=true", req.URL.String())
- require.Equal(t, "deepseek-v4-flash", gjson.GetBytes(wireBody, "model").String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int())
- t.Run("official anthropic base is untouched", func(t *testing.T) {
- official := messagesClampOllamaAccount(402, PlatformAnthropic)
- official.Credentials["base_url"] = "https://api.anthropic.com"
- _, wire, err := svc.buildUpstreamRequest(
- context.Background(), c, official, body, "sk-test", "api_key",
- "deepseek-v4-flash", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wire, "max_tokens").Int())
- })
- t.Run("non deepseek model untouched", func(t *testing.T) {
- nonDeepSeek := messagesClampBody("k3-256k", 256000)
- _, wire, err := svc.buildUpstreamRequest(
- context.Background(), c, account, nonDeepSeek, "sk-test", "api_key",
- "k3-256k", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wire, "max_tokens").Int())
- })
- t.Run("at cap and extra cap zero untouched", func(t *testing.T) {
- atCap := messagesClampBody("deepseek-v4-flash", 65535)
- _, wire, err := svc.buildUpstreamRequest(
- context.Background(), c, account, atCap, "sk-test", "api_key",
- "deepseek-v4-flash", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, int64(65535), gjson.GetBytes(wire, "max_tokens").Int())
- disabled := messagesClampOllamaAccount(403, PlatformAnthropic)
- disabled.Extra[OllamaCloudMaxTokensCapExtraKey] = 0
- _, wire, err = svc.buildUpstreamRequest(
- context.Background(), c, disabled, body, "sk-test", "api_key",
- "deepseek-v4-flash", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wire, "max_tokens").Int())
- })
- }
- // TestBuildUpstreamRequestAnthropicAPIKeyPassthrough_ClampsOllamaCloudDeepSeekMaxTokens
- // 覆盖 Anthropic 平台 APIKey passthrough builder。
- func TestBuildUpstreamRequestAnthropicAPIKeyPassthrough_ClampsOllamaCloudDeepSeekMaxTokens(t *testing.T) {
- svc := &GatewayService{cfg: messagesClampTestConfig()}
- c := newMessagesClampTestContext(t)
- body := messagesClampBody("deepseek-v4-flash", 256000)
- account := messagesClampOllamaAccount(411, PlatformAnthropic)
- req, wireBody, err := svc.buildUpstreamRequestAnthropicAPIKeyPassthrough(
- context.Background(), c, account, body, "sk-test",
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages?beta=true", req.URL.String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int())
- // 官方 Anthropic(api.anthropic.com)即使 max_tokens 超过 65535 也字节级不变。
- official := newAnthropicAPIKeyAccountForTest()
- _, wire, err := svc.buildUpstreamRequestAnthropicAPIKeyPassthrough(
- context.Background(), c, official, body, "upstream-anthropic-key",
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wire, "max_tokens").Int())
- }
- // TestBuildNativeAnthropicUpstreamRequest_ClampsOllamaCloudDeepSeekMaxTokens 覆盖
- // 国产供应商原生 Anthropic 协议 builder(Messages / Responses / CC 桥接共享)。
- func TestBuildNativeAnthropicUpstreamRequest_ClampsOllamaCloudDeepSeekMaxTokens(t *testing.T) {
- svc := &OpenAIGatewayService{cfg: messagesClampTestConfig()}
- c := newMessagesClampTestContext(t)
- body := messagesClampBody("deepseek-v4-flash", 256000)
- t.Run("anthropic protocol ollama base is clamped", func(t *testing.T) {
- account := messagesClampOllamaAccount(421, PlatformDeepseek)
- account.Credentials["api_protocol"] = APIProtocolAnthropic
- targetURL, err := svc.nativeAnthropicTargetURL(account)
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages", targetURL)
- req, wireBody, err := svc.buildNativeAnthropicUpstreamRequest(
- context.Background(), c, account, body, "sk-test", targetURL,
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages", req.URL.String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- // adaptive 账号按 Anthropic 协议地址(api_base_urls[anthropic])判定,而非 CC
- // 地址:anthropic 指向 ollama.com、chat_completions 指向官方 DeepSeek 时命中。
- t.Run("adaptive uses anthropic base not cc base", func(t *testing.T) {
- account := messagesClampOllamaAccount(422, PlatformDeepseek)
- account.Credentials["api_protocol"] = APIProtocolAdaptive
- account.Credentials["api_base_urls"] = map[string]any{
- APIProtocolAnthropic: "https://ollama.com",
- APIProtocolChatCompletions: "https://api.deepseek.com",
- }
- targetURL, err := svc.nativeAnthropicTargetURL(account)
- require.NoError(t, err)
- req, wireBody, err := svc.buildNativeAnthropicUpstreamRequest(
- context.Background(), c, account, body, "sk-test", targetURL,
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages", req.URL.String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- // 反向:实际 Anthropic 上游非 ollama.com(官方 anthropic 端点),即使 CC 地址
- // 指向 ollama.com 且残留 usage extra,也不 clamp。
- t.Run("non-ollama anthropic base with cc ollama base and usage extra untouched", func(t *testing.T) {
- account := messagesClampOllamaAccount(423, PlatformDeepseek)
- account.Credentials["api_protocol"] = APIProtocolAdaptive
- account.Credentials["api_base_urls"] = map[string]any{
- APIProtocolAnthropic: "https://api.deepseek.com/anthropic",
- APIProtocolChatCompletions: "https://ollama.com",
- }
- account.Extra[OllamaCloudUsageSnapshotExtraKey] = map[string]any{"status": "ok"}
- targetURL, err := svc.nativeAnthropicTargetURL(account)
- require.NoError(t, err)
- req, wireBody, err := svc.buildNativeAnthropicUpstreamRequest(
- context.Background(), c, account, body, "sk-test", targetURL,
- )
- require.NoError(t, err)
- require.Equal(t, "https://api.deepseek.com/anthropic/v1/messages", req.URL.String())
- require.Equal(t, int64(256000), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- // 官方 DeepSeek Anthropic 协议(平台默认端点)字节级不变。
- t.Run("official deepseek anthropic default untouched", func(t *testing.T) {
- account := &Account{
- ID: 424,
- Name: "official-deepseek-anthropic",
- Platform: PlatformDeepseek,
- Type: AccountTypeAPIKey,
- Credentials: map[string]any{
- "api_key": "sk-test",
- "api_protocol": APIProtocolAnthropic,
- },
- Extra: map[string]any{},
- }
- targetURL, err := svc.nativeAnthropicTargetURL(account)
- require.NoError(t, err)
- _, wireBody, err := svc.buildNativeAnthropicUpstreamRequest(
- context.Background(), c, account, body, "sk-test", targetURL,
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- // 非 DeepSeek 模型(映射后出站 model)不变。
- t.Run("non deepseek model untouched", func(t *testing.T) {
- account := messagesClampOllamaAccount(425, PlatformDeepseek)
- account.Credentials["api_protocol"] = APIProtocolAnthropic
- nonDeepSeek := messagesClampBody("glm-4.7", 256000)
- targetURL, err := svc.nativeAnthropicTargetURL(account)
- require.NoError(t, err)
- _, wireBody, err := svc.buildNativeAnthropicUpstreamRequest(
- context.Background(), c, account, nonDeepSeek, "sk-test", targetURL,
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- }
- // messagesClampProductionAccount 复刻生产账号 162:platform=deepseek、type=apikey、
- // api_protocol=adaptive、base_url 与 api_base_urls 均指向 ollama.com(anthropic 地址
- // 带 trailing '/',CC/Responses 地址带 /v1 后缀)。
- func messagesClampProductionAccount(id int64) *Account {
- return &Account{
- ID: id,
- Name: "ollama-cloud-prod",
- Platform: PlatformDeepseek,
- Type: AccountTypeAPIKey,
- Credentials: map[string]any{
- "api_key": "sk-test",
- "api_protocol": APIProtocolAdaptive,
- "base_url": "https://ollama.com/v1",
- "api_base_urls": map[string]any{
- APIProtocolAnthropic: "https://ollama.com/",
- APIProtocolResponses: "https://ollama.com/v1",
- APIProtocolChatCompletions: "https://ollama.com/v1",
- },
- },
- Extra: map[string]any{},
- }
- }
- // TestBuildNativeAnthropicUpstreamRequest_ClampsTrailingSlashOllamaBase 复刻生产
- // 失败:anthropic 协议地址配置为 "https://ollama.com/"(trailing '/')。真实出站
- // URL 组装(ValidateURLFormat 与 nativeAnthropicTargetURL 均做 TrimRight "/")会
- // 正确打到 https://ollama.com/v1/messages,因此 clamp 判定也必须对同一归一化后的
- // base 生效,max_tokens=256000 不得原样透传被上游 400。
- func TestBuildNativeAnthropicUpstreamRequest_ClampsTrailingSlashOllamaBase(t *testing.T) {
- svc := &OpenAIGatewayService{cfg: messagesClampTestConfig()}
- c := newMessagesClampTestContext(t)
- account := messagesClampProductionAccount(431)
- body := messagesClampBody("deepseek-v4-flash", 256000)
- // 真实出站 URL:trailing '/' 被既有组装归一化掉,请求确实可达 ollama.com。
- targetURL, err := svc.nativeAnthropicTargetURL(account)
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages", targetURL)
- req, wireBody, err := svc.buildNativeAnthropicUpstreamRequest(
- context.Background(), c, account, body, "sk-test", targetURL,
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages", req.URL.String())
- require.Equal(t, "deepseek-v4-flash", gjson.GetBytes(wireBody, "model").String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int(),
- "anthropic base trailing '/' 时仍须命中 clamp(生产 400 回归)")
- }
- // TestBuildUpstreamRequest_ClampsTrailingSlashOllamaBase 覆盖 builder A / B 的相同
- // 场景:GetBaseURL() 原样返回 trailing '/',而 validateUpstreamBaseURL 归一化后
- // 实际出站 URL 可达 ollama.com/v1/messages,clamp 判定须与归一化同源。
- func TestBuildUpstreamRequest_ClampsTrailingSlashOllamaBase(t *testing.T) {
- svc := &GatewayService{cfg: messagesClampTestConfig()}
- c := newMessagesClampTestContext(t)
- body := messagesClampBody("deepseek-v4-flash", 256000)
- newAccount := func(id int64) *Account {
- account := messagesClampOllamaAccount(id, PlatformAnthropic)
- account.Credentials["base_url"] = "https://ollama.com/"
- return account
- }
- t.Run("builder A trailing slash base is clamped", func(t *testing.T) {
- req, wireBody, err := svc.buildUpstreamRequest(
- context.Background(), c, newAccount(441), body, "sk-test", "api_key",
- "deepseek-v4-flash", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages?beta=true", req.URL.String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- t.Run("builder B trailing slash base is clamped", func(t *testing.T) {
- req, wireBody, err := svc.buildUpstreamRequestAnthropicAPIKeyPassthrough(
- context.Background(), c, newAccount(442), body, "sk-test",
- )
- require.NoError(t, err)
- require.Equal(t, "https://ollama.com/v1/messages?beta=true", req.URL.String())
- require.Equal(t, int64(65535), gjson.GetBytes(wireBody, "max_tokens").Int())
- })
- t.Run("evil suffix and custom path never match", func(t *testing.T) {
- for _, base := range []string{"https://ollama.com.evil.com/", "https://ollama.com/anthropic/"} {
- account := newAccount(443)
- account.Credentials["base_url"] = base
- _, wireBody, err := svc.buildUpstreamRequest(
- context.Background(), c, account, body, "sk-test", "api_key",
- "deepseek-v4-flash", false, false,
- )
- require.NoError(t, err)
- require.Equal(t, int64(256000), gjson.GetBytes(wireBody, "max_tokens").Int(),
- "base %q 不得被识别为 Ollama Cloud", base)
- }
- })
- }
|