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
This commit is contained in:
Luis Pater
2026-10-04 05:50:40 +08:00
parent cbc4cd5532
commit 33ec5502c3
2 changed files with 58 additions and 5 deletions

View File

@@ -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

View File

@@ -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) {