durable-llm 0.1.3 → 0.1.5
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/.envrc +7 -0
- data/CHANGELOG.md +5 -0
- data/CONFIGURE.md +132 -0
- data/Gemfile +7 -9
- data/Gemfile.lock +3 -3
- data/README.md +1 -0
- data/Rakefile +6 -6
- data/devenv.lock +103 -0
- data/devenv.nix +9 -0
- data/devenv.yaml +15 -0
- data/durable-llm.gemspec +44 -0
- data/examples/openai_quick_complete.rb +3 -1
- data/lib/durable/llm/cli.rb +247 -60
- data/lib/durable/llm/client.rb +92 -11
- data/lib/durable/llm/configuration.rb +174 -23
- data/lib/durable/llm/errors.rb +185 -0
- data/lib/durable/llm/providers/anthropic.rb +246 -36
- data/lib/durable/llm/providers/azure_openai.rb +347 -0
- data/lib/durable/llm/providers/base.rb +106 -9
- data/lib/durable/llm/providers/cohere.rb +227 -0
- data/lib/durable/llm/providers/deepseek.rb +233 -0
- data/lib/durable/llm/providers/fireworks.rb +278 -0
- data/lib/durable/llm/providers/google.rb +301 -0
- data/lib/durable/llm/providers/groq.rb +108 -29
- data/lib/durable/llm/providers/huggingface.rb +122 -18
- data/lib/durable/llm/providers/mistral.rb +431 -0
- data/lib/durable/llm/providers/openai.rb +162 -25
- data/lib/durable/llm/providers/opencode.rb +253 -0
- data/lib/durable/llm/providers/openrouter.rb +256 -0
- data/lib/durable/llm/providers/perplexity.rb +273 -0
- data/lib/durable/llm/providers/together.rb +346 -0
- data/lib/durable/llm/providers/xai.rb +355 -0
- data/lib/durable/llm/providers.rb +103 -15
- data/lib/durable/llm/version.rb +5 -1
- data/lib/durable/llm.rb +143 -3
- data/lib/durable.rb +29 -4
- data/sig/durable/llm.rbs +302 -1
- metadata +50 -36
@@ -0,0 +1,346 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'json'
|
5
|
+
require 'event_stream_parser'
|
6
|
+
require 'durable/llm/errors'
|
7
|
+
require 'durable/llm/providers/base'
|
8
|
+
|
9
|
+
module Durable
|
10
|
+
module Llm
|
11
|
+
module Providers
|
12
|
+
# Together AI provider for accessing various language models through the Together API.
|
13
|
+
#
|
14
|
+
# Provides completion, embedding, and streaming capabilities with authentication handling,
|
15
|
+
# error management, and response normalization. It establishes HTTP connections to Together's
|
16
|
+
# API endpoint, processes chat completions and embeddings, handles various API error responses,
|
17
|
+
# and includes comprehensive response classes to format Together's API responses into a
|
18
|
+
# consistent interface.
|
19
|
+
class Together < Durable::Llm::Providers::Base
|
20
|
+
BASE_URL = 'https://api.together.xyz/v1'
|
21
|
+
|
22
|
+
# Returns the default API key for Together AI.
|
23
|
+
#
|
24
|
+
# @return [String, nil] The API key from configuration or environment variable
|
25
|
+
def default_api_key
|
26
|
+
begin
|
27
|
+
Durable::Llm.configuration.together&.api_key
|
28
|
+
rescue NoMethodError
|
29
|
+
nil
|
30
|
+
end || ENV['TOGETHER_API_KEY']
|
31
|
+
end
|
32
|
+
|
33
|
+
attr_accessor :api_key
|
34
|
+
|
35
|
+
# Initializes the Together provider with an API key.
|
36
|
+
#
|
37
|
+
# @param api_key [String, nil] The API key to use. If nil, uses default_api_key
|
38
|
+
def initialize(api_key: nil)
|
39
|
+
super
|
40
|
+
@conn = Faraday.new(url: BASE_URL) do |faraday|
|
41
|
+
faraday.request :json
|
42
|
+
faraday.response :json
|
43
|
+
faraday.adapter Faraday.default_adapter
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Completes a chat conversation using the Together API.
|
48
|
+
#
|
49
|
+
# @param options [Hash] The options for the completion request
|
50
|
+
# @return [TogetherResponse] The response from the API
|
51
|
+
def completion(options)
|
52
|
+
response = @conn.post('chat/completions') do |req|
|
53
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
54
|
+
req.body = options
|
55
|
+
end
|
56
|
+
|
57
|
+
handle_response(response)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generates embeddings for the given input using the Together API.
|
61
|
+
#
|
62
|
+
# @param model [String] The model to use for embedding
|
63
|
+
# @param input [String, Array<String>] The input text(s) to embed
|
64
|
+
# @param options [Hash] Additional options for the embedding request
|
65
|
+
# @return [TogetherEmbeddingResponse] The embedding response
|
66
|
+
def embedding(model:, input:, **options)
|
67
|
+
response = @conn.post('embeddings') do |req|
|
68
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
69
|
+
req.body = { model: model, input: input, **options }
|
70
|
+
end
|
71
|
+
|
72
|
+
handle_response(response, TogetherEmbeddingResponse)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Retrieves the list of available models from the Together API.
|
76
|
+
#
|
77
|
+
# @return [Array<String>] Array of model IDs
|
78
|
+
def models
|
79
|
+
response = @conn.get('models') do |req|
|
80
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
81
|
+
end
|
82
|
+
|
83
|
+
handle_response(response).data.map { |model| model['id'] }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Indicates whether this provider supports streaming.
|
87
|
+
#
|
88
|
+
# @return [Boolean] Always true for Together
|
89
|
+
def self.stream?
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
# Streams a chat completion using the Together API.
|
94
|
+
#
|
95
|
+
# @param options [Hash] The options for the streaming request
|
96
|
+
# @yield [TogetherStreamResponse] Yields stream response chunks
|
97
|
+
def stream(options)
|
98
|
+
options = prepare_stream_options(options)
|
99
|
+
|
100
|
+
@conn.post('chat/completions') do |req|
|
101
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
102
|
+
req.headers['Accept'] = 'text/event-stream'
|
103
|
+
req.body = options
|
104
|
+
req.options.on_data = stream_proc { |chunk| yield TogetherStreamResponse.new(chunk) }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def prepare_stream_options(options)
|
111
|
+
opts = options.dup
|
112
|
+
opts[:stream] = true
|
113
|
+
opts['temperature'] = opts['temperature'].to_f if opts['temperature']
|
114
|
+
opts
|
115
|
+
end
|
116
|
+
|
117
|
+
def stream_proc(&block)
|
118
|
+
user_proc = proc do |chunk, _size, _total|
|
119
|
+
block.call(chunk)
|
120
|
+
end
|
121
|
+
to_json_stream(user_proc: user_proc)
|
122
|
+
end
|
123
|
+
|
124
|
+
# CODE-FROM: ruby-openai @ https://github.com/alexrudall/ruby-openai/blob/main/lib/openai/http.rb
|
125
|
+
# MIT License: https://github.com/alexrudall/ruby-openai/blob/main/LICENSE.md
|
126
|
+
def to_json_stream(user_proc:)
|
127
|
+
parser = EventStreamParser::Parser.new
|
128
|
+
|
129
|
+
proc do |chunk, _bytes, env|
|
130
|
+
if env && env.status != 200
|
131
|
+
raise_error = Faraday::Response::RaiseError.new
|
132
|
+
raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
|
133
|
+
end
|
134
|
+
|
135
|
+
parser.feed(chunk) do |_type, data|
|
136
|
+
user_proc.call(JSON.parse(data)) unless data == '[DONE]'
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def try_parse_json(maybe_json)
|
142
|
+
JSON.parse(maybe_json)
|
143
|
+
rescue JSON::ParserError
|
144
|
+
maybe_json
|
145
|
+
end
|
146
|
+
|
147
|
+
# END-CODE-FROM
|
148
|
+
|
149
|
+
# Handles the HTTP response and raises appropriate errors or returns the response object.
|
150
|
+
#
|
151
|
+
# @param response [Faraday::Response] The HTTP response
|
152
|
+
# @param response_class [Class] The response class to instantiate (default: TogetherResponse)
|
153
|
+
# @return [Object] The response object
|
154
|
+
# @raise [Durable::Llm::AuthenticationError] On 401 status
|
155
|
+
# @raise [Durable::Llm::RateLimitError] On 429 status
|
156
|
+
# @raise [Durable::Llm::InvalidRequestError] On 400-499 status
|
157
|
+
# @raise [Durable::Llm::ServerError] On 500-599 status
|
158
|
+
# @raise [Durable::Llm::APIError] On other error statuses
|
159
|
+
def handle_response(response, response_class = TogetherResponse)
|
160
|
+
case response.status
|
161
|
+
when 200..299
|
162
|
+
response_class.new(response.body)
|
163
|
+
when 401
|
164
|
+
raise Durable::Llm::AuthenticationError, parse_error_message(response)
|
165
|
+
when 429
|
166
|
+
raise Durable::Llm::RateLimitError, parse_error_message(response)
|
167
|
+
when 400..499
|
168
|
+
raise Durable::Llm::InvalidRequestError, parse_error_message(response)
|
169
|
+
when 500..599
|
170
|
+
raise Durable::Llm::ServerError, parse_error_message(response)
|
171
|
+
else
|
172
|
+
raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Parses the error message from the response.
|
177
|
+
#
|
178
|
+
# @param response [Faraday::Response] The HTTP response
|
179
|
+
# @return [String] The formatted error message
|
180
|
+
def parse_error_message(response)
|
181
|
+
body = begin
|
182
|
+
JSON.parse(response.body)
|
183
|
+
rescue StandardError
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
message = body&.dig('error', 'message') || response.body
|
187
|
+
"#{response.status} Error: #{message}"
|
188
|
+
end
|
189
|
+
|
190
|
+
# Response class for Together API completions.
|
191
|
+
class TogetherResponse
|
192
|
+
attr_reader :raw_response
|
193
|
+
|
194
|
+
# Initializes the response with raw API data.
|
195
|
+
#
|
196
|
+
# @param response [Hash] The raw response from the API
|
197
|
+
def initialize(response)
|
198
|
+
@raw_response = response
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns the choices from the response.
|
202
|
+
#
|
203
|
+
# @return [Array<TogetherChoice>] Array of choices
|
204
|
+
def choices
|
205
|
+
@raw_response['choices'].map { |choice| TogetherChoice.new(choice) }
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the data from the response.
|
209
|
+
#
|
210
|
+
# @return [Array, Hash] The data portion of the response
|
211
|
+
def data
|
212
|
+
@raw_response['data']
|
213
|
+
end
|
214
|
+
|
215
|
+
# Converts the response to a string.
|
216
|
+
#
|
217
|
+
# @return [String] The concatenated content of all choices
|
218
|
+
def to_s
|
219
|
+
choices.map(&:to_s).join(' ')
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# Represents a choice in the Together API response.
|
224
|
+
class TogetherChoice
|
225
|
+
attr_reader :message, :finish_reason
|
226
|
+
|
227
|
+
# Initializes a choice.
|
228
|
+
#
|
229
|
+
# @param choice [Hash] The choice data
|
230
|
+
def initialize(choice)
|
231
|
+
@message = TogetherMessage.new(choice['message'])
|
232
|
+
@finish_reason = choice['finish_reason']
|
233
|
+
end
|
234
|
+
|
235
|
+
# Converts the choice to string.
|
236
|
+
#
|
237
|
+
# @return [String] The message content
|
238
|
+
def to_s
|
239
|
+
@message.to_s
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Represents a message in the Together API response.
|
244
|
+
class TogetherMessage
|
245
|
+
attr_reader :role, :content
|
246
|
+
|
247
|
+
# Initializes a message.
|
248
|
+
#
|
249
|
+
# @param message [Hash] The message data
|
250
|
+
def initialize(message)
|
251
|
+
@role = message['role']
|
252
|
+
@content = message['content']
|
253
|
+
end
|
254
|
+
|
255
|
+
# Converts to string.
|
256
|
+
#
|
257
|
+
# @return [String] The content
|
258
|
+
def to_s
|
259
|
+
@content
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Response class for streaming Together API responses.
|
264
|
+
class TogetherStreamResponse
|
265
|
+
attr_reader :choices
|
266
|
+
|
267
|
+
# Initializes the stream response.
|
268
|
+
#
|
269
|
+
# @param parsed [Hash] The parsed JSON data
|
270
|
+
def initialize(parsed)
|
271
|
+
@choices = TogetherStreamChoice.new(parsed['choices'])
|
272
|
+
end
|
273
|
+
|
274
|
+
# Converts to string.
|
275
|
+
#
|
276
|
+
# @return [String] The content
|
277
|
+
def to_s
|
278
|
+
@choices.to_s
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Response class for Together API embeddings.
|
283
|
+
class TogetherEmbeddingResponse
|
284
|
+
attr_reader :embedding
|
285
|
+
|
286
|
+
# Initializes the embedding response.
|
287
|
+
#
|
288
|
+
# @param data [Hash] The raw embedding data
|
289
|
+
def initialize(data)
|
290
|
+
@embedding = data.dig('data', 0, 'embedding')
|
291
|
+
end
|
292
|
+
|
293
|
+
# Returns the embedding as an array.
|
294
|
+
#
|
295
|
+
# @return [Array<Float>] The embedding vector
|
296
|
+
def to_a
|
297
|
+
@embedding
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Represents a choice in streaming responses.
|
302
|
+
class TogetherStreamChoice
|
303
|
+
attr_reader :delta, :finish_reason
|
304
|
+
|
305
|
+
# Initializes a stream choice.
|
306
|
+
#
|
307
|
+
# @param choice [Array, Hash] The choice data
|
308
|
+
def initialize(choice)
|
309
|
+
@choice = [choice].flatten.first
|
310
|
+
@delta = TogetherStreamDelta.new(@choice['delta'])
|
311
|
+
@finish_reason = @choice['finish_reason']
|
312
|
+
end
|
313
|
+
|
314
|
+
# Converts to string.
|
315
|
+
#
|
316
|
+
# @return [String] The delta content
|
317
|
+
def to_s
|
318
|
+
@delta.to_s
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
# Represents a delta in streaming responses.
|
323
|
+
class TogetherStreamDelta
|
324
|
+
attr_reader :role, :content
|
325
|
+
|
326
|
+
# Initializes a stream delta.
|
327
|
+
#
|
328
|
+
# @param delta [Hash] The delta data
|
329
|
+
def initialize(delta)
|
330
|
+
@role = delta['role']
|
331
|
+
@content = delta['content']
|
332
|
+
end
|
333
|
+
|
334
|
+
# Converts to string.
|
335
|
+
#
|
336
|
+
# @return [String] The content or empty string
|
337
|
+
def to_s
|
338
|
+
@content || ''
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|
@@ -0,0 +1,355 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file implements the xAI provider for accessing xAI's Grok language models through their API,
|
4
|
+
# providing completion, embedding, and streaming capabilities with authentication handling, error management,
|
5
|
+
# and response normalization. It establishes HTTP connections to xAI's API endpoint, processes chat completions
|
6
|
+
# and embeddings, handles various API error responses, and includes comprehensive response classes to format
|
7
|
+
# xAI's API responses into a consistent interface.
|
8
|
+
|
9
|
+
require 'faraday'
|
10
|
+
require 'json'
|
11
|
+
require 'durable/llm/errors'
|
12
|
+
require 'durable/llm/providers/base'
|
13
|
+
require 'event_stream_parser'
|
14
|
+
|
15
|
+
module Durable
|
16
|
+
module Llm
|
17
|
+
module Providers
|
18
|
+
# xAI provider for accessing xAI's Grok language models.
|
19
|
+
#
|
20
|
+
# This class provides methods to interact with xAI's API for chat completions,
|
21
|
+
# embeddings, model listing, and streaming responses.
|
22
|
+
class Xai < Durable::Llm::Providers::Base
|
23
|
+
BASE_URL = 'https://api.x.ai/v1'
|
24
|
+
|
25
|
+
# Returns the default API key for xAI, checking configuration and environment variables.
|
26
|
+
#
|
27
|
+
# @return [String, nil] The API key or nil if not found
|
28
|
+
def default_api_key
|
29
|
+
begin
|
30
|
+
Durable::Llm.configuration.xai&.api_key
|
31
|
+
rescue NoMethodError
|
32
|
+
nil
|
33
|
+
end || ENV['XAI_API_KEY']
|
34
|
+
end
|
35
|
+
|
36
|
+
attr_accessor :api_key
|
37
|
+
|
38
|
+
# Initializes the xAI provider with API key and HTTP connection.
|
39
|
+
#
|
40
|
+
# @param api_key [String, nil] The API key to use, defaults to default_api_key
|
41
|
+
def initialize(api_key: nil)
|
42
|
+
super
|
43
|
+
@conn = Faraday.new(url: BASE_URL) do |faraday|
|
44
|
+
faraday.request :json
|
45
|
+
faraday.response :json
|
46
|
+
faraday.adapter Faraday.default_adapter
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Performs a chat completion request to xAI's API.
|
51
|
+
#
|
52
|
+
# @param options [Hash] The completion options including model, messages, etc.
|
53
|
+
# @return [XaiResponse] The parsed response from xAI
|
54
|
+
def completion(options)
|
55
|
+
response = @conn.post('chat/completions') do |req|
|
56
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
57
|
+
req.body = options
|
58
|
+
end
|
59
|
+
|
60
|
+
handle_response(response)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Performs an embedding request to xAI's API.
|
64
|
+
#
|
65
|
+
# @param model [String] The embedding model to use
|
66
|
+
# @param input [String, Array<String>] The text(s) to embed
|
67
|
+
# @param options [Hash] Additional options for the embedding request
|
68
|
+
# @return [XaiEmbeddingResponse] The parsed embedding response
|
69
|
+
def embedding(model:, input:, **options)
|
70
|
+
response = @conn.post('embeddings') do |req|
|
71
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
72
|
+
req.body = { model: model, input: input, **options }
|
73
|
+
end
|
74
|
+
|
75
|
+
handle_response(response, XaiEmbeddingResponse)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Retrieves the list of available models from xAI's API.
|
79
|
+
#
|
80
|
+
# @return [Array<String>] Array of model IDs
|
81
|
+
def models
|
82
|
+
response = @conn.get('models') do |req|
|
83
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
84
|
+
end
|
85
|
+
|
86
|
+
handle_response(response).data.map { |model| model['id'] }
|
87
|
+
end
|
88
|
+
|
89
|
+
# Indicates whether this provider supports streaming responses.
|
90
|
+
#
|
91
|
+
# @return [Boolean] Always true for xAI provider
|
92
|
+
def self.stream?
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Performs a streaming chat completion request to xAI's API.
|
97
|
+
#
|
98
|
+
# @param options [Hash] The completion options including model, messages, etc.
|
99
|
+
# @yield [XaiStreamResponse] Yields each chunk of the streaming response
|
100
|
+
# @return [nil] Returns after streaming is complete
|
101
|
+
def stream(options)
|
102
|
+
options[:stream] = true
|
103
|
+
|
104
|
+
response = @conn.post('chat/completions') do |req|
|
105
|
+
req.headers['Authorization'] = "Bearer #{@api_key}"
|
106
|
+
req.headers['Accept'] = 'text/event-stream'
|
107
|
+
|
108
|
+
options['temperature'] = options['temperature'].to_f if options['temperature']
|
109
|
+
|
110
|
+
req.body = options
|
111
|
+
|
112
|
+
user_proc = proc do |chunk, _size, _total|
|
113
|
+
yield XaiStreamResponse.new(chunk)
|
114
|
+
end
|
115
|
+
|
116
|
+
req.options.on_data = to_json_stream(user_proc: user_proc)
|
117
|
+
end
|
118
|
+
|
119
|
+
handle_response(response)
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
# CODE-FROM: ruby-openai @ https://github.com/alexrudall/ruby-openai/blob/main/lib/openai/http.rb
|
125
|
+
# MIT License: https://github.com/alexrudall/ruby-openai/blob/main/LICENSE.md
|
126
|
+
#
|
127
|
+
# Creates a proc for handling streaming JSON responses from xAI's API.
|
128
|
+
#
|
129
|
+
# @param user_proc [Proc] The proc to call with each parsed chunk
|
130
|
+
# @return [Proc] A proc that handles the streaming data
|
131
|
+
def to_json_stream(user_proc:)
|
132
|
+
parser = EventStreamParser::Parser.new
|
133
|
+
|
134
|
+
proc do |chunk, _bytes, env|
|
135
|
+
if env && env.status != 200
|
136
|
+
raise_error = Faraday::Response::RaiseError.new
|
137
|
+
raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
|
138
|
+
end
|
139
|
+
|
140
|
+
parser.feed(chunk) do |_type, data|
|
141
|
+
user_proc.call(JSON.parse(data)) unless data == '[DONE]'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Attempts to parse a string as JSON, returning the original string on failure.
|
147
|
+
#
|
148
|
+
# @param maybe_json [String] The string to parse
|
149
|
+
# @return [Object, String] Parsed JSON object or original string
|
150
|
+
def try_parse_json(maybe_json)
|
151
|
+
JSON.parse(maybe_json)
|
152
|
+
rescue JSON::ParserError
|
153
|
+
maybe_json
|
154
|
+
end
|
155
|
+
|
156
|
+
# END-CODE-FROM
|
157
|
+
|
158
|
+
# Handles HTTP responses from xAI's API, raising appropriate errors or returning parsed responses.
|
159
|
+
#
|
160
|
+
# @param response [Faraday::Response] The HTTP response
|
161
|
+
# @param response_class [Class] The response class to instantiate for successful responses
|
162
|
+
# @return [Object] The parsed response object
|
163
|
+
# @raise [Durable::Llm::AuthenticationError] For 401 responses
|
164
|
+
# @raise [Durable::Llm::RateLimitError] For 429 responses
|
165
|
+
# @raise [Durable::Llm::InvalidRequestError] For 400-499 responses
|
166
|
+
# @raise [Durable::Llm::ServerError] For 500-599 responses
|
167
|
+
# @raise [Durable::Llm::APIError] For unexpected status codes
|
168
|
+
def handle_response(response, response_class = XaiResponse)
|
169
|
+
case response.status
|
170
|
+
when 200..299
|
171
|
+
response_class.new(response.body)
|
172
|
+
when 401
|
173
|
+
raise Durable::Llm::AuthenticationError, parse_error_message(response)
|
174
|
+
when 429
|
175
|
+
raise Durable::Llm::RateLimitError, parse_error_message(response)
|
176
|
+
when 400..499
|
177
|
+
raise Durable::Llm::InvalidRequestError, parse_error_message(response)
|
178
|
+
when 500..599
|
179
|
+
raise Durable::Llm::ServerError, parse_error_message(response)
|
180
|
+
else
|
181
|
+
raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Parses error messages from xAI API responses.
|
186
|
+
#
|
187
|
+
# @param response [Faraday::Response] The HTTP response
|
188
|
+
# @return [String] Formatted error message
|
189
|
+
def parse_error_message(response)
|
190
|
+
body = begin
|
191
|
+
JSON.parse(response.body)
|
192
|
+
rescue StandardError
|
193
|
+
nil
|
194
|
+
end
|
195
|
+
message = body&.dig('error', 'message') || response.body
|
196
|
+
"#{response.status} Error: #{message}"
|
197
|
+
end
|
198
|
+
|
199
|
+
# Represents a response from xAI's chat completion API.
|
200
|
+
class XaiResponse
|
201
|
+
attr_reader :raw_response
|
202
|
+
|
203
|
+
# Initializes the response with raw API data.
|
204
|
+
#
|
205
|
+
# @param response [Hash] The parsed JSON response from xAI
|
206
|
+
def initialize(response)
|
207
|
+
@raw_response = response
|
208
|
+
end
|
209
|
+
|
210
|
+
# Returns the choices from the response.
|
211
|
+
#
|
212
|
+
# @return [Array<XaiChoice>] Array of choice objects
|
213
|
+
def choices
|
214
|
+
@raw_response['choices'].map { |choice| XaiChoice.new(choice) }
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns the data field from the response.
|
218
|
+
#
|
219
|
+
# @return [Array, nil] The data array or nil
|
220
|
+
def data
|
221
|
+
@raw_response['data']
|
222
|
+
end
|
223
|
+
|
224
|
+
# Converts the response to a string by joining all choice messages.
|
225
|
+
#
|
226
|
+
# @return [String] The concatenated response text
|
227
|
+
def to_s
|
228
|
+
choices.map(&:to_s).join(' ')
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Represents a single choice in an xAI response.
|
233
|
+
class XaiChoice
|
234
|
+
attr_reader :message, :finish_reason
|
235
|
+
|
236
|
+
# Initializes the choice with message and finish reason.
|
237
|
+
#
|
238
|
+
# @param choice [Hash] The choice data from the API response
|
239
|
+
def initialize(choice)
|
240
|
+
@message = XaiMessage.new(choice['message'])
|
241
|
+
@finish_reason = choice['finish_reason']
|
242
|
+
end
|
243
|
+
|
244
|
+
# Converts the choice to a string by returning the message content.
|
245
|
+
#
|
246
|
+
# @return [String] The message content
|
247
|
+
def to_s
|
248
|
+
@message.to_s
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Represents a message in an xAI response.
|
253
|
+
class XaiMessage
|
254
|
+
attr_reader :role, :content
|
255
|
+
|
256
|
+
# Initializes the message with role and content.
|
257
|
+
#
|
258
|
+
# @param message [Hash] The message data from the API response
|
259
|
+
def initialize(message)
|
260
|
+
@role = message['role']
|
261
|
+
@content = message['content']
|
262
|
+
end
|
263
|
+
|
264
|
+
# Converts the message to a string by returning the content.
|
265
|
+
#
|
266
|
+
# @return [String] The message content
|
267
|
+
def to_s
|
268
|
+
@content
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
# Represents a streaming response chunk from xAI's API.
|
273
|
+
class XaiStreamResponse
|
274
|
+
attr_reader :choices
|
275
|
+
|
276
|
+
# Initializes the stream response with parsed chunk data.
|
277
|
+
#
|
278
|
+
# @param parsed [Hash] The parsed JSON chunk from the stream
|
279
|
+
def initialize(parsed)
|
280
|
+
@choices = XaiStreamChoice.new(parsed['choices'])
|
281
|
+
end
|
282
|
+
|
283
|
+
# Converts the stream response to a string by returning the choice content.
|
284
|
+
#
|
285
|
+
# @return [String] The chunk content
|
286
|
+
def to_s
|
287
|
+
@choices.to_s
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Represents an embedding response from xAI's API.
|
292
|
+
class XaiEmbeddingResponse
|
293
|
+
attr_reader :embedding
|
294
|
+
|
295
|
+
# Initializes the embedding response with the embedding data.
|
296
|
+
#
|
297
|
+
# @param data [Hash] The parsed JSON response containing embeddings
|
298
|
+
def initialize(data)
|
299
|
+
@embedding = data.dig('data', 0, 'embedding')
|
300
|
+
end
|
301
|
+
|
302
|
+
# Returns the embedding as an array.
|
303
|
+
#
|
304
|
+
# @return [Array<Float>] The embedding vector
|
305
|
+
def to_a
|
306
|
+
@embedding
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Represents a choice in a streaming response from xAI.
|
311
|
+
class XaiStreamChoice
|
312
|
+
attr_reader :delta, :finish_reason
|
313
|
+
|
314
|
+
# Initializes the stream choice with delta and finish reason.
|
315
|
+
#
|
316
|
+
# @param choice [Array, Hash] The choice data from the stream chunk
|
317
|
+
def initialize(choice)
|
318
|
+
@choice = [choice].flatten.first
|
319
|
+
@delta = XaiStreamDelta.new(@choice['delta'])
|
320
|
+
@finish_reason = @choice['finish_reason']
|
321
|
+
end
|
322
|
+
|
323
|
+
# Converts the choice to a string by returning the delta content.
|
324
|
+
#
|
325
|
+
# @return [String] The delta content
|
326
|
+
def to_s
|
327
|
+
@delta.to_s
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
# Represents a delta (incremental change) in a streaming response.
|
332
|
+
class XaiStreamDelta
|
333
|
+
attr_reader :role, :content
|
334
|
+
|
335
|
+
# Initializes the delta with role and content.
|
336
|
+
#
|
337
|
+
# @param delta [Hash] The delta data from the stream chunk
|
338
|
+
def initialize(delta)
|
339
|
+
@role = delta['role']
|
340
|
+
@content = delta['content']
|
341
|
+
end
|
342
|
+
|
343
|
+
# Converts the delta to a string by returning the content or empty string.
|
344
|
+
#
|
345
|
+
# @return [String] The delta content or empty string
|
346
|
+
def to_s
|
347
|
+
@content || ''
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|