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
67 changes: 31 additions & 36 deletions internal/runtime/executor/kiro_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "-<digit>" 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.
//
Expand Down Expand Up @@ -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 "-<digit>" 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 {
Expand All @@ -1585,34 +1581,33 @@ func trimKiroDateSuffix(s string) string {
return s[:len(s)-9]
}

// normalizeKiroVersion converts a trailing "-<digit>" pair to a dot-separated
// version. For example:
// - "claude-sonnet-4-5" → "claude-sonnet-4.5"
// - "claude-opus-4-7" → "claude-opus-4.7"
// - "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)
//
// 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.
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 {
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 _, alias := range kiroVersionAliases {
if s == alias.hyphenated {
return alias.canonical
}
if strings.HasPrefix(s, alias.hyphenated+"-") {
return alias.canonical + s[len(alias.hyphenated):]
}
}
// 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
Expand Down
66 changes: 65 additions & 1 deletion internal/runtime/executor/kiro_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,61 @@ 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: "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",
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: "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",
expected: "claude-sonnet-5",
},
{
name: "identifier without trailing version unchanged",
model: "kiro-qwen3-coder-next",
Expand All @@ -485,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)
}
})
}
}
Expand Down
Loading