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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5cd3b8b829adb3e5793ac604aa50c40bdd94546a5c2fc0b90125ca9a75efe4c2
4
- data.tar.gz: 28e7211450893a23c2116d1cf96010fe5dd359c6203ecbde6802dda89f06f153
3
+ metadata.gz: c70b5fdd6e77e60cbe437a5698e078b7aa88fb41dca96311dcf9a58585238476
4
+ data.tar.gz: cb9dd1f8aa71c33751ddd270acb849d964ed137b6b3fdd6c96e072082bc6e488
5
5
  SHA512:
6
- metadata.gz: f51eb7792fd0886403c9d3fec068dcd8f00d50c35df747e6735b470ccc170e8129bc8fe5ef18b8cd117a763e95b8cf9bc76c54364ba5c8b5f284f3e0b5ef69a5
7
- data.tar.gz: c0de665f7e70b60814c8ea8185b34205de309736d4c578fee1d48de4990d76b4d437c1c1b74672d7f57c67b115e244e421f4b17882ea34cb875dcd5ed31dec59
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.map do |content_block|
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
@@ -45,6 +45,10 @@ module LlmGateway
45
45
  @state == :busy
46
46
  end
47
47
 
48
+ def idle?
49
+ @state == :idle
50
+ end
51
+
48
52
  def validate_queue!(queue)
49
53
  queue = queue.to_sym
50
54
  raise ArgumentError, "Invalid queue mode: #{queue}" unless QUEUES.include?(queue)
@@ -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
@@ -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; end
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmGateway
4
- VERSION = "0.8.0"
4
+ VERSION = "0.8.1"
5
5
  end
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.0
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-06 00:00:00.000000000 Z
11
+ date: 2026-06-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-struct