ask-llm-providers 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 48397005f7c0a96ae277cc4542f716c389000bb958ef060e474efa763f3cb2e7
4
- data.tar.gz: ad14c0674e82ff5ee6572ee16c2c2c2731222b846ac1ea10e3ed51b59eb453cd
3
+ metadata.gz: 47102c012ea2700606f18c21510b875399a323d5a52ee91eca0b55c176b59468
4
+ data.tar.gz: 9e1e42e298a7e4d4ec40c3d9a3a631fbb9605e48816a439621ac4b4129cd0abe
5
5
  SHA512:
6
- metadata.gz: 05db1db4749217a513b85be42e0b7492466d657ea1c90f50b5fe3bacec83745555d95440c77ffe6df90a356584b99f4f3619645a99d1eb8bf73103aed4c39e94
7
- data.tar.gz: adbb4b2e782d488a87aae1b7c1ba9478dfc9e083c013f701f78984a28fe49c081f42f254197ed071c03a6f31820ecc10de35ef101d97d26e806a5b21d9765074
6
+ metadata.gz: effa863afea39f4dc924a1df6d88dce2ebb8639f6d607230df47233a54ef4e124cd998d0efe1a7e63ef5c2bcb6e7dadc7814804f5b9bb35bd82e0ee3c661de00
7
+ data.tar.gz: 7081eb62a0db815a56cfdaa04212cf4cc96fbd88fd17c4b8f5109535186d30bfa7a2e20d642cfe8617162eb2313a0229f18ad7e4d968cb29e9b8bfec590352e9
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [0.8.1] — 2026-07-17
2
+
3
+ ### Added
4
+
5
+ - **Rich error categories in HTTP mapper** — `Ask::LLM::HTTP.map_error` now sets `category`, `rate_limit_type`, and `retry_after` on `RateLimitError` instances. Rate limit type is detected from error message keywords (token, budget, concurrent, requests). `retry_after` is extracted from response headers.
6
+
7
+ ## [0.8.0] — 2026-07-17
8
+
9
+ ### Added
10
+
11
+ - **OpenRouter model source** (`Ask::LLM::Sources::OpenRouter`) — fetches model data from OpenRouter API and fills gaps that models.dev doesn't cover. Adds models for providers like Groq, Together, Fireworks, Cerebras, Meta, Moonshot, Nvidia NIM that aren't in models.dev. Merges with existing models.dev data — models.dev takes priority for overlapping models.
12
+ - **`CostCalculator.per_million`** — returns per-million token rates for quick display: `{ input: 2.5, output: 10.0, cache_read: 1.25 }`.
13
+ - **Audio token costing** — `calculate` and `breakdown` now accept `audio_input_tokens` and `audio_output_tokens` parameters. Costs are computed from `audio_tokens` pricing data.
14
+ - **Tiered pricing** — both `calculate` and `breakdown` accept a `tier:` parameter (`:standard` or `:batch`) that selects the appropriate rate tier.
15
+ - **`rake models:update`** — now fetches both models.dev and OpenRouter in sequence.
16
+
17
+ ### Changed
18
+
19
+ - **Model coverage: 62 → 406 models** across 12 providers, with 397 (98%) having full pricing data.
20
+ - **OpenRouter source added** — providers without models.dev coverage (meta, moonshot, nvidia_nim) now have bundled models.
21
+ - **`build_model_info`** — pricing hashes are deep-symbolized. `Date.parse` failures handled gracefully via `safe_parse_date`.
22
+
1
23
  ## [0.7.0] — 2026-07-17
2
24
 
3
25
  ### Added
@@ -3,79 +3,107 @@
3
3
  module Ask
4
4
  module LLM
5
5
  # Calculate LLM API costs from model pricing data.
6
- #
7
- # Works with any object responding to +#pricing+ that returns a hash
8
- # in the standard ask-rb pricing format:
9
- #
10
- # {
11
- # text_tokens: {
12
- # standard: {
13
- # input_per_million: 2.5,
14
- # output_per_million: 10.0,
15
- # cache_read_input_per_million: 1.25,
16
- # cache_write_input_per_million: 5.0,
17
- # reasoning_output_per_million: 15.0
18
- # }
19
- # },
20
- # audio_tokens: { standard: { input_per_million: 100.0, output_per_million: 200.0 } }
21
- # }
22
- #
23
6
  module CostCalculator
