llm_providers 0.1.1 → 0.2.0

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: f2f9351f5b2fcd03887b26b61aad5a7efdd810207d9ae96577455b5450730c96
4
- data.tar.gz: 17aa0c0c838045102727f5650ba5be88d232ac6e54b7bf283eb5d8bcc7b1b2d4
3
+ metadata.gz: 11691e71acf36321f3ea66a3fec9ff250eceb606080c4b0d153af8d6ce405329
4
+ data.tar.gz: 64409688e08600e19b7a9cd310b8243783dfba3ae50de28483b2a0c22b0383e4
5
5
  SHA512:
6
- metadata.gz: 6ec192e3c3bd4d66d29e7dd619f7821cd2a79d41bd0df3ee61376dee03bf5fa0fd78b1fb1fb9f0ee2a4481c0f419139decade2f70767bc7493d6c59f619bc158
7
- data.tar.gz: 5482d3ed05b2dedc3285cfc6b337e08ed1334826eefa4549e79a64a1effabe4c97078fd550b0946db24b4b5b9f5f14885636a683352cd6f9559e9982808758e1
6
+ metadata.gz: f481b19793475b67cb25ba44f05e63ecd0b16ac9527e8d2274b3e2a9b8bb21f84639e5980cb1049e10ab4146c9476c0dd53051802c12a52f50655635b96e2ecb
7
+ data.tar.gz: 688e43303a8b97593698091710f4f1e945278974a941e9d5f60a6e4da5262118786d6a10522a4a2f7791242958ad7b57df1a80dccdd88ed6642b0c8315040575
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0] - 2026-02-27
4
+
5
+ ### Added
6
+
7
+ - OpenRouter provider is now fully supported (no longer experimental)
8
+ - Custom headers: `X-Title`, `HTTP-Referer` via `app_name:` / `app_url:` options or ENV
9
+ - Provider routing: `provider:` option for order, fallback, data collection preferences
10
+ - `Openrouter.models` class method for model discovery
11
+ - Improved error handling with upstream provider name from OpenRouter metadata
12
+
13
+ ### Changed
14
+
15
+ - Extracted `request_headers` method in OpenAI provider for extensibility
16
+ - Extracted `format_stream_error` / `parse_sync_error` methods in OpenAI provider for extensibility
17
+
3
18
  ## [0.1.1] - 2026-02-27
4
19
 
5
20
  ### Fixed
@@ -111,6 +111,13 @@ module LlmProviders
111
111
  end
112
112
  end
113
113
 
114
+ def request_headers
115
+ {
116
+ "Content-Type" => "application/json",
117
+ "Authorization" => "Bearer #{api_key}"
118
+ }
119
+ end
120
+
114
121
  def stream_response(payload, &block)
115
122
  payload[:stream] = true
116
123
  payload[:stream_options] = { include_usage: true }
@@ -133,7 +140,7 @@ module LlmProviders
133
140
  event = JSON.parse(data)
134
141
 
135
142
  if event["error"]
136
- stream_error = event.dig("error", "message") || event["error"].to_s
143
+ stream_error = format_stream_error(event)
137
144
  next
138
145
  end
139
146
 
@@ -176,8 +183,7 @@ module LlmProviders
176
183
  end
177
184
 
178
185
  response = conn.post(self.class::API_URL) do |req|
179
- req.headers["Content-Type"] = "application/json"
180
- req.headers["Authorization"] = "Bearer #{api_key}"
186
+ request_headers.each { |k, v| req.headers[k] = v }
181
187
  req.body = payload.to_json
182
188
  req.options.on_data = proc do |chunk, _|
183
189
  raw_chunks += chunk
@@ -192,7 +198,7 @@ module LlmProviders
192
198
  # Process any remaining data in the buffer
193
199
  process_sse_line.call(line_buffer) unless line_buffer.empty?
194
200
 
195
- raise ProviderError.new(stream_error, code: "openai_error") if stream_error
201
+ raise ProviderError.new(stream_error, code: error_code) if stream_error
196
202
 
197
203
  unless response.success?
198
204
  error_body = begin
@@ -203,7 +209,7 @@ module LlmProviders
203
209
  error_msg = error_body.dig("error", "message") || (raw_chunks.empty? ? nil : raw_chunks) || response.body.to_s
204
210
  raise ProviderError.new(
205
211
  error_msg[0, 500],
206
- code: "openai_error"
212
+ code: error_code
207
213
  )
208
214
  end
209
215
 
