ask-llm-providers 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +14 -0
- data/lib/ask/llm/cost_calculator.rb +86 -0
- data/lib/ask/llm/openai_compatible.rb +75 -31
- data/lib/ask/llm/version.rb +1 -1
- data/lib/ask-llm-providers.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 915a6861e0cbc8cdddcc9f1b9f936d9f95896943f1307f199dfe604b8f4d527c
|
|
4
|
+
data.tar.gz: 9f667d8f56d4e1265314c4dfdeaebbefbab7dae0a74724dc31ac79074decf3ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 528d7105a625887f249e0fe7a0f0dad3469e47f73cfad59d02d85613cdecb287d9277c007cc50b1fc574053fc44ac27b6fa198cf3221ec0de8881dca2be65b33
|
|
7
|
+
data.tar.gz: b6d3e1dc25804a7808e7cfa45ec2ff7f9542ca43fad5336a7131c7dd18930264badcf9575f0ee993781f7ced64cd9dfa496a8e14851b4f73e60fa0db4f1c780f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## [0.6.1] — 2026-07-17
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **`Ask::LLM::CostCalculator`** — calculate LLM API costs from model pricing data. Supports input, output, cache read/write, and reasoning tokens. Returns cost in USD or nil if no pricing data available. Works with any object responding to `#pricing` (Ask::ModelInfo, raw hash, etc.).
|
|
6
|
+
- **`CostCalculator.breakdown`** — returns a component-by-component cost breakdown hash.
|
|
7
|
+
|
|
8
|
+
## [0.6.0] — 2026-07-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **14 new OpenAI-compatible providers** — aiml, ai21, anyscale, deepinfra, featherless, friendli, github, hyperbolic, meta, nebius, novita, nscale, nvidia_nim, sambanova. Each is one line in the registry. Total OpenAI-compatible providers: 26. Total providers: 33.
|
|
13
|
+
- **Auto-generated tests** — `OpenAICompatibleTest` now builds its test list from `OPENAI_COMPATIBLE` dynamically. Adding a provider automatically generates 5 identity tests (registered, slug, capabilities, api_base, requires_api_key).
|
|
14
|
+
|
|
1
15
|
## [0.5.0] — 2026-07-17
|
|
2
16
|
|
|
3
17
|
### Added
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module LLM
|
|
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
|
+
module CostCalculator
|
|
24
|
+
MILLION = 1_000_000
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# Calculate the total cost in USD for a model invocation.
|
|
28
|
+
#
|
|
29
|
+
# @param model [Ask::ModelInfo, #pricing] the model
|
|
30
|
+
# @param input_tokens [Integer]
|
|
31
|
+
# @param output_tokens [Integer]
|
|
32
|
+
# @param cache_read_tokens [Integer]
|
|
33
|
+
# @param cache_write_tokens [Integer]
|
|
34
|
+
# @param reasoning_tokens [Integer] tokens billed at reasoning rate
|
|
35
|
+
# @return [Float, nil] cost in USD, or nil if no pricing data
|
|
36
|
+
def calculate(model, input_tokens: 0, output_tokens: 0,
|
|
37
|
+
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
|
|
41
|
+
|
|
42
|
+
sum = cost(input_tokens, rates[:input_per_million])
|
|
43
|
+
sum += cost(output_tokens, rates[:output_per_million])
|
|
44
|
+
|
|
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
|
+
if reasoning_tokens > 0
|
|
52
|
+
rate = rates[:reasoning_output_per_million] || rates[:output_per_million]
|
|
53
|
+
sum += cost(reasoning_tokens, rate)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sum
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Cost breakdown with individual components.
|
|
60
|
+
#
|
|
61
|
+
# @return [Hash, nil]
|
|
62
|
+
def breakdown(model, input_tokens: 0, output_tokens: 0,
|
|
63
|
+
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
|
|
67
|
+
|
|
68
|
+
{
|
|
69
|
+
input: cost(input_tokens, rates[:input_per_million]),
|
|
70
|
+
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
|
|
74
|
+
}.compact
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def cost(tokens, rate)
|
|
80
|
+
return 0.0 unless rate && tokens > 0
|
|
81
|
+
(tokens * rate) / MILLION.to_f
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -7,51 +7,95 @@ module Ask
|
|
|
7
7
|
# Each entry is configuration for {Ask::Providers::OpenAICompatible}.
|
|
8
8
|
# To add a new provider, add one line here — no new file, no subclass.
|
|
9
9
|
#
|
|
10
|
-
# @example
|
|
10
|
+
# @example
|
|
11
11
|
# groq: { api_base: "https://api.groq.com/openai/v1", api_key_env: "GROQ_API_KEY" }
|
|
12
12
|
#
|
|
13
13
|
OPENAI_COMPATIBLE = {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
capabilities: { chat: true, streaming: true, tool_calls: true, thinking: true } },
|
|
14
|
+
aiml: { api_base: "https://api.aimlapi.com/v1", api_key_env: "AIML_API_KEY",
|
|
15
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"X-Title" => "ask-rb" },
|
|
21
|
-
capabilities: { chat: true, streaming: true, tool_calls: true, vision: true,
|
|
22
|
-
thinking: true, structured_output: true } },
|
|
17
|
+
ai21: { api_base: "https://api.ai21.com/studio/v1", api_key_env: "AI21_API_KEY",
|
|
18
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
anyscale: { api_base: "https://api.endpoints.anyscale.com/v1", api_key_env: "ANYSCALE_API_KEY",
|
|
21
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
23
|
+
cerebras: { api_base: "https://api.cerebras.ai/v1", api_key_env: "CEREBRAS_API_KEY",
|
|
24
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
deepinfra: { api_base: "https://api.deepinfra.com/v1/openai", api_key_env: "DEEPINFRA_API_KEY",
|
|
27
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
deepseek: { api_base: "https://api.deepseek.com", api_key_env: "DEEPSEEK_API_KEY",
|
|
30
|
+
reasoning_content: true,
|
|
31
|
+
capabilities: { chat: true, streaming: true, tool_calls: true, thinking: true } },
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
featherless: { api_base: "https://api.featherless.ai/v1", api_key_env: "FEATHERLESS_API_KEY",
|
|
34
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
39
35
|
|
|
40
|
-
fireworks:
|
|
41
|
-
|
|
36
|
+
fireworks: { api_base: "https://api.fireworks.ai/inference/v1", api_key_env: "FIREWORKS_API_KEY",
|
|
37
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
friendli: { api_base: "https://api.friendli.ai/serverless/v1", api_key_env: "FRIENDLI_API_KEY",
|
|
40
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
45
41
|
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
github: { api_base: "https://models.inference.ai.azure.com", api_key_env: "GITHUB_API_KEY",
|
|
43
|
+
capabilities: { chat: true, streaming: true, tool_calls: true, vision: true } },
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
thinking: true } },
|
|
45
|
+
groq: { api_base: "https://api.groq.com/openai/v1", api_key_env: "GROQ_API_KEY",
|
|
46
|
+
capabilities: { chat: true, streaming: true, tool_calls: true, vision: true } },
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
hyperbolic: { api_base: "https://api.hyperbolic.xyz/v1", api_key_env: "HYPERBOLIC_API_KEY",
|
|
49
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
50
|
+
|
|
51
|
+
meta: { api_base: "https://api.llama.com/compat/v1", api_key_env: "LLAMA_API_KEY",
|
|
52
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
53
|
+
|
|
54
|
+
mimo: { api_base: "https://token-plan-sgp.xiaomimimo.com/v1", api_key_env: "MIMO_API_KEY",
|
|
55
|
+
capabilities: { chat: true, streaming: true } },
|
|
56
|
+
|
|
57
|
+
moonshot: { api_base: "https://api.moonshot.ai/v1", api_key_env: "MOONSHOT_API_KEY",
|
|
58
|
+
capabilities: { chat: true, streaming: true } },
|
|
59
|
+
|
|
60
|
+
nebius: { api_base: "https://api.studio.nebius.ai/v1", api_key_env: "NEBIUS_API_KEY",
|
|
61
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
62
|
+
|
|
63
|
+
novita: { api_base: "https://api.novita.ai/v3/openai", api_key_env: "NOVITA_API_KEY",
|
|
64
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
65
|
+
|
|
66
|
+
nscale: { api_base: "https://inference.api.nscale.com/v1", api_key_env: "NSCALE_API_KEY",
|
|
67
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
68
|
+
|
|
69
|
+
nvidia_nim: { api_base: "https://integrate.api.nvidia.com/v1", api_key_env: "NVIDIA_NIM_API_KEY",
|
|
70
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
71
|
+
|
|
72
|
+
opencode: { api_base: "https://opencode.ai/zen/v1", api_key_env: "OPENCODE_API_KEY",
|
|
73
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
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 } },
|
|
78
|
+
|
|
79
|
+
openrouter: { api_base: "https://openrouter.ai/api/v1", api_key_env: "OPENROUTER_API_KEY",
|
|
80
|
+
extra_headers: { "HTTP-Referer" => "https://github.com/ask-rb",
|
|
81
|
+
"X-Title" => "ask-rb" },
|
|
82
|
+
capabilities: { chat: true, streaming: true, tool_calls: true, vision: true,
|
|
83
|
+
thinking: true, structured_output: true } },
|
|
84
|
+
|
|
85
|
+
perplexity: { api_base: "https://api.perplexity.ai", api_key_env: "PERPLEXITY_API_KEY",
|
|
86
|
+
capabilities: { chat: true, streaming: true } },
|
|
87
|
+
|
|
88
|
+
sambanova: { api_base: "https://api.sambanova.ai/v1", api_key_env: "SAMBANOVA_API_KEY",
|
|
89
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
90
|
+
|
|
91
|
+
together: { api_base: "https://api.together.xyz/v1", api_key_env: "TOGETHER_API_KEY",
|
|
92
|
+
capabilities: { chat: true, streaming: true, tool_calls: true } },
|
|
93
|
+
|
|
94
|
+
xai: { api_base: "https://api.x.ai/v1", api_key_env: "XAI_API_KEY",
|
|
95
|
+
capabilities: { chat: true, streaming: true, tool_calls: true, vision: true,
|
|
96
|
+
thinking: true } }
|
|
55
97
|
}.freeze
|
|
98
|
+
|
|
99
|
+
OPENAI_COMPATIBLE_COUNT = OPENAI_COMPATIBLE.size
|
|
56
100
|
end
|
|
57
101
|
end
|
data/lib/ask/llm/version.rb
CHANGED
data/lib/ask-llm-providers.rb
CHANGED
|
@@ -17,6 +17,9 @@ require_relative "ask/llm/aliases"
|
|
|
17
17
|
# Provider transformation contract
|
|
18
18
|
require_relative "ask/llm/provider_config"
|
|
19
19
|
|
|
20
|
+
# Cost calculator
|
|
21
|
+
require_relative "ask/llm/cost_calculator"
|
|
22
|
+
|
|
20
23
|
# OpenAI-compatible provider registry (data, not classes)
|
|
21
24
|
require_relative "ask/llm/openai_compatible"
|
|
22
25
|
|
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.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -180,6 +180,7 @@ files:
|
|
|
180
180
|
- lib/ask/llm/aliases.rb
|
|
181
181
|
- lib/ask/llm/catalog.rb
|
|
182
182
|
- lib/ask/llm/config.rb
|
|
183
|
+
- lib/ask/llm/cost_calculator.rb
|
|
183
184
|
- lib/ask/llm/http.rb
|
|
184
185
|
- lib/ask/llm/models/anthropic.json
|
|
185
186
|
- lib/ask/llm/models/bedrock.json
|