ask-llm-providers 0.1.15 → 0.1.16

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: d05af6c6de7917048a5b86d1fea1c0acfd3ef820c71c7672f19375a5024fe138
4
- data.tar.gz: 3cf21a18360b52e5fd44d01359c0312e05163a234407f07b515f325abf9ebee7
3
+ metadata.gz: 93b16f8655292838315d314b3a57dd94368182372a421f88b3911d6a9b171488
4
+ data.tar.gz: 59fe887341f72aa42c765da7bda10e93454aa8b8cc25bfd1ebb85e316d401a17
5
5
  SHA512:
6
- metadata.gz: 7e91f0e672b159d2d8c7af3de889d0155ede9fc60191b38a78c4f34e675892f0c867ef30dfd6d7feac055d04ef0be347f06fa9888b8d3282b48d34d26dcd59c5
7
- data.tar.gz: 813cc7dc3ca391fca712dae4c59a356019ef363d34a1b2a19376faf1a0b0ac2f2dddb065bd842b64adb7909045936d048bceadc47d2f07de3377115e748f2f08
6
+ metadata.gz: 8627f783b0e7848373fd9358c13e48db978fe728b043c7c5e9f23fd4425c631203c19eae980d870e1f683cdd16d803199cf7b8cba7b8bb0b5f4bfe7b09ffc1ad
7
+ data.tar.gz: 13cbee9ed69bca82917cbfd060e6f8f8a5130c717b975b886f3851203dada60da1ac081200c2613efa53b7625e744baedc639e06e2ee60bc990faa91c14bccfa
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module LLM
5
+ module SSEBuffer
6
+ def init_sse_buffer
7
+ @_sse_buffer = +""
8
+ end
9
+
10
+ def each_sse_event(raw)
11
+ @_sse_buffer ||= +""
12
+ @_sse_buffer << raw
13
+
14
+ while (event_end = @_sse_buffer.index("\n\n"))
15
+ event_data = @_sse_buffer.slice!(0, event_end + 2).strip
16
+ next if event_data.empty?
17
+
18
+ data_content = extract_data(event_data)
19
+ next if data_content.empty?
20
+ break if data_content == "[DONE]"
21
+
22
+ yield data_content
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def extract_data(event_data)
29
+ content = +""
30
+ event_data.each_line do |line|
31
+ line = line.strip
32
+ next if line.empty? || line.start_with?(":")
33
+ if line.start_with?("data: ")
34
+ content << line[6..]
35
+ elsif line.start_with?("data:")
36
+ content << line[5..]
37
+ end
38
+ end
39
+ content
40
+ end
41
+ end
42
+ end
43
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module LLM
5
- VERSION = "0.1.15"
5
+ VERSION = "0.1.16"
6
6
  end
7
7
  end
@@ -4,6 +4,7 @@ module Ask
4
4
  module Providers
5
5
  # Anthropic Claude API provider.
6
6
  class Anthropic < Ask::Provider
7
+ include Ask::LLM::SSEBuffer
7
8
  def initialize(config = {})
8
9
  config = normalize_config(config)
9
10
  super(config)
@@ -179,6 +180,7 @@ module Ask
179
180
 
180
181
  def chat_stream(payload, model, &block)
181
182
  stream = Ask::Stream.new
183
+ init_sse_buffer
182
184
  response = @http.post("v1/messages") do |req|
183
185
  req.body = payload.merge(stream: true)
184
186
  req.options.on_data = proc { |data, _bytes, _env| process_anthropic_chunk(data, stream, model, &block) }
@@ -189,39 +191,24 @@ module Ask
189
191
  end
190
192
 
191
193
  def process_anthropic_chunk(raw, stream, model)
192
- raw.each_line do |line|
193
- line = line.strip
194
- next if line.empty? || line.start_with?(":")
195
- next unless line.start_with?("event:") || line.start_with?("data:")
196
-
197
- if line.start_with?("data: ")
198
- data = line[6..]
199
- begin
200
- parsed = JSON.parse(data)
201
- rescue JSON::ParserError
202
- next
203
- end
204
-
205
- case parsed["type"]
206
- when "content_block_delta"
207
- delta = parsed.dig("delta")
208
- next unless delta
209
- chunk = Ask::Chunk.new(
210
- content: delta["text"],
211
- finish_reason: delta["type"] == "thinking_delta" ? nil : nil
212
- )
194
+ each_sse_event(raw) do |data|
195
+ parsed = JSON.parse(data) rescue next
196
+
197
+ case parsed["type"]
198
+ when "content_block_delta"
199
+ delta = parsed.dig("delta")
200
+ next unless delta
201
+ chunk = Ask::Chunk.new(content: delta["text"])
202
+ stream.add(chunk)
203
+ yield chunk if block_given?
204
+ when "message_stop"
205
+ usage = parsed["usage"] || parsed["message"]&.dig("usage")
206
+ if usage
207
+ chunk = Ask::Chunk.new(finish_reason: "stop", usage: usage)
213
208
  stream.add(chunk)
214
209
  yield chunk if block_given?
215
- when "message_stop"
216
- usage = parsed["usage"] || parsed["message"]&.dig("usage")
217
- if usage
218
- chunk = Ask::Chunk.new(finish_reason: "stop", usage: usage)
219
- stream.add(chunk)
220
- yield chunk if block_given?
221
- end
222
- when "message_start"
223
- # Message started — no content yet
224
210
  end
211
+ when "message_start"
225
212
  end
226
213
  end
227
214
  end
@@ -4,6 +4,7 @@ module Ask
4
4
  module Providers
5
5
  # Cloudflare Workers AI provider. Supports both direct Workers AI and AI Gateway.
6
6
  class Cloudflare < Ask::Provider