@@ -224,14 +230,14 @@ module LlmProviders
224
230
  started_at = Time.now
225
231
 
226
232
  response = http_client.post(self.class::API_URL) do |req|
227
- req.headers["Authorization"] = "Bearer #{api_key}"
233
+ request_headers.each { |k, v| req.headers[k] = v }
228
234
  req.body = payload
229
235
  end
230
236
 
231
237
  unless response.success?
232
238
  raise ProviderError.new(
233
- response.body.dig("error", "message") || "API error",
234
- code: "openai_error"
239
+ parse_sync_error(response),
240
+ code: error_code
235
241
  )
236
242
  end
237
243
 
@@ -260,6 +266,18 @@ module LlmProviders
260
266
  }
261
267
  end
262
268
 
269
+ def error_code
270
+ "openai_error"
271
+ end
272
+
273
+ def format_stream_error(event)
274
+ event.dig("error", "message") || event["error"].to_s
275
+ end
276
+
277
+ def parse_sync_error(response)
278
+ response.body.dig("error", "message") || "API error"
279
+ end
280
+
263
281
  def parse_tool_input(arguments)
264
282
  return {} if arguments.nil? || arguments.empty?
265
283
 
@@ -2,11 +2,45 @@
2
2
 
3
3
  module LlmProviders
4
4
  module Providers
5
- # Experimental: OpenRouter support is provided as-is.
6
- # It wraps the OpenAI-compatible API at openrouter.ai.
7
- # Not all features may work as expected with every model.
8
5
  class Openrouter < Openai
9
6
  API_URL = "https://openrouter.ai/api/v1/chat/completions"
7
+ MODELS_URL = "https://openrouter.ai/api/v1/models"
8
+
9
+ def self.models
10
+ api_key = ENV.fetch("OPENROUTER_API_KEY")
11
+ conn = Faraday.new do |f|
12
+ f.response :json
13
+ f.adapter Faraday.default_adapter
14
+ end
15
+
16
+ response = conn.get(MODELS_URL) do |req|
17
+ req.headers["Authorization"] = "Bearer #{api_key}"
18
+ end
19
+
20
+ unless response.success?
21
+ error_msg = response.body.dig("error", "message") || "Failed to fetch models"
22
+ raise ProviderError.new(error_msg, code: "openrouter_error")
23
+ end
24
+
25
+ (response.body["data"] || []).map do |model|
26
+ {
27
+ id: model["id"],
28
+ name: model["name"],
29
+ context_length: model["context_length"],
30
+ pricing: {
31
+ prompt: model.dig("pricing", "prompt"),
32
+ completion: model.dig("pricing", "completion")
33
+ }
34
+ }
35
+ end
36
+ end
37
+
38
+ def initialize(app_name: nil, app_url: nil, provider: nil, **options)
39
+ super(**options)
40
+ @app_name = app_name || ENV["OPENROUTER_APP_NAME"]
41
+ @app_url = app_url || ENV["OPENROUTER_APP_URL"]
42
+ @provider_preferences = provider
43
+ end
10
44
 
11
45
  protected
12
46
 
@@ -17,6 +51,43 @@ module LlmProviders
17
51
  def api_key
18
52
  ENV.fetch("OPENROUTER_API_KEY")
19
53
  end
54
+
55
+ private
56
+
57
+ def build_payload(messages, system, tools)
58
+ payload = super
59
+ payload[:provider] = @provider_preferences if @provider_preferences
60
+ payload
61
+ end
62
+
63
+ def request_headers
64
+ headers = super
65
+ headers["X-Title"] = @app_name if @app_name
66
+ headers["HTTP-Referer"] = @app_url if @app_url
67
+ headers
68
+ end
69
+
70
+ def error_code
71
+ "openrouter_error"
72
+ end
73
+
74
+ def format_stream_error(event)
75
+ message = event.dig("error", "message") || event["error"].to_s
76
+ provider_name = event.dig("error", "metadata", "provider_name")
77
+ provider_name ? "[#{provider_name}] #{message}" : message
78
+ end
79
+
80
+ def parse_sync_error(response)
81
+ body = response.body
82
+ body = begin
83
+ JSON.parse(body)
84
+ rescue StandardError
85
+ {}
86
+ end if body.is_a?(String)
87
+ message = body.dig("error", "message") || "API error"
88
+ provider_name = body.dig("error", "metadata", "provider_name")
89
+ provider_name ? "[#{provider_name}] #{message}" : message
90
+ end
20
91
  end
21
92
  end
22
93
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmProviders
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_providers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaba