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
@@ -1,22 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Anthropic provider for Claude models with completion and streaming support.
|
1
4
|
|
2
5
|
require 'faraday'
|
3
6
|
require 'json'
|
4
7
|
require 'durable/llm/errors'
|
5
8
|
require 'durable/llm/providers/base'
|
9
|
+
require 'event_stream_parser'
|
6
10
|
|
7
11
|
module Durable
|
8
12
|
module Llm
|
9
13
|
module Providers
|
14
|
+
# Anthropic provider for accessing Claude language models through their API.
|
15
|
+
#
|
16
|
+
# This provider implements the Durable::Llm::Providers::Base interface to provide
|
17
|
+
# completion and streaming capabilities for Anthropic's Claude models including
|
18
|
+
# Claude 3.5 Sonnet, Claude 3 Opus, and Claude 3 Haiku. It handles authentication
|
19
|
+
# via API keys, supports system messages, and provides comprehensive error handling
|
20
|
+
# for various Anthropic API error conditions.
|
21
|
+
#
|
22
|
+
# Key features:
|
23
|
+
# - Message-based chat completions with multi-turn conversations
|
24
|
+
# - Real-time streaming responses for interactive applications
|
25
|
+
# - System message support for setting context
|
26
|
+
# - Automatic model listing from predefined supported models
|
27
|
+
# - Comprehensive error handling with specific exception types
|
28
|
+
#
|
29
|
+
# @example Basic completion
|
30
|
+
# provider = Durable::Llm::Providers::Anthropic.new(api_key: 'your-api-key')
|
31
|
+
# response = provider.completion(
|
32
|
+
# model: 'claude-3-5-sonnet-20240620',
|
33
|
+
# messages: [{ role: 'user', content: 'Hello, world!' }]
|
34
|
+
# )
|
35
|
+
# puts response.choices.first.to_s
|
36
|
+
#
|
37
|
+
# @example Completion with system message
|
38
|
+
# response = provider.completion(
|
39
|
+
# model: 'claude-3-5-sonnet-20240620',
|
40
|
+
# messages: [
|
41
|
+
# { role: 'system', content: 'You are a helpful assistant.' },
|
42
|
+
# { role: 'user', content: 'Hello!' }
|
43
|
+
# ]
|
44
|
+
# )
|
45
|
+
#
|
46
|
+
# @example Streaming response
|
47
|
+
# provider.stream(model: 'claude-3-5-sonnet-20240620', messages: messages) do |chunk|
|
48
|
+
# print chunk.to_s
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @see https://docs.anthropic.com/claude/docs/messages-overview Anthropic Messages API Documentation
|
10
52
|
class Anthropic < Durable::Llm::Providers::Base
|
11
53
|
BASE_URL = 'https://api.anthropic.com'
|
12
54
|
|
55
|
+
# @return [String, nil] The default API key for Anthropic, or nil if not configured
|
13
56
|
def default_api_key
|
14
57
|
Durable::Llm.configuration.anthropic&.api_key || ENV['ANTHROPIC_API_KEY']
|
15
58
|
end
|
16
59
|
|
60
|
+
# @!attribute [rw] api_key
|
61
|
+
# @return [String, nil] The API key used for authentication with Anthropic
|
17
62
|
attr_accessor :api_key
|
18
63
|
|
64
|
+
# Initializes a new Anthropic provider instance.
|
65
|
+
#
|
66
|
+
# @param api_key [String, nil] The Anthropic API key. If nil, uses default_api_key
|
67
|
+
# @return [Anthropic] A new Anthropic provider instance
|
19
68
|
def initialize(api_key: nil)
|
69
|
+
super()
|
20
70
|
@api_key = api_key || default_api_key
|
21
71
|
|
22
72
|
@conn = Faraday.new(url: BASE_URL) do |faraday|
|
@@ -26,38 +76,111 @@ module Durable
|
|
26
76
|
end
|
27
77
|
end
|
28
78
|
|
79
|
+
# Performs a completion request to Anthropic's messages API.
|
80
|
+
#
|
81
|
+
# @param options [Hash] The completion options
|
82
|
+
# @option options [String] :model The Claude model to use
|
83
|
+
# @option options [Array<Hash>] :messages Array of message objects with role and content
|
84
|
+
# @option options [Integer] :max_tokens Maximum number of tokens to generate (default: 1024)
|
85
|
+
# @option options [Float] :temperature Sampling temperature between 0 and 1
|
86
|
+
# @option options [String] :system System message to set context
|
87
|
+
# @return [AnthropicResponse] The completion response object
|
88
|
+
# @raise [Durable::Llm::AuthenticationError] If API key is invalid
|
89
|
+
# @raise [Durable::Llm::RateLimitError] If rate limit is exceeded
|
90
|
+
# @raise [Durable::Llm::InvalidRequestError] If request parameters are invalid
|
91
|
+
# @raise [Durable::Llm::ServerError] If Anthropic's servers encounter an error
|
29
92
|
def completion(options)
|
30
|
-
|
93
|
+
# Convert symbol keys to strings for consistency
|
94
|
+
options = options.transform_keys(&:to_s)
|
95
|
+
|
96
|
+
# Ensure max_tokens is set
|
97
|
+
options['max_tokens'] ||= 1024
|
98
|
+
|
99
|
+
# Handle system message separately as Anthropic expects it as a top-level parameter
|
100
|
+
system_message = nil
|
101
|
+
messages = options['messages']&.dup || []
|
102
|
+
if messages.first && (messages.first['role'] || messages.first[:role]) == 'system'
|
103
|
+
system_message = messages.first['content'] || messages.first[:content]
|
104
|
+
messages = messages[1..] || []
|
105
|
+
end
|
106
|
+
|
107
|
+
request_body = options.merge('messages' => messages)
|
108
|
+
request_body['system'] = system_message if system_message
|
109
|
+
|
31
110
|
response = @conn.post('/v1/messages') do |req|
|
32
111
|
req.headers['x-api-key'] = @api_key
|
33
112
|
req.headers['anthropic-version'] = '2023-06-01'
|
34
|
-
req.body =
|
113
|
+
req.body = request_body
|
35
114
|
end
|
36
115
|
|
37
116
|
handle_response(response)
|
38
117
|
end
|
39
118
|
|
119
|
+
# Retrieves the list of available models for this provider instance.
|
120
|
+
#
|
121
|
+
# @return [Array<String>] The list of available Claude model names
|
40
122
|
def models
|
41
123
|
self.class.models
|
42
124
|
end
|
125
|
+
|
126
|
+
# Retrieves the list of supported Claude models.
|
127
|
+
#
|
128
|
+
# @return [Array<String>] Array of supported Claude model identifiers
|
43
129
|
def self.models
|
44
130
|
['claude-3-5-sonnet-20240620', 'claude-3-opus-20240229', 'claude-3-haiku-20240307']
|
45
131
|
end
|
46
132
|
|
133
|
+
# @return [Boolean] True, indicating this provider supports streaming
|
47
134
|
def self.stream?
|
48
135
|
true
|
49
136
|
end
|
50
|
-
|
51
|
-
|
137
|
+
|
138
|
+
# Performs an embedding request (not supported by Anthropic).
|
139
|
+
#
|
140
|
+
# @param model [String] The model to use for generating embeddings
|
141
|
+
# @param input [String, Array<String>] The input text(s) to embed
|
142
|
+
# @param options [Hash] Additional options for the embedding request
|
143
|
+
# @raise [NotImplementedError] Anthropic does not provide embedding APIs
|
144
|
+
def embedding(model:, input:, **options)
|
145
|
+
raise NotImplementedError, 'Anthropic does not provide embedding APIs'
|
146
|
+
end
|
147
|
+
|
148
|
+
# Performs a streaming completion request to Anthropic's messages API.
|
149
|
+
#
|
150
|
+
# @param options [Hash] The stream options (same as completion plus stream: true)
|
151
|
+
# @yield [AnthropicStreamResponse] Yields stream response chunks as they arrive
|
152
|
+
# @return [Object] The final response object
|
153
|
+
# @raise [Durable::Llm::AuthenticationError] If API key is invalid
|
154
|
+
# @raise [Durable::Llm::RateLimitError] If rate limit is exceeded
|
155
|
+
# @raise [Durable::Llm::InvalidRequestError] If request parameters are invalid
|
156
|
+
# @raise [Durable::Llm::ServerError] If Anthropic's servers encounter an error
|
157
|
+
def stream(options)
|
158
|
+
options = options.transform_keys(&:to_s)
|
159
|
+
options['stream'] = true
|
160
|
+
|
161
|
+
# Handle system message separately
|
162
|
+
system_message = nil
|
163
|
+
messages = options['messages']&.dup || []
|
164
|
+
if messages.first && (messages.first['role'] || messages.first[:role]) == 'system'
|
165
|
+
system_message = messages.first['content'] || messages.first[:content]
|
166
|
+
messages = messages[1..] || []
|
167
|
+
end
|
168
|
+
|
169
|
+
request_body = options.merge('messages' => messages)
|
170
|
+
request_body['system'] = system_message if system_message
|
171
|
+
|
52
172
|
response = @conn.post('/v1/messages') do |req|
|
53
173
|
req.headers['x-api-key'] = @api_key
|
54
174
|
req.headers['anthropic-version'] = '2023-06-01'
|
55
175
|
req.headers['Accept'] = 'text/event-stream'
|
56
|
-
|
57
|
-
req.
|
58
|
-
|
59
|
-
|
176
|
+
|
177
|
+
req.body = request_body
|
178
|
+
|
179
|
+
user_proc = proc do |chunk, _size, _total|
|
180
|
+
yield AnthropicStreamResponse.new(chunk)
|
60
181
|
end
|
182
|
+
|
183
|
+
req.options.on_data = to_json_stream(user_proc: user_proc)
|
61
184
|
end
|
62
185
|
|
63
186
|
handle_response(response)
|
@@ -65,23 +188,82 @@ module Durable
|
|
65
188
|
|
66
189
|
private
|
67
190
|
|
191
|
+
# Converts JSON stream chunks to individual data objects for processing.
|
192
|
+
#
|
193
|
+
# This method handles Server-Sent Events from Anthropic's streaming API.
|
194
|
+
# It parses the event stream and yields individual JSON objects for each data chunk.
|
195
|
+
#
|
196
|
+
# @param user_proc [Proc] The proc to call with each parsed JSON object
|
197
|
+
# @return [Proc] A proc that can be used as Faraday's on_data callback
|
198
|
+
def to_json_stream(user_proc:)
|
199
|
+
parser = EventStreamParser::Parser.new
|
200
|
+
|
201
|
+
proc do |chunk, _bytes, env|
|
202
|
+
if env && env.status != 200
|
203
|
+
raise_error = Faraday::Response::RaiseError.new
|
204
|
+
raise_error.on_complete(env.merge(body: try_parse_json(chunk)))
|
205
|
+
end
|
206
|
+
|
207
|
+
parser.feed(chunk) do |_type, data|
|
208
|
+
user_proc.call(JSON.parse(data)) unless data == '[DONE]'
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
# Attempts to parse a string as JSON, returning the string if parsing fails.
|
214
|
+
#
|
215
|
+
# @param maybe_json [String] The string that might be JSON
|
216
|
+
# @return [Hash, Array, String] The parsed JSON object or the original string
|
217
|
+
def try_parse_json(maybe_json)
|
218
|
+
JSON.parse(maybe_json)
|
219
|
+
rescue JSON::ParserError
|
220
|
+
maybe_json
|
221
|
+
end
|
222
|
+
|
223
|
+
# Processes the API response and handles errors appropriately.
|
224
|
+
#
|
225
|
+
# @param response [Faraday::Response] The HTTP response from the API
|
226
|
+
# @return [AnthropicResponse] An instance of AnthropicResponse for successful responses
|
227
|
+
# @raise [Durable::Llm::AuthenticationError] For 401 responses
|
228
|
+
# @raise [Durable::Llm::RateLimitError] For 429 responses
|
229
|
+
# @raise [Durable::Llm::InvalidRequestError] For 4xx client errors
|
230
|
+
# @raise [Durable::Llm::ServerError] For 5xx server errors
|
231
|
+
# @raise [Durable::Llm::APIError] For unexpected status codes
|
68
232
|
def handle_response(response)
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
233
|
+
case response.status
|
234
|
+
when 200..299
|
235
|
+
AnthropicResponse.new(response.body)
|
236
|
+
when 401
|
237
|
+
raise Durable::Llm::AuthenticationError, parse_error_message(response)
|
238
|
+
when 429
|
239
|
+
raise Durable::Llm::RateLimitError, parse_error_message(response)
|
240
|
+
when 400..499
|
241
|
+
raise Durable::Llm::InvalidRequestError, parse_error_message(response)
|
242
|
+
when 500..599
|
243
|
+
raise Durable::Llm::ServerError, parse_error_message(response)
|
244
|
+
else
|
245
|
+
raise Durable::Llm::APIError, "Unexpected response code: #{response.status}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Extracts and formats error messages from API error responses.
|
250
|
+
#
|
251
|
+
# @param response [Faraday::Response] The error response from the API
|
252
|
+
# @return [String] The formatted error message
|
253
|
+
def parse_error_message(response)
|
254
|
+
body = begin
|
255
|
+
JSON.parse(response.body)
|
256
|
+
rescue StandardError
|
257
|
+
nil
|
258
|
+
end
|
259
|
+
message = body&.dig('error', 'message') || response.body
|
260
|
+
"#{response.status} Error: #{message}"
|
83
261
|
end
|
84
262
|
|
263
|
+
# Response object for Anthropic messages API responses.
|
264
|
+
#
|
265
|
+
# This class wraps the raw response from Anthropic's messages endpoint
|
266
|
+
# and provides a consistent interface for accessing content and metadata.
|
85
267
|
class AnthropicResponse
|
86
268
|
attr_reader :raw_response
|
87
269
|
|
@@ -90,7 +272,11 @@ module Durable
|
|
90
272
|
end
|
91
273
|
|
92
274
|
def choices
|
93
|
-
[
|
275
|
+
[AnthropicChoice.new(@raw_response)]
|
276
|
+
end
|
277
|
+
|
278
|
+
def usage
|
279
|
+
@raw_response['usage']
|
94
280
|
end
|
95
281
|
|
96
282
|
def to_s
|
@@ -98,11 +284,15 @@ module Durable
|
|
98
284
|
end
|
99
285
|
end
|
100
286
|
|
287
|
+
# Represents a single choice in an Anthropic messages response.
|
288
|
+
#
|
289
|
+
# Anthropic typically returns only one choice, containing the assistant's message.
|
101
290
|
class AnthropicChoice
|
102
|
-
attr_reader :message
|
291
|
+
attr_reader :message, :stop_reason
|
103
292
|
|
104
|
-
def initialize(
|
105
|
-
@message = AnthropicMessage.new(
|
293
|
+
def initialize(response)
|
294
|
+
@message = AnthropicMessage.new(response)
|
295
|
+
@stop_reason = response['stop_reason']
|
106
296
|
end
|
107
297
|
|
108
298
|
def to_s
|
@@ -110,12 +300,15 @@ module Durable
|
|
110
300
|
end
|
111
301
|
end
|
112
302
|
|
303
|
+
# Represents a message in an Anthropic conversation.
|
304
|
+
#
|
305
|
+
# Messages have a role (user, assistant) and content composed of text blocks.
|
113
306
|
class AnthropicMessage
|
114
307
|
attr_reader :role, :content
|
115
308
|
|
116
|
-
def initialize(
|
117
|
-
@role = [
|
118
|
-
@content = [content]
|
309
|
+
def initialize(response)
|
310
|
+
@role = response['role']
|
311
|
+
@content = response['content']&.map { |block| block['text'] }&.join(' ') || ''
|
119
312
|
end
|
120
313
|
|
121
314
|
def to_s
|
@@ -123,12 +316,21 @@ module Durable
|
|
123
316
|
end
|
124
317
|
end
|
125
318
|
|
319
|
+
# Response object for streaming Anthropic messages chunks.
|
320
|
+
#
|
321
|
+
# This wraps individual chunks from the Server-Sent Events stream,
|
322
|
+
# providing access to the incremental content updates.
|
126
323
|
class AnthropicStreamResponse
|
127
|
-
attr_reader :choices
|
128
|
-
|
129
|
-
def initialize(
|
130
|
-
|
131
|
-
@choices =
|
324
|
+
attr_reader :choices, :type
|
325
|
+
|
326
|
+
def initialize(parsed)
|
327
|
+
@type = parsed['type']
|
328
|
+
@choices = case @type
|
329
|
+
when 'content_block_delta'
|
330
|
+
[AnthropicStreamChoice.new(parsed)]
|
331
|
+
else
|
332
|
+
[]
|
333
|
+
end
|
132
334
|
end
|
133
335
|
|
134
336
|
def to_s
|
@@ -136,11 +338,14 @@ module Durable
|
|
136
338
|
end
|
137
339
|
end
|
138
340
|
|
341
|
+
# Represents a single choice in a streaming Anthropic response chunk.
|
342
|
+
#
|
343
|
+
# Contains the delta (incremental content) for the choice.
|
139
344
|
class AnthropicStreamChoice
|
140
345
|
attr_reader :delta
|
141
346
|
|
142
|
-
def initialize(
|
143
|
-
@delta = AnthropicStreamDelta.new(delta)
|
347
|
+
def initialize(event)
|
348
|
+
@delta = AnthropicStreamDelta.new(event['delta'])
|
144
349
|
end
|
145
350
|
|
146
351
|
def to_s
|
@@ -148,6 +353,9 @@ module Durable
|
|
148
353
|
end
|
149
354
|
end
|
150
355
|
|
356
|
+
# Represents the incremental content delta in a streaming response.
|
357
|
+
#
|
358
|
+
# Contains the type and text content of the delta.
|
151
359
|
class AnthropicStreamDelta
|
152
360
|
attr_reader :type, :text
|
153
361
|
|
@@ -164,3 +372,5 @@ module Durable
|
|
164
372
|
end
|
165
373
|
end
|
166
374
|
end
|
375
|
+
|
376
|
+
# Copyright (c) 2025 Durable Programming, LLC. All rights reserved.
|