ruby_llm 1.10.0 → 1.12.0
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/README.md +14 -2
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +41 -7
- data/lib/ruby_llm/active_record/chat_methods.rb +41 -7
- data/lib/ruby_llm/agent.rb +323 -0
- data/lib/ruby_llm/aliases.json +50 -32
- data/lib/ruby_llm/chat.rb +27 -3
- data/lib/ruby_llm/configuration.rb +4 -0
- data/lib/ruby_llm/models.json +19806 -5991
- data/lib/ruby_llm/models.rb +35 -6
- data/lib/ruby_llm/provider.rb +13 -1
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/azure/chat.rb +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +297 -56
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +88 -65
- data/lib/ruby_llm/providers/bedrock/streaming.rb +305 -8
- data/lib/ruby_llm/providers/bedrock.rb +61 -52
- data/lib/ruby_llm/providers/openai/media.rb +1 -1
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +28 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +10 -4
- data/lib/tasks/vcr.rake +32 -0
- metadata +16 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -128
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -85
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class Bedrock
|
|
6
|
-
module Streaming
|
|
7
|
-
# Base module for AWS Bedrock streaming functionality.
|
|
8
|
-
module Base
|
|
9
|
-
def self.included(base)
|
|
10
|
-
base.include ContentExtraction
|
|
11
|
-
base.include MessageProcessing
|
|
12
|
-
base.include PayloadProcessing
|
|
13
|
-
base.include PreludeHandling
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def stream_url
|
|
17
|
-
"model/#{@model_id}/invoke-with-response-stream"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def stream_response(connection, payload, additional_headers = {}, &block)
|
|
21
|
-
signature = sign_request("#{connection.connection.url_prefix}#{stream_url}", payload:)
|
|
22
|
-
accumulator = StreamAccumulator.new
|
|
23
|
-
|
|
24
|
-
response = connection.post stream_url, payload do |req|
|
|
25
|
-
req.headers.merge! build_headers(signature.headers, streaming: block_given?)
|
|
26
|
-
# Merge additional headers, with existing headers taking precedence
|
|
27
|
-
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
|
28
|
-
req.options.on_data = handle_stream do |chunk|
|
|
29
|
-
accumulator.add chunk
|
|
30
|
-
block.call chunk
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
accumulator.to_message(response)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def handle_stream(&block)
|
|
38
|
-
buffer = +''
|
|
39
|
-
proc do |chunk, _bytes, env|
|
|
40
|
-
if env && env.status != 200
|
|
41
|
-
handle_failed_response(chunk, buffer, env)
|
|
42
|
-
else
|
|
43
|
-
process_chunk(chunk, &block)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class Bedrock
|
|
6
|
-
module Streaming
|
|
7
|
-
# Module for handling content extraction from AWS Bedrock streaming responses.
|
|
8
|
-
module ContentExtraction
|
|
9
|
-
def json_delta?(data)
|
|
10
|
-
data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def extract_streaming_content(data)
|
|
14
|
-
return '' unless data.is_a?(Hash)
|
|
15
|
-
|
|
16
|
-
extract_content_by_type(data)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def extract_thinking_delta(data)
|
|
20
|
-
return nil unless data.is_a?(Hash)
|
|
21
|
-
|
|
22
|
-
if data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'thinking_delta'
|
|
23
|
-
return data.dig('delta', 'thinking')
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
if data['type'] == 'content_block_start' && data.dig('content_block', 'type') == 'thinking'
|
|
27
|
-
return data.dig('content_block', 'thinking') || data.dig('content_block', 'text')
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
nil
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def extract_signature_delta(data)
|
|
34
|
-
return nil unless data.is_a?(Hash)
|
|
35
|
-
|
|
36
|
-
signature = extract_signature_from_delta(data)
|
|
37
|
-
return signature if signature
|
|
38
|
-
|
|
39
|
-
return nil unless data['type'] == 'content_block_start'
|
|
40
|
-
|
|
41
|
-
extract_signature_from_block(data['content_block'])
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def extract_tool_calls(data)
|
|
45
|
-
data.dig('message', 'tool_calls') || data['tool_calls']
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def extract_model_id(data)
|
|
49
|
-
data.dig('message', 'model') || @model_id
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def extract_input_tokens(data)
|
|
53
|
-
data.dig('message', 'usage', 'input_tokens')
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def extract_output_tokens(data)
|
|
57
|
-
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def extract_cached_tokens(data)
|
|
61
|
-
data.dig('message', 'usage', 'cache_read_input_tokens') || data.dig('usage', 'cache_read_input_tokens')
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def extract_cache_creation_tokens(data)
|
|
65
|
-
direct = data.dig('message', 'usage',
|
|
66
|
-
'cache_creation_input_tokens') || data.dig('usage', 'cache_creation_input_tokens')
|
|
67
|
-
return direct if direct
|
|
68
|
-
|
|
69
|
-
breakdown = data.dig('message', 'usage', 'cache_creation') || data.dig('usage', 'cache_creation')
|
|
70
|
-
return unless breakdown.is_a?(Hash)
|
|
71
|
-
|
|
72
|
-
breakdown.values.compact.sum
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def extract_thinking_tokens(data)
|
|
76
|
-
data.dig('message', 'usage', 'thinking_tokens') ||
|
|
77
|
-
data.dig('message', 'usage', 'output_tokens_details', 'thinking_tokens') ||
|
|
78
|
-
data.dig('usage', 'thinking_tokens') ||
|
|
79
|
-
data.dig('usage', 'output_tokens_details', 'thinking_tokens') ||
|
|
80
|
-
data.dig('message', 'usage', 'reasoning_tokens') ||
|
|
81
|
-
data.dig('message', 'usage', 'output_tokens_details', 'reasoning_tokens') ||
|
|
82
|
-
data.dig('usage', 'reasoning_tokens') ||
|
|
83
|
-
data.dig('usage', 'output_tokens_details', 'reasoning_tokens')
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
def extract_content_by_type(data)
|
|
89
|
-
case data['type']
|
|
90
|
-
when 'content_block_start' then extract_block_start_content(data)
|
|
91
|
-
when 'content_block_delta' then extract_delta_content(data)
|
|
92
|
-
else ''
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def extract_block_start_content(data)
|
|
97
|
-
content_block = data['content_block'] || {}
|
|
98
|
-
return '' if %w[thinking redacted_thinking].include?(content_block['type'])
|
|
99
|
-
|
|
100
|
-
content_block['text'].to_s
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def extract_delta_content(data)
|
|
104
|
-
delta = data['delta'] || {}
|
|
105
|
-
return '' if %w[thinking_delta signature_delta].include?(delta['type'])
|
|
106
|
-
|
|
107
|
-
delta['text'].to_s
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def extract_signature_from_delta(data)
|
|
111
|
-
return unless data['type'] == 'content_block_delta'
|
|
112
|
-
return unless data.dig('delta', 'type') == 'signature_delta'
|
|
113
|
-
|
|
114
|
-
data.dig('delta', 'signature')
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def extract_signature_from_block(content_block)
|
|
118
|
-
block = content_block || {}
|
|
119
|
-
return block['signature'] if block['type'] == 'thinking' && block['signature']
|
|
120
|
-
return block['data'] if block['type'] == 'redacted_thinking'
|
|
121
|
-
|
|
122
|
-
nil
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
end
|
|
128
|
-
end
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class Bedrock
|
|
6
|
-
module Streaming
|
|
7
|
-
# Module for processing streaming messages from AWS Bedrock.
|
|
8
|
-
module MessageProcessing
|
|
9
|
-
def process_chunk(chunk, &)
|
|
10
|
-
offset = 0
|
|
11
|
-
offset = process_message(chunk, offset, &) while offset < chunk.bytesize
|
|
12
|
-
rescue StandardError => e
|
|
13
|
-
RubyLLM.logger.debug "Error processing chunk: #{e.message}"
|
|
14
|
-
RubyLLM.logger.debug "Chunk size: #{chunk.bytesize}"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def process_message(chunk, offset, &)
|
|
18
|
-
return chunk.bytesize unless can_read_prelude?(chunk, offset)
|
|
19
|
-
|
|
20
|
-
message_info = extract_message_info(chunk, offset)
|
|
21
|
-
return find_next_message(chunk, offset) unless message_info
|
|
22
|
-
|
|
23
|
-
process_valid_message(chunk, offset, message_info, &)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def process_valid_message(chunk, offset, message_info, &)
|
|
27
|
-
payload = extract_payload(chunk, message_info[:headers_end], message_info[:payload_end])
|
|
28
|
-
return find_next_message(chunk, offset) unless valid_payload?(payload)
|
|
29
|
-
|
|
30
|
-
process_payload(payload, &)
|
|
31
|
-
offset + message_info[:total_length]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
private
|
|
35
|
-
|
|
36
|
-
def extract_message_info(chunk, offset)
|
|
37
|
-
total_length, headers_length = read_prelude(chunk, offset)
|
|
38
|
-
return unless valid_lengths?(total_length, headers_length)
|
|
39
|
-
|
|
40
|
-
message_end = offset + total_length
|
|
41
|
-
return unless chunk.bytesize >= message_end
|
|
42
|
-
|
|
43
|
-
headers_end, payload_end = calculate_positions(offset, total_length, headers_length)
|
|
44
|
-
return unless valid_positions?(headers_end, payload_end, chunk.bytesize)
|
|
45
|
-
|
|
46
|
-
{ total_length:, headers_length:, headers_end:, payload_end: }
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def extract_payload(chunk, headers_end, payload_end)
|
|
50
|
-
chunk[headers_end...payload_end]
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def valid_payload?(payload)
|
|
54
|
-
return false if payload.nil? || payload.empty?
|
|
55
|
-
|
|
56
|
-
json_start = payload.index('{')
|
|
57
|
-
json_end = payload.rindex('}')
|
|
58
|
-
|
|
59
|
-
return false if json_start.nil? || json_end.nil? || json_start >= json_end
|
|
60
|
-
|
|
61
|
-
true
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'base64'
|
|
4
|
-
|
|
5
|
-
module RubyLLM
|
|
6
|
-
module Providers
|
|
7
|
-
class Bedrock
|
|
8
|
-
module Streaming
|
|
9
|
-
# Module for processing payloads from AWS Bedrock streaming responses.
|
|
10
|
-
module PayloadProcessing
|
|
11
|
-
def process_payload(payload, &)
|
|
12
|
-
json_payload = extract_json_payload(payload)
|
|
13
|
-
parse_and_process_json(json_payload, &)
|
|
14
|
-
rescue JSON::ParserError => e
|
|
15
|
-
log_json_parse_error(e, json_payload)
|
|
16
|
-
rescue StandardError => e
|
|
17
|
-
log_general_error(e)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def extract_json_payload(payload)
|
|
23
|
-
json_start = payload.index('{')
|
|
24
|
-
json_end = payload.rindex('}')
|
|
25
|
-
payload[json_start..json_end]
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def parse_and_process_json(json_payload, &)
|
|
29
|
-
json_data = JSON.parse(json_payload)
|
|
30
|
-
process_json_data(json_data, &)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def process_json_data(json_data, &)
|
|
34
|
-
return unless json_data['bytes']
|
|
35
|
-
|
|
36
|
-
data = decode_and_parse_data(json_data)
|
|
37
|
-
create_and_yield_chunk(data, &)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def decode_and_parse_data(json_data)
|
|
41
|
-
decoded_bytes = Base64.strict_decode64(json_data['bytes'])
|
|
42
|
-
JSON.parse(decoded_bytes)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def create_and_yield_chunk(data, &block)
|
|
46
|
-
block.call(build_chunk(data))
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def build_chunk(data)
|
|
50
|
-
Chunk.new(
|
|
51
|
-
**extract_chunk_attributes(data)
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def extract_chunk_attributes(data)
|
|
56
|
-
{
|
|
57
|
-
role: :assistant,
|
|
58
|
-
model_id: extract_model_id(data),
|
|
59
|
-
content: extract_streaming_content(data),
|
|
60
|
-
thinking: Thinking.build(
|
|
61
|
-
text: extract_thinking_delta(data),
|
|
62
|
-
signature: extract_signature_delta(data)
|
|
63
|
-
),
|
|
64
|
-
input_tokens: extract_input_tokens(data),
|
|
65
|
-
output_tokens: extract_output_tokens(data),
|
|
66
|
-
cached_tokens: extract_cached_tokens(data),
|
|
67
|
-
cache_creation_tokens: extract_cache_creation_tokens(data),
|
|
68
|
-
thinking_tokens: extract_thinking_tokens(data),
|
|
69
|
-
tool_calls: extract_tool_calls(data)
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def log_json_parse_error(error, json_payload)
|
|
74
|
-
RubyLLM.logger.debug "Failed to parse payload as JSON: #{error.message}"
|
|
75
|
-
RubyLLM.logger.debug "Attempted JSON payload: #{json_payload.inspect}"
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def log_general_error(error)
|
|
79
|
-
RubyLLM.logger.debug "Error processing payload: #{error.message}"
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Providers
|
|
5
|
-
class Bedrock
|
|
6
|
-
module Streaming
|
|
7
|
-
# Module for handling message preludes in AWS Bedrock streaming responses.
|
|
8
|
-
module PreludeHandling
|
|
9
|
-
def can_read_prelude?(chunk, offset)
|
|
10
|
-
chunk.bytesize - offset >= 12
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def read_prelude(chunk, offset)
|
|
14
|
-
total_length = chunk[offset...(offset + 4)].unpack1('N')
|
|
15
|
-
headers_length = chunk[(offset + 4)...(offset + 8)].unpack1('N')
|
|
16
|
-
[total_length, headers_length]
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def valid_lengths?(total_length, headers_length)
|
|
20
|
-
valid_length_constraints?(total_length, headers_length)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def calculate_positions(offset, total_length, headers_length)
|
|
24
|
-
headers_end = offset + 12 + headers_length
|
|
25
|
-
payload_end = offset + total_length - 4 # Subtract 4 bytes for message CRC
|
|
26
|
-
[headers_end, payload_end]
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def valid_positions?(headers_end, payload_end, chunk_size)
|
|
30
|
-
return false if headers_end >= payload_end
|
|
31
|
-
return false if headers_end >= chunk_size
|
|
32
|
-
return false if payload_end > chunk_size
|
|
33
|
-
|
|
34
|
-
true
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def find_next_message(chunk, offset)
|
|
38
|
-
next_prelude = find_next_prelude(chunk, offset + 4)
|
|
39
|
-
next_prelude || chunk.bytesize
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def find_next_prelude(chunk, start_offset)
|
|
43
|
-
scan_range(chunk, start_offset).each do |pos|
|
|
44
|
-
return pos if valid_prelude_at_position?(chunk, pos)
|
|
45
|
-
end
|
|
46
|
-
nil
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
private
|
|
50
|
-
|
|
51
|
-
def scan_range(chunk, start_offset)
|
|
52
|
-
(start_offset...(chunk.bytesize - 8))
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def valid_prelude_at_position?(chunk, pos)
|
|
56
|
-
lengths = extract_potential_lengths(chunk, pos)
|
|
57
|
-
valid_length_constraints?(*lengths)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def extract_potential_lengths(chunk, pos)
|
|
61
|
-
[
|
|
62
|
-
chunk[pos...(pos + 4)].unpack1('N'),
|
|
63
|
-
chunk[(pos + 4)...(pos + 8)].unpack1('N')
|
|
64
|
-
]
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def valid_length_constraints?(total_length, headers_length)
|
|
68
|
-
return false if total_length.nil? || headers_length.nil?
|
|
69
|
-
return false if total_length <= 0 || total_length > 1_000_000
|
|
70
|
-
return false if headers_length <= 0 || headers_length >= total_length
|
|
71
|
-
|
|
72
|
-
true
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|