From 33ec5502c38ac6841a0754577ebff2483fa10cef Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sun, 4 Oct 2026 05:50:40 +0800 Subject: [PATCH] fix(claude): refine model version parsing for assistant prefill rejection - Strip provider namespaces and prefix before evaluating model family - Parse major and minor version numbers dynamically while ignoring snapshot dates - Correctly identify unsupported assistant prefill models for Fable, Opus 5+, and Sonnet 4.6+ Closes: #5483 --- .../claude_openai-responses_request.go | 37 ++++++++++++++++--- .../claude_openai-responses_request_test.go | 26 +++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index 9c880b7ca..d4251a627 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -727,12 +727,39 @@ func stripTrailingClaudeThinkingBlocks(messages [][]byte) [][]byte { // disallows trailing assistant prefill in its conversation history. func claudeModelRejectsAssistantPrefill(modelName string) bool { normalized := strings.ToLower(strings.TrimSpace(modelName)) - for _, family := range []string{"fable", "opus-5", "sonnet-4-6"} { - if strings.Contains(normalized, family) { - return true - } + // Provider namespaces are not part of the model family. + if index := strings.LastIndexByte(normalized, '/'); index >= 0 { + normalized = normalized[index+1:] } - return false + normalized = strings.TrimPrefix(normalized, "claude-") + tokens := strings.Split(strings.ReplaceAll(normalized, ".", "-"), "-") + if tokens[0] == "fable" { + return true + } + if len(tokens) < 2 || (tokens[0] != "opus" && tokens[0] != "sonnet") { + return false + } + parseVersion := func(token string) int { + // Eight-digit snapshot dates must never be treated as versions. + if token == "" || len(token) >= 8 { + return -1 + } + for _, digit := range token { + if digit < '0' || digit > '9' { + return -1 + } + } + version, errAtoi := strconv.Atoi(token) + if errAtoi != nil { + return -1 + } + return version + } + major := parseVersion(tokens[1]) + if major >= 5 { + return true + } + return tokens[0] == "sonnet" && major == 4 && len(tokens) > 2 && parseVersion(tokens[2]) >= 6 } // responsesSystemUnsupportedBlock represents a system-level content part that diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request_test.go b/internal/translator/claude/openai/responses/claude_openai-responses_request_test.go index 655adac67..d95d83727 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request_test.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request_test.go @@ -2036,6 +2036,18 @@ func TestConvertOpenAIResponsesRequestToClaude_UnsupportedPrefillModelsStripTrai "claude-fable-5", "claude-opus-5", "claude-sonnet-4-6", + "fable", + "opus-5", + "sonnet-4.6", + "anthropic/claude-opus-5-thinking", + "claude-sonnet-4.6", + "claude-sonnet-4-7", + "claude-sonnet-4-10", + "claude-sonnet-5", + "claude-opus-6", + "claude-opus-5.1", + "claude-sonnet-4-6-20260217", + " ANTHROPIC/CLAUDE-OPUS-5-THINKING ", } for _, model := range unsupportedModels { t.Run(model, func(t *testing.T) { @@ -2065,6 +2077,20 @@ func TestConvertOpenAIResponsesRequestToClaude_SupportedPrefillModelsPreserveAss supportedModels := []string{ "claude-sonnet-4-5", "claude-haiku-4-5", + "claude-3-opus-20240229", + "claude-opus-20240229", + "claude-sonnet-4-20260217", + "claude-sonnet-4.5", + "not-a-fable-model", + "my-custom-opus-5-wrapper", + "my-sonnet-4-6-wrapper", + "claude-fabled-5", + "claude-opus-5foo", + "claude-sonnet-4-6foo", + "fable/gpt-4o", + "opus", + "sonnet", + "", } for _, model := range supportedModels { t.Run(model, func(t *testing.T) {