diff --git a/lib/ruby_llm/models.json b/lib/ruby_llm/models.json index ad3ad7427..9f37c75ef 100644 --- a/lib/ruby_llm/models.json +++ b/lib/ruby_llm/models.json @@ -12182,7 +12182,7 @@ "standard": { "input_per_million": 2, "output_per_million": 10, - "cache_read_input_per_million": 0.2, + "cache_read_input_per_million": 0.1, "cache_write_input_per_million": 2.5 } } @@ -12209,7 +12209,7 @@ "cost": { "input": 2, "output": 10, - "cache_read": 0.2, + "cache_read": 0.1, "cache_write": 2.5 }, "limit": { @@ -14033,7 +14033,7 @@ "standard": { "input_per_million": 2.2, "output_per_million": 11, - "cache_read_input_per_million": 0.22, + "cache_read_input_per_million": 0.11, "cache_write_input_per_million": 2.75 } } @@ -14060,7 +14060,7 @@ "cost": { "input": 2.2, "output": 11, - "cache_read": 0.22, + "cache_read": 0.11, "cache_write": 2.75 }, "limit": { @@ -14893,7 +14893,7 @@ "standard": { "input_per_million": 2, "output_per_million": 10, - "cache_read_input_per_million": 0.2, + "cache_read_input_per_million": 0.1, "cache_write_input_per_million": 2.5 } } @@ -14920,7 +14920,7 @@ "cost": { "input": 2, "output": 10, - "cache_read": 0.2, + "cache_read": 0.1, "cache_write": 2.5 }, "limit": { @@ -19413,10 +19413,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 10, - "output_per_million": 50, - "cache_read_input_per_million": 1, - "cache_write_input_per_million": 12.5 + "input_per_million": 11, + "output_per_million": 55, + "cache_read_input_per_million": 1.1, + "cache_write_input_per_million": 13.75 } } }, @@ -19465,10 +19465,10 @@ } ], "cost": { - "input": 10, - "output": 50, - "cache_read": 1, - "cache_write": 12.5 + "input": 11, + "output": 55, + "cache_read": 1.1, + "cache_write": 13.75 }, "limit": { "context": 1000000, @@ -19504,10 +19504,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 10, - "output_per_million": 50, - "cache_read_input_per_million": 0.25, - "cache_write_input_per_million": 12.5 + "input_per_million": 11, + "output_per_million": 55, + "cache_read_input_per_million": 0.275, + "cache_write_input_per_million": 13.75 } } }, @@ -19556,10 +19556,10 @@ } ], "cost": { - "input": 10, - "output": 50, - "cache_read": 0.25, - "cache_write": 12.5 + "input": 11, + "output": 55, + "cache_read": 0.275, + "cache_write": 13.75 }, "limit": { "context": 1000000, @@ -19770,10 +19770,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 5, - "output_per_million": 25, - "cache_read_input_per_million": 0.5, - "cache_write_input_per_million": 6.25 + "input_per_million": 5.5, + "output_per_million": 27.5, + "cache_read_input_per_million": 0.55, + "cache_write_input_per_million": 6.875 } } }, @@ -19824,10 +19824,10 @@ } ], "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 5.5, + "output": 27.5, + "cache_read": 0.55, + "cache_write": 6.875 }, "limit": { "context": 200000, @@ -19865,10 +19865,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 5, - "output_per_million": 25, - "cache_read_input_per_million": 0.5, - "cache_write_input_per_million": 6.25 + "input_per_million": 5.5, + "output_per_million": 27.5, + "cache_read_input_per_million": 0.55, + "cache_write_input_per_million": 6.875 } } }, @@ -19920,10 +19920,10 @@ } ], "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 5.5, + "output": 27.5, + "cache_read": 0.55, + "cache_write": 6.875 }, "limit": { "context": 1000000, @@ -19960,10 +19960,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 5, - "output_per_million": 25, - "cache_read_input_per_million": 0.5, - "cache_write_input_per_million": 6.25 + "input_per_million": 5.5, + "output_per_million": 27.5, + "cache_read_input_per_million": 0.55, + "cache_write_input_per_million": 6.875 } } }, @@ -20012,10 +20012,10 @@ } ], "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 5.5, + "output": 27.5, + "cache_read": 0.55, + "cache_write": 6.875 }, "limit": { "context": 1000000, @@ -20052,10 +20052,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 5, - "output_per_million": 25, - "cache_read_input_per_million": 0.5, - "cache_write_input_per_million": 6.25 + "input_per_million": 5.5, + "output_per_million": 27.5, + "cache_read_input_per_million": 0.55, + "cache_write_input_per_million": 6.875 } } }, @@ -20104,10 +20104,10 @@ } ], "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 5.5, + "output": 27.5, + "cache_read": 0.55, + "cache_write": 6.875 }, "limit": { "context": 1000000, @@ -20142,10 +20142,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 5, - "output_per_million": 25, - "cache_read_input_per_million": 0.5, - "cache_write_input_per_million": 6.25 + "input_per_million": 5.5, + "output_per_million": 27.5, + "cache_read_input_per_million": 0.55, + "cache_write_input_per_million": 6.875 } } }, @@ -20169,10 +20169,10 @@ } ], "cost": { - "input": 5, - "output": 25, - "cache_read": 0.5, - "cache_write": 6.25 + "input": 5.5, + "output": 27.5, + "cache_read": 0.55, + "cache_write": 6.875 }, "limit": { "context": 1000000, @@ -20208,10 +20208,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 4, - "output_per_million": 20, - "cache_read_input_per_million": 0.2, - "cache_write_input_per_million": 5 + "input_per_million": 4.4, + "output_per_million": 22, + "cache_read_input_per_million": 0.22, + "cache_write_input_per_million": 5.5 } } }, @@ -20235,10 +20235,10 @@ } ], "cost": { - "input": 4, - "output": 20, - "cache_read": 0.2, - "cache_write": 5 + "input": 4.4, + "output": 22, + "cache_read": 0.22, + "cache_write": 5.5 }, "limit": { "context": 1000000, @@ -20328,10 +20328,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 3, - "output_per_million": 15, - "cache_read_input_per_million": 0.3, - "cache_write_input_per_million": 3.75 + "input_per_million": 3.3, + "output_per_million": 16.5, + "cache_read_input_per_million": 0.33, + "cache_write_input_per_million": 4.125 } } }, @@ -20374,10 +20374,10 @@ } ], "cost": { - "input": 3, - "output": 15, - "cache_read": 0.3, - "cache_write": 3.75 + "input": 3.3, + "output": 16.5, + "cache_read": 0.33, + "cache_write": 4.125 }, "limit": { "context": 200000, @@ -20415,10 +20415,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 3, - "output_per_million": 15, - "cache_read_input_per_million": 0.3, - "cache_write_input_per_million": 3.75 + "input_per_million": 3.3, + "output_per_million": 16.5, + "cache_read_input_per_million": 0.33, + "cache_write_input_per_million": 4.125 } } }, @@ -20470,10 +20470,10 @@ } ], "cost": { - "input": 3, - "output": 15, - "cache_read": 0.3, - "cache_write": 3.75 + "input": 3.3, + "output": 16.5, + "cache_read": 0.33, + "cache_write": 4.125 }, "limit": { "context": 1000000, @@ -20510,10 +20510,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 2, - "output_per_million": 10, - "cache_read_input_per_million": 0.2, - "cache_write_input_per_million": 2.5 + "input_per_million": 2.2, + "output_per_million": 11, + "cache_read_input_per_million": 0.22, + "cache_write_input_per_million": 2.75 } } }, @@ -20540,10 +20540,10 @@ } ], "cost": { - "input": 2, - "output": 10, - "cache_read": 0.2, - "cache_write": 2.5 + "input": 2.2, + "output": 11, + "cache_read": 0.22, + "cache_write": 2.75 }, "limit": { "context": 1000000, @@ -20579,10 +20579,10 @@ "pricing": { "text_tokens": { "standard": { - "input_per_million": 2, - "output_per_million": 10, - "cache_read_input_per_million": 0.2, - "cache_write_input_per_million": 2.5 + "input_per_million": 2.2, + "output_per_million": 11, + "cache_read_input_per_million": 0.11, + "cache_write_input_per_million": 2.75 } } }, @@ -20606,10 +20606,10 @@ } ], "cost": { - "input": 2, - "output": 10, - "cache_read": 0.2, - "cache_write": 2.5 + "input": 2.2, + "output": 11, + "cache_read": 0.11, + "cache_write": 2.75 }, "limit": { "context": 1000000, diff --git a/ruby_llm.gemspec b/ruby_llm.gemspec index 18ca04f77..45215d4c4 100644 --- a/ruby_llm.gemspec +++ b/ruby_llm.gemspec @@ -58,7 +58,7 @@ Gem::Specification.new do |spec| # Runtime dependencies spec.add_dependency 'base64' spec.add_dependency 'event_stream_parser', '~> 1' - spec.add_dependency 'faraday', ENV['FARADAY_VERSION'] || '>= 1.10.0' + spec.add_dependency 'faraday', ENV['FARADAY_VERSION'] || ['>= 1.10.0', '!= 2.0.0'] spec.add_dependency 'faraday-multipart', '>= 1' spec.add_dependency 'faraday-net_http', '>= 1' spec.add_dependency 'faraday-retry', '>= 1' diff --git a/spec/ruby_llm/models_bedrock_regional_pricing_spec.rb b/spec/ruby_llm/models_bedrock_regional_pricing_spec.rb new file mode 100644 index 000000000..933ff8a3e --- /dev/null +++ b/spec/ruby_llm/models_bedrock_regional_pricing_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe RubyLLM::Models do + include_context 'with configured RubyLLM' + + # A us. inference profile is a regional Bedrock endpoint. Anthropic's pricing + # page (platform.claude.com/docs/en/about-claude/pricing, read 2026-10-14), + # under "Regional and multi-region endpoint pricing for Claude 4.5 models and + # beyond", says: "Bedrock offers two endpoint types: global endpoints + # (dynamic routing for maximum availability) and regional endpoints + # (guaranteed data routing through specific geographic regions)." and + # "Regional and multi-region endpoints include a 10% premium over global + # endpoints." + # + # Cache reads use the same page's footnotes. Footnote 1: "Cache hits and + # refreshes on Claude Fable 5.1 and Claude Mythos 5.1 are priced at 0.025x + # the base input price." Footnote 2: "Cache hits and refreshes on Claude + # Opus 5.5 and Claude Sonnet 5.5 are priced at 0.05x the base input price." + # "All other models use the standard 0.1x multiplier." + # + # The AWS bill for 2026-10-05 to 2026-10-12 (Cost Explorer, cost divided by + # usage, USW2-MP:USW2_* usage types) matches this rule: Sonnet 5 on us. + # billed 2.20 / 11 / 0.22 / 2.75 and Sonnet 5.5 on us. billed + # 2.20 / 11 / 0.11 / 2.75 (input/output/cache_read/cache_write per million + # tokens). + # + # Unchanged on purpose: + # - Haiku 4.5 billed 1.034 / 5.17 / 0.1034 / 1.2925, which is 94% of its + # regional rate, for a reason not yet known. Its global price + # (1 / 5 / 0.10 / 1.25) is closer to the bill, so it stays at that price. + # - Opus 4.1 is older than Claude 4.5, so it has no regional premium. + # - Sonnet 4 has no pricing in the registry. + sonnet45 = { input: 3.3, output: 16.5, cache_read: 0.33, cache_write: 4.125 } + opus45 = { input: 5.5, output: 27.5, cache_read: 0.55, cache_write: 6.875 } + regional = { + 'us.anthropic.claude-sonnet-4-5-20250929-v1:0' => sonnet45, + 'us.anthropic.claude-sonnet-4-6' => sonnet45, + 'us.anthropic.claude-opus-4-5-20251101-v1:0' => opus45, + 'us.anthropic.claude-opus-4-6-v1' => opus45, + 'us.anthropic.claude-opus-4-7' => opus45, + 'us.anthropic.claude-opus-4-8' => opus45, + 'us.anthropic.claude-opus-5' => opus45, + 'us.anthropic.claude-sonnet-5' => { input: 2.2, output: 11, cache_read: 0.22, cache_write: 2.75 }, + 'us.anthropic.claude-sonnet-5-5' => { input: 2.2, output: 11, cache_read: 0.11, cache_write: 2.75 }, + 'us.anthropic.claude-opus-5-5' => { input: 4.4, output: 22, cache_read: 0.22, cache_write: 5.5 }, + 'us.anthropic.claude-fable-5' => { input: 11, output: 55, cache_read: 1.1, cache_write: 13.75 }, + 'us.anthropic.claude-fable-5-1' => { input: 11, output: 55, cache_read: 0.275, cache_write: 13.75 } + } + + unchanged = { + 'us.anthropic.claude-haiku-4-5-20251001-v1:0' => { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 }, + 'us.anthropic.claude-opus-4-1-20250805-v1:0' => { input: 15, output: 75, cache_read: 1.5, cache_write: 18.75 } + } + + # Match on provider and raw id: RubyLLM.models.find(id, :bedrock) + # renormalizes the region prefix to the configured bedrock_region. + def bedrock_entry(id) + RubyLLM.models.all.find { |m| m.provider == 'bedrock' && m.id == id } + end + + def standard_pricing(model) + model.pricing.text_tokens.standard.to_h.slice( + :input_per_million, :output_per_million, + :cache_read_input_per_million, :cache_write_input_per_million + ) + end + + def expected_standard(cost) + { + input_per_million: cost[:input], + output_per_million: cost[:output], + cache_read_input_per_million: cost[:cache_read], + cache_write_input_per_million: cost[:cache_write] + } + end + + regional.merge(unchanged).each do |id, cost| + it "prices #{id} at #{cost.values.join(' / ')} in both price fields" do + model = bedrock_entry(id) + + expect(model).not_to be_nil + expect(model.metadata[:cost]).to eq(cost) + expect(standard_pricing(model)).to eq(expected_standard(cost)) + end + end + + it 'keeps us.anthropic.claude-sonnet-4-20250514-v1:0 without pricing' do + model = bedrock_entry('us.anthropic.claude-sonnet-4-20250514-v1:0') + + expect(model).not_to be_nil + expect(model.metadata[:cost]).to be_nil + expect(model.pricing.to_h).to eq({}) + end +end diff --git a/spec/ruby_llm/models_fable_5_1_spec.rb b/spec/ruby_llm/models_fable_5_1_spec.rb index 87a5ee8df..6c9d8e216 100644 --- a/spec/ruby_llm/models_fable_5_1_spec.rb +++ b/spec/ruby_llm/models_fable_5_1_spec.rb @@ -5,21 +5,18 @@ RSpec.describe RubyLLM::Models do include_context 'with configured RubyLLM' - # cache_read_input_per_million (0.25 on the base/global/US entries, 0.275 on - # EU) is NOT independently verified against Anthropic's live pricing page. - # It comes from Anthropic's Claude Fable 5.1 launch announcement - # (anthropic.com/claude-fable-and-mythos-5-1) and same-day AWS/Microsoft - # partner blog posts reporting a ~75% cache-read price cut versus Claude - # Fable 5's cache_read_input_per_million of 1 (i.e. roughly 1/4 of Fable 5's - # rate, scaled per-region the same way Fable 5's own regional entries are). - # Base input/output pricing ($10/$50) and cache_write_input_per_million - # (12.5, i.e. 1.25x input, unchanged from Fable 5) are confirmed unchanged - # by the same sources. Revisit the cache_read figures once models.dev or - # Anthropic's pricing page publish real data for claude-fable-5-1. + # Prices are per million tokens (input/output/cache_read/cache_write). + # Anthropic's pricing page (platform.claude.com/docs/en/about-claude/pricing, + # read 2026-10-14) lists Claude Fable 5.1 at 10 / 50 / 0.25 / 12.5 on global + # endpoints. The 0.25 cache read is verified by its footnote 1: "Cache hits + # and refreshes on Claude Fable 5.1 and Claude Mythos 5.1 are priced at + # 0.025x the base input price." The eu. and us. entries are regional + # endpoints and are recorded at 1.1x: the same page says "Regional and + # multi-region endpoints include a 10% premium over global endpoints." { 'claude-fable-5-1' => { input: 10, output: 50, cache_read: 0.25, cache_write: 12.5 }, 'global.anthropic.claude-fable-5-1' => { input: 10, output: 50, cache_read: 0.25, cache_write: 12.5 }, - 'us.anthropic.claude-fable-5-1' => { input: 10, output: 50, cache_read: 0.25, cache_write: 12.5 }, + 'us.anthropic.claude-fable-5-1' => { input: 11, output: 55, cache_read: 0.275, cache_write: 13.75 }, 'eu.anthropic.claude-fable-5-1' => { input: 11, output: 55, cache_read: 0.275, cache_write: 13.75 } }.each do |id, cost| it "registers #{id} with the documented effort values and pricing" do diff --git a/spec/ruby_llm/models_opus_5_5_spec.rb b/spec/ruby_llm/models_opus_5_5_spec.rb index 0eed9c190..6b7d58a38 100644 --- a/spec/ruby_llm/models_opus_5_5_spec.rb +++ b/spec/ruby_llm/models_opus_5_5_spec.rb @@ -9,16 +9,15 @@ # id is recorded at the same rate structure as that id's Opus 5 entry. Base # rates (input/output/cache_read/cache_write per million tokens) come from # AWS's price list ("Standard, Global" tier for Claude Opus 5.5 on Amazon - # Bedrock), and only the eu. entry is recorded at 1.1x base, matching the - # 1.1x the eu.anthropic.claude-opus-5 entry already uses. This does not - # correct the separate, pre-existing question of whether us. (and other - # non-eu regional prefixes) should also be at 1.1x on Bedrock's own price - # list; that is out of scope here and applies equally to the existing Opus - # 5 entries. + # Bedrock). The eu. and us. entries are regional endpoints and are recorded + # at 1.1x base: Anthropic's pricing page + # (platform.claude.com/docs/en/about-claude/pricing, read 2026-10-14) says + # "Regional and multi-region endpoints include a 10% premium over global + # endpoints." { 'claude-opus-5-5' => { input: 4, output: 20, cache_read: 0.20, cache_write: 5 }, 'global.anthropic.claude-opus-5-5' => { input: 4, output: 20, cache_read: 0.20, cache_write: 5 }, - 'us.anthropic.claude-opus-5-5' => { input: 4, output: 20, cache_read: 0.20, cache_write: 5 }, + 'us.anthropic.claude-opus-5-5' => { input: 4.4, output: 22, cache_read: 0.22, cache_write: 5.5 }, 'eu.anthropic.claude-opus-5-5' => { input: 4.4, output: 22, cache_read: 0.22, cache_write: 5.5 } }.each do |id, cost| it "registers #{id} with the documented effort values and pricing" do diff --git a/spec/ruby_llm/models_sonnet_5_5_spec.rb b/spec/ruby_llm/models_sonnet_5_5_spec.rb index ae773dd55..76a7d6ff5 100644 --- a/spec/ruby_llm/models_sonnet_5_5_spec.rb +++ b/spec/ruby_llm/models_sonnet_5_5_spec.rb @@ -5,11 +5,19 @@ RSpec.describe RubyLLM::Models do include_context 'with configured RubyLLM' - # Pricing is unchanged from each id's Claude Sonnet 5 entry: every id except - # eu. is 2 / 10 / 0.20 / 2.5 (input/output/cache_read/cache_write per million - # tokens), and eu. is 2.2 / 11 / 0.22 / 2.75. Source: the AWS Marketplace rate - # card for the Claude Sonnet 5.5 offer accepted on the appfolio-ci-dev-tooling - # account, global standard tier, checked 2026-10-05. + # Prices (input/output/cache_read/cache_write per million tokens) come from + # Anthropic's pricing page (platform.claude.com/docs/en/about-claude/pricing, + # read 2026-10-14): global list price is 2 / 10 / 0.10 / 2.5, "Regional and + # multi-region endpoints include a 10% premium over global endpoints", and + # "Cache hits and refreshes on Claude Opus 5.5 and Claude Sonnet 5.5 are + # priced at 0.05x the base input price." The AWS bill for 2026-10-05 to + # 2026-10-12 (Cost Explorer, USW2-MP:USW2_* usage types) shows us. at + # 2.20 / 11 / 0.11 / 2.75, which matches that rule. + # + # The global. and bare bedrock entries are at the global price with the + # 0.05x cache read, and eu. is at the regional price. The au., jp., + # anthropic and vertexai entries are not called by Supernova and keep their + # earlier values (cache read still at 0.1x input). # # Unlike Sonnet 5, these entries have no toggle reasoning option: Sonnet 5.5 # rejects thinking: {type: "disabled"} (verified live on the @@ -17,15 +25,16 @@ # container) and only accepts adaptive thinking with output_config.effort, # the same as Opus 5.5. standard = { input: 2, output: 10, cache_read: 0.20, cache_write: 2.5 } - eu = { input: 2.2, output: 11, cache_read: 0.22, cache_write: 2.75 } + global = { input: 2, output: 10, cache_read: 0.1, cache_write: 2.5 } + regional = { input: 2.2, output: 11, cache_read: 0.11, cache_write: 2.75 } { %w[anthropic claude-sonnet-5-5] => standard, %w[vertexai claude-sonnet-5-5] => standard, - %w[bedrock anthropic.claude-sonnet-5-5] => standard, - %w[bedrock global.anthropic.claude-sonnet-5-5] => standard, - %w[bedrock us.anthropic.claude-sonnet-5-5] => standard, - %w[bedrock eu.anthropic.claude-sonnet-5-5] => eu, + %w[bedrock anthropic.claude-sonnet-5-5] => global, + %w[bedrock global.anthropic.claude-sonnet-5-5] => global, + %w[bedrock us.anthropic.claude-sonnet-5-5] => regional, + %w[bedrock eu.anthropic.claude-sonnet-5-5] => regional, %w[bedrock au.anthropic.claude-sonnet-5-5] => standard, %w[bedrock jp.anthropic.claude-sonnet-5-5] => standard }.each do |(provider, id), cost| diff --git a/spec/ruby_llm_gemspec_spec.rb b/spec/ruby_llm_gemspec_spec.rb index 48573b32b..2988a97f5 100644 --- a/spec/ruby_llm_gemspec_spec.rb +++ b/spec/ruby_llm_gemspec_spec.rb @@ -11,7 +11,14 @@ def runtime_dependency(name) end it 'keeps faraday compatible with Ruby < 4.0' do - expect(runtime_dependency('faraday').requirement.to_s).to eq('>= 1.10.0') + expect(runtime_dependency('faraday').requirement).to be_satisfied_by(Gem::Version.new('1.10.3')) + end + + it 'excludes Faraday 2.0.0, which ships without its default adapter' do + requirement = runtime_dependency('faraday').requirement + + expect(requirement).not_to be_satisfied_by(Gem::Version.new('2.0.0')) + expect(requirement).to be_satisfied_by(Gem::Version.new('2.14.4')) end it 'keeps faraday-retry compatible with Faraday v1 and v2' do