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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +62 -88
- data/lib/ask/llm/http.rb +1 -0
- data/lib/ask/llm/version.rb +1 -1
- data/lib/ask/provider/openai.rb +8 -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: 3eae68598ee63c7ef7688b717687b06a434f12c740141a3b84bde44752a873d9
|
|
4
|
+
data.tar.gz: f21d623f34793e0eff9a08dcb5c1f39c8bba236c541022f1f969a5a273bcb5b7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
4
|
-
from `ask-core` with a capabilities-based interface
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
##
|
|
15
|
+
## Quick Start
|
|
26
16
|
|
|
27
17
|
```ruby
|
|
28
18
|
require "ask-llm-providers"
|
|
29
19
|
|
|
30
|
-
#
|
|
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(
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
##
|
|
70
|
+
## Entry Points
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
+
## Full documentation
|
|
104
85
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
data/lib/ask/llm/version.rb
CHANGED
data/lib/ask/provider/openai.rb
CHANGED
|
@@ -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)
|
|
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(
|