mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-19 16:17:56 +00:00
Merge pull request #7827 from Achordchan/fix/anthropic-tool-change-betas
fix(anthropic): preserve requested tool change beta headers
This commit is contained in:
@@ -20,6 +20,9 @@ const (
|
||||
BetaFastMode = "fast-mode-2026-02-01"
|
||||
// Legacy structured output compatibility; forwarded only when explicitly requested.
|
||||
BetaStructuredOutputs = "structured-outputs-2025-11-13"
|
||||
// Tool-change compatibility tokens; never enabled without an explicit client request.
|
||||
BetaMidConversationToolChanges = "mid-conversation-tool-changes-2026-07-01"
|
||||
BetaInlineTools = "inline-tools-2026-09-15"
|
||||
|
||||
// 新增(对齐官方 CLI 2.1.9x 以来的流量)
|
||||
BetaPromptCachingScope = "prompt-caching-scope-2026-01-05"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
//go:build unit
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Wei-Shaw/sub2api/internal/config"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestBuildUpstreamRequestToolChangeBetas(t *testing.T) {
|
||||
const mid = "mid-conversation-tool-changes-2026-07-01"
|
||||
const inline = "inline-tools-2026-09-15"
|
||||
for _, tc := range []struct {
|
||||
name, header string
|
||||
drop map[string]struct{}
|
||||
wantMid, wantInline bool
|
||||
}{
|
||||
{"both", mid + "," + inline, nil, true, true},
|
||||
{"mid only", mid, nil, true, false},
|
||||
{"inline only", inline, nil, false, true},
|
||||
{"absent", "", nil, false, false},
|
||||
{"unknown", inline + "-other", nil, false, false},
|
||||
{"duplicate", inline + ", " + inline + "," + mid, nil, true, true},
|
||||
{"filter inline", mid + "," + inline, map[string]struct{}{inline: {}}, true, false},
|
||||
{"filter mid", mid + "," + inline, map[string]struct{}{mid: {}}, false, true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
c, _ := gin.CreateTestContext(httptest.NewRecorder())
|
||||
c.Request = httptest.NewRequest(http.MethodPost, "/v1/messages", nil)
|
||||
c.Request.Header.Set("anthropic-beta", tc.header+",unrelated-beta")
|
||||
if tc.drop != nil {
|
||||
c.Set(betaPolicyFilterSetKey, tc.drop)
|
||||
}
|
||||
account := &Account{ID: 701, Platform: PlatformAnthropic, Type: AccountTypeOAuth,
|
||||
Credentials: map[string]any{"access_token": "test-token"}, Status: StatusActive, Schedulable: true}
|
||||
body := []byte(`{"model":"claude-opus-5-5","max_tokens":1024,"messages":[{"role":"user","content":[{"type":"tool_addition","tool":{"type":"tool_reference","tool_name":"lookup"}},{"type":"text","text":"Use lookup"}]}]}`)
|
||||
svc := &GatewayService{cfg: &config.Config{}}
|
||||
req, _, err := svc.buildUpstreamRequest(context.Background(), c, account, body,
|
||||
"test-token", "oauth", "claude-opus-5-5", false, true)
|
||||
require.NoError(t, err)
|
||||
out := readUpstreamBodyForTest(t, req)
|
||||
require.JSONEq(t, gjson.GetBytes(body, "messages").Raw, gjson.GetBytes(out, "messages").Raw)
|
||||
header := getHeaderRaw(req.Header, "anthropic-beta")
|
||||
require.Equal(t, tc.wantMid, containsBetaToken(header, mid))
|
||||
require.Equal(t, tc.wantInline, containsBetaToken(header, inline))
|
||||
require.False(t, containsBetaToken(header, "unrelated-beta"))
|
||||
require.False(t, containsBetaToken(header, inline+"-other"))
|
||||
require.True(t, containsBetaToken(header, "oauth-2025-04-20"))
|
||||
if tc.wantInline {
|
||||
require.Equal(t, 1, strings.Count(header, inline))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -545,14 +545,20 @@ func (s *GatewayService) computeFinalAnthropicBeta(
|
||||
|
||||
if tokenType == "oauth" {
|
||||
if mimicClaudeCode {
|
||||
// Keep the default beta set, with a narrow opt-in for legacy structured
|
||||
// output requests. Unknown client betas remain excluded and policy drops
|
||||
// still take precedence over this compatibility token.
|
||||
incomingBeta := ""
|
||||
if containsBetaToken(clientBeta, claude.BetaStructuredOutputs) {
|
||||
incomingBeta = claude.BetaStructuredOutputs
|
||||
// Preserve explicitly requested compatibility tokens so the forwarded
|
||||
// body and headers agree. Unknown betas remain excluded; policy drops
|
||||
// still take precedence. Missing companion tokens are not inferred.
|
||||
var incomingBetas []string
|
||||
for _, token := range []string{
|
||||
claude.BetaStructuredOutputs,
|
||||
claude.BetaMidConversationToolChanges,
|
||||
claude.BetaInlineTools,
|
||||
} {
|
||||
if containsBetaToken(clientBeta, token) {
|
||||
incomingBetas = append(incomingBetas, token)
|
||||
}
|
||||
}
|
||||
return mergeAnthropicBetaDropping(claude.FullClaudeCodeMimicryBetas(), incomingBeta, effectiveDropSet), true
|
||||
return mergeAnthropicBetaDropping(claude.FullClaudeCodeMimicryBetas(), strings.Join(incomingBetas, ","), effectiveDropSet), true
|
||||
}
|
||||
// 真 Claude Code 客户端透传路径
|
||||
return stripBetaTokensWithSet(s.getBetaHeader(modelID, clientBeta), effectiveDropSet), true
|
||||
|
||||
Reference in New Issue
Block a user