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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +7 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CONFIGURE.md +132 -0
  5. data/Gemfile +7 -9
  6. data/Gemfile.lock +3 -3
  7. data/README.md +1 -0
  8. data/Rakefile +6 -6
  9. data/devenv.lock +103 -0
  10. data/devenv.nix +9 -0
  11. data/devenv.yaml +15 -0
  12. data/durable-llm.gemspec +44 -0
  13. data/examples/openai_quick_complete.rb +3 -1
  14. data/lib/durable/llm/cli.rb +247 -60
  15. data/lib/durable/llm/client.rb +92 -11
  16. data/lib/durable/llm/configuration.rb +174 -23
  17. data/lib/durable/llm/errors.rb +185 -0
  18. data/lib/durable/llm/providers/anthropic.rb +246 -36
  19. data/lib/durable/llm/providers/azure_openai.rb +347 -0
  20. data/lib/durable/llm/providers/base.rb +106 -9
  21. data/lib/durable/llm/providers/cohere.rb +227 -0
  22. data/lib/durable/llm/providers/deepseek.rb +233 -0
  23. data/lib/durable/llm/providers/fireworks.rb +278 -0
  24. data/lib/durable/llm/providers/google.rb +301 -0
  25. data/lib/durable/llm/providers/groq.rb +108 -29
  26. data/lib/durable/llm/providers/huggingface.rb +122 -18
  27. data/lib/durable/llm/providers/mistral.rb +431 -0
  28. data/lib/durable/llm/providers/openai.rb +162 -25
  29. data/lib/durable/llm/providers/opencode.rb +253 -0
  30. data/lib/durable/llm/providers/openrouter.rb +256 -0
  31. data/lib/durable/llm/providers/perplexity.rb +273 -0
  32. data/lib/durable/llm/providers/together.rb +346 -0
  33. data/lib/durable/llm/providers/xai.rb +355 -0
  34. data/lib/durable/llm/providers.rb +103 -15
  35. data/lib/durable/llm/version.rb +5 -1
  36. data/lib/durable/llm.rb +143 -3
  37. data/lib/durable.rb +29 -4
  38. data/sig/durable/llm.rbs +302 -1
  39. 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.