From 64db6a841fd5ba6b23d97a7186187fe1e5054a47 Mon Sep 17 00:00:00 2001 From: chenzhihui Date: Thu, 23 Jul 2026 16:13:42 +0800 Subject: [PATCH 1/2] fix(kiro): normalize version separator mid-identifier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeKiroVersion only rewrote a trailing "-" pair to a dot, so backend IDs where the version sits mid-identifier were left with a dash (e.g. "gpt-5-6-terra", "kimi-k2-7-code"). Rewrite the first dash that sits directly between two digits instead, which correctly produces "gpt-5.6-terra" while leaving trailing build/date segments like "grok-4-20-0309" → "grok-4.20-0309" intact. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/runtime/executor/kiro_executor.go | 41 +++++++++---------- .../runtime/executor/kiro_executor_test.go | 35 ++++++++++++++++ 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/internal/runtime/executor/kiro_executor.go b/internal/runtime/executor/kiro_executor.go index 0ebe7a065..87bb1877c 100644 --- a/internal/runtime/executor/kiro_executor.go +++ b/internal/runtime/executor/kiro_executor.go @@ -1585,34 +1585,33 @@ func trimKiroDateSuffix(s string) string { return s[:len(s)-9] } -// normalizeKiroVersion converts a trailing "-" pair to a dot-separated -// version. For example: +// normalizeKiroVersion converts the version separator from a dash to a dot. +// The version separator is the FIRST dash that sits directly between two +// digits (i.e. "-"), since that is how Kiro encodes the +// "major.minor" version in its backend IDs. For example: // - "claude-sonnet-4-5" → "claude-sonnet-4.5" // - "claude-opus-4-7" → "claude-opus-4.7" +// - "gpt-5-6-terra" → "gpt-5.6-terra" (version sits mid-identifier) // - "minimax-m2-1" → "minimax-m2.1" (the "m2" retains its digit) -// - "qwen3-coder-next" → "qwen3-coder-next" (no trailing digit pair) -// - "glm-5" → "glm-5" (only one digit segment; left alone) +// - "kimi-k2-7-code" → "kimi-k2.7-code" +// - "qwen3-coder-next" → "qwen3-coder-next" (no digit-dash-digit pair) +// - "glm-5" → "glm-5" (single digit segment; left alone) +// - "claude-sonnet-5" → "claude-sonnet-5" (dash is letter-digit) // -// The rule: if the last dash-separated segment is all digits AND the -// second-to-last segment ends with a digit, replace that last dash with a dot. +// Only the first such dash is rewritten so that trailing build/date segments +// like "grok-4-20-0309" collapse to "grok-4.20-0309" rather than eating the +// "-0309" separator too. func normalizeKiroVersion(s string) string { - lastDash := strings.LastIndex(s, "-") - if lastDash <= 0 || lastDash == len(s)-1 { - return s - } - tail := s[lastDash+1:] - for _, r := range tail { - if r < '0' || r > '9' { - return s + for i := 1; i < len(s)-1; i++ { + if s[i] != '-' { + continue + } + prev, next := s[i-1], s[i+1] + if prev >= '0' && prev <= '9' && next >= '0' && next <= '9' { + return s[:i] + "." + s[i+1:] } } - // Require the preceding character to be a digit — otherwise we'd rewrite - // "glm-5" into "glm.5" which isn't a backend ID Kiro recognizes. - prev := s[lastDash-1] - if prev < '0' || prev > '9' { - return s - } - return s[:lastDash] + "." + tail + return s } // EventStreamError represents an Event Stream processing error diff --git a/internal/runtime/executor/kiro_executor_test.go b/internal/runtime/executor/kiro_executor_test.go index 088ae4087..9787fca24 100644 --- a/internal/runtime/executor/kiro_executor_test.go +++ b/internal/runtime/executor/kiro_executor_test.go @@ -475,6 +475,41 @@ func TestMapModelToKiro_MapsClaudeOpus47Variants(t *testing.T) { model: "kiro-minimax-m2-5", expected: "minimax-m2.5", }, + { + name: "gpt version with trailing codename (terra)", + model: "kiro-gpt-5-6-terra", + expected: "gpt-5.6-terra", + }, + { + name: "gpt version with trailing codename (sol)", + model: "kiro-gpt-5-6-sol", + expected: "gpt-5.6-sol", + }, + { + name: "gpt version with trailing codename (luna)", + model: "kiro-gpt-5-6-luna", + expected: "gpt-5.6-luna", + }, + { + name: "kimi version with trailing codename", + model: "kiro-kimi-k2-7-code", + expected: "kimi-k2.7-code", + }, + { + name: "deepseek versioned", + model: "kiro-deepseek-3-2", + expected: "deepseek-3.2", + }, + { + name: "grok version keeps trailing build segment", + model: "kiro-grok-4-20-0309-reasoning", + expected: "grok-4.20-0309-reasoning", + }, + { + name: "single digit segment left alone", + model: "kiro-claude-sonnet-5", + expected: "claude-sonnet-5", + }, { name: "identifier without trailing version unchanged", model: "kiro-qwen3-coder-next", From 7df096b8cf693bbf231c3353b7e75c244daf1438 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 29 Jul 2026 13:24:56 -0400 Subject: [PATCH 2/2] fix(kiro): constrain version alias normalization --- internal/runtime/executor/kiro_executor.go | 62 +++++++++---------- .../runtime/executor/kiro_executor_test.go | 31 +++++++++- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/internal/runtime/executor/kiro_executor.go b/internal/runtime/executor/kiro_executor.go index 87bb1877c..559c47c42 100644 --- a/internal/runtime/executor/kiro_executor.go +++ b/internal/runtime/executor/kiro_executor.go @@ -1516,20 +1516,17 @@ func getEffectiveProfileArnWithWarning(auth *cliproxyauth.Auth, profileArn strin // mapModelToKiro maps external model names to Kiro backend model IDs. // -// It accepts any of the surface forms clients use and returns the ID that -// Kiro's API expects. The transformation is algorithmic so new models added -// by Kiro (e.g. glm-5, deepseek-3.2, future releases) route correctly -// without code changes: +// It accepts the supported surface forms clients use and returns the ID that +// Kiro's API expects: // // 1. Trim surrounding whitespace and lowercase. // 2. Strip the leading "kiro-" or "amazonq-" prefix if present. // 3. Strip the trailing "-agentic" suffix (agentic variants share the // underlying backend ID; the agentic behavior is applied separately // via determineAgenticMode). -// 4. Normalize the version segment from dashes to dots — e.g. -// "claude-sonnet-4-5" → "claude-sonnet-4.5", "minimax-m2-1" → -// "minimax-m2.1". Only the last "-" pair is rewritten so -// identifiers like "qwen3-coder-next" pass through unchanged. +// 4. Normalize version separators for known aliases derived from canonical +// model IDs — e.g. "claude-sonnet-4-5" → "claude-sonnet-4.5" and +// "gpt-5-6-terra" → "gpt-5.6-terra". // 5. Map a few historical dated aliases (e.g. "claude-sonnet-4-5-20250929") // back to their canonical version. // @@ -1559,9 +1556,8 @@ func (e *KiroExecutor) mapModelToKiro(model string) string { // canonical version. Only handles the common 8-digit date suffix. m = trimKiroDateSuffix(m) - // 4. Normalize final version segment: last "-" pair becomes "." - // e.g. "claude-sonnet-4-5" → "claude-sonnet-4.5", but "qwen3-coder-next" - // is left alone because the final segment isn't a digit. + // 4. Normalize only known hyphenated aliases. Broad digit-based rewriting + // would corrupt canonical or unrelated numeric IDs. m = normalizeKiroVersion(m) if m != original { @@ -1585,30 +1581,30 @@ func trimKiroDateSuffix(s string) string { return s[:len(s)-9] } -// normalizeKiroVersion converts the version separator from a dash to a dot. -// The version separator is the FIRST dash that sits directly between two -// digits (i.e. "-"), since that is how Kiro encodes the -// "major.minor" version in its backend IDs. For example: -// - "claude-sonnet-4-5" → "claude-sonnet-4.5" -// - "claude-opus-4-7" → "claude-opus-4.7" -// - "gpt-5-6-terra" → "gpt-5.6-terra" (version sits mid-identifier) -// - "minimax-m2-1" → "minimax-m2.1" (the "m2" retains its digit) -// - "kimi-k2-7-code" → "kimi-k2.7-code" -// - "qwen3-coder-next" → "qwen3-coder-next" (no digit-dash-digit pair) -// - "glm-5" → "glm-5" (single digit segment; left alone) -// - "claude-sonnet-5" → "claude-sonnet-5" (dash is letter-digit) -// -// Only the first such dash is rewritten so that trailing build/date segments -// like "grok-4-20-0309" collapse to "grok-4.20-0309" rather than eating the -// "-0309" separator too. +var kiroVersionAliases = [...]struct { + hyphenated string + canonical string +}{ + {"claude-sonnet-4-5", "claude-sonnet-4.5"}, + {"claude-opus-4-7", "claude-opus-4.7"}, + {"minimax-m2-5", "minimax-m2.5"}, + {"gpt-5-6", "gpt-5.6"}, + {"kimi-k2-7", "kimi-k2.7"}, + {"deepseek-3-2", "deepseek-3.2"}, + {"grok-4-20", "grok-4.20"}, +} + +// normalizeKiroVersion reverses the hyphenated version aliases generated for +// known canonical Kiro model families. Matching the full alias prefix keeps +// suffixes such as "-terra" and "-0309-reasoning", while canonical dotted IDs +// and unknown numeric IDs pass through unchanged. func normalizeKiroVersion(s string) string { - for i := 1; i < len(s)-1; i++ { - if s[i] != '-' { - continue + for _, alias := range kiroVersionAliases { + if s == alias.hyphenated { + return alias.canonical } - prev, next := s[i-1], s[i+1] - if prev >= '0' && prev <= '9' && next >= '0' && next <= '9' { - return s[:i] + "." + s[i+1:] + if strings.HasPrefix(s, alias.hyphenated+"-") { + return alias.canonical + s[len(alias.hyphenated):] } } return s diff --git a/internal/runtime/executor/kiro_executor_test.go b/internal/runtime/executor/kiro_executor_test.go index 9787fca24..9b28abe79 100644 --- a/internal/runtime/executor/kiro_executor_test.go +++ b/internal/runtime/executor/kiro_executor_test.go @@ -480,6 +480,11 @@ func TestMapModelToKiro_MapsClaudeOpus47Variants(t *testing.T) { model: "kiro-gpt-5-6-terra", expected: "gpt-5.6-terra", }, + { + name: "canonical gpt version remains unchanged", + model: "gpt-5.6-terra", + expected: "gpt-5.6-terra", + }, { name: "gpt version with trailing codename (sol)", model: "kiro-gpt-5-6-sol", @@ -495,16 +500,31 @@ func TestMapModelToKiro_MapsClaudeOpus47Variants(t *testing.T) { model: "kiro-kimi-k2-7-code", expected: "kimi-k2.7-code", }, + { + name: "canonical kimi version remains unchanged", + model: "kimi-k2.7-code", + expected: "kimi-k2.7-code", + }, { name: "deepseek versioned", model: "kiro-deepseek-3-2", expected: "deepseek-3.2", }, + { + name: "canonical deepseek version remains unchanged", + model: "deepseek-3.2", + expected: "deepseek-3.2", + }, { name: "grok version keeps trailing build segment", model: "kiro-grok-4-20-0309-reasoning", expected: "grok-4.20-0309-reasoning", }, + { + name: "canonical grok version remains unchanged", + model: "grok-4.20-0309-reasoning", + expected: "grok-4.20-0309-reasoning", + }, { name: "single digit segment left alone", model: "kiro-claude-sonnet-5", @@ -520,13 +540,22 @@ func TestMapModelToKiro_MapsClaudeOpus47Variants(t *testing.T) { model: "kiro-future-model-9", expected: "future-model-9", }, + { + name: "unknown date-like numeric ID passes through unchanged", + model: "kiro-model-2024-05-preview", + expected: "model-2024-05-preview", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := executor.mapModelToKiro(tt.model); got != tt.expected { + got := executor.mapModelToKiro(tt.model) + if got != tt.expected { t.Fatalf("mapModelToKiro(%q) = %q, want %q", tt.model, got, tt.expected) } + if normalizedAgain := executor.mapModelToKiro(got); normalizedAgain != got { + t.Fatalf("mapModelToKiro() is not idempotent: first = %q, second = %q", got, normalizedAgain) + } }) } }