ask-llm-providers 0.10.1 → 0.10.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: 15fc5c9c298a6f88b722fde13139a1c3fe2dfa795255e8d7226b9ca0b5187bd5
4
- data.tar.gz: f9f1c3d677c470c2fffeac5720024ecc9c40fb58cf3b058ac2a99e1e5769100e
3
+ metadata.gz: 3eae68598ee63c7ef7688b717687b06a434f12c740141a3b84bde44752a873d9
4
+ data.tar.gz: f21d623f34793e0eff9a08dcb5c1f39c8bba236c541022f1f969a5a273bcb5b7
5
5
  SHA512:
6
- metadata.gz: 63e97ca4cf04aac0d2e9de99a2d4b6425bd1c95466be13a7172667729830c83b3025fb54094e4a5b20bd1b6b49374bdd1575ef8a60a9eedf3395438a559cc1cf
7
- data.tar.gz: 50dfb8a0ca28288df43be9f8126530fa6bddbb6da4d1f951023b4ad7e592298b8c5ed9abb04f53494b65a049421447ba1ef4c02ea594a3af9f7b63176c707598
6
+ metadata.gz: 87589c9effd3da495a57912d6151d44db8c55673cd74d0e50ba6e0c107e90703905e0e94490e5ae880e5e07d27a2375556837cf7da6917bfa9764f7b1028b49d
7
+ data.tar.gz: 189cd4cfdfcc19cc1bece8dd6fe561f955dd1e8279a8470cb14549a2d25d05689f06b91687a60920f97237075e5027e287803628579324d98f96a3bd76cffd26
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.10.2] - 2026-08-06
2
+
3
+ ### Fixed
4
+
5
+ - **Streamed token usage is now captured.** OpenAI-style streams end with a
6
+ chunk whose `choices` is empty but `usage` populated; `dig("choices", 0)
7
+ or next` silently dropped it, so every streamed call reported ~1 output
8
+ token (input 0) — token billing, cost accounting, and usage metrics
9
+ under-counted by orders of magnitude. The usage-only chunk is now added
10
+ to the stream (without yielding — it carries no content).
11
+
1
12
  ## [0.10.1] - 2026-07-31
2
13
 
3
14
  ### Fixed
data/README.md CHANGED
@@ -1,20 +1,10 @@
1
1
  # ask-llm-providers
2
2
 
3
- All LLM providers for the ask-rb ecosystem in one gem. Implements `Ask::Provider`
4
- from `ask-core` with a capabilities-based interface.
5
-
6
- ## Supported Providers
7
-
8
- | Provider | Auth | Implementation |
9
- |---|---|---|
10
- | **OpenAI** + all OpenAI-compatible | `Ask::Auth.resolve(:openai_api_key)` | `Ask::Providers::OpenAI` |
11
- | **Anthropic** (Claude) | `Ask::Auth.resolve(:anthropic_api_key)` | `Ask::Providers::Anthropic` |
12
- | **Google Gemini** | `Ask::Auth.resolve(:gemini_api_key)` | `Ask::Providers::Google` |
13
- | **Vertex AI** | GCP service account | `Ask::Providers::Google` (via Vertex) |
14
- | **Amazon Bedrock** | AWS credentials chain | `Ask::Providers::Bedrock` |
15
- | **Ollama** (local) | None needed | `Ask::Providers::Ollama` |
16
- | **Mistral AI** | `Ask::Auth.resolve(:mistral_api_key)` | `Ask::Providers::Mistral` |
17
- | **Cloudflare Workers AI** | `Ask::Auth.resolve(:cloudflare_api_key)` | `Ask::Providers::Cloudflare` |
3
+ All LLM providers for the ask-rb ecosystem in one gem. Implements the
4
+ `Ask::Provider` interface from `ask-core` with a capabilities-based interface:
5
+ 7 canonical provider classes plus 26 OpenAI-compatible registry entries,
6
+ a bundled model catalog, and cost calculation. Providers auto-register and
7
+ the model catalog auto-loads when the gem is required.
18
8
 
19
9
  ## Installation
20
10
 
@@ -22,102 +12,86 @@ from `ask-core` with a capabilities-based interface.
22
12
  gem "ask-llm-providers"
23
13
  ```
24
14
 
25
- ## Usage
15
+ ## Quick Start
26
16
 
27
17
  ```ruby
28
18
  require "ask-llm-providers"
29
19
 
