From a39b3c4af49aec5c8ab34d8f2696d7c8bdbae076 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Tue, 11 Aug 2026 13:04:41 +0000 Subject: [PATCH] [proxy] Anchor the Anthropic date strip to Claude ids The release-date normalizer matched a bare "-YYYYMMDD" suffix on any id. Pricing looks every model up through it regardless of surface, and an operator can register a custom model under any id at all, so a custom "internal-llm-20250101" would silently inherit the rate registered for "internal-llm". Anchor the pattern on "claude" so it still covers the vendor-prefixed Bedrock forms while leaving every other vendor's id untouched. --- shared/llm/model.go | 27 ++++++++++++++++----------- shared/llm/model_test.go | 28 ++++++++++++++++++---------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/shared/llm/model.go b/shared/llm/model.go index 95a6f694e..4fb631520 100644 --- a/shared/llm/model.go +++ b/shared/llm/model.go @@ -46,20 +46,25 @@ func NormalizeBedrockModel(modelID string) string { return bedrockVersionSuffix.ReplaceAllString(m, "") } -// anthropicDateSuffix matches the trailing "-YYYYMMDD" release-date suffix -// Anthropic appends to a pinned model id. No other vendor in the catalog -// ends an id in eight consecutive digits, so the pattern is safe to apply -// before a lookup regardless of surface. -var anthropicDateSuffix = regexp.MustCompile(`-\d{8}$`) +// anthropicDatedModel matches a Claude model id carrying the trailing +// "-YYYYMMDD" release-date suffix Anthropic appends to a pinned release, +// capturing the id without it. The "claude" anchor is load-bearing: pricing +// looks every model up through this helper regardless of surface, and an +// operator may register a custom id with any shape at all, so an unanchored +// "-\d{8}$" would let "internal-llm-20250101" silently inherit the rate +// registered for "internal-llm". The anchor also covers the vendor-prefixed +// forms ("anthropic.claude-...", "us.anthropic.claude-..."). +var anthropicDatedModel = regexp.MustCompile(`(?i)^(.*claude.*)-\d{8}$`) // NormalizeAnthropicModel strips the trailing release-date suffix from a -// first-party Anthropic model id, e.g. "claude-sonnet-4-5-20250929" -> -// "claude-sonnet-4-5", so a dated id a client pins matches the undated one -// the operator registered. Callers try the verbatim id first and fall back -// to this, so two dated releases of the same family stay distinct wherever -// both are registered explicitly. +// Claude model id, e.g. "claude-sonnet-4-5-20250929" -> "claude-sonnet-4-5", +// so a dated id a client pins matches the undated one the operator +// registered. Ids that are not Claude-family are returned untouched. +// Callers try the verbatim id first and fall back to this, so two dated +// releases of the same family stay distinct wherever both are registered +// explicitly. func NormalizeAnthropicModel(modelID string) string { - return anthropicDateSuffix.ReplaceAllString(modelID, "") + return anthropicDatedModel.ReplaceAllString(modelID, "$1") } // NormalizeVertexModel strips the "@version" suffix from a Vertex AI model id diff --git a/shared/llm/model_test.go b/shared/llm/model_test.go index c1d5b63f6..5ce2ff497 100644 --- a/shared/llm/model_test.go +++ b/shared/llm/model_test.go @@ -37,16 +37,24 @@ func TestNormalizeVertexModel(t *testing.T) { func TestNormalizeAnthropicModel(t *testing.T) { cases := map[string]string{ - "claude-sonnet-4-5-20250929": "claude-sonnet-4-5", - "claude-3-5-haiku-20241022": "claude-3-5-haiku", - "claude-sonnet-5": "claude-sonnet-5", - "claude-opus-4-8": "claude-opus-4-8", - // Other vendors' ids must survive untouched: none of them end in - // eight consecutive digits. - "gpt-4o": "gpt-4o", - "gpt-4o-2024-08-06": "gpt-4o-2024-08-06", - "anthropic.claude-haiku-4-5": "anthropic.claude-haiku-4-5", - "": "", + "claude-sonnet-4-5-20250929": "claude-sonnet-4-5", + "claude-3-5-haiku-20241022": "claude-3-5-haiku", + "claude-sonnet-5": "claude-sonnet-5", + "claude-opus-4-8": "claude-opus-4-8", + "anthropic.claude-haiku-4-5": "anthropic.claude-haiku-4-5", + "anthropic.claude-sonnet-4-5-20250929": "anthropic.claude-sonnet-4-5", + "us.anthropic.claude-opus-4-8-20250101": "us.anthropic.claude-opus-4-8", + // Non-Claude ids must survive untouched even when they end in eight + // consecutive digits: an operator can register a custom model under + // any id, and pricing looks every one of them up through this helper. + "gpt-4o": "gpt-4o", + "gpt-4o-2024-08-06": "gpt-4o-2024-08-06", + "gpt-4o-20240806": "gpt-4o-20240806", + "internal-llm-20250101": "internal-llm-20250101", + "deepseek-r1-20250120": "deepseek-r1-20250120", + "Qwen/Qwen2.5-20250101": "Qwen/Qwen2.5-20250101", + "gemini-2-5-pro-20250101": "gemini-2-5-pro-20250101", + "": "", } for in, want := range cases { require.Equal(t, want, NormalizeAnthropicModel(in), "normalize %q", in)