llm_gateway 0.8.0 → 0.8.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 +10 -0
- data/README.md +15 -0
- data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +7 -1
- data/lib/llm_gateway/agents/harness.rb +7 -0
- data/lib/llm_gateway/agents/in_memory_session_manager.rb +4 -0
- data/lib/llm_gateway/clients/openai.rb +84 -1
- data/lib/llm_gateway/errors.rb +10 -1
- data/lib/llm_gateway/proxy/adapter.rb +3 -2
- data/lib/llm_gateway/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c70b5fdd6e77e60cbe437a5698e078b7aa88fb41dca96311dcf9a58585238476
|
|
4
|
+
data.tar.gz: cb9dd1f8aa71c33751ddd270acb849d964ed137b6b3fdd6c96e072082bc6e488
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d21ab094dd5f600731e12541c6ba0a672eb8d0862c732ce2f5f2a415c394024ff87565dee5820e2d1dcec5ee665715d7beacc5eb5fb5a35d518e98b4c3c6c6e1
|
|
7
|
+
data.tar.gz: 97f4cabb1416beddc261fc849d23823722f9c36afb70843fcb1ef9e748e7f7eeec7f8bb4cfa0f92b25dbc864bf2c27e8751eeb9367f5c6233a906ac79d483219
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.8.1](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.8.1) (2026-06-27)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.8.0...v0.8.1)
|
|
6
|
+
|
|
7
|
+
**Merged pull requests:**
|
|
8
|
+
|
|
9
|
+
- fix: do not return empty assistant blocks [\#94](https://github.com/Hyper-Unearthing/llm_gateway/pull/94) ([billybonks](https://github.com/billybonks))
|
|
10
|
+
- feat: when codex hits a rate limit pass all the information [\#93](https://github.com/Hyper-Unearthing/llm_gateway/pull/93) ([billybonks](https://github.com/billybonks))
|
|
11
|
+
- Feat/fix proxy client [\#92](https://github.com/Hyper-Unearthing/llm_gateway/pull/92) ([billybonks](https://github.com/billybonks))
|
|
12
|
+
|
|
3
13
|
## [v0.8.0](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.8.0) (2026-06-06)
|
|
4
14
|
|
|
5
15
|
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.7.0...v0.8.0)
|
data/README.md
CHANGED
|
@@ -916,6 +916,21 @@ puts result.content.select { |b| b.type == "text" }.map(&:text).join
|
|
|
916
916
|
|
|
917
917
|
If your app refreshes tokens in the background, rebuild the adapter (or recreate client state) with the newest `access_token` before subsequent calls.
|
|
918
918
|
|
|
919
|
+
### Codex rate-limit reset metadata
|
|
920
|
+
|
|
921
|
+
OpenAI Codex usage-limit responses include reset information on `LlmGateway::Errors::RateLimitError`:
|
|
922
|
+
|
|
923
|
+
```ruby
|
|
924
|
+
begin
|
|
925
|
+
adapter.stream("Hello from OAuth auth", model: "gpt-5.4")
|
|
926
|
+
rescue LlmGateway::Errors::RateLimitError => e
|
|
927
|
+
puts e.message # "The usage limit has been reached"
|
|
928
|
+
puts e.reset_after_seconds # primary reset window, when available
|
|
929
|
+
puts e.reset_at # Time for the primary reset, when available
|
|
930
|
+
puts e.rate_limit_info.inspect # full parsed Codex headers/body metadata
|
|
931
|
+
end
|
|
932
|
+
```
|
|
933
|
+
|
|
919
934
|
### Token refresh responsibility
|
|
920
935
|
|
|
921
936
|
#### Library’s role (llm_gateway)
|
|
@@ -330,7 +330,9 @@ module LlmGateway
|
|
|
330
330
|
end
|
|
331
331
|
|
|
332
332
|
def serialized_blocks
|
|
333
|
-
blocks.compact.
|
|
333
|
+
blocks.compact.filter_map do |content_block|
|
|
334
|
+
next if blank_text_block?(content_block)
|
|
335
|
+
|
|
334
336
|
if [ "tool_use", "server_tool_use" ].include?(content_block[:type])
|
|
335
337
|
next content_block.merge(input: parse_tool_input(content_block[:input]).deep_symbolize_keys)
|
|
336
338
|
end
|
|
@@ -348,6 +350,10 @@ module LlmGateway
|
|
|
348
350
|
end
|
|
349
351
|
end
|
|
350
352
|
|
|
353
|
+
def blank_text_block?(content_block)
|
|
354
|
+
content_block[:type] == "text" && content_block[:text].to_s.blank?
|
|
355
|
+
end
|
|
356
|
+
|
|
351
357
|
def parse_tool_input(input)
|
|
352
358
|
return {} if input.blank?
|
|
353
359
|
|
|
@@ -128,6 +128,11 @@ module LlmGateway
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def enqueue_or_run_message(message, queue, &block)
|
|
131
|
+
if session_manager.idle?
|
|
132
|
+
drain_queue(:steer)
|
|
133
|
+
drain_queue(:next_turn)
|
|
134
|
+
drain_queue(:follow_up)
|
|
135
|
+
end
|
|
131
136
|
prepared_input = LlmGateway::Utils.deep_symbolize_keys(message)
|
|
132
137
|
result = session_manager.start_or_enqueue_user_message(prepared_input, queue: queue) do
|
|
133
138
|
compact_if_needed
|
|
@@ -135,6 +140,8 @@ module LlmGateway
|
|
|
135
140
|
return if result == session_manager.class::MESSAGE_QUEUED
|
|
136
141
|
|
|
137
142
|
begin
|
|
143
|
+
|
|
144
|
+
|
|
138
145
|
continue(&block)
|
|
139
146
|
|
|
140
147
|
loop do
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
require_relative "../base_client"
|
|
4
6
|
|
|
5
7
|
module LlmGateway
|
|
@@ -179,6 +181,87 @@ module LlmGateway
|
|
|
179
181
|
}
|
|
180
182
|
end
|
|
181
183
|
|
|
184
|
+
def rate_limit_error_options(response, error)
|
|
185
|
+
info = rate_limit_info(response, error)
|
|
186
|
+
return {} if info.empty?
|
|
187
|
+
|
|
188
|
+
{
|
|
189
|
+
reset_at: info[:primary_reset_at] || info[:reset_at],
|
|
190
|
+
reset_after_seconds: info[:primary_reset_after_seconds] || info[:reset_after_seconds],
|
|
191
|
+
rate_limit_info: info
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def rate_limit_info(response, error)
|
|
196
|
+
headers = response_headers(response)
|
|
197
|
+
codex_headers = headers.select { |key, _value| key.start_with?("x-codex-") }
|
|
198
|
+
info = {}
|
|
199
|
+
|
|
200
|
+
info[:provider] = "openai_codex" if codex_headers.any?
|
|
201
|
+
info[:error_type] = error["type"]
|
|
202
|
+
info[:plan_type] = headers["x-codex-plan-type"] || error["plan_type"]
|
|
203
|
+
info[:active_limit] = headers["x-codex-active-limit"]
|
|
204
|
+
info[:primary_used_percent] = integer_header(headers, "x-codex-primary-used-percent")
|
|
205
|
+
info[:secondary_used_percent] = integer_header(headers, "x-codex-secondary-used-percent")
|
|
206
|
+
info[:primary_window_minutes] = integer_header(headers, "x-codex-primary-window-minutes")
|
|
207
|
+
info[:secondary_window_minutes] = integer_header(headers, "x-codex-secondary-window-minutes")
|
|
208
|
+
info[:primary_over_secondary_limit_percent] = integer_header(headers, "x-codex-primary-over-secondary-limit-percent")
|
|
209
|
+
info[:primary_reset_after_seconds] = integer_header(headers, "x-codex-primary-reset-after-seconds")
|
|
210
|
+
info[:secondary_reset_after_seconds] = integer_header(headers, "x-codex-secondary-reset-after-seconds")
|
|
211
|
+
info[:primary_reset_at] = epoch_time(headers["x-codex-primary-reset-at"])
|
|
212
|
+
info[:secondary_reset_at] = epoch_time(headers["x-codex-secondary-reset-at"])
|
|
213
|
+
info[:credits_has_credits] = boolean_header(headers, "x-codex-credits-has-credits")
|
|
214
|
+
info[:credits_balance] = integer_header(headers, "x-codex-credits-balance")
|
|
215
|
+
info[:credits_unlimited] = boolean_header(headers, "x-codex-credits-unlimited")
|
|
216
|
+
info[:reset_after_seconds] = integer_value(error["resets_in_seconds"]) || retry_after_seconds(headers["retry-after"])
|
|
217
|
+
info[:reset_at] = epoch_time(error["resets_at"]) || retry_after_time(headers["retry-after"])
|
|
218
|
+
|
|
219
|
+
info.compact
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def response_headers(response)
|
|
223
|
+
headers = {}
|
|
224
|
+
response.each_header { |key, value| headers[key.downcase] = value }
|
|
225
|
+
headers
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def integer_header(headers, key)
|
|
229
|
+
integer_value(headers[key])
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def integer_value(value)
|
|
233
|
+
return nil if value.nil?
|
|
234
|
+
|
|
235
|
+
Integer(value)
|
|
236
|
+
rescue ArgumentError, TypeError
|
|
237
|
+
nil
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def boolean_header(headers, key)
|
|
241
|
+
case headers[key].to_s.downcase
|
|
242
|
+
when "true" then true
|
|
243
|
+
when "false" then false
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def epoch_time(value)
|
|
248
|
+
integer = integer_value(value)
|
|
249
|
+
Time.at(integer) if integer
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def retry_after_seconds(value)
|
|
253
|
+
integer_value(value)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def retry_after_time(value)
|
|
257
|
+
seconds = retry_after_seconds(value)
|
|
258
|
+
return Time.now + seconds if seconds
|
|
259
|
+
|
|
260
|
+
Time.httpdate(value)
|
|
261
|
+
rescue ArgumentError, TypeError
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
182
265
|
def handle_client_specific_errors(response, error)
|
|
183
266
|
# OpenAI uses 'code' instead of 'type' for error codes
|
|
184
267
|
error_code = error["code"]
|
|
@@ -190,7 +273,7 @@ module LlmGateway
|
|
|
190
273
|
|
|
191
274
|
case response.code.to_i
|
|
192
275
|
when 429
|
|
193
|
-
raise Errors::RateLimitError.new(error_message, error_code)
|
|
276
|
+
raise Errors::RateLimitError.new(error_message, error_code, **rate_limit_error_options(response, error))
|
|
194
277
|
when 503
|
|
195
278
|
raise Errors::OverloadError.new(error_message, error_code)
|
|
196
279
|
end
|
data/lib/llm_gateway/errors.rb
CHANGED
|
@@ -19,7 +19,16 @@ module LlmGateway
|
|
|
19
19
|
class NotFoundError < ClientError; end
|
|
20
20
|
class ConflictError < ClientError; end
|
|
21
21
|
class UnprocessableEntityError < ClientError; end
|
|
22
|
-
class RateLimitError < ClientError
|
|
22
|
+
class RateLimitError < ClientError
|
|
23
|
+
attr_reader :reset_at, :reset_after_seconds, :rate_limit_info
|
|
24
|
+
|
|
25
|
+
def initialize(message = nil, code = nil, reset_at: nil, reset_after_seconds: nil, rate_limit_info: {})
|
|
26
|
+
@reset_at = reset_at
|
|
27
|
+
@reset_after_seconds = reset_after_seconds
|
|
28
|
+
@rate_limit_info = rate_limit_info
|
|
29
|
+
super(message, code)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
23
32
|
class InternalServerError < ClientError; end
|
|
24
33
|
class APIStatusError < ClientError; end
|
|
25
34
|
class APITimeoutError < ClientError; end
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module LlmGateway
|
|
4
4
|
module Proxy
|
|
5
5
|
class Adapter
|
|
6
|
-
attr_reader :client
|
|
6
|
+
attr_reader :client, :provider_key
|
|
7
7
|
|
|
8
|
-
def initialize(client)
|
|
8
|
+
def initialize(client, provider_key: nil)
|
|
9
9
|
@client = client
|
|
10
|
+
@provider_key = provider_key
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
def stream(message, tools: nil, system: nil, **options, &block)
|
data/lib/llm_gateway/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_gateway
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- billybonks
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-struct
|