24
7
  MILLION = 1_000_000
25
8
 
26
9
  class << self
27
- # Calculate the total cost in USD for a model invocation.
10
+ # Calculate total cost in USD for a model invocation.
28
11
  #
29
- # @param model [Ask::ModelInfo, #pricing] the model
12
+ # @param model [Ask::ModelInfo, #pricing] the model or a pricing hash
30
13
  # @param input_tokens [Integer]
31
14
  # @param output_tokens [Integer]
32
15
  # @param cache_read_tokens [Integer]
33
16
  # @param cache_write_tokens [Integer]
34
- # @param reasoning_tokens [Integer] tokens billed at reasoning rate
17
+ # @param reasoning_tokens [Integer]
18
+ # @param audio_input_tokens [Integer]
19
+ # @param audio_output_tokens [Integer]
20
+ # @param tier [Symbol] pricing tier (:standard or :batch)
35
21
  # @return [Float, nil] cost in USD, or nil if no pricing data
36
22
  def calculate(model, input_tokens: 0, output_tokens: 0,
37
23
  cache_read_tokens: 0, cache_write_tokens: 0,
38
- reasoning_tokens: 0)
39
- pricing = model.respond_to?(:pricing) ? model.pricing : model
40
- rates = pricing.dig(:text_tokens, :standard) or return nil
24
+ reasoning_tokens: 0,
25
+ audio_input_tokens: 0, audio_output_tokens: 0,
26
+ tier: :standard)
27
+ pricing = extract_pricing(model)
28
+ return nil unless pricing
29
+
30
+ rates = pricing.dig(:text_tokens, tier) or return nil
41
31
 
42
32
  sum = cost(input_tokens, rates[:input_per_million])
43
33
  sum += cost(output_tokens, rates[:output_per_million])
34
+ sum += cost(cache_read_tokens, rates[:cache_read_input_per_million])
35
+ sum += cost(cache_write_tokens, rates[:cache_write_input_per_million])
44
36
 
45
- if cache_read_tokens > 0
46
- sum += cost(cache_read_tokens, rates[:cache_read_input_per_million])
47
- end
48
- if cache_write_tokens > 0
49
- sum += cost(cache_write_tokens, rates[:cache_write_input_per_million])
50
- end
51
37
  if reasoning_tokens > 0
52
38
  rate = rates[:reasoning_output_per_million] || rates[:output_per_million]
53
39
  sum += cost(reasoning_tokens, rate)
54
40
  end
55
41
 
42
+ if audio_input_tokens > 0 || audio_output_tokens > 0
43
+ audio = pricing.dig(:audio_tokens, tier)
44
+ sum += cost(audio_input_tokens, audio[:input_per_million]) if audio
45
+ sum += cost(audio_output_tokens, audio[:output_per_million]) if audio
46
+ end
47
+
56
48
  sum
57
49
  end
58
50
 
59
- # Cost breakdown with individual components.
51
+ # Per-million rates for quick display.
52
+ #
53
+ # @param model [Ask::ModelInfo, #pricing]
54
+ # @param tier [Symbol] (:standard or :batch)
55
+ # @return [Hash, nil]
56
+ def per_million(model, tier: :standard)
57
+ pricing = extract_pricing(model)
58
+ return nil unless pricing
59
+
60
+ rates = pricing.dig(:text_tokens, tier) or return nil
61
+
62
+ {
63
+ input: rates[:input_per_million],
64
+ output: rates[:output_per_million],
65
+ cache_read: rates[:cache_read_input_per_million],
66
+ cache_write: rates[:cache_write_input_per_million],
67
+ reasoning: rates[:reasoning_output_per_million]
68
+ }.compact
69
+ end
70
+
71
+ # Per-component cost breakdown.
60
72
  #
61
73
  # @return [Hash, nil]
62
74
  def breakdown(model, input_tokens: 0, output_tokens: 0,
63
75
  cache_read_tokens: 0, cache_write_tokens: 0,
64
- reasoning_tokens: 0)
65
- pricing = model.respond_to?(:pricing) ? model.pricing : model
66
- rates = pricing.dig(:text_tokens, :standard) or return nil
76
+ reasoning_tokens: 0,
77
+ audio_input_tokens: 0, audio_output_tokens: 0,
78
+ tier: :standard)
79
+ pricing = extract_pricing(model)
80
+ return nil unless pricing
67
81
 
