diff --git a/plugins/wasm-go/extensions/ai-proxy/provider/vertex.go b/plugins/wasm-go/extensions/ai-proxy/provider/vertex.go index 09bb602b9..44c9b9077 100644 --- a/plugins/wasm-go/extensions/ai-proxy/provider/vertex.go +++ b/plugins/wasm-go/extensions/ai-proxy/provider/vertex.go @@ -197,11 +197,16 @@ func (v *vertexProvider) TransformRequestHeaders(ctx wrapper.HttpContext, apiNam util.OverwriteRequestHostHeader(headers, finalVertexDomain) - // 剥除 Anthropic 客户端可能携带的凭据头, 避免泄漏到 Google. - // vertex 一律用 OAuth Bearer (标准模式) 或 ?key= (Express 模式) 鉴权, - // 这些头对 vertex 没有任何意义, 留着只会把 sk-ant-... 这类密钥转发到上游日志. + // 剥除 Anthropic 客户端携带的凭据头和协议头. + // 凭据头: vertex 一律用 OAuth Bearer 或 ?key= 鉴权, 留着只会把 sk-ant-... 泄漏到上游日志. headers.Del("x-api-key") headers.Del("anthropic-api-key") + // 协议头: vertex 的 Anthropic 端点不接受这些头 — + // anthropic-beta → vertex 不支持 Anthropic beta feature flags, 会 400 + // anthropic-version → vertex 的版本通过 body 里的 anthropic_version 字段传递, + // 头里的 "2023-06-01" 与 vertex 预期的 "vertex-2023-10-16" 不符 + headers.Del("anthropic-beta") + headers.Del("anthropic-version") } func (v *vertexProvider) getToken() (cached bool, err error) { @@ -427,6 +432,14 @@ func (v *vertexProvider) onAnthropicMessagesRequestBody(ctx wrapper.HttpContext, return nil, fmt.Errorf("unable to inject anthropic_version: %v", err) } + // 剥除 Anthropic beta-only 的 body 字段, vertex 的 :rawPredict 不认这些字段会 400. + // 例如 Claude Code 交互模式会发 context_management (上下文压缩配置). + for _, betaField := range []string{"context_management"} { + if gjson.GetBytes(body, betaField).Exists() { + body, _ = sjson.DeleteBytes(body, betaField) + } + } + // vertex Anthropic 端点要求 max_tokens 必填, 客户端漏传会被 400. // 跟 claude provider buildClaudeTextGenRequest 保持一致, 缺省补 claudeDefaultMaxTokens. if !gjson.GetBytes(body, "max_tokens").Exists() { diff --git a/plugins/wasm-go/extensions/ai-proxy/provider/vertex_test.go b/plugins/wasm-go/extensions/ai-proxy/provider/vertex_test.go index 716f14844..cdf264089 100644 --- a/plugins/wasm-go/extensions/ai-proxy/provider/vertex_test.go +++ b/plugins/wasm-go/extensions/ai-proxy/provider/vertex_test.go @@ -513,24 +513,29 @@ func TestVertexAnthropicPassthrough_StreamingChunkUnchanged(t *testing.T) { assert.Equal(t, chunk, out, "vertex Anthropic SSE chunk must be returned byte-for-byte") } -// TestVertexTransformRequestHeaders_StripsAnthropicCredentialHeaders ensures -// that Anthropic-style client auth headers (carried by SDKs like the Anthropic -// Python/TypeScript SDKs and Claude Code) are NOT forwarded to vertex. -// Vertex uses OAuth Bearer / API key in URL — these headers are meaningless to -// vertex and forwarding them would leak the client's sk-ant-... credential to -// Google logs. -func TestVertexTransformRequestHeaders_StripsAnthropicCredentialHeaders(t *testing.T) { +// TestVertexTransformRequestHeaders_StripsAnthropicHeaders ensures that +// Anthropic-specific headers (credentials + protocol) are NOT forwarded to +// vertex. The regular Anthropic SDK sends these but vertex's Anthropic +// endpoint rejects or misinterprets them: +// - x-api-key / anthropic-api-key: credential leak to Google logs +// - anthropic-beta: vertex 400 "Unexpected value(s) ... for the anthropic-beta header" +// - anthropic-version: conflicts with body-level anthropic_version "vertex-2023-10-16" +func TestVertexTransformRequestHeaders_StripsAnthropicHeaders(t *testing.T) { v := newAnthropicVertexProvider(false) ctx := newMapCtx() headers := http.Header{} headers.Set("x-api-key", "sk-ant-api03-secret") headers.Set("anthropic-api-key", "sk-ant-api03-secret") + headers.Set("anthropic-beta", "advanced-tool-use-2025-11-20,prompt-caching-scope-2026-01-05") + headers.Set("anthropic-version", "2023-06-01") headers.Set("content-type", "application/json") v.TransformRequestHeaders(ctx, ApiNameAnthropicMessages, headers) assert.Empty(t, headers.Get("x-api-key"), "x-api-key must be stripped before forwarding to vertex") assert.Empty(t, headers.Get("anthropic-api-key"), "anthropic-api-key must be stripped before forwarding to vertex") + assert.Empty(t, headers.Get("anthropic-beta"), "anthropic-beta must be stripped — vertex rejects unknown beta flags with 400") + assert.Empty(t, headers.Get("anthropic-version"), "anthropic-version must be stripped — vertex uses body-level anthropic_version instead") // Sanity: unrelated headers untouched. assert.Equal(t, "application/json", headers.Get("content-type")) }