From 29030d648edcae78f59f52e368dca6171bead585 Mon Sep 17 00:00:00 2001 From: Guillem Arias Date: Mon, 25 May 2026 20:30:41 +0200 Subject: [PATCH] fix(ai): attribute Bedrock model IDs to anthropic + clean nil enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LlmUsage.infer_provider now returns "anthropic" for Bedrock / Vertex shaped IDs (anthropic.* and anthropic/*), so cost-ledger filtering by provider stays correct even when no per-MTok rate is stored. Previously these IDs fell through to the "openai" default. - AutoCategorizer drops the redundant nil sentinel from the category_name enum — the union type [string, null] already permits null, and some JSON Schema validators reject nil literals inside enum arrays. --- app/models/llm_usage.rb | 7 +++++++ .../provider/anthropic/auto_categorizer.rb | 2 +- test/models/llm_usage_test.rb | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/models/llm_usage.rb b/app/models/llm_usage.rb index d4e2eeda0..0f9e4e7c7 100644 --- a/app/models/llm_usage.rb +++ b/app/models/llm_usage.rb @@ -92,6 +92,13 @@ class LlmUsage < ApplicationRecord def self.infer_provider(model) return "openai" if model.blank? + # Bedrock + Vertex prefix model IDs with "anthropic." regardless of + # whether the Claude family is in the local PRICING map. Attribute them + # to the Anthropic provider so cost-ledger filtering by provider is + # correct even when we can't compute a per-token rate (custom endpoints + # bill via their own provider, not Anthropic directly). + return "anthropic" if model.start_with?("anthropic.", "anthropic/") + # Check each provider to see if they have pricing for this model PRICING.each do |provider_name, provider_pricing| # Try exact match first diff --git a/app/models/provider/anthropic/auto_categorizer.rb b/app/models/provider/anthropic/auto_categorizer.rb index 816a954e9..e5d014b9a 100644 --- a/app/models/provider/anthropic/auto_categorizer.rb +++ b/app/models/provider/anthropic/auto_categorizer.rb @@ -77,7 +77,7 @@ class Provider::Anthropic::AutoCategorizer category_name: { type: [ "string", "null" ], description: "Matched category name from the user's categories, or null when uncertain.", - enum: [ *user_categories.map { |c| c[:name] }, nil ] + enum: user_categories.map { |c| c[:name] } } }, required: [ "transaction_id", "category_name" ], diff --git a/test/models/llm_usage_test.rb b/test/models/llm_usage_test.rb index 423544ea3..7c0380358 100644 --- a/test/models/llm_usage_test.rb +++ b/test/models/llm_usage_test.rb @@ -12,6 +12,22 @@ class LlmUsageTest < ActiveSupport::TestCase assert_equal "openai", LlmUsage.infer_provider("gpt-5") end + test "infer_provider attributes Bedrock and Vertex prefixed IDs to anthropic" do + assert_equal "anthropic", LlmUsage.infer_provider("anthropic.claude-sonnet-4-5-20250929-v1:0") + assert_equal "anthropic", LlmUsage.infer_provider("anthropic.claude-opus-4-20250514-v1:0") + assert_equal "anthropic", LlmUsage.infer_provider("anthropic/claude-3-5-sonnet@20240620") + end + + test "calculate_cost returns nil for Bedrock IDs (no per-token rate stored)" do + # Bedrock bills through AWS not Anthropic — we don't store a per-MTok rate, + # but the row must still attribute to anthropic for provider filtering. + assert_nil LlmUsage.calculate_cost( + model: "anthropic.claude-sonnet-4-5-20250929-v1:0", + prompt_tokens: 1000, + completion_tokens: 500 + ) + end + test "calculate_cost returns Anthropic pricing for Claude models" do cost = LlmUsage.calculate_cost(model: "claude-sonnet-4-6", prompt_tokens: 1_000_000, completion_tokens: 100_000)