ask-core 0.2.2 → 0.2.4
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 +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/ask/errors.rb +34 -7
- data/lib/ask/models.rb +20 -46
- data/lib/ask/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b573cb0709bd479e2a888becc0ce41cacafcf5f2e48afee24b2d6af1a7b99d5
|
|
4
|
+
data.tar.gz: c690b789d00985df0d9723c4f436c6b4a51e62f1857479af9ff2f652b59cd7d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4506564311573fcd2a43d277eef97157d1abafa7add2968bc336551ad1f6e4133f87b1aa2d47607963e2c0b3d0fdfb92ad332c6ff55eddb3a927276cee647a3e
|
|
7
|
+
data.tar.gz: ec2c10ed1152e01783272fffc16ff0695334c53bae829d9a0ebb7077317c084b3587490d6bc80820e2c78fd10e19e676dfe319147f667a4a453f763fb08782a7
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [0.2.4] — 2026-07-17
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **Rich error categories** — `RateLimitError` now carries `category` (`RateLimitCategory::VENDOR` or `::LOCAL`), `rate_limit_type` (`RateLimitType::REQUESTS`, `::TOKENS`, `::CONCURRENT`, `::BUDGET`), and `retry_after` (seconds) for intelligent error handling. Inspired by LiteLLM's error hierarchy.
|
|
6
|
+
|
|
7
|
+
## [0.2.3] — 2026-07-14
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- `Ask::ModelCatalog.find(model_id)` now returns `Array<Ask::ModelInfo>` (all matches) — provider preference disambiguation is removed. Provider-scoped `find(model_id, provider)` still returns a single model or raises.
|
|
11
|
+
- Removed `Ask::ModelCatalog::PROVIDER_PREFERENCE`. No preference list anywhere — all providers and models are treated equally.
|
|
12
|
+
|
|
1
13
|
## [0.2.2] — 2026-07-14
|
|
2
14
|
|
|
3
15
|
### Added
|
data/lib/ask/errors.rb
CHANGED
|
@@ -28,9 +28,6 @@ module Ask
|
|
|
28
28
|
# Raised when the context window is exceeded.
|
|
29
29
|
class ContextLengthExceeded < Error; end
|
|
30
30
|
|
|
31
|
-
# Raised when the API rate limit is hit.
|
|
32
|
-
class RateLimitError < Error; end
|
|
33
|
-
|
|
34
31
|
# Raised when authentication fails.
|
|
35
32
|
class Unauthorized < Error; end
|
|
36
33
|
|
|
@@ -40,11 +37,41 @@ module Ask
|
|
|
40
37
|
# Raised when the service is unavailable.
|
|
41
38
|
class ServiceUnavailable < Error; end
|
|
42
39
|
|
|
40
|
+
# Categories for {RateLimitError} — tells you who rate-limited the request.
|
|
41
|
+
module RateLimitCategory
|
|
42
|
+
VENDOR = :vendor # upstream LLM provider (OpenAI, Anthropic, etc.)
|
|
43
|
+
LOCAL = :local # ask-rb's own limiter (ask-auth thresholds)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Rate limit dimension that was exceeded — orthogonal to {RateLimitCategory}.
|
|
47
|
+
module RateLimitType
|
|
48
|
+
REQUESTS = :requests # requests-per-minute ceiling
|
|
49
|
+
TOKENS = :tokens # tokens-per-minute ceiling
|
|
50
|
+
CONCURRENT = :concurrent # max parallel requests
|
|
51
|
+
BUDGET = :budget # spend budget cap
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Raised when the API rate limit is hit.
|
|
55
|
+
# Carries optional category, type, and retry_after for intelligent handling.
|
|
56
|
+
class RateLimitError < Error
|
|
57
|
+
# @return [Symbol, nil] who rate-limited (:vendor, :local)
|
|
58
|
+
attr_reader :category
|
|
59
|
+
|
|
60
|
+
# @return [Symbol, nil] which limit was hit (:requests, :tokens, :concurrent, :budget)
|
|
61
|
+
attr_reader :rate_limit_type
|
|
62
|
+
|
|
63
|
+
# @return [Integer, Float, nil] suggested seconds to wait before retrying
|
|
64
|
+
attr_reader :retry_after
|
|
65
|
+
|
|
66
|
+
def initialize(message, category: nil, rate_limit_type: nil, retry_after: nil)
|
|
67
|
+
@category = category
|
|
68
|
+
@rate_limit_type = rate_limit_type
|
|
69
|
+
@retry_after = retry_after
|
|
70
|
+
super(message)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
43
74
|
# Raised when a provider's API returns an unexpected response.
|
|
44
|
-
# @!attribute [r] status_code
|
|
45
|
-
# @return [Integer, nil] the HTTP status code
|
|
46
|
-
# @!attribute [r] response_body
|
|
47
|
-
# @return [String, nil] the raw response body
|
|
48
75
|
class ProviderError < Error
|
|
49
76
|
attr_reader :status_code, :response_body
|
|
50
77
|
|
data/lib/ask/models.rb
CHANGED
|
@@ -256,12 +256,11 @@ module Ask
|
|
|
256
256
|
end
|
|
257
257
|
end
|
|
258
258
|
|
|
259
|
-
# Catalog of available AI models.
|
|
260
|
-
# filtering by capability, and refresh from the models.dev API.
|
|
259
|
+
# Catalog of available AI models.
|
|
261
260
|
#
|
|
262
|
-
# Ask::ModelCatalog.find("gpt-4o")
|
|
263
|
-
# Ask::ModelCatalog.
|
|
264
|
-
# Ask::ModelCatalog.
|
|
261
|
+
# Ask::ModelCatalog.find("gpt-4o") # => ModelInfo or raises
|
|
262
|
+
# Ask::ModelCatalog.find("gpt-4o", provider: "openai") # => ModelInfo or raises
|
|
263
|
+
# Ask::ModelCatalog.where("gpt-4o") # => [ModelInfo, ...] (all matches)
|
|
265
264
|
#
|
|
266
265
|
class ModelCatalog
|
|
267
266
|
include Enumerable
|
|
@@ -269,17 +268,8 @@ module Ask
|
|
|
269
268
|
# @return [String] URL for the models.dev API
|
|
270
269
|
MODELS_DEV_URL = "https://models.dev/api.json".freeze
|
|
271
270
|
|
|
272
|
-
# Ordered provider preference for disambiguation.
|
|
273
|
-
PROVIDER_PREFERENCE = %w[
|
|
274
|
-
openai anthropic gemini vertexai bedrock
|
|
275
|
-
opencode opencode_go
|
|
276
|
-
openrouter deepseek mistral perplexity xai
|
|
277
|
-
mimo
|
|
278
|
-
azure ollama gpustack github
|
|
279
|
-
].freeze
|
|
280
|
-
|
|
281
271
|
# Methods delegated to the singleton instance.
|
|
282
|
-
DELEGATES = %i[all each find chat_models embedding_models
|
|
272
|
+
DELEGATES = %i[all each find where chat_models embedding_models
|
|
283
273
|
audio_models image_models by_family by_provider
|
|
284
274
|
refresh!].freeze
|
|
285
275
|
|
|
@@ -319,19 +309,28 @@ module Ask
|
|
|
319
309
|
@models.each(&block)
|
|
320
310
|
end
|
|
321
311
|
|
|
322
|
-
# Find a model by ID
|
|
312
|
+
# Find a single model by ID. Scopes to provider when given.
|
|
323
313
|
# @param model_id [String] model identifier
|
|
324
|
-
# @param provider [String, nil] provider slug
|
|
314
|
+
# @param provider [String, nil] provider slug (optional keyword)
|
|
325
315
|
# @return [Ask::ModelInfo]
|
|
326
|
-
# @raise [ModelNotFound] if
|
|
327
|
-
def find(model_id, provider
|
|
316
|
+
# @raise [ModelNotFound] if no matching model is found
|
|
317
|
+
def find(model_id, provider: nil)
|
|
328
318
|
if provider
|
|
329
|
-
|
|
319
|
+
@models.find { |m| m.id == model_id && m.provider == provider.to_s } ||
|
|
320
|
+
raise(ModelNotFound, "Model #{model_id.inspect} not found for provider #{provider.inspect}.")
|
|
330
321
|
else
|
|
331
|
-
|
|
322
|
+
@models.find { |m| m.id == model_id } ||
|
|
323
|
+
raise(ModelNotFound, "Unknown model: #{model_id.inspect}.")
|
|
332
324
|
end
|
|
333
325
|
end
|
|
334
326
|
|
|
327
|
+
# Find all models matching an ID. Never raises.
|
|
328
|
+
# @param model_id [String] model identifier
|
|
329
|
+
# @return [Array<Ask::ModelInfo>] matching models (empty if none)
|
|
330
|
+
def where(model_id)
|
|
331
|
+
@models.select { |m| m.id == model_id }
|
|
332
|
+
end
|
|
333
|
+
|
|
335
334
|
# @return [Ask::ModelCatalog] new catalog containing only chat models
|
|
336
335
|
def chat_models
|
|
337
336
|
self.class.new(@models.select(&:chat?))
|
|
@@ -391,31 +390,6 @@ module Ask
|
|
|
391
390
|
|
|
392
391
|
private
|
|
393
392
|
|
|
394
|
-
def find_with_provider(model_id, provider)
|
|
395
|
-
exact = @models.find { |m| m.id == model_id && m.provider == provider }
|
|
396
|
-
return exact if exact
|
|
397
|
-
|
|
398
|
-
@models.find { |m| m.id == model_id && m.provider == provider } ||
|
|
399
|
-
raise(ModelNotFound, "Model #{model_id.inspect} not found for provider #{provider.inspect}. " \
|
|
400
|
-
"Try ModelCatalog.refresh! to update the catalog.")
|
|
401
|
-
end
|
|
402
|
-
|
|
403
|
-
def find_without_provider(model_id)
|
|
404
|
-
matches = @models.select { |m| m.id == model_id }
|
|
405
|
-
return preferred_match(matches) if matches.any?
|
|
406
|
-
|
|
407
|
-
raise ModelNotFound, "Unknown model: #{model_id.inspect}. " \
|
|
408
|
-
"Try ModelCatalog.refresh! to update the catalog."
|
|
409
|
-
end
|
|
410
|
-
|
|
411
|
-
def preferred_match(candidates)
|
|
412
|
-
return candidates.first if candidates.size == 1
|
|
413
|
-
|
|
414
|
-
candidates.min_by do |model|
|
|
415
|
-
PROVIDER_PREFERENCE.index(model.provider) || PROVIDER_PREFERENCE.length
|
|
416
|
-
end
|
|
417
|
-
end
|
|
418
|
-
|
|
419
393
|
def fetch_from_models_dev(timeout: 10)
|
|
420
394
|
uri = URI(MODELS_DEV_URL)
|
|
421
395
|
http = Net::HTTP.new(uri.host, uri.port)
|
data/lib/ask/version.rb
CHANGED