7
+ include Ask::LLM::SSEBuffer
7
8
  def initialize(config = {})
8
9
  config = normalize_config(config)
9
10
  super(config)
@@ -96,6 +97,7 @@ module Ask
96
97
 
97
98
  def chat_stream_gateway(endpoint, payload, model, &block)
98
99
  stream = Ask::Stream.new
100
+ init_sse_buffer
99
101
  response = @http.post(endpoint) do |req|
100
102
  req.body = payload.merge(stream: true)
101
103
  req.options.on_data = proc { |data, _bytes, _env| process_stream_chunk(data, stream, model, &block) }
@@ -106,11 +108,7 @@ module Ask
106
108
  end
107
109
 
108
110
  def process_stream_chunk(raw, stream, model)
109
- raw.each_line do |line|
110
- line = line.strip
111
- next unless line.start_with?("data: ")
112
- data = line[6..]
113
- next if data == "[DONE]"
111
+ each_sse_event(raw) do |data|
114
112
  parsed = JSON.parse(data) rescue next
115
113
  delta = parsed.dig("choices", 0, "delta") || {}
116
114
  chunk = Ask::Chunk.new(content: delta["content"])
@@ -4,6 +4,7 @@ module Ask
4
4
  module Providers
5
5
  # Google Gemini API provider. Also supports Vertex AI via GCP service account auth.
6
6
  class Google < Ask::Provider
7
+ include Ask::LLM::SSEBuffer
7
8
  def initialize(config = {})
8
9
  config = normalize_config(config)
9
10
  super(config)
@@ -187,6 +188,7 @@ module Ask
187
188
 
188
189
  def chat_stream(path, payload, model, &block)
189
190
  stream = Ask::Stream.new
191
+ init_sse_buffer
190
192
  response = @http.post(path) do |req|
191
193
  req.body = payload
192
194
  req.params["key"] = @config.api_key if @config.api_key
@@ -198,10 +200,7 @@ module Ask
198
200
  end
199
201
 
200
202
  def process_google_chunk(raw, stream, model)
201
- raw.each_line do |line|
202
- next unless line.start_with?("data: ")
203
- data = line[6..]
204
- next if data.strip == "[DONE]"
203
+ each_sse_event(raw) do |data|
205
204
  parsed = JSON.parse(data) rescue next
206
205
  candidate = parsed.dig("candidates", 0) or next
207
206
  part = candidate.dig("content", "parts", 0)
@@ -79,6 +79,7 @@ module Ask
79
79
 
80
80
  def chat_stream(payload, model, &block)
81
81
  stream = Ask::Stream.new
82
+ @_sse_buffer = +""
82
83
  response = @http.post("api/chat") do |req|
83
84
  req.body = payload.merge(stream: true)
84
85
  req.options.on_data = proc { |data, _bytes, _env| process_ollama_chunk(data, stream, model, &block) }
@@ -89,7 +90,13 @@ module Ask
89
90
  end
90
91
 
91
92
  def process_ollama_chunk(raw, stream, model)
92
- raw.each_line do |line|
93
+ @_sse_buffer ||= +""
94
+ @_sse_buffer << raw
95
+
96
+ while (line_end = @_sse_buffer.index("\n"))
97
+ line = @_sse_buffer.slice!(0, line_end + 1).strip
98
+ next if line.empty?
99
+
93
100
  parsed = JSON.parse(line) rescue next
94
101
  msg = parsed["message"] || {}
95
102
  chunk = Ask::Chunk.new(content: msg["content"])
@@ -6,6 +6,7 @@ module Ask
6
6
  # (OpenRouter, DeepSeek, Azure, XAI, Perplexity, GPUStack, etc.) via
7
7
  # +base_url+ override.
8
8
  class OpenAI < Ask::Provider
9
+ include Ask::LLM::SSEBuffer
9
10
  def initialize(config = {})
10
11
  @provider_keys = extract_provider_keys(config)
11
12
  config = normalize_config(config)
@@ -148,6 +149,7 @@ module Ask
148
149
 
149
150
  def chat_stream(payload, model, &block)
150
151
  stream = Ask::Stream.new
152
+ init_sse_buffer
151
153
  @http.post("chat/completions") do |req|
152
154
  req.body = payload.merge(stream: true)
153
155
  req.options.on_data = proc { |data, _bytes, _env| process_chunk(data, stream, model, &block) }
@@ -167,12 +169,8 @@ module Ask
167
169
  stream
168
170
  end
169
171
 
170
-
171
172
  def process_chunk(raw, stream, model)
172
- raw.each_line do |line|
173
- line = line.strip
174
- next if line.empty? || line.start_with?(":") || !line.start_with?("data: ")
175
- data = line[6..]; next if data == "[DONE]"
173
+ each_sse_event(raw) do |data|
176
174
  parsed = JSON.parse(data) rescue next
177
175
  choice = parsed.dig("choices", 0) or next
178
176
  delta = choice["delta"] || {}
@@ -10,6 +10,7 @@ require "base64"
10
10
  # Common infrastructure
11
11
  require_relative "ask/llm/config"
12
12
  require_relative "ask/llm/http"
13
+ require_relative "ask/llm/sse_buffer"
13
14
  require_relative "ask/llm/models/openai"
14
15
 
15
16
  # Load providers
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.1.15
4
+ version: 0.1.16
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -178,6 +178,7 @@ files:
178
178
  - lib/ask/llm/config.rb
179
179
  - lib/ask/llm/http.rb
180
180
  - lib/ask/llm/models/openai.rb
181
+ - lib/ask/llm/sse_buffer.rb
181
182
  - lib/ask/llm/version.rb
182
183
  - lib/ask/provider/anthropic.rb
183
184
  - lib/ask/provider/bedrock.rb