30
- # All providers are auto-registered with Ask::Models
31
- models = Ask::Models.find("gpt-4o")
32
- # => { provider: :openai, capabilities: [...] }
33
-
34
- # Use a provider directly
20
+ # Use a provider directly; streaming yields chunks to the block
35
21
  provider = Ask::Providers::OpenAI.new
36
- provider.chat(conversation, tools: [], model: "gpt-4o") do |chunk|
22
+ provider.chat([{ role: "user", content: "Tell me a story" }], model: "gpt-4o") do |chunk|
37
23
  print chunk.content
38
24
  end
39
- ```
40
25
 
41
- ## Capabilities
42
-
43
- Each provider and model exposes its capabilities:
44
-
45
- ```ruby
46
- provider = Ask::Providers::OpenAI.new
47
- provider.capabilities
48
- # => { chat: true, streaming: true, tool_calls: true, vision: true, thinking: true,
49
- # :structured_output, :embed, :transcribe, :paint, :moderate]
50
-
51
- model = Ask::Models.find("claude-sonnet-4-5")
52
- model[:capabilities]
53
- # => { chat: true, streaming: true, tool_calls: true, vision: true, thinking: true, :prompt_caching]
54
-
55
- # Unsupported capabilities raise a helpful error
56
- provider = Ask::Providers::Anthropic.new
57
- provider.embed(["text"], model: "claude-sonnet-4-5")
58
- # => Ask::CapabilityNotSupported: Anthropic (claude-sonnet-4-5) does not support embeddings.
26
+ # Look up model metadata (capabilities, pricing, context window)
27
+ model = Ask::ModelCatalog.find("gpt-4o")
28
+ model.capabilities # => ["chat", "streaming", "tool_calls", ...]
59
29
  ```
60
30
 
31
+ ## Supported Providers
61
32
 
62
-
63
- ## Streaming
33
+ | Provider | Auth |
34
+ |---|---|
35
+ | OpenAI | `Ask::Auth.resolve(:openai_api_key)` (env `OPENAI_API_KEY`) |
36
+ | Anthropic (Claude) | `Ask::Auth.resolve(:anthropic_api_key)` (env `ANTHROPIC_API_KEY`) |
37
+ | Google Gemini | `Ask::Auth.resolve(:gemini_api_key)` (env `GEMINI_API_KEY`); Vertex AI via GCP service account |
38
+ | Amazon Bedrock | AWS credentials chain (env, `~/.aws`, instance profile) |
39
+ | Ollama (local) | none needed |
40
+ | Mistral AI | `Ask::Auth.resolve(:mistral_api_key)` (env `MISTRAL_API_KEY`) |
41
+ | Cloudflare Workers AI | `Ask::Auth.resolve(:cloudflare_api_key)` (env `CLOUDFLARE_API_KEY`) |
42
+ | 26 OpenAI-compatible | per-provider `*_API_KEY` env var (e.g. `DEEPSEEK_API_KEY`, `GROQ_API_KEY`, `OPENROUTER_API_KEY`) |
43
+
44
+ Credentials resolve through `Ask::Auth` in order: environment variables,
45
+ `~/.ask/credentials.yml`, Rails credentials, then database and OAuth
46
+ providers. Keys are read via `Ask::Auth.resolve(:<name>_api_key)`.
47
+
48
+ The 26 OpenAI-compatible entries are registered from
49
+ `Ask::LLM::OPENAI_COMPATIBLE`: DeepSeek, Groq, Together, Fireworks, Cerebras,
50
+ xAI, Perplexity, DeepInfra, Anyscale, SambaNova, Nebius, Nvidia NIM, Friendli,
51
+ Hyperbolic, Novita, Nscale, Featherless, AI/ML API, AI21, Meta, GitHub Models,
52
+ OpenRouter, OpenCode, OpenCode Go, Mimo, and Moonshot. Each uses its own
53
+ `*_API_KEY` env var; note that `opencode_go` uses `OPENCODE_GO_API_KEY` (its
54
+ own key, not `OPENCODE_API_KEY`).
55
+
56
+ ## Model Catalog
57
+
58
+ The gem bundles model metadata (capabilities, pricing, context windows,
59
+ modalities) as JSON for 12 providers: openai, anthropic, gemini, vertex_ai,
60
+ bedrock, deepseek, mistral, perplexity, xai, meta, moonshot, and nvidia_nim,
61
+ with 400+ models in total.
64
62
 
65
63
  ```ruby
