From 390028403c1c039974b3592c6a339d87937afa86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=94=A1=E5=8F=8A?= <522caiji@gmail.com> Date: Thu, 24 Sep 2026 09:35:12 +0800 Subject: [PATCH] fix(devin): resolve chat model uid from catalog and pass tool result images Drop hardcoded swe-1-7/swe-1-6/glm-5-2 suffix tables that drifted from the upstream registry; suffixed UIDs are built from catalog thinking levels. Never pick "none" as an implicit default effort. Refresh the models fixture (64k completions, swe-1-7-lightning, glm-5-2 [none,max], image modalities). Tool results now carry images through to the Devin prompt. --- .../devin-catalog-drift-and-tool-images.md | 9 ++++ internal/providers/devin/devin_test.go | 54 +++++++++++++++++++ internal/providers/devin/mapping.go | 32 ++++------- internal/providers/devin/payload.go | 7 ++- .../devin/testdata/devin_models.sample.json | 46 +++++++++++----- 5 files changed, 111 insertions(+), 37 deletions(-) create mode 100644 changelog/unreleased/devin-catalog-drift-and-tool-images.md diff --git a/changelog/unreleased/devin-catalog-drift-and-tool-images.md b/changelog/unreleased/devin-catalog-drift-and-tool-images.md new file mode 100644 index 0000000..11ffd08 --- /dev/null +++ b/changelog/unreleased/devin-catalog-drift-and-tool-images.md @@ -0,0 +1,9 @@ +### English + +- Devin: resolve `chat_model_uid` from the live model catalog instead of hardcoded suffix tables, so renamed or removed thinking variants (e.g. `swe-1-7`, `glm-5-2`) no longer emit stale upstream model IDs. "None" is never chosen as an implicit default effort. +- Devin: pass images embedded in tool results through to the upstream prompt instead of dropping them. + +### 中文 + +- Devin:`chat_model_uid` 改为按实时模型目录解析,不再使用硬编码后缀表;上游已改名或移除的思考档变体(如 `swe-1-7`、`glm-5-2`)不会再发出过期模型 ID。默认档不会隐式选择 `none`。 +- Devin:工具结果中携带的图片现在会透传给上游,不再被丢弃。 diff --git a/internal/providers/devin/devin_test.go b/internal/providers/devin/devin_test.go index 49a6161..78c93f7 100644 --- a/internal/providers/devin/devin_test.go +++ b/internal/providers/devin/devin_test.go @@ -296,6 +296,60 @@ func TestResolveChatModelUIDWithFixture(t *testing.T) { if got != "MODEL_PRIVATE_11" { t.Fatalf("alias=%q", got) } + // Registry drift checks: suffix variants must be built from catalog levels, + // not from a hardcoded table the upstream registry has since renamed. + got = ResolveChatModelUID("glm-5-2", "max", 0, levels) + if got != "glm-5-2-max" { + t.Fatalf("glm-5-2 max=%q", got) + } + got = ResolveChatModelUID("glm-5-2", "", 0, levels) + if got != "glm-5-2-max" { + t.Fatalf("glm-5-2 default=%q", got) + } + got = ResolveChatModelUID("swe-1-7", "", 0, levels) + if got != "swe-1-7-medium" { + t.Fatalf("swe-1-7 default=%q", got) + } + got = ResolveChatModelUID("swe-1-7-lightning", "", 0, levels) + if got != "swe-1-7-lightning-medium" { + t.Fatalf("lightning default=%q", got) + } + got = ResolveChatModelUID("glm-5-3-flash", "high", 0, levels) + if got != "glm-5-3-flash-high" { + t.Fatalf("glm-5-3-flash high=%q", got) + } + // Explicit suffixed ids pass through untouched. + got = ResolveChatModelUID("gpt-5-6-sol-low", "", 0, levels) + if got != "gpt-5-6-sol-low" { + t.Fatalf("suffixed passthrough=%q", got) + } +} + +func TestToolResultCarriesImages(t *testing.T) { + payload := BuildChatPayload(translate.ChatRequest{ + Model: "swe-2", + Messages: []translate.ChatMessage{ + {Role: "user", Content: "hi"}, + {Role: "assistant", Content: "", ToolCalls: json.RawMessage(`[{"id":"call_1","type":"function","function":{"name":"screenshot","arguments":"{}"}}]`)}, + {Role: "tool", ToolCallID: "call_1", Content: []any{ + map[string]any{"type": "text", "text": "here is the screenshot"}, + map[string]any{"type": "image_url", "image_url": map[string]any{"url": "data:image/png;base64,aGVsbG8="}}, + }}, + }, + }, nil) + if len(payload.Prompts) != 3 { + t.Fatalf("prompts=%d want 3", len(payload.Prompts)) + } + tool := payload.Prompts[2] + if tool.Source != 4 || tool.ToolCallID != "call_1" { + t.Fatalf("tool prompt=%+v", tool) + } + if tool.Content != "here is the screenshot" { + t.Fatalf("tool content=%q", tool.Content) + } + if len(tool.Images) != 1 || tool.Images[0].Base64Data != "aGVsbG8=" || tool.Images[0].MimeType != "image/png" { + t.Fatalf("tool images=%+v", tool.Images) + } } func TestFingerprintLength(t *testing.T) { diff --git a/internal/providers/devin/mapping.go b/internal/providers/devin/mapping.go index 68a6c5a..4f19e14 100644 --- a/internal/providers/devin/mapping.go +++ b/internal/providers/devin/mapping.go @@ -118,28 +118,11 @@ func ResolveChatModelUID(rawModel, thinkingLevel string, budgetTokens int, catal allowedLevels = catalogLevels[lowerBase] } - switch canonicalBase { - case "swe-1-7": - if effort == "medium" { - return "swe-1-7-medium" - } - return "swe-1-7" - case "swe-1-6": - if effort == "fast" { - return "swe-1-6-fast" - } - return "swe-1-6" - case "glm-5-2": - if effort == "none" { - return "glm-5-2-none" - } - if effort == "max" { - return "glm-5-2-max" - } - return "glm-5-2" - } - if len(allowedLevels) == 0 { + // No catalog levels: keep the bare model UID rather than guessing an + // effort suffix the upstream registry no longer publishes (the remote + // catalog is authoritative for suffixed variants like base-low/base-max + // or base-low-fast). return canonicalBase } defaultEffort := selectDefaultEffort(canonicalBase, allowedLevels) @@ -184,6 +167,13 @@ func selectDefaultEffort(baseModel string, levels []string) string { if hasLow { return "low" } + // "none" means no thinking; never pick it as an implicit default — callers + // ask for it explicitly. A [none, max] catalog should default to "max". + for _, l := range levels { + if strings.ToLower(strings.TrimSpace(l)) != "none" { + return l + } + } return levels[0] } diff --git a/internal/providers/devin/payload.go b/internal/providers/devin/payload.go index fa8cb6e..8effb95 100644 --- a/internal/providers/devin/payload.go +++ b/internal/providers/devin/payload.go @@ -57,8 +57,11 @@ func BuildChatPayload(req translate.ChatRequest, catalogLevels map[string][]stri p.ToolCalls = parseToolCalls(msg.ToolCalls, aliases) prompts = append(prompts, p) case "tool": - text := translate.ContentToString(msg.Content) - prompts = append(prompts, Prompt{Source: 4, Content: text, ToolCallID: strings.TrimSpace(msg.ToolCallID)}) + // Tool results can carry images (screenshots, rendered output); + // Devin accepts Images on any prompt, so pass them through instead + // of flattening to text. + text, images := splitContent(msg.Content) + prompts = append(prompts, Prompt{Source: 4, Content: text, Images: images, ToolCallID: strings.TrimSpace(msg.ToolCallID)}) default: text := translate.ContentToString(msg.Content) if strings.TrimSpace(text) == "" && len(msg.ToolCalls) == 0 { diff --git a/internal/providers/devin/testdata/devin_models.sample.json b/internal/providers/devin/testdata/devin_models.sample.json index 2455b63..f970e55 100644 --- a/internal/providers/devin/testdata/devin_models.sample.json +++ b/internal/providers/devin/testdata/devin_models.sample.json @@ -5,7 +5,7 @@ "type": "devin", "display_name": "SWE-2", "context_length": 262000, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ "text", "image" @@ -23,15 +23,30 @@ "type": "devin", "display_name": "SWE-1.7", "context_length": 262000, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ "text", "image" ], "thinking": { "levels": [ - "medium", - "max" + "medium" + ] + } + }, + { + "id": "swe-1-7-lightning", + "type": "devin", + "display_name": "SWE-1.7 Lightning", + "context_length": 202752, + "max_completion_tokens": 64000, + "supportedInputModalities": [ + "text", + "image" + ], + "thinking": { + "levels": [ + "medium" ] } }, @@ -40,9 +55,10 @@ "type": "devin", "display_name": "SWE-1.6", "context_length": 200000, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ - "text" + "text", + "image" ] }, { @@ -50,14 +66,14 @@ "type": "devin", "display_name": "GLM-5.2", "context_length": 200000, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ "text" ], "thinking": { "levels": [ "none", - "high" + "max" ] } }, @@ -66,7 +82,7 @@ "type": "devin", "display_name": "GLM-5.3", "context_length": 1048576, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ "text" ], @@ -83,9 +99,10 @@ "type": "devin", "display_name": "GLM-5.3 Flash", "context_length": 1000000, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ - "text" + "text", + "image" ], "thinking": { "levels": [ @@ -100,7 +117,7 @@ "type": "devin", "display_name": "DeepSeek V4 Flash", "context_length": 1048576, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ "text" ], @@ -116,9 +133,10 @@ "type": "devin", "display_name": "DeepSeek V4.1 Flash", "context_length": 1048576, - "max_completion_tokens": 128000, + "max_completion_tokens": 64000, "supportedInputModalities": [ - "text" + "text", + "image" ], "thinking": { "levels": [