ask-llm-providers 0.13.2 → 0.13.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: 3ce937df3ae9db671946c8d5c3d837498fd521066bb45996e595dac977ff9f24
4
- data.tar.gz: 9702d0c8aea1b61aa24df7e8bb76c5cc19e576ce926076fb274bdb095691b666
3
+ metadata.gz: 53c2599e28335ad6cb5602315bb0667bb40f9424d1b5fa89c334126c6a3e441b
4
+ data.tar.gz: 9eebe712434c856a9705f92ad98ee8133fc7e4d2bad86f97b38ecb30d4c61436
5
5
  SHA512:
6
- metadata.gz: 3ea28d9f251de830592fb92f7d78c71d9b7983dbfc69aa29617ef06df5d598edfa5d9e5a9c8f386a2f9b2d9be22cb76a9c54594c7710acfea17779aa7de9c16d
7
- data.tar.gz: d67134217af9ddb29c91b8dd4c64cb1bbe68f2fd79da31bf0bac7a7b64abb681ca0f34e11e7327d587de13c531b722466075cb5e10f35c302acf174af4d3e17b
6
+ metadata.gz: e267ef9ed4356b8f083e73b2fe5bf7253be512d11d7af7e5618e3f068339147f0e39d20e5dda6b49c62136c1bc90f614e2fc927b862e0b4f08a3d0a3dbec2981
7
+ data.tar.gz: 5d89c48fd89ce57af5c336a60e879a323724f69b81521438690edb5d11a1a8dd0ea7aa839b063ed239dfc53955ed4da80be5342049518936d9b1efbbdbf62f81
@@ -76,6 +76,11 @@ module Ask
76
76
  capabilities: { chat: true, streaming: true, tool_calls: true } },
77
77
 
78
78
  opencode_go: { api_base: "https://opencode.ai/zen/go/v1", api_key_env: "OPENCODE_GO_API_KEY",
79
+ # The Go gateway routes by session and refuses a request
80
+ # without this header (400 MissingSessionID) — the whole
81
+ # call fails, not just its efficiency. A per-client
82
+ # UUID keeps one agent's requests on one route.
83
+ session_header: "x-opencode-session",
79
84
  capabilities: { chat: true, streaming: true, tool_calls: true } },
80
85
 
81
86
  github_copilot: { api_base: "https://api.githubcopilot.com", api_key_env: "GITHUB_COPILOT_TOKEN",
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.13.2"
5
+ VERSION = "0.13.3"
6
6
  end
7
7
  end
@@ -496,16 +496,20 @@ module Ask
496
496
  def chat_stream(payload, model, &block)
497
497
  stream = Ask::Stream.new
498
498
  init_sse_buffer
499
+ # The response body is consumed by the stream callback below, so on
500
+ # failure `resp.body` is empty — for exactly the responses whose text
501
+ # matters most. Keeping the first chunk of what actually arrived is
502
+ # what lets an error carry the provider's own words.
503
+ raw = +""
499
504
  @http.post("chat/completions") do |req|
500
505
  req.body = payload.merge(stream: true)
501
- req.options.on_data = proc { |data, _bytes, _env| parse_stream(data, stream, model, &block) }
506
+ req.options.on_data = proc { |data, _bytes, _env|
507
+ raw << data if raw.bytesize < MAX_ERROR_BODY
508
+ parse_stream(data, stream, model, &block)
509
+ }
502
510
  end.tap { |resp|
503
511
  unless resp.success?
504
- err_body = case resp.body
505
- when Hash then resp.body
506
- when String then (JSON.parse(resp.body) rescue { "error" => { "message" => "HTTP #{resp.status}: #{resp.body[0..200]}" } })
507
- else { "error" => { "message" => "HTTP #{resp.status}: empty response body" } }
508
- end
512
+ err_body = stream_error_body(resp, raw)
509
513
  err_body["error"] ||= {}
510
514
  err_body["error"]["_status"] = resp.status
511
515
  raise LLM::HTTP.map_error(resp.status, err_body, provider: "OpenAI")
@@ -515,6 +519,34 @@ module Ask
515
519
  stream
516
520
  end
517
521
 
522
+ # How much of a failed response to keep for the error message. Enough
523
+ # for a JSON error document, not enough to hold a whole streamed body
524
+ # in memory.
525
+ MAX_ERROR_BODY = 4096
526
+
527
+ # The provider's error, from wherever it survived.
528
+ #
529
+ # Faraday hands the body to `on_data` as it arrives, so a response that
530
+ # failed has nothing left in `resp.body` — which is how a specific,
531
+ # actionable 400 ("Request is missing x-opencode-session") reached the
532
+ # operator as "empty response body" and cost an afternoon. Fall back to
533
+ # the buffered text, then to whatever we do have.
534
+ def stream_error_body(resp, raw)
535
+ body = resp.body
536
+ return body if body.is_a?(Hash)
537
+
538
+ text = body.to_s
539
+ text = raw if text.strip.empty?
540
+ parsed = begin
541
+ JSON.parse(text)
542
+ rescue
543
+ nil
544
+ end
545
+ return parsed if parsed.is_a?(Hash) && parsed["error"]
546
+
547
+ {"error" => {"message" => "HTTP #{resp.status}: #{text.to_s.strip[0..400]}"}}
548
+ end
549
+
518
550
  def parse_tool_calls(calls)
519
551
  return nil unless calls&.any?
520
552
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+
3
5
  module Ask
4
6
  module Providers
5
7
  # Single class for all OpenAI-compatible providers.
@@ -57,9 +59,23 @@ module Ask
57
59
  if (extra = @compat_cfg[:extra_headers])
58
60
  extra.each { |k, v| h[k] = v }
59
61
  end
62
+ # A gateway that routes by session needs an id on every request, and
63
+ # a stable one per client so requests land on the same route. This is
64
+ # why the failure mode is a hard 400 rather than a slow call: the
65
+ # gateway cannot place the request at all.
66
+ if (header = @compat_cfg[:session_header])
67
+ h[header] = session_id
68
+ end
60
69
  h
61
70
  end
62
71
 
72
+ # One id per provider instance — a conversation's requests stay on one
73
+ # route, which is what the header is for. Memoized so every request
74
+ # from this client carries the same value.
75
+ def session_id
76
+ @session_id ||= SecureRandom.uuid
77
+ end
78
+
63
79
  def format_messages(messages)
64
80
  result = super
65
81
  if @compat_cfg[:reasoning_content]
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.13.2
4
+ version: 0.13.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto