ask-llm-providers 0.8.0 → 0.8.2

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: 3cd9e34fd4befbdd393f4b4496b0d0e5dde254dc2ef10c3d86bb7043f81406de
4
- data.tar.gz: 6de8ecfb85b0aa79f5623a0968ea8a787dc8c64ebce9da6d5d49122ef82e037c
3
+ metadata.gz: fcbf569b97e70a5c06deac427eb7cc500bb959295b2f8ce10ed20d9698cec241
4
+ data.tar.gz: 20a6ac87a6a4e35d84d3596d604644e2280616676399632ce2b481c3591a96f2
5
5
  SHA512:
6
- metadata.gz: 10878f289f4fc79c814e839554adf05f7931e5a4eaa100cc5b584a20ee0bc16fece12c65472af23872abb30885e4e1cbdfc05310baffce4cd4f1dc4be60a622d
7
- data.tar.gz: c2fbe34457dcd128b2601baf756257d6147761505150b700d988d130e580017b9e6839ad9b5ba0ea6f91ec398ec1e09ae2bf3c363c2b6508a2d44a9c16368235
6
+ metadata.gz: 57f783f4c2bdd7335e45d06f177df09ea565acec8cf65d4cc9bdadb747276b94e944cdacf0505dc29e553697ad21ef7574a357c1d7ac5f5386bc59c76fa4b835
7
+ data.tar.gz: d4ffb871a8e1702aaa1b27763bb1481689f1c61f8c55efd27de0db198cf3f9482ca691c5c0e36c0806cf11057178418a3cfd467016afd79207d40dbe39214328
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
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
+
1
7
  ## [0.8.0] — 2026-07-17
2
8
 
3
9
  ### Added
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
@@ -0,0 +1,552 @@
1
+ [
2
+ {
3
+ "id": "minimax-m3",
4
+ "name": "minimax-m3",
5
+ "family": "minimax",
6
+ "provider": "opencode",
7
+ "context_window": 128000,
8
+ "max_output_tokens": 4096,
9
+ "capabilities": [
10
+ "function_calling",
11
+ "streaming",
12
+ "reasoning"
13
+ ],
14
+ "modalities": {
15
+ "input": [
16
+ "text"
17
+ ],
18
+ "output": [
19
+ "text"
20
+ ]
21
+ },
22
+ "pricing": {
23
+ "input": 0,
24
+ "output": 0
25
+ }
26
+ },
27
+ {
28
+ "id": "minimax-m2.7",
29
+ "name": "minimax-m2.7",
30
+ "family": "minimax",
31
+ "provider": "opencode",
32
+ "context_window": 128000,
33
+ "max_output_tokens": 4096,
34
+ "capabilities": [
35
+ "function_calling",
36
+ "streaming",
37
+ "reasoning"
38
+ ],
39
+ "modalities": {
40
+ "input": [
41
+ "text"
42
+ ],
43
+ "output": [
44
+ "text"
45
+ ]
46
+ },
47
+ "pricing": {
48
+ "input": 0,
49
+ "output": 0
50
+ }
51
+ },
52
+ {
53
+ "id": "minimax-m2.5",
54
+ "name": "minimax-m2.5",
55
+ "family": "minimax",
56
+ "provider": "opencode",
57
+ "context_window": 128000,
58
+ "max_output_tokens": 4096,
59
+ "capabilities": [
60
+ "function_calling",
61
+ "streaming",
62
+ "reasoning"
63
+ ],
64
+ "modalities": {
65
+ "input": [
66
+ "text"
67
+ ],
68
+ "output": [
69
+ "text"
70
+ ]
71
+ },
72
+ "pricing": {
73
+ "input": 0,
74
+ "output": 0
75
+ }
76
+ },
77
+ {
78
+ "id": "kimi-k3",
79
+ "name": "kimi-k3",
80
+ "family": "kimi",
81
+ "provider": "opencode",
82
+ "context_window": 200000,
83
+ "max_output_tokens": 4096,
84
+ "capabilities": [
85
+ "function_calling",
86
+ "streaming",
87
+ "reasoning"
88
+ ],
89
+ "modalities": {
90
+ "input": [
91
+ "text"
92
+ ],
93
+ "output": [
94
+ "text"
95
+ ]
96
+ },
97
+ "pricing": {
98
+ "input": 0,
99
+ "output": 0
100
+ }
101
+ },
102
+ {
103
+ "id": "kimi-k2.7-code",
104
+ "name": "kimi-k2.7-code",
105
+ "family": "kimi",
106
+ "provider": "opencode",
107
+ "context_window": 200000,
108
+ "max_output_tokens": 4096,
109
+ "capabilities": [
110
+ "function_calling",
111
+ "streaming",
112
+ "reasoning"
113
+ ],
114
+ "modalities": {
115
+ "input": [
116
+ "text"
117
+ ],
118
+ "output": [
119
+ "text"
120
+ ]
121
+ },
122
+ "pricing": {
123
+ "input": 0,
124
+ "output": 0
125
+ }
126
+ },
127
+ {
128
+ "id": "kimi-k2.6",
129
+ "name": "kimi-k2.6",
130
+ "family": "kimi",
131
+ "provider": "opencode",
132
+ "context_window": 200000,
133
+ "max_output_tokens": 4096,
134
+ "capabilities": [
135
+ "function_calling",
136
+ "streaming",
137
+ "reasoning"
138
+ ],
139
+ "modalities": {
140
+ "input": [
141
+ "text"
142
+ ],
143
+ "output": [
144
+ "text"
145
+ ]
146
+ },
147
+ "pricing": {
148
+ "input": 0,
149
+ "output": 0
150
+ }
151
+ },
152
+ {
153
+ "id": "kimi-k2.5",
154
+ "name": "kimi-k2.5",
155
+ "family": "kimi",
156
+ "provider": "opencode",
157
+ "context_window": 200000,
158
+ "max_output_tokens": 4096,
159
+ "capabilities": [
160
+ "function_calling",
161
+ "streaming",
162
+ "reasoning"
163
+ ],
164
+ "modalities": {
165
+ "input": [
166
+ "text"
167
+ ],
168
+ "output": [
169
+ "text"
170
+ ]
171
+ },
172
+ "pricing": {
173
+ "input": 0,
174
+ "output": 0
175
+ }
176
+ },
177
+ {
178
+ "id": "glm-5.2",
179
+ "name": "glm-5.2",
180
+ "family": "glm",
181
+ "provider": "opencode",
182
+ "context_window": 128000,
183
+ "max_output_tokens": 4096,
184
+ "capabilities": [
185
+ "function_calling",
186
+ "streaming",
187
+ "reasoning"
188
+ ],
189
+ "modalities": {
190
+ "input": [
191
+ "text"
192
+ ],
193
+ "output": [
194
+ "text"
195
+ ]
196
+ },
197
+ "pricing": {
198
+ "input": 0,
199
+ "output": 0
200
+ }
201
+ },
202
+ {
203
+ "id": "glm-5.1",
204
+ "name": "glm-5.1",
205
+ "family": "glm",
206
+ "provider": "opencode",
207
+ "context_window": 128000,
208
+ "max_output_tokens": 4096,
209
+ "capabilities": [
210
+ "function_calling",
211
+ "streaming",
212
+ "reasoning"
213
+ ],
214
+ "modalities": {
215
+ "input": [
216
+ "text"
217
+ ],
218
+ "output": [
219
+ "text"
220
+ ]
221
+ },
222
+ "pricing": {
223
+ "input": 0,
224
+ "output": 0
225
+ }
226
+ },
227
+ {
228
+ "id": "glm-5",
229
+ "name": "glm-5",
230
+ "family": "glm",
231
+ "provider": "opencode",
232
+ "context_window": 128000,
233
+ "max_output_tokens": 4096,
234
+ "capabilities": [
235
+ "function_calling",
236
+ "streaming",
237
+ "reasoning"
238
+ ],
239
+ "modalities": {
240
+ "input": [
241
+ "text"
242
+ ],
243
+ "output": [
244
+ "text"
245
+ ]
246
+ },
247
+ "pricing": {
248
+ "input": 0,
249
+ "output": 0
250
+ }
251
+ },
252
+ {
253
+ "id": "deepseek-v4-pro",
254
+ "name": "deepseek-v4-pro",
255
+ "family": "deepseek",
256
+ "provider": "opencode",
257
+ "context_window": 1000000,
258
+ "max_output_tokens": 64000,
259
+ "capabilities": [
260
+ "function_calling",
261
+ "streaming",
262
+ "reasoning"
263
+ ],
264
+ "modalities": {
265
+ "input": [
266
+ "text"
267
+ ],
268
+ "output": [
269
+ "text"
270
+ ]
271
+ },
272
+ "pricing": {
273
+ "input": 0,
274
+ "output": 0
275
+ }
276
+ },
277
+ {
278
+ "id": "deepseek-v4-flash",
279
+ "name": "deepseek-v4-flash",
280
+ "family": "deepseek",
281
+ "provider": "opencode",
282
+ "context_window": 1000000,
283
+ "max_output_tokens": 384000,
284
+ "capabilities": [
285
+ "function_calling",
286
+ "streaming",
287
+ "reasoning"
288
+ ],
289
+ "modalities": {
290
+ "input": [
291
+ "text"
292
+ ],
293
+ "output": [
294
+ "text"
295
+ ]
296
+ },
297
+ "pricing": {
298
+ "input": 0,
299
+ "output": 0
300
+ }
301
+ },
302
+ {
303
+ "id": "qwen3.7-max",
304
+ "name": "qwen3.7-max",
305
+ "family": "qwen3.7",
306
+ "provider": "opencode",
307
+ "context_window": 128000,
308
+ "max_output_tokens": 4096,
309
+ "capabilities": [
310
+ "function_calling",
311
+ "streaming",
312
+ "reasoning"
313
+ ],
314
+ "modalities": {
315
+ "input": [
316
+ "text"
317
+ ],
318
+ "output": [
319
+ "text"
320
+ ]
321
+ },
322
+ "pricing": {
323
+ "input": 0,
324
+ "output": 0
325
+ }
326
+ },
327
+ {
328
+ "id": "qwen3.7-plus",
329
+ "name": "qwen3.7-plus",
330
+ "family": "qwen3.7",
331
+ "provider": "opencode",
332
+ "context_window": 128000,
333
+ "max_output_tokens": 4096,
334
+ "capabilities": [
335
+ "function_calling",
336
+ "streaming",
337
+ "reasoning"
338
+ ],
339
+ "modalities": {
340
+ "input": [
341
+ "text"
342
+ ],
343
+ "output": [
344
+ "text"
345
+ ]
346
+ },
347
+ "pricing": {
348
+ "input": 0,
349
+ "output": 0
350
+ }
351
+ },
352
+ {
353
+ "id": "qwen3.6-plus",
354
+ "name": "qwen3.6-plus",
355
+ "family": "qwen3.6",
356
+ "provider": "opencode",
357
+ "context_window": 128000,
358
+ "max_output_tokens": 4096,
359
+ "capabilities": [
360
+ "function_calling",
361
+ "streaming",
362
+ "reasoning"
363
+ ],
364
+ "modalities": {
365
+ "input": [
366
+ "text"
367
+ ],
368
+ "output": [
369
+ "text"
370
+ ]
371
+ },
372
+ "pricing": {
373
+ "input": 0,
374
+ "output": 0
375
+ }
376
+ },
377
+ {
378
+ "id": "qwen3.5-plus",
379
+ "name": "qwen3.5-plus",
380
+ "family": "qwen3.5",
381
+ "provider": "opencode",
382
+ "context_window": 128000,
383
+ "max_output_tokens": 4096,
384
+ "capabilities": [
385
+ "function_calling",
386
+ "streaming",
387
+ "reasoning"
388
+ ],
389
+ "modalities": {
390
+ "input": [
391
+ "text"
392
+ ],
393
+ "output": [
394
+ "text"
395
+ ]
396
+ },
397
+ "pricing": {
398
+ "input": 0,
399
+ "output": 0
400
+ }
401
+ },
402
+ {
403
+ "id": "mimo-v2-pro",
404
+ "name": "mimo-v2-pro",
405
+ "family": "mimo",
406
+ "provider": "opencode",
407
+ "context_window": 128000,
408
+ "max_output_tokens": 4096,
409
+ "capabilities": [
410
+ "function_calling",
411
+ "streaming",
412
+ "reasoning"
413
+ ],
414
+ "modalities": {
415
+ "input": [
416
+ "text"
417
+ ],
418
+ "output": [
419
+ "text"
420
+ ]
421
+ },
422
+ "pricing": {
423
+ "input": 0,
424
+ "output": 0
425
+ }
426
+ },
427
+ {
428
+ "id": "mimo-v2-omni",
429
+ "name": "mimo-v2-omni",
430
+ "family": "mimo",
431
+ "provider": "opencode",
432
+ "context_window": 128000,
433
+ "max_output_tokens": 4096,
434
+ "capabilities": [
435
+ "function_calling",
436
+ "streaming",
437
+ "reasoning"
438
+ ],
439
+ "modalities": {
440
+ "input": [
441
+ "text"
442
+ ],
443
+ "output": [
444
+ "text"
445
+ ]
446
+ },
447
+ "pricing": {
448
+ "input": 0,
449
+ "output": 0
450
+ }
451
+ },
452
+ {
453
+ "id": "mimo-v2.5-pro",
454
+ "name": "mimo-v2.5-pro",
455
+ "family": "mimo",
456
+ "provider": "opencode",
457
+ "context_window": 128000,
458
+ "max_output_tokens": 4096,
459
+ "capabilities": [
460
+ "function_calling",
461
+ "streaming",
462
+ "reasoning"
463
+ ],
464
+ "modalities": {
465
+ "input": [
466
+ "text"
467
+ ],
468
+ "output": [
469
+ "text"
470
+ ]
471
+ },
472
+ "pricing": {
473
+ "input": 0,
474
+ "output": 0
475
+ }
476
+ },
477
+ {
478
+ "id": "mimo-v2.5",
479
+ "name": "mimo-v2.5",
480
+ "family": "mimo",
481
+ "provider": "opencode",
482
+ "context_window": 128000,
483
+ "max_output_tokens": 4096,
484
+ "capabilities": [
485
+ "function_calling",
486
+ "streaming",
487
+ "reasoning"
488
+ ],
489
+ "modalities": {
490
+ "input": [
491
+ "text"
492
+ ],
493
+ "output": [
494
+ "text"
495
+ ]
496
+ },
497
+ "pricing": {
498
+ "input": 0,
499
+ "output": 0
500
+ }
501
+ },
502
+ {
503
+ "id": "hy3-preview",
504
+ "name": "hy3-preview",
505
+ "family": "hy3",
506
+ "provider": "opencode",
507
+ "context_window": 128000,
508
+ "max_output_tokens": 4096,
509
+ "capabilities": [
510
+ "function_calling",
511
+ "streaming",
512
+ "reasoning"
513
+ ],
514
+ "modalities": {
515
+ "input": [
516
+ "text"
517
+ ],
518
+ "output": [
519
+ "text"
520
+ ]
521
+ },
522
+ "pricing": {
523
+ "input": 0,
524
+ "output": 0
525
+ }
526
+ },
527
+ {
528
+ "id": "grok-4.5",
529
+ "name": "grok-4.5",
530
+ "family": "grok",
531
+ "provider": "opencode",
532
+ "context_window": 128000,
533
+ "max_output_tokens": 4096,
534
+ "capabilities": [
535
+ "function_calling",
536
+ "streaming",
537
+ "reasoning"
538
+ ],
539
+ "modalities": {
540
+ "input": [
541
+ "text"
542
+ ],
543
+ "output": [
544
+ "text"
545
+ ]
546
+ },
547
+ "pricing": {
548
+ "input": 0,
549
+ "output": 0
550
+ }
551
+ }
552
+ ]
@@ -72,9 +72,8 @@ module Ask
72
72
  opencode: { api_base: "https://opencode.ai/zen/v1", api_key_env: "OPENCODE_API_KEY",
73
73
  capabilities: { chat: true, streaming: true, tool_calls: true } },
