From d56f5e787976c8b2a7bd471b9a3c30b0ccee50c1 Mon Sep 17 00:00:00 2001 From: xcrong Date: Fri, 9 Oct 2026 10:07:36 +0800 Subject: [PATCH] feat(gateway): adapt Claude Haiku 5.5 Closes #7919. Wire claude-haiku-5-5 (released 2026-10-07) across model registry, effort catalog, pricing, billing, validation and Codex descriptors, following the opus-5-5/sonnet-5-5 template and the official migration guide + overview specs. - registry: DefaultModels entry (fixed ID, no date suffix per guide) - effort: full low/medium/high/xhigh/max family + IsHaiku55 + claude-haiku-5.5 dotted normalization (official default: medium) - pricing catalog: 1M/128k limits, base (<=100k) prices and *_above_100k_tokens absolute prices; the existing ladder derivation yields threshold=100000 with uniform 5x multipliers, matching the official two-tier table exactly (cache tiers follow input x5) - billing/pricing fallbacks carry the same tier so a missing catalog entry can never silently bill >100k prompts at base rate - validation: adaptive-only thinking (enabled always 400; disabled 400 at xhigh/max), sonnet-style sampling locks, temp+top_p combo rejected, forced tool_choice explicitly allowed (differs from opus/sonnet) - oauth mimicry: skip temperature:1 top-up when top_p is present on sampling-locked models (sonnet-5-5 included), otherwise the gateway manufactures the temp+top_p 400 itself - toolset beta filter + Codex 1M context cover haiku-5-5 - Bedrock/Antigravity mappings intentionally left out (no 5.5 entries exist there at all, same as opus-5-5); Vertex passes the fixed ID through untouched --- backend/internal/pkg/claude/constants.go | 6 +++ .../pkg/claude/constants_model_test.go | 13 +++++ backend/internal/pkg/claude/effort_catalog.go | 9 ++++ .../pkg/claude/effort_catalog_test.go | 19 +++++++ backend/internal/service/billing_service.go | 17 ++++++ .../internal/service/billing_service_test.go | 27 ++++++++++ .../service/gateway_claude_oauth_body.go | 6 ++- .../gateway_context_management_test.go | 19 +++++++ backend/internal/service/gateway_request.go | 46 ++++++++++++---- .../service/gateway_request_haiku55_test.go | 54 +++++++++++++++++++ .../service/gateway_upstream_request.go | 4 +- .../service/openai_codex_models_service.go | 2 +- .../openai_codex_models_service_test.go | 8 +++ backend/internal/service/pricing_service.go | 14 +++++ .../model_prices_and_context_window.json | 34 ++++++++++++ 15 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 backend/internal/service/gateway_request_haiku55_test.go diff --git a/backend/internal/pkg/claude/constants.go b/backend/internal/pkg/claude/constants.go index 5e0f9fc276..1d0d6d3a3b 100644 --- a/backend/internal/pkg/claude/constants.go +++ b/backend/internal/pkg/claude/constants.go @@ -223,6 +223,12 @@ var DefaultModels = []Model{ DisplayName: "Claude Haiku 4.5", CreatedAt: "2025-10-01T00:00:00Z", }, + { + ID: "claude-haiku-5-5", + Type: "model", + DisplayName: "Claude Haiku 5.5", + CreatedAt: "2026-10-07T00:00:00Z", + }, } // DefaultModelIDs 返回默认模型的 ID 列表 diff --git a/backend/internal/pkg/claude/constants_model_test.go b/backend/internal/pkg/claude/constants_model_test.go index 4a58d540d8..0a15c8d214 100644 --- a/backend/internal/pkg/claude/constants_model_test.go +++ b/backend/internal/pkg/claude/constants_model_test.go @@ -43,3 +43,16 @@ func TestDefaultModelsContainsSonnet55(t *testing.T) { } t.Fatal("claude-sonnet-5-5 missing from DefaultModels") } + +func TestDefaultModelsContainsHaiku55(t *testing.T) { + t.Parallel() + + for _, model := range DefaultModels { + if model.ID == "claude-haiku-5-5" { + require.Equal(t, "Claude Haiku 5.5", model.DisplayName) + require.Equal(t, "2026-10-07T00:00:00Z", model.CreatedAt) + return + } + } + t.Fatal("claude-haiku-5-5 missing from DefaultModels") +} diff --git a/backend/internal/pkg/claude/effort_catalog.go b/backend/internal/pkg/claude/effort_catalog.go index a3039faac7..2764adc076 100644 --- a/backend/internal/pkg/claude/effort_catalog.go +++ b/backend/internal/pkg/claude/effort_catalog.go @@ -27,6 +27,7 @@ var effortFamilies = []struct { {family: "claude-opus-4-5", levels: effortLowMediumHigh}, {family: "claude-opus-5-5", levels: effortLowMediumHighXHighMax}, {family: "claude-opus-5", levels: effortLowMediumHighXHighMax}, + {family: "claude-haiku-5-5", levels: effortLowMediumHighXHighMax}, } // EffortLevelsForModel returns the output_config.effort values accepted by a @@ -51,6 +52,11 @@ func IsSonnet55(model string) bool { return normalizeEffortModelID(model) == "claude-sonnet-5-5" } +// IsHaiku55 identifies the fixed Haiku 5.5 ID after provider/local suffix normalization. +func IsHaiku55(model string) bool { + return normalizeEffortModelID(model) == "claude-haiku-5-5" +} + func normalizeEffortModelID(model string) string { id := strings.ToLower(strings.TrimSpace(model)) id = strings.TrimPrefix(id, "models/") @@ -70,6 +76,9 @@ func normalizeEffortModelID(model string) string { if id == "claude-sonnet-5.5" { id = "claude-sonnet-5-5" } + if id == "claude-haiku-5.5" { + id = "claude-haiku-5-5" + } if mapped, ok := ModelIDReverseOverrides[id]; ok { id = mapped } diff --git a/backend/internal/pkg/claude/effort_catalog_test.go b/backend/internal/pkg/claude/effort_catalog_test.go index e8f8e2f15a..12f7cd451e 100644 --- a/backend/internal/pkg/claude/effort_catalog_test.go +++ b/backend/internal/pkg/claude/effort_catalog_test.go @@ -18,6 +18,9 @@ func TestEffortLevelsForModel(t *testing.T) { {model: "claude-opus-5", want: []string{"low", "medium", "high", "xhigh", "max"}}, {model: "anthropic/claude-opus-5.5", want: []string{"low", "medium", "high", "xhigh", "max"}}, {model: "claude-sonnet-5-5", want: []string{"low", "medium", "high", "xhigh", "max"}}, + {model: "claude-haiku-5-5", want: []string{"low", "medium", "high", "xhigh", "max"}}, + {model: "anthropic/claude-haiku-5.5", want: []string{"low", "medium", "high", "xhigh", "max"}}, + {model: "us.anthropic.claude-haiku-5-5", want: []string{"low", "medium", "high", "xhigh", "max"}}, {model: "us.anthropic.claude-sonnet-5-5", want: []string{"low", "medium", "high", "xhigh", "max"}}, {model: "claude-opus-4-5-20251101", want: []string{"low", "medium", "high"}}, {model: "claude-haiku-4-5-20251001", want: nil}, @@ -57,3 +60,19 @@ func TestIsSonnet55(t *testing.T) { require.False(t, IsSonnet55(model), model) } } + +func TestIsHaiku55(t *testing.T) { + t.Parallel() + for _, model := range []string{ + "claude-haiku-5-5", + "anthropic/claude-haiku-5.5", + "anthropic.claude-haiku-5-5", + "us.anthropic.claude-haiku-5-5", + "global.anthropic.claude-haiku-5-5-thinking", + } { + require.True(t, IsHaiku55(model), model) + } + for _, model := range []string{"claude-haiku-5", "claude-haiku-4-5", "claude-haiku-4-5-20251001", "claude-sonnet-5-5", "claude-opus-5-5"} { + require.False(t, IsHaiku55(model), model) + } +} diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index 449acf1597..c447b0b6fc 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -444,6 +444,20 @@ func (s *BillingService) initFallbackPricing() { CacheCreation1hPrice: 4e-6, SupportsCacheBreakdown: true, } + // Claude Haiku 5.5(2026-10-07,$0.10/$0.50,prompt 超 100k tokens 整单 5x)。 + // 长文本阶梯与目录 *_above_100k_tokens 绝对价同口径,缺目录时回退也不丢阶梯。 + s.fallbackPrices["claude-haiku-5-5"] = &ModelPricing{ + InputPricePerToken: 0.1e-6, + OutputPricePerToken: 0.5e-6, + CacheCreationPricePerToken: 0.125e-6, + CacheReadPricePerToken: 0.01e-6, + CacheCreation5mPrice: 0.125e-6, + CacheCreation1hPrice: 0.2e-6, + SupportsCacheBreakdown: true, + LongContextInputThreshold: 100000, + LongContextInputMultiplier: 5, + LongContextOutputMultiplier: 5, + } // Claude Fable 5.x uses the same input/output and cache-write prices, while // Fable 5.1 reduces cache reads from $1 to $0.25 per MTok. s.fallbackPrices["claude-fable-5"] = &ModelPricing{ @@ -1017,6 +1031,9 @@ func (s *BillingService) getFallbackPricing(model string) *ModelPricing { if claude.IsSonnet55(modelLower) { return s.fallbackPrices["claude-sonnet-5-5"] } + if claude.IsHaiku55(modelLower) { + return s.fallbackPrices["claude-haiku-5-5"] + } if strings.Contains(modelLower, "opus") { // "opus-5" 必须先判:不能用裸 "5" 匹配,否则 claude-opus-4-5 会被误判。 if strings.Contains(modelLower, "opus-5") || strings.Contains(modelLower, "opus5") { diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index b8ae7b3491..45544508ec 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -2055,6 +2055,33 @@ func TestNewModelPricingCatalogFallbackAndContext(t *testing.T) { require.False(t, cost.LongContextBillingApplied) }) } + // claude-haiku-5-5:prompt 超 100k tokens 整单 5x(官方定价), + // billing/catalog/fallback 三源口径一致(issue #7919)。 + for _, model := range []string{ + "claude-haiku-5-5", + "anthropic/claude-haiku-5.5", + } { + t.Run(source+"/"+model, func(t *testing.T) { + for _, n := range []int{99999, 100000, 100001} { + tokens := UsageTokens{ + InputTokens: n - 3000, OutputTokens: 500, + CacheReadTokens: 2000, CacheCreationTokens: 1000, + } + cost, err := svc.CalculateCost(model, tokens, 1) + require.NoError(t, err) + im, om := 1.0, 1.0 + if n > 100000 { + im, om = 5.0, 5.0 + } + require.InDelta(t, float64(tokens.InputTokens)*1e-7*im, cost.InputCost, 1e-10) + require.InDelta(t, 1000*1.25e-7*im, cost.CacheCreationCost, 1e-10) + require.InDelta(t, 2000*1e-8*im, cost.CacheReadCost, 1e-10) + require.InDelta(t, 500*5e-7*om, cost.OutputCost, 1e-10) + require.InDelta(t, cost.InputCost+cost.OutputCost+cost.CacheCreationCost+cost.CacheReadCost, cost.TotalCost, 1e-10) + require.Equal(t, n > 100000, cost.LongContextBillingApplied) + } + }) + } } } diff --git a/backend/internal/service/gateway_claude_oauth_body.go b/backend/internal/service/gateway_claude_oauth_body.go index 085cfefbd9..a637b5a984 100644 --- a/backend/internal/service/gateway_claude_oauth_body.go +++ b/backend/internal/service/gateway_claude_oauth_body.go @@ -263,7 +263,11 @@ func normalizeClaudeOAuthRequestBody(body []byte, modelID string, opts claudeOAu // temperature:真实 Claude Code CLI 总是发送 temperature(默认 1,客户端可覆盖)。 // 之前的实现直接 delete 会导致 payload 缺字段,与真实 CLI 字节级不一致。 // 策略:客户端传了什么就透传;没传则补默认 1。 - if !gjson.GetBytes(out, "temperature").Exists() && !claude.IsOpus55(modelID) { + // 例外:采样锁定的模型(sonnet-5-5/haiku-5-5)上 temperature+top_p 共存即 400, + // 客户端只传 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()) { if next, ok := setJSONValueBytes(out, "temperature", 1); ok { out = next modified = true diff --git a/backend/internal/service/gateway_context_management_test.go b/backend/internal/service/gateway_context_management_test.go index 60dc89489c..1e7a7d3eee 100644 --- a/backend/internal/service/gateway_context_management_test.go +++ b/backend/internal/service/gateway_context_management_test.go @@ -371,6 +371,25 @@ func TestNormalizeClaudeOAuthRequestBody_HaikuShortModelStillNormalizesToDatedID require.Equal(t, "claude-haiku-4-5-20251001", gjson.GetBytes(out, "model").String()) } +// issue #7919:采样锁定的模型上 temperature+top_p 共存即 400, +// 只传 top_p 时不得再补 temperature,否则网关自己制造 400。 +func TestNormalizeClaudeOAuthRequestBody_SkipsTemperatureTopUpWithTopPOnLockedModels(t *testing.T) { + for _, model := range []string{"claude-haiku-5-5", "claude-sonnet-5-5"} { + body := []byte(`{"model":"` + model + `","top_p":0.99,"messages":[]}`) + out, _ := normalizeClaudeOAuthRequestBody(body, model, claudeOAuthNormalizeOptions{}) + require.False(t, gjson.GetBytes(out, "temperature").Exists(), + "model %s with top_p must not gain temperature", model) + require.Equal(t, 0.99, gjson.GetBytes(out, "top_p").Float()) + } +} + +func TestNormalizeClaudeOAuthRequestBody_StillTopsUpTemperatureWithoutTopP(t *testing.T) { + body := []byte(`{"model":"claude-haiku-5-5","messages":[]}`) + out, _ := normalizeClaudeOAuthRequestBody(body, "claude-haiku-5-5", claudeOAuthNormalizeOptions{}) + require.Equal(t, int64(1), gjson.GetBytes(out, "temperature").Int(), + "无 top_p 时 temperature:1 补齐保持不变") +} + func TestApplyClaudeCodeOAuthMimicryToBody_HaikuRewritesSystem(t *testing.T) { account := &Account{ID: 405, Platform: PlatformAnthropic, Type: AccountTypeOAuth} body := []byte(`{"model":"claude-haiku-4-5","system":"Pi project instructions","messages":[{"role":"user","content":"hello"}]}`) diff --git a/backend/internal/service/gateway_request.go b/backend/internal/service/gateway_request.go index 678d7d3d3b..4ebd3ddd1a 100644 --- a/backend/internal/service/gateway_request.go +++ b/backend/internal/service/gateway_request.go @@ -570,7 +570,7 @@ func StripEmptyTextBlocks(body []byte) []byte { // isClaude55SignedThinkingModel identifies models whose default thinking mode // requires signed history to survive protocol conversion and request filtering. func isClaude55SignedThinkingModel(model string) bool { - return claude.IsOpus55(model) || claude.IsSonnet55(model) + return claude.IsOpus55(model) || claude.IsSonnet55(model) || claude.IsHaiku55(model) } // validateClaude55Request rejects settings that the upstream cannot honor. @@ -580,8 +580,22 @@ func validateClaude55Request(body []byte, model string) error { return nil } isSonnet55 := claude.IsSonnet55(model) - switch gjson.GetBytes(body, "thinking.type").String() { + isHaiku55 := claude.IsHaiku55(model) + thinkingType := gjson.GetBytes(body, "thinking.type").String() + switch thinkingType { case "disabled", "enabled": + if isHaiku55 { + // haiku-5-5:enabled 一律 400(用 adaptive);disabled 仅 high 及以下 + // 可用,xhigh/max 上 400。见官方 migration guide。 + if thinkingType == "enabled" { + return fmt.Errorf("claude-haiku-5-5 requires adaptive thinking; omit thinking or use thinking.type=adaptive and output_config.effort") + } + switch gjson.GetBytes(body, "output_config.effort").String() { + case "xhigh", "max": + return fmt.Errorf("claude-haiku-5-5 thinking.type=disabled supports only low, medium or high effort") + } + break + } if isSonnet55 { return fmt.Errorf("claude-sonnet-5-5 requires adaptive thinking or thinking.type=between_tools; omit thinking or use one of those modes") } @@ -604,22 +618,32 @@ func validateClaude55Request(body []byte, model string) error { if isSonnet55 { modelName = "claude-sonnet-5-5" } - if gjson.GetBytes(body, "tool_choice").String() == "required" { - return fmt.Errorf("%s does not support forced tool_choice; use auto or none", modelName) + if isHaiku55 { + modelName = "claude-haiku-5-5" } - switch gjson.GetBytes(body, "tool_choice.type").String() { - case "any", "tool", "function", "custom", "namespace": - return fmt.Errorf("%s does not support forced tool_choice; use auto or none", modelName) + // haiku-5-5 接受强制 tool_choice(响应以 tool call 开头、无 thinking 块), + // 与 opus/sonnet-5-5 不同,不拦截。见官方 migration guide。 + if !isHaiku55 { + if gjson.GetBytes(body, "tool_choice").String() == "required" { + return fmt.Errorf("%s does not support forced tool_choice; use auto or none", modelName) + } + switch gjson.GetBytes(body, "tool_choice.type").String() { + case "any", "tool", "function", "custom", "namespace": + return fmt.Errorf("%s does not support forced tool_choice; use auto or none", modelName) + } } - if isSonnet55 { + if isSonnet55 || isHaiku55 { if temperature := gjson.GetBytes(body, "temperature"); temperature.Exists() && (temperature.Type != gjson.Number || temperature.Float() != 1) { - return fmt.Errorf("claude-sonnet-5-5 does not support non-default temperature") + 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) { - return fmt.Errorf("claude-sonnet-5-5 does not support non-default top_p") + return fmt.Errorf("%s does not support non-default top_p", modelName) } if gjson.GetBytes(body, "top_k").Exists() { - return fmt.Errorf("claude-sonnet-5-5 does not support top_k") + return fmt.Errorf("%s does not support top_k", modelName) + } + if isHaiku55 && gjson.GetBytes(body, "temperature").Exists() && gjson.GetBytes(body, "top_p").Exists() { + return fmt.Errorf("%s does not allow temperature and top_p together; omit both", modelName) } } return nil diff --git a/backend/internal/service/gateway_request_haiku55_test.go b/backend/internal/service/gateway_request_haiku55_test.go new file mode 100644 index 0000000000..f29cdfa633 --- /dev/null +++ b/backend/internal/service/gateway_request_haiku55_test.go @@ -0,0 +1,54 @@ +//go:build unit + +package service + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +// issue #7919:claude-haiku-5-5 请求校验(官方 migration guide 口径)。 +func TestValidateClaude55Request_Haiku55(t *testing.T) { + const model = "claude-haiku-5-5" + const prefix = `{"model":"claude-haiku-5-5","max_tokens":16000,"messages":[{"role":"user","content":"hi"}]` + cases := []struct { + name string + // extra 为追加进最小合法请求的 JSON 片段(含前导逗号) + extra string + wantErr string // 为空表示期望通过 + }{ + {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: "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"}, + {name: "disabled thinking rejected at max", extra: `,"thinking":{"type":"disabled"},"output_config":{"effort":"max"}`, wantErr: "supports only low, medium or high effort"}, + {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.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"}, + {name: "forced tool_choice any allowed", extra: `,"tool_choice":{"type":"any"}`}, + {name: "forced tool_choice required allowed", extra: `,"tool_choice":"required"`}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + body := []byte(prefix + tc.extra + "}") + err := validateClaude55Request(body, model) + if tc.wantErr == "" { + require.NoError(t, err) + return + } + require.Error(t, err) + require.True(t, strings.Contains(err.Error(), tc.wantErr), + "error %q should contain %q", err.Error(), tc.wantErr) + }) + } +} diff --git a/backend/internal/service/gateway_upstream_request.go b/backend/internal/service/gateway_upstream_request.go index 06ee09822c..cdf2319ebb 100644 --- a/backend/internal/service/gateway_upstream_request.go +++ b/backend/internal/service/gateway_upstream_request.go @@ -419,11 +419,13 @@ func defaultAPIKeyBetaHeader(body []byte) string { // The legacy streaming beta is incompatible with the stable computer and // browser toolsets. The per-tool eager_input_streaming field remains available. +// Applies to Sonnet 5.5 and Haiku 5.5, which share the computer_toolset_20260801 +// / browser_toolset_20260801 toolsets (Haiku 5.5 migration guide). func filterSonnet55ToolsetBeta(header string, body []byte, modelID string) string { if at := strings.IndexByte(modelID, '@'); at >= 0 { modelID = modelID[:at] // Vertex's dated model ID } - if !claude.IsSonnet55(modelID) { + if !claude.IsSonnet55(modelID) && !claude.IsHaiku55(modelID) { return header } for _, tool := range gjson.GetBytes(body, "tools").Array() { diff --git a/backend/internal/service/openai_codex_models_service.go b/backend/internal/service/openai_codex_models_service.go index 934f770979..1629ad9976 100644 --- a/backend/internal/service/openai_codex_models_service.go +++ b/backend/internal/service/openai_codex_models_service.go @@ -487,7 +487,7 @@ func newConfiguredCodexModelDescriptor(modelID string) configuredCodexModelDescr } if isClaudeCodexModel(modelID) { - if claude.IsOpus55(modelID) || claude.IsSonnet55(modelID) { + if claude.IsOpus55(modelID) || claude.IsSonnet55(modelID) || claude.IsHaiku55(modelID) { descriptor.ContextWindow = 1_000_000 descriptor.MaxContextWindow = 1_000_000 } diff --git a/backend/internal/service/openai_codex_models_service_test.go b/backend/internal/service/openai_codex_models_service_test.go index a825ba2208..d4bc9c3c93 100644 --- a/backend/internal/service/openai_codex_models_service_test.go +++ b/backend/internal/service/openai_codex_models_service_test.go @@ -334,6 +334,14 @@ func TestNewConfiguredCodexModelDescriptorUsesProviderMetadataAndSafeFallback(t require.Equal(t, "none", *claudeHaiku.DefaultReasoningLevel) require.Equal(t, []string{"none"}, effortsFromConfiguredCodexLevels(claudeHaiku.SupportedReasoningLevels)) + claudeHaiku55 := newConfiguredCodexModelDescriptor("claude-haiku-5-5") + require.Equal(t, "Claude Haiku 5.5", claudeHaiku55.DisplayName) + require.Equal(t, int64(1_000_000), claudeHaiku55.ContextWindow) + require.Equal(t, int64(1_000_000), claudeHaiku55.MaxContextWindow) + require.NotNil(t, claudeHaiku55.DefaultReasoningLevel) + require.Equal(t, "medium", *claudeHaiku55.DefaultReasoningLevel) + require.Equal(t, []string{"low", "medium", "high", "xhigh", "max"}, effortsFromConfiguredCodexLevels(claudeHaiku55.SupportedReasoningLevels)) + gpt56 := newConfiguredCodexModelDescriptor("gpt-5.6-sol") require.Equal(t, "GPT-5.6 Sol", gpt56.DisplayName) require.Equal(t, "OpenAI GPT coding model routed through Sub2API.", gpt56.Description) diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index f2a1eee9f7..ac29fd0f3a 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -139,6 +139,14 @@ var ( CacheReadInputTokenCost: 0.2e-6, LiteLLMProvider: "anthropic", Mode: "chat", SupportsPromptCaching: true, } + claudeHaiku55FallbackPricing = &LiteLLMModelPricing{ + InputCostPerToken: 1e-7, OutputCostPerToken: 5e-7, + CacheCreationInputTokenCost: 1.25e-7, CacheCreationInputTokenCostAbove1hr: 2e-7, + CacheReadInputTokenCost: 1e-8, + LongContextInputTokenThreshold: 100000, + LongContextInputCostMultiplier: 5, LongContextOutputCostMultiplier: 5, + LiteLLMProvider: "anthropic", Mode: "chat", SupportsPromptCaching: true, + } openAIGPT56SolFallbackPricing = &LiteLLMModelPricing{ InputCostPerToken: 5e-06, InputCostPerTokenPriority: 1e-05, @@ -1408,6 +1416,12 @@ func (s *PricingService) matchByModelFamily(model string) *LiteLLMModelPricing { } return claudeSonnet55FallbackPricing } + if claude.IsHaiku55(model) { + if pricing, ok := s.pricingData["claude-haiku-5-5"]; ok { + return pricing + } + return claudeHaiku55FallbackPricing + } // modelFamily 定义一个模型系列的匹配和定价查找规则。 type modelFamily struct { name string // 系列名称 diff --git a/backend/resources/model-pricing/model_prices_and_context_window.json b/backend/resources/model-pricing/model_prices_and_context_window.json index 99db4dbbdd..174366de04 100644 --- a/backend/resources/model-pricing/model_prices_and_context_window.json +++ b/backend/resources/model-pricing/model_prices_and_context_window.json @@ -381,6 +381,40 @@ "supports_tool_choice": true, "supports_vision": true }, + "claude-haiku-5-5": { + "cache_creation_input_token_cost": 1.25e-07, + "cache_creation_input_token_cost_above_1hr": 2e-07, + "cache_creation_input_token_cost_above_100k_tokens": 6.25e-07, + "cache_creation_input_token_cost_above_1hr_above_100k_tokens": 1e-06, + "cache_read_input_token_cost": 1e-08, + "cache_read_input_token_cost_above_100k_tokens": 5e-08, + "input_cost_per_token": 1e-07, + "input_cost_per_token_above_100k_tokens": 5e-07, + "litellm_provider": "anthropic", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 5e-07, + "output_cost_per_token_above_100k_tokens": 2.5e-06, + "source": "https://platform.claude.com/docs/en/models/haiku-5-5/overview", + "supports_adaptive_thinking": true, + "supports_assistant_prefill": false, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_max_reasoning_effort": true, + "supports_output_config": true, + "supports_pdf_input": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_response_schema": true, + "supports_sampling_params": false, + "supports_tool_choice": true, + "supports_vision": true, + "supports_xhigh_reasoning_effort": true, + "input_cost_per_token_batches": 5e-08, + "output_cost_per_token_batches": 2.5e-07 + }, "claude-opus-4-1": { "cache_creation_input_token_cost": 1.875e-05, "cache_creation_input_token_cost_above_1hr": 3e-05,