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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +14 -2
  3. data/lib/ruby_llm/active_record/acts_as_legacy.rb +41 -7
  4. data/lib/ruby_llm/active_record/chat_methods.rb +41 -7
  5. data/lib/ruby_llm/agent.rb +323 -0
  6. data/lib/ruby_llm/aliases.json +50 -32
  7. data/lib/ruby_llm/chat.rb +27 -3
  8. data/lib/ruby_llm/configuration.rb +4 -0
  9. data/lib/ruby_llm/models.json +19806 -5991
  10. data/lib/ruby_llm/models.rb +35 -6
  11. data/lib/ruby_llm/provider.rb +13 -1
  12. data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
  13. data/lib/ruby_llm/providers/azure/chat.rb +29 -0
  14. data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
  15. data/lib/ruby_llm/providers/azure/media.rb +45 -0
  16. data/lib/ruby_llm/providers/azure/models.rb +14 -0
  17. data/lib/ruby_llm/providers/azure.rb +56 -0
  18. data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
  19. data/lib/ruby_llm/providers/bedrock/chat.rb +297 -56
  20. data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
  21. data/lib/ruby_llm/providers/bedrock/models.rb +88 -65
  22. data/lib/ruby_llm/providers/bedrock/streaming.rb +305 -8
  23. data/lib/ruby_llm/providers/bedrock.rb +61 -52
  24. data/lib/ruby_llm/providers/openai/media.rb +1 -1
  25. data/lib/ruby_llm/providers/xai/chat.rb +15 -0
  26. data/lib/ruby_llm/providers/xai/models.rb +75 -0
  27. data/lib/ruby_llm/providers/xai.rb +28 -0
  28. data/lib/ruby_llm/version.rb +1 -1
  29. data/lib/ruby_llm.rb +14 -8
  30. data/lib/tasks/models.rake +10 -4
  31. data/lib/tasks/vcr.rake +32 -0
  32. metadata +16 -13
  33. data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
  34. data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
  35. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
  36. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -128
  37. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
  38. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -85
  39. 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