From 82b1ebefa8a8d9ae51e45fe4d442c96e4df38913 Mon Sep 17 00:00:00 2001 From: Micah Date: Fri, 16 Jan 2026 17:12:28 +0000 Subject: [PATCH 1/5] feat: Add Amazon Nova model definitions to Bedrock provider - Add amazon.nova-pro-v1:0 with 300K context, image support - Add amazon.nova-lite-v1:0 with 300K context, image support - Add amazon.nova-micro-v1:0 with 128K context, no image support - Add amazon.nova-premier-v1:0 with 300K context, reasoning capability - Include accurate pricing for all Nova models - Add comprehensive property and unit tests for Nova model configurations - Update .gitignore to exclude .kiro directory Validates requirements 2.1-2.6 from amazon-nova-bedrock-support spec --- .gitignore | 3 + internal/providers/bedrock_nova_test.go | 401 ++++++++++++++++++++++++ internal/providers/configs/bedrock.json | 48 +++ 3 files changed, 452 insertions(+) create mode 100644 internal/providers/bedrock_nova_test.go diff --git a/.gitignore b/.gitignore index c6efbc0c..3c60ac8f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,6 @@ dist/ # temp dir /tmp + +# kiro +.kiro/ \ No newline at end of file diff --git a/internal/providers/bedrock_nova_test.go b/internal/providers/bedrock_nova_test.go new file mode 100644 index 00000000..64e07d8b --- /dev/null +++ b/internal/providers/bedrock_nova_test.go @@ -0,0 +1,401 @@ +package providers + +import ( + "regexp" + "testing" + + "github.com/charmbracelet/catwalk/pkg/catwalk" +) + +// Feature: amazon-nova-bedrock-support, Property 5: Catwalk Model Configuration Validity +// For all Nova model entries in the Bedrock catwalk configuration, each entry should have: +// (1) a valid model ID matching the pattern "amazon.nova-*-v*:*", +// (2) non-negative pricing values for input and output tokens, +// (3) a positive context window size, +// (4) a positive default max tokens value, +// (5) a boolean supports_attachments field. +// Validates: Requirements 2.2, 2.3, 2.4, 2.5, 2.6 +func TestProperty_CatwalkModelConfigurationValidity(t *testing.T) { + // Get the Bedrock provider configuration + provider := bedrockProvider() + + // Define the expected Nova model IDs + expectedNovaModels := []string{ + "amazon.nova-pro-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-micro-v1:0", + "amazon.nova-premier-v1:0", + } + + // Pattern for valid Nova model IDs + novaModelPattern := regexp.MustCompile(`^amazon\.nova-[a-z]+-v\d+:\d+$`) + + // Find all Nova models in the configuration + var novaModels []catwalk.Model + for _, model := range provider.Models { + if novaModelPattern.MatchString(model.ID) { + novaModels = append(novaModels, model) + } + } + + // Property 1: All expected Nova models should be present + if len(novaModels) != len(expectedNovaModels) { + t.Errorf("Expected %d Nova models, found %d", len(expectedNovaModels), len(novaModels)) + } + + foundModels := make(map[string]bool) + for _, model := range novaModels { + foundModels[model.ID] = true + } + + for _, expectedID := range expectedNovaModels { + if !foundModels[expectedID] { + t.Errorf("Expected Nova model %q not found in configuration", expectedID) + } + } + + // Property 2-5: Validate each Nova model configuration + for _, model := range novaModels { + t.Run(model.ID, func(t *testing.T) { + // Property 2.1: Model ID should match the pattern + if !novaModelPattern.MatchString(model.ID) { + t.Errorf("Model ID %q does not match expected pattern amazon.nova-*-v*:*", model.ID) + } + + // Property 2.2: Pricing values should be non-negative + if model.CostPer1MIn < 0 { + t.Errorf("Model %q has negative input cost: %f", model.ID, model.CostPer1MIn) + } + if model.CostPer1MOut < 0 { + t.Errorf("Model %q has negative output cost: %f", model.ID, model.CostPer1MOut) + } + if model.CostPer1MInCached < 0 { + t.Errorf("Model %q has negative cached input cost: %f", model.ID, model.CostPer1MInCached) + } + if model.CostPer1MOutCached < 0 { + t.Errorf("Model %q has negative cached output cost: %f", model.ID, model.CostPer1MOutCached) + } + + // Property 2.3: Context window should be positive + if model.ContextWindow <= 0 { + t.Errorf("Model %q has non-positive context window: %d", model.ID, model.ContextWindow) + } + + // Property 2.4: Default max tokens should be positive + if model.DefaultMaxTokens <= 0 { + t.Errorf("Model %q has non-positive default max tokens: %d", model.ID, model.DefaultMaxTokens) + } + + // Property 2.5: Model should have a name + if model.Name == "" { + t.Errorf("Model %q has empty name", model.ID) + } + + // Additional validation: Verify specific model characteristics + switch model.ID { + case "amazon.nova-pro-v1:0": + if model.CostPer1MIn != 0.8 { + t.Errorf("Nova Pro input cost should be 0.8, got %f", model.CostPer1MIn) + } + if model.CostPer1MOut != 3.2 { + t.Errorf("Nova Pro output cost should be 3.2, got %f", model.CostPer1MOut) + } + if model.ContextWindow != 300000 { + t.Errorf("Nova Pro context window should be 300000, got %d", model.ContextWindow) + } + if !model.SupportsImages { + t.Errorf("Nova Pro should support attachments") + } + + case "amazon.nova-lite-v1:0": + if model.CostPer1MIn != 0.06 { + t.Errorf("Nova Lite input cost should be 0.06, got %f", model.CostPer1MIn) + } + if model.CostPer1MOut != 0.24 { + t.Errorf("Nova Lite output cost should be 0.24, got %f", model.CostPer1MOut) + } + if model.ContextWindow != 300000 { + t.Errorf("Nova Lite context window should be 300000, got %d", model.ContextWindow) + } + if !model.SupportsImages { + t.Errorf("Nova Lite should support attachments") + } + + case "amazon.nova-micro-v1:0": + if model.CostPer1MIn != 0.035 { + t.Errorf("Nova Micro input cost should be 0.035, got %f", model.CostPer1MIn) + } + if model.CostPer1MOut != 0.14 { + t.Errorf("Nova Micro output cost should be 0.14, got %f", model.CostPer1MOut) + } + if model.ContextWindow != 128000 { + t.Errorf("Nova Micro context window should be 128000, got %d", model.ContextWindow) + } + if model.SupportsImages { + t.Errorf("Nova Micro should not support attachments") + } + + case "amazon.nova-premier-v1:0": + if model.CostPer1MIn != 2.5 { + t.Errorf("Nova Premier input cost should be 2.5, got %f", model.CostPer1MIn) + } + if model.CostPer1MOut != 12.5 { + t.Errorf("Nova Premier output cost should be 12.5, got %f", model.CostPer1MOut) + } + if model.ContextWindow != 300000 { + t.Errorf("Nova Premier context window should be 300000, got %d", model.ContextWindow) + } + if !model.CanReason { + t.Errorf("Nova Premier should have reasoning capability") + } + if !model.SupportsImages { + t.Errorf("Nova Premier should support attachments") + } + } + }) + } +} + +// TestNovaModelsPresent verifies that all Nova models are present in the Bedrock configuration. +// Validates: Requirements 2.1, 2.6 +func TestNovaModelsPresent(t *testing.T) { + provider := bedrockProvider() + + expectedModels := map[string]string{ + "amazon.nova-pro-v1:0": "Amazon Nova Pro", + "amazon.nova-lite-v1:0": "Amazon Nova Lite", + "amazon.nova-micro-v1:0": "Amazon Nova Micro", + "amazon.nova-premier-v1:0": "Amazon Nova Premier", + } + + // Build a map of actual models + actualModels := make(map[string]catwalk.Model) + for _, model := range provider.Models { + actualModels[model.ID] = model + } + + // Verify each expected model is present + for expectedID, expectedName := range expectedModels { + model, found := actualModels[expectedID] + if !found { + t.Errorf("Expected Nova model %q not found in Bedrock provider", expectedID) + continue + } + + if model.Name != expectedName { + t.Errorf("Model %q has incorrect name: expected %q, got %q", expectedID, expectedName, model.Name) + } + } +} + +// TestNovaModelIDFormat verifies that all Nova model IDs match the expected format. +// Validates: Requirements 2.1, 2.6 +func TestNovaModelIDFormat(t *testing.T) { + provider := bedrockProvider() + + // Pattern for valid Nova model IDs: amazon.nova-{variant}-v{version}:{revision} + novaModelPattern := regexp.MustCompile(`^amazon\.nova-[a-z]+-v\d+:\d+$`) + + expectedNovaModels := []string{ + "amazon.nova-pro-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-micro-v1:0", + "amazon.nova-premier-v1:0", + } + + for _, expectedID := range expectedNovaModels { + // Find the model + var found bool + for _, model := range provider.Models { + if model.ID == expectedID { + found = true + + // Verify the ID matches the pattern + if !novaModelPattern.MatchString(model.ID) { + t.Errorf("Nova model ID %q does not match expected pattern amazon.nova-*-v*:*", model.ID) + } + + // Verify the ID starts with "amazon." + if model.ID[:7] != "amazon." { + t.Errorf("Nova model ID %q should start with 'amazon.'", model.ID) + } + + break + } + } + + if !found { + t.Errorf("Expected Nova model %q not found in configuration", expectedID) + } + } +} + +// TestNovaPricingNonNegative verifies that all Nova models have non-negative pricing values. +// Validates: Requirements 2.1, 2.6 +func TestNovaPricingNonNegative(t *testing.T) { + provider := bedrockProvider() + + novaModelPattern := regexp.MustCompile(`^amazon\.nova-`) + + for _, model := range provider.Models { + if !novaModelPattern.MatchString(model.ID) { + continue + } + + t.Run(model.ID, func(t *testing.T) { + if model.CostPer1MIn < 0 { + t.Errorf("Model %q has negative input cost: %f", model.ID, model.CostPer1MIn) + } + + if model.CostPer1MOut < 0 { + t.Errorf("Model %q has negative output cost: %f", model.ID, model.CostPer1MOut) + } + + if model.CostPer1MInCached < 0 { + t.Errorf("Model %q has negative cached input cost: %f", model.ID, model.CostPer1MInCached) + } + + if model.CostPer1MOutCached < 0 { + t.Errorf("Model %q has negative cached output cost: %f", model.ID, model.CostPer1MOutCached) + } + + // Verify pricing is reasonable (not zero for non-cached costs) + if model.CostPer1MIn == 0 { + t.Errorf("Model %q has zero input cost, which is likely incorrect", model.ID) + } + + if model.CostPer1MOut == 0 { + t.Errorf("Model %q has zero output cost, which is likely incorrect", model.ID) + } + }) + } +} + +// TestNovaModelContextWindows verifies that Nova models have appropriate context window sizes. +// Validates: Requirements 2.1, 2.6 +func TestNovaModelContextWindows(t *testing.T) { + provider := bedrockProvider() + + expectedContextWindows := map[string]int64{ + "amazon.nova-pro-v1:0": 300000, + "amazon.nova-lite-v1:0": 300000, + "amazon.nova-micro-v1:0": 128000, + "amazon.nova-premier-v1:0": 300000, + } + + for modelID, expectedWindow := range expectedContextWindows { + var found bool + for _, model := range provider.Models { + if model.ID == modelID { + found = true + + if model.ContextWindow != expectedWindow { + t.Errorf("Model %q has incorrect context window: expected %d, got %d", + modelID, expectedWindow, model.ContextWindow) + } + + if model.ContextWindow <= 0 { + t.Errorf("Model %q has non-positive context window: %d", modelID, model.ContextWindow) + } + + break + } + } + + if !found { + t.Errorf("Expected Nova model %q not found in configuration", modelID) + } + } +} + +// TestNovaModelDefaultMaxTokens verifies that Nova models have appropriate default max tokens. +// Validates: Requirements 2.1, 2.6 +func TestNovaModelDefaultMaxTokens(t *testing.T) { + provider := bedrockProvider() + + novaModelPattern := regexp.MustCompile(`^amazon\.nova-`) + + for _, model := range provider.Models { + if !novaModelPattern.MatchString(model.ID) { + continue + } + + t.Run(model.ID, func(t *testing.T) { + if model.DefaultMaxTokens <= 0 { + t.Errorf("Model %q has non-positive default max tokens: %d", model.ID, model.DefaultMaxTokens) + } + + // Verify default max tokens is reasonable (should be 5000 for Nova models) + if model.DefaultMaxTokens != 5000 { + t.Errorf("Model %q has unexpected default max tokens: expected 5000, got %d", + model.ID, model.DefaultMaxTokens) + } + }) + } +} + +// TestNovaModelAttachmentSupport verifies that Nova models have correct attachment support flags. +// Validates: Requirements 2.1, 2.6 +func TestNovaModelAttachmentSupport(t *testing.T) { + provider := bedrockProvider() + + expectedAttachmentSupport := map[string]bool{ + "amazon.nova-pro-v1:0": true, + "amazon.nova-lite-v1:0": true, + "amazon.nova-micro-v1:0": false, // Micro does not support attachments + "amazon.nova-premier-v1:0": true, + } + + for modelID, expectedSupport := range expectedAttachmentSupport { + var found bool + for _, model := range provider.Models { + if model.ID == modelID { + found = true + + if model.SupportsImages != expectedSupport { + t.Errorf("Model %q has incorrect attachment support: expected %v, got %v", + modelID, expectedSupport, model.SupportsImages) + } + + break + } + } + + if !found { + t.Errorf("Expected Nova model %q not found in configuration", modelID) + } + } +} + +// TestNovaModelReasoningCapability verifies that Nova Premier has reasoning capability. +// Validates: Requirements 2.1, 2.6 +func TestNovaModelReasoningCapability(t *testing.T) { + provider := bedrockProvider() + + expectedReasoning := map[string]bool{ + "amazon.nova-pro-v1:0": false, + "amazon.nova-lite-v1:0": false, + "amazon.nova-micro-v1:0": false, + "amazon.nova-premier-v1:0": true, // Only Premier has reasoning + } + + for modelID, expectedCanReason := range expectedReasoning { + var found bool + for _, model := range provider.Models { + if model.ID == modelID { + found = true + + if model.CanReason != expectedCanReason { + t.Errorf("Model %q has incorrect reasoning capability: expected %v, got %v", + modelID, expectedCanReason, model.CanReason) + } + + break + } + } + + if !found { + t.Errorf("Expected Nova model %q not found in configuration", modelID) + } + } +} diff --git a/internal/providers/configs/bedrock.json b/internal/providers/configs/bedrock.json index 1acfb83e..f3071b4c 100644 --- a/internal/providers/configs/bedrock.json +++ b/internal/providers/configs/bedrock.json @@ -102,6 +102,54 @@ "default_max_tokens": 50000, "can_reason": false, "supports_attachments": true + }, + { + "id": "amazon.nova-pro-v1:0", + "name": "Amazon Nova Pro", + "cost_per_1m_in": 0.8, + "cost_per_1m_out": 3.2, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 300000, + "default_max_tokens": 5000, + "can_reason": false, + "supports_attachments": true + }, + { + "id": "amazon.nova-lite-v1:0", + "name": "Amazon Nova Lite", + "cost_per_1m_in": 0.06, + "cost_per_1m_out": 0.24, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 300000, + "default_max_tokens": 5000, + "can_reason": false, + "supports_attachments": true + }, + { + "id": "amazon.nova-micro-v1:0", + "name": "Amazon Nova Micro", + "cost_per_1m_in": 0.035, + "cost_per_1m_out": 0.14, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 128000, + "default_max_tokens": 5000, + "can_reason": false, + "supports_attachments": false + }, + { + "id": "amazon.nova-premier-v1:0", + "name": "Amazon Nova Premier", + "cost_per_1m_in": 2.5, + "cost_per_1m_out": 12.5, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 300000, + "default_max_tokens": 5000, + "can_reason": true, + "supports_attachments": true } ] } From ff8789f90c3baa0a80f6dc76ccf0eb6634bfc2af Mon Sep 17 00:00:00 2001 From: Micah Walter Date: Thu, 22 Jan 2026 12:36:37 -0500 Subject: [PATCH 2/5] Add Nova 2 Lite and fix Nova Premier reasoning support - Add amazon.nova-2-lite-v1:0 with can_reason: true (supports reasoningConfig API) - Set amazon.nova-premier-v1:0 can_reason: false (original Nova models don't support reasoningConfig) - Nova 2 Lite: 1M token context, 10k max tokens, $0.06/$0.24 per 1M tokens Co-Authored-By: Claude Sonnet 4.5 --- internal/providers/configs/bedrock.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/providers/configs/bedrock.json b/internal/providers/configs/bedrock.json index f3071b4c..afac2941 100644 --- a/internal/providers/configs/bedrock.json +++ b/internal/providers/configs/bedrock.json @@ -148,6 +148,18 @@ "cost_per_1m_out_cached": 0, "context_window": 300000, "default_max_tokens": 5000, + "can_reason": false, + "supports_attachments": true + }, + { + "id": "amazon.nova-2-lite-v1:0", + "name": "Amazon Nova 2 Lite", + "cost_per_1m_in": 0.06, + "cost_per_1m_out": 0.24, + "cost_per_1m_in_cached": 0, + "cost_per_1m_out_cached": 0, + "context_window": 1000000, + "default_max_tokens": 10000, "can_reason": true, "supports_attachments": true } From 97f9ab1605f6135ca32f4e0319e344571eaaf79e Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 5 Feb 2026 14:48:03 -0300 Subject: [PATCH 3/5] fix: update import path to fix build --- internal/providers/bedrock_nova_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/providers/bedrock_nova_test.go b/internal/providers/bedrock_nova_test.go index 64e07d8b..a7c54179 100644 --- a/internal/providers/bedrock_nova_test.go +++ b/internal/providers/bedrock_nova_test.go @@ -4,7 +4,7 @@ import ( "regexp" "testing" - "github.com/charmbracelet/catwalk/pkg/catwalk" + "charm.land/catwalk/pkg/catwalk" ) // Feature: amazon-nova-bedrock-support, Property 5: Catwalk Model Configuration Validity From 18dfbbc5d95576bdf91c2b69a8c17fc36ebaf203 Mon Sep 17 00:00:00 2001 From: Andrey Nering Date: Thu, 5 Feb 2026 15:06:01 -0300 Subject: [PATCH 4/5] fix(amazon-nova): fix models data and tests --- internal/providers/configs/bedrock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/providers/configs/bedrock.json b/internal/providers/configs/bedrock.json index afac2941..e355cfc7 100644 --- a/internal/providers/configs/bedrock.json +++ b/internal/providers/configs/bedrock.json @@ -148,7 +148,7 @@ "cost_per_1m_out_cached": 0, "context_window": 300000, "default_max_tokens": 5000, - "can_reason": false, + "can_reason": true, "supports_attachments": true }, { @@ -159,7 +159,7 @@ "cost_per_1m_in_cached": 0, "cost_per_1m_out_cached": 0, "context_window": 1000000, - "default_max_tokens": 10000, + "default_max_tokens": 5000, "can_reason": true, "supports_attachments": true } From 008f3750806afee7682987fbb5700fe2a11848fd Mon Sep 17 00:00:00 2001 From: Micah Walter Date: Wed, 8 Jul 2026 15:52:02 -0400 Subject: [PATCH 5/5] feat(bedrock): catalog Nova 2 Lite and drop Gen 1 Nova models Remove legacy Gen 1 Nova entries including Premier, keep Nova 2 Lite as the sole Bedrock Nova model with extended thinking metadata. Co-authored-by: Cursor --- internal/providers/bedrock_nova_test.go | 414 ++---------------- .../configs/bedrock-united-states.json | 50 +-- 2 files changed, 45 insertions(+), 419 deletions(-) diff --git a/internal/providers/bedrock_nova_test.go b/internal/providers/bedrock_nova_test.go index adf1608a..309cc9d7 100644 --- a/internal/providers/bedrock_nova_test.go +++ b/internal/providers/bedrock_nova_test.go @@ -7,411 +7,85 @@ import ( "charm.land/catwalk/pkg/catwalk" ) -// Feature: amazon-nova-bedrock-support, Property 5: Catwalk Model Configuration Validity -// For all Nova model entries in the Bedrock catwalk configuration, each entry should have: -// (1) a valid model ID matching the pattern "amazon.nova-*-v*:*", -// (2) non-negative pricing values for input and output tokens, -// (3) a positive context window size, -// (4) a positive default max tokens value, -// (5) a boolean supports_attachments field. -// Validates: Requirements 2.2, 2.3, 2.4, 2.5, 2.6 -func TestProperty_CatwalkModelConfigurationValidity(t *testing.T) { - // Get the Bedrock provider configuration - provider := bedrockUnitedStatesProvider() - - // Define the expected Nova model IDs - expectedNovaModels := []string{ - "amazon.nova-pro-v1:0", - "amazon.nova-lite-v1:0", - "amazon.nova-micro-v1:0", - "amazon.nova-premier-v1:0", - "amazon.nova-2-lite-v1:0", - } +const nova2LiteModelID = "amazon.nova-2-lite-v1:0" - // Pattern for valid Nova model IDs +func novaModels(provider catwalk.Provider) []catwalk.Model { novaModelPattern := regexp.MustCompile(`^amazon\.nova-.+-v\d+:\d+$`) - - // Find all Nova models in the configuration - var novaModels []catwalk.Model + var models []catwalk.Model for _, model := range provider.Models { if novaModelPattern.MatchString(model.ID) { - novaModels = append(novaModels, model) - } - } - - // Property 1: All expected Nova models should be present - if len(novaModels) != len(expectedNovaModels) { - t.Errorf("Expected %d Nova models, found %d", len(expectedNovaModels), len(novaModels)) - } - - foundModels := make(map[string]bool) - for _, model := range novaModels { - foundModels[model.ID] = true - } - - for _, expectedID := range expectedNovaModels { - if !foundModels[expectedID] { - t.Errorf("Expected Nova model %q not found in configuration", expectedID) + models = append(models, model) } } - - // Property 2-5: Validate each Nova model configuration - for _, model := range novaModels { - t.Run(model.ID, func(t *testing.T) { - // Property 2.1: Model ID should match the pattern - if !novaModelPattern.MatchString(model.ID) { - t.Errorf("Model ID %q does not match expected pattern amazon.nova-*-v*:*", model.ID) - } - - // Property 2.2: Pricing values should be non-negative - if model.CostPer1MIn < 0 { - t.Errorf("Model %q has negative input cost: %f", model.ID, model.CostPer1MIn) - } - if model.CostPer1MOut < 0 { - t.Errorf("Model %q has negative output cost: %f", model.ID, model.CostPer1MOut) - } - if model.CostPer1MInCached < 0 { - t.Errorf("Model %q has negative cached input cost: %f", model.ID, model.CostPer1MInCached) - } - if model.CostPer1MOutCached < 0 { - t.Errorf("Model %q has negative cached output cost: %f", model.ID, model.CostPer1MOutCached) - } - - // Property 2.3: Context window should be positive - if model.ContextWindow <= 0 { - t.Errorf("Model %q has non-positive context window: %d", model.ID, model.ContextWindow) - } - - // Property 2.4: Default max tokens should be positive - if model.DefaultMaxTokens <= 0 { - t.Errorf("Model %q has non-positive default max tokens: %d", model.ID, model.DefaultMaxTokens) - } - - // Property 2.5: Model should have a name - if model.Name == "" { - t.Errorf("Model %q has empty name", model.ID) - } - - // Additional validation: Verify specific model characteristics - switch model.ID { - case "amazon.nova-pro-v1:0": - if model.CostPer1MIn != 0.8 { - t.Errorf("Nova Pro input cost should be 0.8, got %f", model.CostPer1MIn) - } - if model.CostPer1MOut != 3.2 { - t.Errorf("Nova Pro output cost should be 3.2, got %f", model.CostPer1MOut) - } - if model.ContextWindow != 300000 { - t.Errorf("Nova Pro context window should be 300000, got %d", model.ContextWindow) - } - if !model.SupportsImages { - t.Errorf("Nova Pro should support attachments") - } - - case "amazon.nova-lite-v1:0": - if model.CostPer1MIn != 0.06 { - t.Errorf("Nova Lite input cost should be 0.06, got %f", model.CostPer1MIn) - } - if model.CostPer1MOut != 0.24 { - t.Errorf("Nova Lite output cost should be 0.24, got %f", model.CostPer1MOut) - } - if model.ContextWindow != 300000 { - t.Errorf("Nova Lite context window should be 300000, got %d", model.ContextWindow) - } - if !model.SupportsImages { - t.Errorf("Nova Lite should support attachments") - } - - case "amazon.nova-micro-v1:0": - if model.CostPer1MIn != 0.035 { - t.Errorf("Nova Micro input cost should be 0.035, got %f", model.CostPer1MIn) - } - if model.CostPer1MOut != 0.14 { - t.Errorf("Nova Micro output cost should be 0.14, got %f", model.CostPer1MOut) - } - if model.ContextWindow != 128000 { - t.Errorf("Nova Micro context window should be 128000, got %d", model.ContextWindow) - } - if model.SupportsImages { - t.Errorf("Nova Micro should not support attachments") - } - - case "amazon.nova-premier-v1:0": - if model.CostPer1MIn != 2.5 { - t.Errorf("Nova Premier input cost should be 2.5, got %f", model.CostPer1MIn) - } - if model.CostPer1MOut != 12.5 { - t.Errorf("Nova Premier output cost should be 12.5, got %f", model.CostPer1MOut) - } - if model.ContextWindow != 300000 { - t.Errorf("Nova Premier context window should be 300000, got %d", model.ContextWindow) - } - if model.CanReason { - t.Errorf("Nova Premier should not advertise reasoning capability") - } - if !model.SupportsImages { - t.Errorf("Nova Premier should support attachments") - } - - case "amazon.nova-2-lite-v1:0": - if model.ContextWindow != 1000000 { - t.Errorf("Nova 2 Lite context window should be 1000000, got %d", model.ContextWindow) - } - if !model.CanReason { - t.Errorf("Nova 2 Lite should support extended thinking") - } - if !model.SupportsImages { - t.Errorf("Nova 2 Lite should support attachments") - } - } - }) - } + return models } -// TestNovaModelsPresent verifies that all Nova models are present in the Bedrock configuration. -// Validates: Requirements 2.1, 2.6 -func TestNovaModelsPresent(t *testing.T) { +func TestNova2LiteModelPresent(t *testing.T) { provider := bedrockUnitedStatesProvider() + models := novaModels(provider) - expectedModels := map[string]string{ - "amazon.nova-pro-v1:0": "Amazon Nova Pro", - "amazon.nova-lite-v1:0": "Amazon Nova Lite", - "amazon.nova-micro-v1:0": "Amazon Nova Micro", - "amazon.nova-premier-v1:0": "Amazon Nova Premier", - "amazon.nova-2-lite-v1:0": "Amazon Nova 2 Lite", + if len(models) != 1 { + t.Fatalf("expected exactly 1 Nova model, found %d", len(models)) } - // Build a map of actual models - actualModels := make(map[string]catwalk.Model) - for _, model := range provider.Models { - actualModels[model.ID] = model + model := models[0] + if model.ID != nova2LiteModelID { + t.Fatalf("expected %q, got %q", nova2LiteModelID, model.ID) } - - // Verify each expected model is present - for expectedID, expectedName := range expectedModels { - model, found := actualModels[expectedID] - if !found { - t.Errorf("Expected Nova model %q not found in Bedrock provider", expectedID) - continue - } - - if model.Name != expectedName { - t.Errorf("Model %q has incorrect name: expected %q, got %q", expectedID, expectedName, model.Name) - } + if model.Name != "Amazon Nova 2 Lite" { + t.Fatalf("unexpected model name: %q", model.Name) } } -// TestNovaModelIDFormat verifies that all Nova model IDs match the expected format. -// Validates: Requirements 2.1, 2.6 -func TestNovaModelIDFormat(t *testing.T) { +func TestNova2LiteModelConfiguration(t *testing.T) { provider := bedrockUnitedStatesProvider() - // Pattern for valid Nova model IDs: amazon.nova-{variant}-v{version}:{revision} - novaModelPattern := regexp.MustCompile(`^amazon\.nova-.+-v\d+:\d+$`) - - expectedNovaModels := []string{ - "amazon.nova-pro-v1:0", - "amazon.nova-lite-v1:0", - "amazon.nova-micro-v1:0", - "amazon.nova-premier-v1:0", - "amazon.nova-2-lite-v1:0", - } - - for _, expectedID := range expectedNovaModels { - // Find the model - var found bool - for _, model := range provider.Models { - if model.ID == expectedID { - found = true - - // Verify the ID matches the pattern - if !novaModelPattern.MatchString(model.ID) { - t.Errorf("Nova model ID %q does not match expected pattern amazon.nova-*-v*:*", model.ID) - } - - // Verify the ID starts with "amazon." - if model.ID[:7] != "amazon." { - t.Errorf("Nova model ID %q should start with 'amazon.'", model.ID) - } - - break - } - } - - if !found { - t.Errorf("Expected Nova model %q not found in configuration", expectedID) + var model catwalk.Model + for _, m := range provider.Models { + if m.ID == nova2LiteModelID { + model = m + break } } -} - -// TestNovaPricingNonNegative verifies that all Nova models have non-negative pricing values. -// Validates: Requirements 2.1, 2.6 -func TestNovaPricingNonNegative(t *testing.T) { - provider := bedrockUnitedStatesProvider() - - novaModelPattern := regexp.MustCompile(`^amazon\.nova-`) - - for _, model := range provider.Models { - if !novaModelPattern.MatchString(model.ID) { - continue - } - - t.Run(model.ID, func(t *testing.T) { - if model.CostPer1MIn < 0 { - t.Errorf("Model %q has negative input cost: %f", model.ID, model.CostPer1MIn) - } - - if model.CostPer1MOut < 0 { - t.Errorf("Model %q has negative output cost: %f", model.ID, model.CostPer1MOut) - } - - if model.CostPer1MInCached < 0 { - t.Errorf("Model %q has negative cached input cost: %f", model.ID, model.CostPer1MInCached) - } - - if model.CostPer1MOutCached < 0 { - t.Errorf("Model %q has negative cached output cost: %f", model.ID, model.CostPer1MOutCached) - } - - // Verify pricing is reasonable (not zero for non-cached costs) - if model.CostPer1MIn == 0 { - t.Errorf("Model %q has zero input cost, which is likely incorrect", model.ID) - } - - if model.CostPer1MOut == 0 { - t.Errorf("Model %q has zero output cost, which is likely incorrect", model.ID) - } - }) + if model.ID == "" { + t.Fatal("Nova 2 Lite model not found") } -} - -// TestNovaModelContextWindows verifies that Nova models have appropriate context window sizes. -// Validates: Requirements 2.1, 2.6 -func TestNovaModelContextWindows(t *testing.T) { - provider := bedrockUnitedStatesProvider() - expectedContextWindows := map[string]int64{ - "amazon.nova-pro-v1:0": 300000, - "amazon.nova-lite-v1:0": 300000, - "amazon.nova-micro-v1:0": 128000, - "amazon.nova-premier-v1:0": 300000, - "amazon.nova-2-lite-v1:0": 1000000, + if model.CostPer1MIn != 0.06 { + t.Errorf("input cost should be 0.06, got %f", model.CostPer1MIn) } - - for modelID, expectedWindow := range expectedContextWindows { - var found bool - for _, model := range provider.Models { - if model.ID == modelID { - found = true - - if model.ContextWindow != expectedWindow { - t.Errorf("Model %q has incorrect context window: expected %d, got %d", - modelID, expectedWindow, model.ContextWindow) - } - - if model.ContextWindow <= 0 { - t.Errorf("Model %q has non-positive context window: %d", modelID, model.ContextWindow) - } - - break - } - } - - if !found { - t.Errorf("Expected Nova model %q not found in configuration", modelID) - } + if model.CostPer1MOut != 0.24 { + t.Errorf("output cost should be 0.24, got %f", model.CostPer1MOut) } -} - -// TestNovaModelDefaultMaxTokens verifies that Nova models have appropriate default max tokens. -// Validates: Requirements 2.1, 2.6 -func TestNovaModelDefaultMaxTokens(t *testing.T) { - provider := bedrockUnitedStatesProvider() - - novaModelPattern := regexp.MustCompile(`^amazon\.nova-`) - - for _, model := range provider.Models { - if !novaModelPattern.MatchString(model.ID) { - continue - } - - t.Run(model.ID, func(t *testing.T) { - if model.DefaultMaxTokens <= 0 { - t.Errorf("Model %q has non-positive default max tokens: %d", model.ID, model.DefaultMaxTokens) - } - - // Verify default max tokens is reasonable (should be 5000 for Nova models) - if model.DefaultMaxTokens != 5000 { - t.Errorf("Model %q has unexpected default max tokens: expected 5000, got %d", - model.ID, model.DefaultMaxTokens) - } - }) + if model.ContextWindow != 1_000_000 { + t.Errorf("context window should be 1000000, got %d", model.ContextWindow) } -} - -// TestNovaModelAttachmentSupport verifies that Nova models have correct attachment support flags. -// Validates: Requirements 2.1, 2.6 -func TestNovaModelAttachmentSupport(t *testing.T) { - provider := bedrockUnitedStatesProvider() - - expectedAttachmentSupport := map[string]bool{ - "amazon.nova-pro-v1:0": true, - "amazon.nova-lite-v1:0": true, - "amazon.nova-micro-v1:0": false, - "amazon.nova-premier-v1:0": true, - "amazon.nova-2-lite-v1:0": true, + if model.DefaultMaxTokens != 64_000 { + t.Errorf("default max tokens should be 64000, got %d", model.DefaultMaxTokens) } - - for modelID, expectedSupport := range expectedAttachmentSupport { - var found bool - for _, model := range provider.Models { - if model.ID == modelID { - found = true - - if model.SupportsImages != expectedSupport { - t.Errorf("Model %q has incorrect attachment support: expected %v, got %v", - modelID, expectedSupport, model.SupportsImages) - } - - break - } - } - - if !found { - t.Errorf("Expected Nova model %q not found in configuration", modelID) - } + if !model.CanReason { + t.Error("Nova 2 Lite should support extended thinking") + } + if !model.SupportsImages { + t.Error("Nova 2 Lite should support attachments") } } -// TestNovaModelReasoningCapability verifies extended thinking metadata. -func TestNovaModelReasoningCapability(t *testing.T) { +func TestNovaGen1ModelsNotCataloged(t *testing.T) { provider := bedrockUnitedStatesProvider() - expectedReasoning := map[string]bool{ - "amazon.nova-pro-v1:0": false, - "amazon.nova-lite-v1:0": false, - "amazon.nova-micro-v1:0": false, - "amazon.nova-premier-v1:0": false, - "amazon.nova-2-lite-v1:0": true, + legacyIDs := []string{ + "amazon.nova-pro-v1:0", + "amazon.nova-lite-v1:0", + "amazon.nova-micro-v1:0", + "amazon.nova-premier-v1:0", } - for modelID, expectedCanReason := range expectedReasoning { - var found bool + for _, id := range legacyIDs { for _, model := range provider.Models { - if model.ID == modelID { - found = true - - if model.CanReason != expectedCanReason { - t.Errorf("Model %q has incorrect reasoning capability: expected %v, got %v", - modelID, expectedCanReason, model.CanReason) - } - - break + if model.ID == id { + t.Errorf("Gen 1 Nova model %q should not be in the Bedrock catalog", id) } } - - if !found { - t.Errorf("Expected Nova model %q not found in configuration", modelID) - } } } diff --git a/internal/providers/configs/bedrock-united-states.json b/internal/providers/configs/bedrock-united-states.json index bafa3d27..d644d80d 100644 --- a/internal/providers/configs/bedrock-united-states.json +++ b/internal/providers/configs/bedrock-united-states.json @@ -139,54 +139,6 @@ "default_reasoning_effort": "high", "supports_attachments": true }, - { - "id": "amazon.nova-pro-v1:0", - "name": "Amazon Nova Pro", - "cost_per_1m_in": 0.8, - "cost_per_1m_out": 3.2, - "cost_per_1m_in_cached": 0, - "cost_per_1m_out_cached": 0, - "context_window": 300000, - "default_max_tokens": 5000, - "can_reason": false, - "supports_attachments": true - }, - { - "id": "amazon.nova-lite-v1:0", - "name": "Amazon Nova Lite", - "cost_per_1m_in": 0.06, - "cost_per_1m_out": 0.24, - "cost_per_1m_in_cached": 0, - "cost_per_1m_out_cached": 0, - "context_window": 300000, - "default_max_tokens": 5000, - "can_reason": false, - "supports_attachments": true - }, - { - "id": "amazon.nova-micro-v1:0", - "name": "Amazon Nova Micro", - "cost_per_1m_in": 0.035, - "cost_per_1m_out": 0.14, - "cost_per_1m_in_cached": 0, - "cost_per_1m_out_cached": 0, - "context_window": 128000, - "default_max_tokens": 5000, - "can_reason": false, - "supports_attachments": false - }, - { - "id": "amazon.nova-premier-v1:0", - "name": "Amazon Nova Premier", - "cost_per_1m_in": 2.5, - "cost_per_1m_out": 12.5, - "cost_per_1m_in_cached": 0, - "cost_per_1m_out_cached": 0, - "context_window": 300000, - "default_max_tokens": 5000, - "can_reason": false, - "supports_attachments": true - }, { "id": "amazon.nova-2-lite-v1:0", "name": "Amazon Nova 2 Lite", @@ -195,7 +147,7 @@ "cost_per_1m_in_cached": 0, "cost_per_1m_out_cached": 0, "context_window": 1000000, - "default_max_tokens": 5000, + "default_max_tokens": 64000, "can_reason": true, "supports_attachments": true }