From c4bdc484753acc95f9a775854af303146389286c Mon Sep 17 00:00:00 2001 From: shaw Date: Tue, 13 Oct 2026 14:38:16 +0800 Subject: [PATCH] =?UTF-8?q?fix(haiku):=20=E4=BF=AE=E6=AD=A3=E9=87=87?= =?UTF-8?q?=E6=A0=B7=E8=BE=B9=E7=95=8C=E3=80=81=E9=BB=98=E8=AE=A4=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE=E4=B8=8E=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/pkg/apicompat/anthropic_responses_test.go | 10 ++++++---- .../pkg/apicompat/responses_to_anthropic_request.go | 2 +- backend/internal/service/gateway_claude_oauth_body.go | 2 +- backend/internal/service/gateway_request.go | 4 +++- .../internal/service/gateway_request_haiku55_test.go | 7 ++++--- .../composables/__tests__/useModelWhitelist.spec.ts | 10 ++++++++++ frontend/src/composables/useModelWhitelist.ts | 2 ++ 7 files changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/internal/pkg/apicompat/anthropic_responses_test.go b/backend/internal/pkg/apicompat/anthropic_responses_test.go index 511b30e1ea..700af63d4d 100644 --- a/backend/internal/pkg/apicompat/anthropic_responses_test.go +++ b/backend/internal/pkg/apicompat/anthropic_responses_test.go @@ -1966,13 +1966,15 @@ func TestHaiku55ResponsesThinkingAndSampling(t *testing.T) { temperature := 0.7 _, err := ResponsesToAnthropicRequest(&ResponsesRequest{Model: "claude-haiku-5-5", Input: json.RawMessage(`"hello"`), Temperature: &temperature}) require.ErrorContains(t, err, "temperature") - topP := 0.5 - _, err = ResponsesToAnthropicRequest(&ResponsesRequest{Model: "claude-haiku-5-5", Input: json.RawMessage(`"hello"`), TopP: &topP}) - require.ErrorContains(t, err, "top_p") + for _, topP := range []float64{0.5, 0.995, 1} { + _, err = ResponsesToAnthropicRequest(&ResponsesRequest{Model: "claude-haiku-5-5", Input: json.RawMessage(`"hello"`), TopP: &topP}) + require.ErrorContains(t, err, "top_p", "top_p=%v", topP) + } _, err = ResponsesToAnthropicRequest(&ResponsesRequest{Model: "claude-haiku-5-5", Input: json.RawMessage(`"hello"`), Reasoning: &ResponsesReasoning{Effort: "minimal"}}) require.ErrorContains(t, err, "reasoning effort") - temperature, topP = 1, 0.99 + temperature = 1 + topP := 0.99 _, err = ResponsesToAnthropicRequest(&ResponsesRequest{Model: "claude-haiku-5-5", Input: json.RawMessage(`"hello"`), Temperature: &temperature}) require.NoError(t, err) _, err = ResponsesToAnthropicRequest(&ResponsesRequest{Model: "claude-haiku-5-5", Input: json.RawMessage(`"hello"`), TopP: &topP}) diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic_request.go b/backend/internal/pkg/apicompat/responses_to_anthropic_request.go index cf3666ca88..3ba7f638f1 100644 --- a/backend/internal/pkg/apicompat/responses_to_anthropic_request.go +++ b/backend/internal/pkg/apicompat/responses_to_anthropic_request.go @@ -89,7 +89,7 @@ func ResponsesToAnthropicRequest(req *ResponsesRequest) (*AnthropicRequest, erro if req.Temperature != nil && *req.Temperature != 1 { return nil, fmt.Errorf("claude-haiku-5-5 does not support non-default temperature") } - if req.TopP != nil && (*req.TopP < 0.99 || *req.TopP > 1) { + if req.TopP != nil && *req.TopP != 0.99 { return nil, fmt.Errorf("claude-haiku-5-5 does not support non-default top_p") } if req.Temperature != nil && req.TopP != nil { diff --git a/backend/internal/service/gateway_claude_oauth_body.go b/backend/internal/service/gateway_claude_oauth_body.go index a637b5a984..8a3eea4129 100644 --- a/backend/internal/service/gateway_claude_oauth_body.go +++ b/backend/internal/service/gateway_claude_oauth_body.go @@ -267,7 +267,7 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu // 客户端只传 top_p 时不得再补 temperature,否则网关自己制造 400(issue #7919)。 samplingLocked := claude.IsSonnet55(modelID) || claude.IsHaiku55(modelID) if !gjson.GetBytes(out, "temperature").Exists() && !claude.IsOpus55(modelID) && - !(samplingLocked && gjson.GetBytes(out, "top_p").Exists()) { + (!samplingLocked || !gjson.GetBytes(out, "top_p").Exists()) { if next, ok := setJSONValueBytes(out, "temperature", 1); ok { out = next modified = true diff --git a/backend/internal/service/gateway_request.go b/backend/internal/service/gateway_request.go index ff6acec33f..b77fac3187 100644 --- a/backend/internal/service/gateway_request.go +++ b/backend/internal/service/gateway_request.go @@ -639,7 +639,9 @@ func validateClaude55Request(body []byte, model string) error { if temperature := gjson.GetBytes(body, "temperature"); temperature.Exists() && (temperature.Type != gjson.Number || temperature.Float() != 1) { return fmt.Errorf("%s does not support non-default temperature", modelName) } - if topP := gjson.GetBytes(body, "top_p"); topP.Exists() && (topP.Type != gjson.Number || topP.Float() < 0.99 || topP.Float() > 1) { + // Haiku 5.5 只接受显式 top_p=0.99;保留 Sonnet 5.5 既有的区间策略。 + if topP := gjson.GetBytes(body, "top_p"); topP.Exists() && (topP.Type != gjson.Number || + (isHaiku55 && topP.Float() != 0.99) || (isSonnet55 && (topP.Float() < 0.99 || topP.Float() > 1))) { return fmt.Errorf("%s does not support non-default top_p", modelName) } if gjson.GetBytes(body, "top_k").Exists() { diff --git a/backend/internal/service/gateway_request_haiku55_test.go b/backend/internal/service/gateway_request_haiku55_test.go index f29cdfa633..c81309792c 100644 --- a/backend/internal/service/gateway_request_haiku55_test.go +++ b/backend/internal/service/gateway_request_haiku55_test.go @@ -22,6 +22,7 @@ func TestValidateClaude55Request_Haiku55(t *testing.T) { {name: "adaptive thinking passes", extra: `,"thinking":{"type":"adaptive"},"output_config":{"effort":"medium"}`}, {name: "omitted thinking passes", extra: ``}, {name: "enabled thinking rejected", extra: `,"thinking":{"type":"enabled","budget_tokens":8000}`, wantErr: "requires adaptive thinking"}, + {name: "拒绝 between_tools 并返回 Haiku 错误说明", extra: `,"thinking":{"type":"between_tools"}`, wantErr: "claude-haiku-5-5 requires adaptive thinking; thinking.type=between_tools is unsupported"}, {name: "disabled thinking allowed at medium effort", extra: `,"thinking":{"type":"disabled"},"output_config":{"effort":"medium"}`}, {name: "disabled thinking allowed when effort omitted", extra: `,"thinking":{"type":"disabled"}`}, {name: "disabled thinking rejected at xhigh", extra: `,"thinking":{"type":"disabled"},"output_config":{"effort":"xhigh"}`, wantErr: "supports only low, medium or high effort"}, @@ -29,9 +30,9 @@ func TestValidateClaude55Request_Haiku55(t *testing.T) { {name: "temperature 1 passes", extra: `,"temperature":1`}, {name: "non-default temperature rejected", extra: `,"temperature":0.5`, wantErr: "non-default temperature"}, {name: "top_p 0.99 passes", extra: `,"top_p":0.99`}, - // 与 sonnet-5-5 同口径:区间 [0.99, 1.0] 放行,越界才拦; - // 官方称 top_p=1 会 400,如上游收紧以它为准,网关保持 fail-open。 - {name: "top_p 1 passes (sonnet bar)", extra: `,"top_p":1`}, + {name: "拒绝 top_p=0.995", extra: `,"top_p":0.995`, wantErr: "non-default top_p"}, + {name: "拒绝 top_p=1", extra: `,"top_p":1`, wantErr: "non-default top_p"}, + {name: "拒绝字符串 top_p", extra: `,"top_p":"0.99"`, wantErr: "non-default top_p"}, {name: "top_p 0.5 rejected", extra: `,"top_p":0.5`, wantErr: "non-default top_p"}, {name: "top_k rejected", extra: `,"top_k":5`, wantErr: "does not support top_k"}, {name: "temperature and top_p together rejected", extra: `,"temperature":1,"top_p":0.99`, wantErr: "does not allow temperature and top_p together"}, diff --git a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts index 9cd3ec3648..738d3b2abe 100644 --- a/frontend/src/composables/__tests__/useModelWhitelist.spec.ts +++ b/frontend/src/composables/__tests__/useModelWhitelist.spec.ts @@ -71,6 +71,16 @@ describe('useModelWhitelist', () => { ])) }) + it('Claude 默认白名单保存后允许 Haiku 5.5,预设使用同一模型 ID', () => { + const models: string[] = getModelsByPlatform('anthropic') + const mapping: Record | null = buildModelMappingObject('whitelist', models, []) + + expect(mapping?.['claude-haiku-5-5']).toBe('claude-haiku-5-5') + expect(getPresetMappingsByPlatform('anthropic')).toEqual(expect.arrayContaining([ + expect.objectContaining({ label: 'Haiku 5.5', from: 'claude-haiku-5-5', to: 'claude-haiku-5-5' }) + ])) + }) + it('xAI 模型列表包含 Grok 4.5 官方模型和别名', () => { const models = getModelsByPlatform('grok') diff --git a/frontend/src/composables/useModelWhitelist.ts b/frontend/src/composables/useModelWhitelist.ts index 765ef379c4..407258dbbb 100644 --- a/frontend/src/composables/useModelWhitelist.ts +++ b/frontend/src/composables/useModelWhitelist.ts @@ -37,6 +37,7 @@ export const claudeModels = [ 'claude-opus-5-5', 'claude-opus-5', 'claude-sonnet-5-5', + 'claude-haiku-5-5', 'claude-sonnet-5', 'claude-sonnet-4-6', 'claude-fable-5-1', @@ -297,6 +298,7 @@ const anthropicPresetMappings = [ { label: 'Opus 5', from: 'claude-opus-5', to: 'claude-opus-5', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' }, { label: 'Haiku 3.5', from: 'claude-3-5-haiku-20241022', to: 'claude-3-5-haiku-20241022', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }, { label: 'Haiku 4.5', from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4-5-20251001', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, + { label: 'Haiku 5.5', from: 'claude-haiku-5-5', to: 'claude-haiku-5-5', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' }, { label: 'Opus->Sonnet', from: 'claude-opus-4-6', to: 'claude-sonnet-4-5-20250929', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' } ]