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 +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/llm_providers/providers/openai.rb +26 -8
- data/lib/llm_providers/providers/openrouter.rb +74 -3
- data/lib/llm_providers/version.rb +1 -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: 11691e71acf36321f3ea66a3fec9ff250eceb606080c4b0d153af8d6ce405329
|
|
4
|
+
data.tar.gz: 64409688e08600e19b7a9cd310b8243783dfba3ae50de28483b2a0c22b0383e4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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[
|
|
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:
|
|
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:
|
|
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[
|
|
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
|
|
234
|
-
code:
|
|
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
|