66
- stream = provider.chat(
67
- [{ role: "user", content: "Tell me a story" }],
68
- model: "gpt-4o",
69
- stream: true
70
- ) do |chunk|
71
- print chunk.content
72
- end
73
-
74
- # After streaming completes, you can access the full response
75
- puts stream.accumulated_text
76
- puts stream.accumulated_usage
64
+ Ask::ModelCatalog.find("claude-sonnet-4-5") # => Ask::ModelInfo
65
+ Ask::ModelCatalog.chat_models # => filtered catalog
66
+ Ask::ModelCatalog.by_provider(:gemini)
67
+ Ask::ModelCatalog.refresh! # fetch latest from models.dev
77
68
  ```
78
69
 
79
- ## Tool Calls
70
+ ## Entry Points
80
71
 
81
- ```ruby
82
- tools = [{
83
- name: "get_weather",
84
- description: "Get weather for a location",
85
- parameters: {
86
- type: "object",
87
- properties: { location: { type: "string" } },
88
- required: ["location"]
89
- }
90
- }]
91
-
92
- response = provider.chat(
93
- [{ role: "user", content: "What's the weather in NYC?" }],
94
- model: "gpt-4o",
95
- tools: tools
96
- )
97
- # response.tool_call? => true
98
- # response.tool_calls => [{ id: "call_1", name: "get_weather", arguments: '{"location":"NYC"}' }]
99
- ```
72
+ | API | Purpose |
73
+ |---|---|
74
+ | `Ask::Providers::OpenAI/Anthropic/Google/Bedrock/Ollama/Mistral/Cloudflare` | Canonical provider classes |
75
+ | `Ask::LLM::OPENAI_COMPATIBLE` | Registry data for the 26 compatible providers |
76
+ | `Ask::LLM::Catalog.load!` / `refresh!` | Load bundled + user model data into `Ask::ModelCatalog` |
77
+ | `Ask::LLM::Aliases.resolve("claude-sonnet-4")` | Short-name to canonical model ID resolution |
78
+ | `Ask::LLM::CostCalculator.calculate(model, input_tokens:, output_tokens:)` | USD cost from model pricing |
79
+ | `Ask::Provider.resolve(:openai)` | Class lookup by registered slug |
100
80
 
101
- ## Error Handling
81
+ Providers accept a `base_url` override, so any OpenAI-compatible endpoint can
82
+ be used through `Ask::Providers::OpenAI` or `Ask::Providers::OpenAICompatible`.
102
83
 
103
- Provider errors map to structured `Ask::Error` types:
84
+ ## Full documentation
104
85
 
105
- ```ruby
106
- Ask::RateLimitError # 429 retry with backoff
107
- Ask::Unauthorized # 401/403 check your API key
108
- Ask::ServerError # 500 — provider issue
109
- Ask::ServiceUnavailable # 503 — temporary
110
- Ask::ContextLengthExceeded # context window exceeded
111
- Ask::ProviderError # other provider errors
112
- Ask::CapabilityNotSupported # feature not available on this model
113
- ```
86
+ The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
87
+ https://ask-rb.github.io/ask-docs/core/providers covers ask-llm-providers in
88
+ depth, including capabilities, streaming, tool calls, and error handling.
89
+ API reference: https://ask-rb.github.io/ask-docs/reference/api.
114
90
 
115
91
  ## Development
116
92
 
117
- ```bash
118
- bin/setup
93
+ bundle install
119
94
  bundle exec rake test
120
- ```
121
95
 
122
96
  ## License
123
97
 
data/lib/ask/llm/http.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "time"
2
3
 
3
4
  module Ask
4
5
  module LLM
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.10.1"
5
+ VERSION = "0.10.2"
6
6
  end
7
7
  end
@@ -124,7 +124,14 @@ module Ask
124
124
  def parse_stream(raw, stream, model, &block)
125
125
  each_sse_event(raw) do |data|
126
126
  parsed = JSON.parse(data) rescue next
127
- choice = parsed.dig("choices", 0) or next
127
+ choice = parsed.dig("choices", 0)
128
+ if choice.nil?
129
+ # Final chunk of an OpenAI-style stream: choices is empty but
130
+ # usage is populated. Added to the stream (for token
131
+ # accounting) without yielding — it carries no content.
132
+ stream.add(Ask::Chunk.new(content: nil, usage: parsed["usage"])) if parsed["usage"]
133
+ next
134
+ end
128
135
  delta = choice["delta"] || {}
129
136
  thinking = extract_thinking(parsed, delta)
130
137
  chunk = Ask::Chunk.new(
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.10.1
4
+ version: 0.10.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto