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
This commit is contained in:
xcrong
2026-10-09 10:07:36 +08:00
parent 5fc0e486c3
commit d56f5e7879
15 changed files with 264 additions and 14 deletions
+6
View File
@@ -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 列表
@@ -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")
}
@@ -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
}
@@ -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)
}
}