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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83d9430295753c63779ef81db7fbbc4e9c56c3e1848f44e53a763c70c43cc1f8
4
- data.tar.gz: a9ea83f80669da1e5869204a42af7bc52e071d81f355da80df26002246d3b828
3
+ metadata.gz: 915a6861e0cbc8cdddcc9f1b9f936d9f95896943f1307f199dfe604b8f4d527c
4
+ data.tar.gz: 9f667d8f56d4e1265314c4dfdeaebbefbab7dae0a74724dc31ac79074decf3ef
5
5
  SHA512:
6
- metadata.gz: 870a8543f6810948e62f65b18b39f0924a78ec393108f74e992ecff25cfe8e14ae8331d287d436afdb88f3792379c778598b3188642c4c5a002bb374f56a0c8e
7
- data.tar.gz: 211593c5668424aab22e2f03c6230d6ad791a4f7f6002b60b8e26e879f2ae8d6e28782b29790611c796c8fa2d0edab1494e1a9d61ec638a4b78fde1fbba639a2
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 Adding Groq
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
- deepseek: { api_base: "https://api.deepseek.com", api_key_env: "DEEPSEEK_API_KEY",
15
- reasoning_content: true,
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
- openrouter: { api_base: "https://openrouter.ai/api/v1", api_key_env: "OPENROUTER_API_KEY",
19
- extra_headers: { "HTTP-Referer" => "https://github.com/ask-rb",
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
- opencode: { api_base: "https://opencode.ai/zen/v1", api_key_env: "OPENCODE_API_KEY",
25
- capabilities: { chat: true, streaming: true, tool_calls: true } },
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
- opencode_go: { api_base: "https://opencode.ai/zen/go/v1", api_key_env: "OPENCODE_GO_API_KEY",
28
- alternate_env: "OPENCODE_API_KEY",
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
- mimo: { api_base: "https://token-plan-sgp.xiaomimimo.com/v1", api_key_env: "MIMO_API_KEY",
32
- capabilities: { chat: true, streaming: true } },
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
- groq: { api_base: "https://api.groq.com/openai/v1", api_key_env: "GROQ_API_KEY",
35
- capabilities: { chat: true, streaming: true, tool_calls: true, vision: true } },
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
- together: { api_base: "https://api.together.xyz/v1", api_key_env: "TOGETHER_API_KEY",
38
- capabilities: { chat: true, streaming: true, tool_calls: true } },
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: { api_base: "https://api.fireworks.ai/inference/v1", api_key_env: "FIREWORKS_API_KEY",
41
- capabilities: { chat: true, streaming: true, tool_calls: true } },
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
- perplexity: { api_base: "https://api.perplexity.ai", api_key_env: "PERPLEXITY_API_KEY",
44
- capabilities: { chat: true, streaming: true } },
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
- cerebras: { api_base: "https://api.cerebras.ai/v1", api_key_env: "CEREBRAS_API_KEY",
47
- capabilities: { chat: true, streaming: true, tool_calls: true } },
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
- xai: { api_base: "https://api.x.ai/v1", api_key_env: "XAI_API_KEY",
50
- capabilities: { chat: true, streaming: true, tool_calls: true, vision: true,
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
- moonshot: { api_base: "https://api.moonshot.ai/v1", api_key_env: "MOONSHOT_API_KEY",
54
- capabilities: { chat: true, streaming: true } }
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.1"
6
6
  end
7
7
  end
@@ -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.5.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