68
- {
82
+ rates = pricing.dig(:text_tokens, tier) or return nil
83
+
84
+ result = {
69
85
  input: cost(input_tokens, rates[:input_per_million]),
70
86
  output: cost(output_tokens, rates[:output_per_million]),
71
- cache_read: cache_read_tokens > 0 ? cost(cache_read_tokens, rates[:cache_read_input_per_million]) : 0,
72
- cache_write: cache_write_tokens > 0 ? cost(cache_write_tokens, rates[:cache_write_input_per_million]) : 0,
73
- reasoning: reasoning_tokens > 0 ? cost(reasoning_tokens, rates[:reasoning_output_per_million] || rates[:output_per_million]) : 0
87
+ cache_read: cost(cache_read_tokens, rates[:cache_read_input_per_million]),
88
+ cache_write: cost(cache_write_tokens, rates[:cache_write_input_per_million]),
89
+ reasoning: cost(reasoning_tokens, rates[:reasoning_output_per_million] || rates[:output_per_million])
74
90
  }.compact
91
+
92
+ if audio_input_tokens > 0 || audio_output_tokens > 0
93
+ audio = pricing.dig(:audio_tokens, tier)
94
+ result[:audio_input] = cost(audio_input_tokens, audio[:input_per_million]) if audio
95
+ result[:audio_output] = cost(audio_output_tokens, audio[:output_per_million]) if audio
96
+ end
97
+
98
+ result
75
99
  end
76
100
 
77
101
  private
78
102
 
103
+ def extract_pricing(model)
104
+ model.respond_to?(:pricing) ? model.pricing : model
105
+ end
106
+
79
107
  def cost(tokens, rate)
80
108
  return 0.0 unless rate && tokens > 0
81
109
  (tokens * rate) / MILLION.to_f
data/lib/ask/llm/http.rb CHANGED
@@ -14,7 +14,13 @@ module Ask
14
14
  end
15
15
 
16
16
  # Map an HTTP exception or error response to the appropriate Ask::Error.
17
- def self.map_error(status, body, provider:)
17
+ #
18
+ # @param status [Integer] HTTP status code
19
+ # @param body [Hash, String, nil] response body
20
+ # @param provider [String] provider name for error messages
21
+ # @param headers [Hash, nil] response headers (for retry-after etc.)
22
+ # @return [Ask::Error]
23
+ def self.map_error(status, body, provider:, headers: nil)
18
24
  body = JSON.parse(body) rescue body if body.is_a?(String)
19
25
  message = extract_error_message(body, status) || "HTTP #{status} from #{provider}"
20
26
 
@@ -25,12 +31,25 @@ module Ask
25
31
  end
26
32
 
27
33
  case status
28
- when 400 then Ask::ProviderError.new(message, status_code: status, response_body: body&.to_json)
29
- when 401, 403 then Ask::Unauthorized.new("#{provider}: #{message}")
30
- when 429 then Ask::RateLimitError.new("#{provider}: #{message}")
31
- when 500 then Ask::ServerError.new("#{provider}: #{message}")
32
- when 503 then Ask::ServiceUnavailable.new("#{provider}: #{message}")
33
- else Ask::ProviderError.new("#{provider}: #{message}", status_code: status, response_body: body&.to_json)
34
+ when 400
35
+ Ask::ProviderError.new(message, status_code: status, response_body: body&.to_json)
36
+ when 401, 403
37
+ Ask::Unauthorized.new("#{provider}: #{message}")
38
+ when 429
39
+ retry_after = extract_retry_after(headers)
40
+ rate_limit_type = detect_rate_limit_type(body)
41
+ Ask::RateLimitError.new(
42
+ "#{provider}: #{message}",
43
+ category: Ask::RateLimitCategory::VENDOR,
44
+ rate_limit_type: rate_limit_type,
45
+ retry_after: retry_after
46
+ )
47
+ when 500
48
+ Ask::ServerError.new("#{provider}: #{message}")
49
+ when 503
50
+ Ask::ServiceUnavailable.new("#{provider}: #{message}")
51
+ else
52
+ Ask::ProviderError.new("#{provider}: #{message}", status_code: status, response_body: body&.to_json)
34
53
  end
35
54
  end
36
55
 
@@ -48,6 +67,37 @@ module Ask
48
67
  body.to_s
49
68
  end
50
69
  end
70
+
71
+ # Extract retry_after from response headers.
72
+ # Providers send Retry-After as seconds (integer) or HTTP-date.
73
+ # @param headers [Hash, nil]
74
+ # @return [Integer, nil]
75
+ def self.extract_retry_after(headers)
76
+ return nil unless headers.is_a?(Hash)
77
+
78
+ raw = headers["retry-after"] || headers["Retry-After"] || headers["retry_after"]
79
+ return nil unless raw
80
+
81
+ int = Integer(raw) rescue nil
82
+ return int if int
83
+
84
+ Time.parse(raw) - Time.now rescue nil
85
+ end
86
+
87
+ # Detect rate limit type from error body.
88
+ # @param body [Hash, nil]
89
+ # @return [Symbol, nil]
90
+ def self.detect_rate_limit_type(body)
91
+ return nil unless body.respond_to?(:dig)
92
+
93
+ msg = body.dig("error", "message") || body.dig("error", "code") || ""
94
+ msg_lower = msg.to_s.downcase
95
+
96
+ return Ask::RateLimitType::BUDGET if msg_lower.include?("budget") || msg_lower.include?("quota")
97
+ return Ask::RateLimitType::TOKENS if msg_lower.include?("token") || msg_lower.include?("tpm")
98
+ return Ask::RateLimitType::CONCURRENT if msg_lower.include?("concurrent") || msg_lower.include?("parallel")
99
+ Ask::RateLimitType::REQUESTS
100
+ end
51
101
  end
52
102
  end
53
103
  end
@@ -1,4 +1,371 @@
1
1
  [
2
+ {
3
+ "id": "claude-3-haiku",
4
+ "name": "Anthropic: Claude 3 Haiku",
5
+ "provider": "anthropic",
6
+ "context_window": 200000,
7
+ "max_output_tokens": 4096,
8
+ "capabilities": [
9
+ "vision"
10
+ ],
11
+ "modalities": {
12
+ "input": [
13
+ "text",
14
+ "image"
15
+ ],
16
+ "output": [
17
+ "text"
18
+ ]
19
+ },
20
+ "pricing": {
21
+ "text_tokens": {
22
+ "standard": {
23
+ "input_per_million": 0.25,
24
+ "output_per_million": 1.25,
25
+ "cache_read_input_per_million": 0.03,
26
+ "cache_write_input_per_million": 0.3
27
+ }
28
+ }
29
+ },
30
+ "architecture": "text+image->text",
31
+ "source": "openrouter"
32
+ },
33
+ {
34
+ "id": "claude-haiku-4.5",
35
+ "name": "Anthropic: Claude Haiku 4.5",
36
+ "provider": "anthropic",
37
+ "context_window": 200000,
38
+ "max_output_tokens": 64000,
39
+ "capabilities": [],
40
+ "modalities": {
41
+ "input": [
42
+ "text"
43
+ ],
44
+ "output": [
45
+ "text"
46
+ ]
47
+ },
48
+ "pricing": {
49
+ "text_tokens": {
50
+ "standard": {
51
+ "input_per_million": 1.0,
52
+ "output_per_million": 5.0,
53
+ "cache_read_input_per_million": 0.1,
54
+ "cache_write_input_per_million": 1.25
55
+ }
56
+ }
57
+ },
58
+ "architecture": "text+image+file->text",
59
+ "source": "openrouter"
60
+ },
61
+ {
62
+ "id": "claude-opus-4",
63
+ "name": "Anthropic: Claude Opus 4",
64
+ "provider": "anthropic",
65
+ "context_window": 200000,
66
+ "max_output_tokens": 32000,
67
+ "capabilities": [],
68
+ "modalities": {
69
+ "input": [
70
+ "text"
71
+ ],
72
+ "output": [
73
+ "text"
74
+ ]
75
+ },
76
+ "pricing": {
77
+ "text_tokens": {
78
+ "standard": {
79
+ "input_per_million": 15.0,
80
+ "output_per_million": 75.0,
81
+ "cache_read_input_per_million": 1.5,
82
+ "cache_write_input_per_million": 18.75
83
+ }
84
+ }
85
+ },
86
+ "architecture": "text+image+file->text",
87
+ "source": "openrouter"
88
+ },
89
+ {
90
+ "id": "claude-opus-4.1",
91
+ "name": "Anthropic: Claude Opus 4.1",
92
+ "provider": "anthropic",
93
+ "context_window": 200000,
94
+ "max_output_tokens": 32000,
95
+ "capabilities": [],
96
+ "modalities": {
97
+ "input": [
98
+ "text"
99
+ ],
100
+ "output": [
101
+ "text"
102
+ ]
103
+ },
104
+ "pricing": {
105
+ "text_tokens": {
106
+ "standard": {
107
+ "input_per_million": 15.0,
108
+ "output_per_million": 75.0,
109
+ "cache_read_input_per_million": 1.5,
110
+ "cache_write_input_per_million": 18.75
111
+ }
112
+ }
113
+ },
114
+ "architecture": "text+image+file->text",
115
+ "source": "openrouter"
116
+ },
117
+ {
118
+ "id": "claude-opus-4.5",
119
+ "name": "Anthropic: Claude Opus 4.5",
120
+ "provider": "anthropic",
121
+ "context_window": 200000,
122
+ "max_output_tokens": 64000,
123
+ "capabilities": [],
124
+ "modalities": {
125
+ "input": [
126
+ "text"
127
+ ],
128
+ "output": [
129
+ "text"
130
+ ]
131
+ },
132
+ "pricing": {
133
+ "text_tokens": {
134
+ "standard": {
135
+ "input_per_million": 5.0,
136
+ "output_per_million": 25.0,
137
+ "cache_read_input_per_million": 0.5,
138
+ "cache_write_input_per_million": 6.25
139
+ }
140
+ }
141
+ },
142
+ "architecture": "text+image+file->text",
143
+ "source": "openrouter"
144
+ },
145
+ {
146
+ "id": "claude-opus-4.6",
147
+ "name": "Anthropic: Claude Opus 4.6",
148
+ "provider": "anthropic",
149
+ "context_window": 1000000,
150
+ "max_output_tokens": 128000,
151
+ "capabilities": [],
152
+ "modalities": {
153
+ "input": [
154
+ "text"
155
+ ],
156
+ "output": [
157
+ "text"
158
+ ]
159
+ },
160
+ "pricing": {
161
+ "text_tokens": {
162
+ "standard": {
163
+ "input_per_million": 5.0,
164
+ "output_per_million": 25.0,
165
+ "cache_read_input_per_million": 0.5,
166
+ "cache_write_input_per_million": 6.25
167
+ }
168
+ }
169
+ },
170
+ "architecture": "text+image+file->text",
171
+ "source": "openrouter"
172
+ },
173
+ {
174
+ "id": "claude-opus-4.7",
175
+ "name": "Anthropic: Claude Opus 4.7",
176
+ "provider": "anthropic",
177
+ "context_window": 1000000,
178
+ "max_output_tokens": 128000,
179
+ "capabilities": [],
180
+ "modalities": {
181
+ "input": [
182
+ "text"
183
+ ],
184
+ "output": [
185
+ "text"
186
+ ]
187
+ },
188
+ "pricing": {
189
+ "text_tokens": {
190
+ "standard": {
191
+ "input_per_million": 5.0,
192
+ "output_per_million": 25.0,
193
+ "cache_read_input_per_million": 0.5,
194
+ "cache_write_input_per_million": 6.25
195
+ }
196
+ }
197
+ },
198
+ "architecture": "text+image+file->text",
199
+ "source": "openrouter"
200
+ },
201
+ {
202
+ "id": "claude-opus-4.7-fast",
203
+ "name": "Anthropic: Claude Opus 4.7 (Fast)",
204
+ "provider": "anthropic",
205
+ "context_window": 1000000,
206
+ "max_output_tokens": 128000,
207
+ "capabilities": [],
208
+ "modalities": {
209
+ "input": [
210
+ "text"
211
+ ],
212
+ "output": [
213
+ "text"
214
+ ]
215
+ },
216
+ "pricing": {
217
+ "text_tokens": {
218
+ "standard": {
219
+ "input_per_million": 30.0,
220
+ "output_per_million": 150.0,
221
+ "cache_read_input_per_million": 3.0,
222
+ "cache_write_input_per_million": 37.5
223
+ }
224
+ }
225
+ },
226
+ "architecture": "text+image+file->text",
227
+ "source": "openrouter"
228
+ },
229
+ {
230
+ "id": "claude-opus-4.8",
231
+ "name": "Anthropic: Claude Opus 4.8",
232
+ "provider": "anthropic",
233
+ "context_window": 1000000,
234
+ "max_output_tokens": 128000,
235
+ "capabilities": [],
236
+ "modalities": {
237
+ "input": [
238
+ "text"
239
+ ],
240
+ "output": [
241
+ "text"
242
+ ]
243
+ },
244
+ "pricing": {
245
+ "text_tokens": {
246
+ "standard": {
247
+ "input_per_million": 5.0,
248
+ "output_per_million": 25.0,
249
+ "cache_read_input_per_million": 0.5,
250
+ "cache_write_input_per_million": 6.25
251
+ }
252
+ }
253
+ },
254
+ "architecture": "text+image+file->text",
255
+ "source": "openrouter"
256
+ },
257
+ {
258
+ "id": "claude-opus-4.8-fast",
259
+ "name": "Anthropic: Claude Opus 4.8 (Fast)",
260
+ "provider": "anthropic",
261
+ "context_window": 1000000,
262
+ "max_output_tokens": 128000,
263
+ "capabilities": [],
264
+ "modalities": {
265
+ "input": [
266
+ "text"
267
+ ],
268
+ "output": [
269
+ "text"
270
+ ]
271
+ },
272
+ "pricing": {
273
+ "text_tokens": {
274
+ "standard": {
275
+ "input_per_million": 10.0,
276
+ "output_per_million": 50.0,
277
+ "cache_read_input_per_million": 1.0,
278
+ "cache_write_input_per_million": 12.5
279
+ }
280
+ }
281
+ },
282
+ "architecture": "text+image+file->text",
283
+ "source": "openrouter"
284
+ },
285
+ {
286
+ "id": "claude-sonnet-4",
287
+ "name": "Anthropic: Claude Sonnet 4",
288
+ "provider": "anthropic",
289
+ "context_window": 1000000,
290
+ "max_output_tokens": 64000,
291
+ "capabilities": [],
292
+ "modalities": {
293
+ "input": [
294
+ "text"
295
+ ],
296
+ "output": [
297
+ "text"
298
+ ]
299
+ },
300
+ "pricing": {
301
+ "text_tokens": {
302
+ "standard": {
303
+ "input_per_million": 3.0,
304
+ "output_per_million": 15.0,
305
+ "cache_read_input_per_million": 0.3,
306
+ "cache_write_input_per_million": 3.75
307
+ }
308
+ }
309
+ },
310
+ "architecture": "text+image+file->text",
311
+ "source": "openrouter"
312
+ },
313
+ {
314
+ "id": "claude-sonnet-4.5",
315
+ "name": "Anthropic: Claude Sonnet 4.5",
316
+ "provider": "anthropic",
317
+ "context_window": 1000000,
318
+ "max_output_tokens": 64000,
319
+ "capabilities": [],
320
+ "modalities": {
321
+ "input": [
322
+ "text"
323
+ ],
324
+ "output": [
325
+ "text"
326
+ ]
327
+ },
328
+ "pricing": {
329
+ "text_tokens": {
330
+ "standard": {
331
+ "input_per_million": 3.0,
332
+ "output_per_million": 15.0,
333
+ "cache_read_input_per_million": 0.3,
334
+ "cache_write_input_per_million": 3.75
335
+ }
336
+ }
337
+ },
338
+ "architecture": "text+image+file->text",
339
+ "source": "openrouter"
340
+ },
341
+ {
342
+ "id": "claude-sonnet-4.6",
343
+ "name": "Anthropic: Claude Sonnet 4.6",
344
+ "provider": "anthropic",
345
+ "context_window": 1000000,
346
+ "max_output_tokens": 128000,
347
+ "capabilities": [],
348
+ "modalities": {
349
+ "input": [
350
+ "text"
351
+ ],
352
+ "output": [
353
+ "text"
354
+ ]
355
+ },
356
+ "pricing": {
357
+ "text_tokens": {
358
+ "standard": {
359
+ "input_per_million": 3.0,
360
+ "output_per_million": 15.0,
361
+ "cache_read_input_per_million": 0.3,
362
+ "cache_write_input_per_million": 3.75
363
+ }
364
+ }
365
+ },
366
+ "architecture": "text+image+file->text",
367
+ "source": "openrouter"
368
+ },
2
369
  {
3
370
  "id": "claude-fable-5",
4
371
  "name": "Claude Fable 5",