summaryrefslogtreecommitdiffhomepage
path: root/spec/dispatch/adapter/claude/model_catalog_spec.rb
blob: 7e27d45619a5ef66938d2c027681aa182045e5d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# frozen_string_literal: true

RSpec.describe Dispatch::Adapter::Claude::ModelCatalog do
  describe ".build" do
    let(:model_id) { "claude-sonnet-4-5-20250929" }

    subject(:info) { described_class.build(model_id) }

    it "returns a ModelInfo" do
      expect(info).to be_a(Dispatch::Adapter::ModelInfo)
    end

    it "sets id correctly" do
      expect(info.id).to eq(model_id)
    end

    it "sets name to the id (no separate label in JSON)" do
      expect(info.name).to eq(model_id)
    end

    it "sets max_context_tokens from the pricing table" do
      expect(info.max_context_tokens).to eq(
        Dispatch::Adapter::Claude::PricingTable.context_window(model_id)
      )
    end

    it "sets supports_vision to true" do
      expect(info.supports_vision).to be(true)
    end

    it "sets supports_tool_use to true" do
      expect(info.supports_tool_use).to be(true)
    end

    it "sets supports_streaming to true" do
      expect(info.supports_streaming).to be(true)
    end

    it "sets premium_request_multiplier to nil" do
      expect(info.premium_request_multiplier).to be_nil
    end

    it "sets pricing from the pricing table" do
      expect(info.pricing).to be_a(Dispatch::Adapter::ModelPricing)
    end

    it "falls back to 200_000 for context_window when table has no entry" do
      # Build with an id that is not in the table — simulate by using an
      # id absent from the table; ModelCatalog falls back to 200_000.
      # (We can't do that cleanly without a stub, so test the fallback
      # directly via a non-existent id.)
      unknown_info = described_class.build("some-future-model-not-in-table")
      expect(unknown_info.max_context_tokens).to eq(200_000)
    end

    it "returns nil pricing for an id not in the pricing table" do
      unknown_info = described_class.build("some-future-model-not-in-table")
      expect(unknown_info.pricing).to be_nil
    end
  end
end

RSpec.describe Dispatch::Adapter::Claude, "#list_models" do
  subject(:adapter) { described_class.allocate }

  it "returns a non-empty array" do
    expect(adapter.list_models).to be_an(Array)
    expect(adapter.list_models).not_to be_empty
  end

  it "every entry is a ModelInfo" do
    adapter.list_models.each do |info|
      expect(info).to be_a(Dispatch::Adapter::ModelInfo)
    end
  end

  it "every entry has pricing populated" do
    adapter.list_models.each do |info|
      expect(info.pricing).to be_a(Dispatch::Adapter::ModelPricing),
                              "expected pricing on #{info.id}"
    end
  end

  it "Pricing.calculate works with every entry" do
    usage = Dispatch::Adapter::Usage.new(
      input_tokens: 1_000,
      output_tokens: 500
    )

    adapter.list_models.each do |info|
      cost = Dispatch::Adapter::Pricing.calculate(usage, info)
      expect(cost).to be_a(Dispatch::Adapter::UsageCost),
                      "expected UsageCost for #{info.id}"
      expect(cost.total).to be >= 0
    end
  end

  it "includes the three required models" do
    ids = adapter.list_models.map(&:id)
    expect(ids).to include("claude-opus-4-7-20251018")
    expect(ids).to include("claude-sonnet-4-5-20250929")
    expect(ids).to include("claude-haiku-4-5-20251001")
  end
end