ask-llm-providers 0.1.1 → 0.1.3

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: e1c7c2b703b28c45fa414ddf18bb9aa7ddf896c00d97418c01b8f55966e61b52
4
- data.tar.gz: d0c7a219f49a0f981f2991fa72e249c813450a9d84f2269478ac733bfdad608a
3
+ metadata.gz: 22c3960f084ce514405b73a25093a058458f9f71ec5d3bf076a62ad312c74b94
4
+ data.tar.gz: eaf4407367c1adc93c55c35762772d599d774c8a856967c74ae9da73870243c6
5
5
  SHA512:
6
- metadata.gz: dd4dbd35bc0efe7d19a5844246c76e117601d3da4d90831eedc2e53a6345c048666b28af0d5c831dfe62557eb1434c95c51668fa010bfb676032a3159ba57195
7
- data.tar.gz: 47be20c465f50f4cd586df677d5f984d14c034df110606c965085bcf2a17a05f19d45e4716eda4f41884abffb3afca622a39b6ed81961a27c4bea88754b258cf
6
+ metadata.gz: 9c5fc807124321bde12f4b239c0c5d32ff278edf9a446949346237cd24bd562582c9b6f0acecd27dc64be27adb2d8250eee34a4b4b64ae779c413da0e15f34d1
7
+ data.tar.gz: 2691464aefbae83c68ddf63c3a278e5f459d7fc222adb320d96d2c57a4cf2702ed50da1a056f8a83b22b64d7dbf804261371b7ceff7b8569799096d6b590b65f
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.3"
6
6
  end
7
7
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Providers
5
+ # Mimo API — an OpenAI-compatible provider
6
+ class Mimo < OpenAI
7
+ def api_base
8
+ @config.base_url || ENV["MIMO_API_BASE"] || "https://token-plan-sgp.xiaomimimo.com/v1"
9
+ end
10
+
11
+ def headers
12
+ key = @config.api_key || ENV["MIMO_API_KEY"]
13
+ h = { "Content-Type" => "application/json" }
14
+ h["Authorization"] = "Bearer #{key}" if key
15
+ h
16
+ end
17
+
18
+ class << self
19
+ def slug; "mimo"; end
20
+ def configuration_options; %i[api_key base_url]; end
21
+ def configuration_requirements; %i[api_key]; end
22
+ def configured?(config)
23
+ key = config.respond_to?(:api_key) ? config.api_key : nil
24
+ key ||= ENV["MIMO_API_KEY"]
25
+ key.to_s.length > 0
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -7,6 +7,7 @@ module Ask
7
7
  # +base_url+ override.
8
8
  class OpenAI < Ask::Provider
9
9
  def initialize(config = {})
10
+ @provider_keys = extract_provider_keys(config)
10
11
  config = normalize_config(config)
11
12
  super(config)
12
13
  @http = build_http
@@ -57,22 +58,31 @@ module Ask
57
58
  end
58
59
  def configuration_options; %i[api_key base_url organization_id project_id]; end
59
60
  def configuration_requirements; %i[api_key]; end
60
- def configured?(config)
61
- (config.respond_to?(:api_key) && !config.api_key.to_s.empty?) ||
62
- (config.respond_to?(:openai_api_key) && !config.openai_api_key.to_s.empty?)
63
- end
61
+ def assume_models_exist?; false; end
64
62
  end
65
63
 
66
64
  private
67
65
 
66
+ # Extract and store any provider-specific config keys (e.g., opencode_api_key).
67
+ # These are not part of the standard OpenAI config but are used by subclasses.
68
+ def extract_provider_keys(config)
69
+ return {} unless config.is_a?(Hash)
70
+ known = %i[api_key base_url organization_id project_id openai_api_key]
71
+ config.reject { |k, _| known.include?(k.to_sym) }
72
+ end
73
+
74
+ # Restore provider-specific keys after normalize_config strips standard ones.
68
75
  def normalize_config(config)
69
76
  return config if !config.is_a?(Hash)
70
- Ask::LLM::Config.new(
77
+
78
+ merged = {
71
79
  api_key: config[:api_key] || config["api_key"] || config[:openai_api_key],
72
80
  base_url: config[:base_url] || config["base_url"],
73
81
  organization_id: config[:organization_id] || config["organization_id"],
74
82
  project_id: config[:project_id] || config["project_id"]
75
- )
83
+ }.merge(@provider_keys)
84
+
85
+ Ask::LLM::Config.new(merged)
76
86
  end
77
87
 
78
88
  def build_http
@@ -140,12 +150,22 @@ module Ask
140
150
  parsed = JSON.parse(data) rescue next
141
151
  choice = parsed.dig("choices", 0) or next
142
152
  delta = choice["delta"] || {}
143
- chunk = Ask::Chunk.new(content: delta["content"], tool_calls: parse_stream_tool_calls(delta["tool_calls"]), finish_reason: choice["finish_reason"], usage: parsed["usage"])
153
+ thinking = extract_thinking(parsed, delta)
154
+ chunk = Ask::Chunk.new(content: delta["content"], tool_calls: parse_stream_tool_calls(delta["tool_calls"]), finish_reason: choice["finish_reason"], usage: parsed["usage"], thinking: thinking)
144
155
  stream.add(chunk)
145
156
  yield chunk if block_given?
146
157
  end
147
158
  end
148
159
 