74
74
 
75
- opencode_go: { api_base: "https://opencode.ai/zen/go/v1", api_key_env: "OPENCODE_GO_API_KEY",
76
- alternate_env: "OPENCODE_API_KEY",
77
- capabilities: { chat: true, streaming: true, tool_calls: true } },
75
+ opencode_go: { api_base: "https://opencode.ai/zen/go/v1", api_key_env: "OPENCODE_API_KEY",
76
+ capabilities: { chat: true, streaming: true, tool_calls: true } },
78
77
 
79
78
  openrouter: { api_base: "https://openrouter.ai/api/v1", api_key_env: "OPENROUTER_API_KEY",
80
79
  extra_headers: { "HTTP-Referer" => "https://github.com/ask-rb",
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.8.0"
5
+ VERSION = "0.8.2"
6
6
  end
7
7
  end
@@ -56,7 +56,6 @@ module Ask
56
56
  def configuration_options; %i[api_base]; end
57
57
  def configuration_requirements; %i[]; end
58
58
  def local?; true; end
59
- def assume_models_exist?; true; end
60
59
  end
61
60
 
62
61
  # --- Config transformation contract ---
@@ -71,7 +71,6 @@ module Ask
71
71
 
72
72
  def configuration_options; %i[api_key base_url organization_id project_id]; end
73
73
  def configuration_requirements; %i[api_key]; end
74
- def assume_models_exist?; false; end
75
74
  end
76
75
 
77
76
  # --- Config transformation contract ---
@@ -37,9 +37,7 @@ module Ask
37
37
  key.to_s.length > 0
38
38
  end
39
39
 
40
- def assume_models_exist?
41
- false
42
- end
40
+
43
41
  end
44
42
 
45
43
  def initialize(config = {})
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-llm-providers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -191,6 +191,7 @@ files:
191
191
  - lib/ask/llm/models/moonshot.json
192
192
  - lib/ask/llm/models/nvidia_nim.json
193
193
  - lib/ask/llm/models/openai.json
194
+ - lib/ask/llm/models/opencode.json
194
195
  - lib/ask/llm/models/perplexity.json
195
196
  - lib/ask/llm/models/vertex_ai.json
196
197
  - lib/ask/llm/models/xai.json