Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions changelog/unreleased/devin-catalog-drift-and-tool-images.md
Original file line number Diff line number Diff line change
@@ -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:工具结果中携带的图片现在会透传给上游,不再被丢弃。
54 changes: 54 additions & 0 deletions internal/providers/devin/devin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
32 changes: 11 additions & 21 deletions internal/providers/devin/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
}

Expand Down
7 changes: 5 additions & 2 deletions internal/providers/devin/payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
46 changes: 32 additions & 14 deletions internal/providers/devin/testdata/devin_models.sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "devin",
"display_name": "SWE-2",
"context_length": 262000,
"max_completion_tokens": 128000,
"max_completion_tokens": 64000,
"supportedInputModalities": [
"text",
"image"
Expand All @@ -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"
]
}
},
Expand All @@ -40,24 +55,25 @@
"type": "devin",
"display_name": "SWE-1.6",
"context_length": 200000,
"max_completion_tokens": 128000,
"max_completion_tokens": 64000,
"supportedInputModalities": [
"text"
"text",
"image"
]
},
{
"id": "glm-5-2",
"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"
]
}
},
Expand All @@ -66,7 +82,7 @@
"type": "devin",
"display_name": "GLM-5.3",
"context_length": 1048576,
"max_completion_tokens": 128000,
"max_completion_tokens": 64000,
"supportedInputModalities": [
"text"
],
Expand All @@ -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": [
Expand All @@ -100,7 +117,7 @@
"type": "devin",
"display_name": "DeepSeek V4 Flash",
"context_length": 1048576,
"max_completion_tokens": 128000,
"max_completion_tokens": 64000,
"supportedInputModalities": [
"text"
],
Expand All @@ -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": [
Expand Down
Loading