160
+ # Extract thinking/reasoning content from provider response.
161
+ # Some providers (Anthropic, DeepSeek) send thinking in a separate field.
162
+ def extract_thinking(parsed, delta)
163
+ delta["reasoning_content"] || delta["thinking"] ||
164
+ parsed.dig("choices", 0, "delta", "reasoning_content") ||
165
+ parsed.dig("choices", 0, "delta", "thinking") ||
166
+ parsed.dig("choices", 0, "reasoning_content")
167
+ end
168
+
149
169
  def parse_stream_tool_calls(calls)
150
170
  return nil unless calls&.any?
151
171
  calls.map { |tc| { id: tc["id"], name: tc.dig("function", "name"), arguments: tc.dig("function", "arguments"), index: tc["index"] } }
@@ -153,3 +173,23 @@ module Ask
153
173
  end
154
174
  end
155
175
  end
176
+
177
+ # When the OpenAI provider is subclassed (e.g. OpenCode), normalize_config
178
+ # should also check for env vars matching the subclass slug.
179
+ def normalize_config(config)
180
+ return config if !config.is_a?(Hash)
181
+
182
+ slug = self.class.slug
183
+ env_key = ENV["#{slug.upcase}_API_KEY"]
184
+ env_base = ENV["#{slug.upcase}_API_BASE"]
185
+
186
+ merged = {
187
+ api_key: config[:api_key] || config["api_key"] || config[:"#{slug}_api_key"] || config[:"#{slug}_api_key"] || config[:openai_api_key] || env_key,
188
+ base_url: config[:base_url] || config["base_url"] || env_base,
189
+ organization_id: config[:organization_id] || config["organization_id"],
190
+ project_id: config[:project_id] || config["project_id"]
191
+ }.merge(config.reject { |k, _| %i[api_key base_url organization_id project_id openai_api_key].include?(k.to_sym) })
192
+
193
+ # Also preserve original config for subclass-specific key access
194
+ Ask::LLM::Config.new(merged)
195
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Providers
5
+ # OpenCode API — an OpenAI-compatible provider at opencode.ai
6
+ class OpenCode < OpenAI
7
+ def api_base
8
+ @config.base_url || ENV["OPENCODE_API_BASE"] || "https://opencode.ai/zen/v1"
9
+ end
10
+
11
+ def headers
12
+ key = @config.api_key || ENV["OPENCODE_API_KEY"]
13
+ h = { "Content-Type" => "application/json" }
14
+ h["Authorization"] = "Bearer #{key}" if key
15
+ h
16
+ end
17
+
18
+ class << self
19
+ def slug; "opencode"; end
20
+ def configuration_options; %i[api_key base_url]; end
21
+ def configuration_requirements; %i[api_key]; end
22
+ def configured?(config)
23
+ key = config.respond_to?(:api_key) ? config.api_key : nil
24
+ key ||= ENV["OPENCODE_API_KEY"]
25
+ key.to_s.length > 0
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Providers
5
+ # OpenCode Go API — an OpenAI-compatible provider at opencode.ai/zen/go
6
+ class OpenCodeGo < OpenAI
7
+ def api_base
8
+ @config.base_url || ENV["OPENCODE_GO_API_BASE"] || "https://opencode.ai/zen/go/v1"
9
+ end
10
+
11
+ def headers
12
+ key = @config.api_key || ENV["OPENCODE_API_KEY"] || ENV["OPENCODE_GO_API_KEY"]
13
+ h = { "Content-Type" => "application/json" }
14
+ h["Authorization"] = "Bearer #{key}" if key
15
+ h
16
+ end
17
+
18
+ class << self
19
+ def slug; "opencode_go"; end
20
+ def configuration_options; %i[api_key base_url]; end
21
+ def configuration_requirements; %i[api_key]; end
22
+ def configured?(config)
23
+ key = config.respond_to?(:api_key) ? config.api_key : nil
24
+ key ||= ENV["OPENCODE_API_KEY"] || ENV["OPENCODE_GO_API_KEY"]
25
+ key.to_s.length > 0
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -20,6 +20,9 @@ require_relative "ask/provider/bedrock"
20
20
  require_relative "ask/provider/ollama"
21
21
  require_relative "ask/provider/mistral"
22
22
  require_relative "ask/provider/cloudflare"
23
+ require_relative "ask/provider/opencode"
24
+ require_relative "ask/provider/opencode_go"
25
+ require_relative "ask/provider/mimo"
23
26
 
24
27
  # Register providers with the Ask::Provider registry
25
28
  Ask::Provider.register(:openai, Ask::Providers::OpenAI)
@@ -47,3 +50,6 @@ Ask::Provider.register(:cloudflare, Ask::Providers::Cloudflare)
47
50
  ))
48
51
  end
49
52
  end
53
+
54
+ # configured via environment variables:
55
+
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.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -183,9 +183,12 @@ files:
183
183
  - lib/ask/provider/bedrock.rb
184
184
  - lib/ask/provider/cloudflare.rb
185
185
  - lib/ask/provider/google.rb
186
+ - lib/ask/provider/mimo.rb
186
187
  - lib/ask/provider/mistral.rb
187
188
  - lib/ask/provider/ollama.rb
188
189
  - lib/ask/provider/openai.rb
190
+ - lib/ask/provider/opencode.rb
191
+ - lib/ask/provider/opencode_go.rb
189
192
  homepage: https://github.com/ask-rb/ask-llm-providers
190
193
  licenses:
191
194
  - MIT