ruby_llm_community 0.0.1 → 0.0.2
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/LICENSE +22 -0
- data/README.md +172 -0
- data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install_generator.rb +121 -0
- data/lib/ruby_llm/active_record/acts_as.rb +382 -0
- data/lib/ruby_llm/aliases.json +217 -0
- data/lib/ruby_llm/aliases.rb +56 -0
- data/lib/ruby_llm/attachment.rb +164 -0
- data/lib/ruby_llm/chat.rb +219 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +75 -0
- data/lib/ruby_llm/connection.rb +126 -0
- data/lib/ruby_llm/content.rb +52 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +30 -0
- data/lib/ruby_llm/error.rb +84 -0
- data/lib/ruby_llm/image.rb +53 -0
- data/lib/ruby_llm/message.rb +76 -0
- data/lib/ruby_llm/mime_type.rb +67 -0
- data/lib/ruby_llm/model/info.rb +101 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +51 -0
- data/lib/ruby_llm/model/pricing_category.rb +48 -0
- data/lib/ruby_llm/model/pricing_tier.rb +34 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +29924 -0
- data/lib/ruby_llm/models.rb +218 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +219 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
- data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
- data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
- data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
- data/lib/ruby_llm/providers/anthropic.rb +37 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +65 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
- data/lib/ruby_llm/providers/bedrock.rb +83 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +139 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
- data/lib/ruby_llm/providers/gemini/images.rb +48 -0
- data/lib/ruby_llm/providers/gemini/media.rb +55 -0
- data/lib/ruby_llm/providers/gemini/models.rb +41 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +58 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
- data/lib/ruby_llm/providers/gpustack.rb +33 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
- data/lib/ruby_llm/providers/mistral/models.rb +49 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
- data/lib/ruby_llm/providers/ollama/media.rb +50 -0
- data/lib/ruby_llm/providers/ollama.rb +29 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
- data/lib/ruby_llm/providers/openai/chat.rb +86 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +81 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/response.rb +115 -0
- data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +190 -0
- data/lib/ruby_llm/providers/openai/tools.rb +100 -0
- data/lib/ruby_llm/providers/openai.rb +44 -0
- data/lib/ruby_llm/providers/openai_base.rb +44 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +52 -0
- data/lib/ruby_llm/railtie.rb +17 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +162 -0
- data/lib/ruby_llm/tool.rb +100 -0
- data/lib/ruby_llm/tool_call.rb +31 -0
- data/lib/ruby_llm/utils.rb +49 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +98 -0
- data/lib/tasks/aliases.rake +235 -0
- data/lib/tasks/models_docs.rake +224 -0
- data/lib/tasks/models_update.rake +108 -0
- data/lib/tasks/release.rake +32 -0
- data/lib/tasks/vcr.rake +99 -0
- metadata +128 -7
@@ -0,0 +1,63 @@
|
|
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
|
+
# Serves as the core module that includes all other streaming-related modules
|
9
|
+
# and provides fundamental streaming operations.
|
10
|
+
#
|
11
|
+
# Responsibilities:
|
12
|
+
# - Stream URL management
|
13
|
+
# - Stream handling and error processing
|
14
|
+
# - Coordinating the functionality of other streaming modules
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# module MyStreamingImplementation
|
18
|
+
# include RubyLLM::Providers::Bedrock::Streaming::Base
|
19
|
+
# end
|
20
|
+
module Base
|
21
|
+
def self.included(base)
|
22
|
+
base.include ContentExtraction
|
23
|
+
base.include MessageProcessing
|
24
|
+
base.include PayloadProcessing
|
25
|
+
base.include PreludeHandling
|
26
|
+
end
|
27
|
+
|
28
|
+
def stream_url
|
29
|
+
"model/#{@model_id}/invoke-with-response-stream"
|
30
|
+
end
|
31
|
+
|
32
|
+
def stream_response(connection, payload, additional_headers = {}, &block)
|
33
|
+
signature = sign_request("#{connection.connection.url_prefix}#{stream_url}", payload:)
|
34
|
+
accumulator = StreamAccumulator.new
|
35
|
+
|
36
|
+
response = connection.post stream_url, payload do |req|
|
37
|
+
req.headers.merge! build_headers(signature.headers, streaming: block_given?)
|
38
|
+
# Merge additional headers, with existing headers taking precedence
|
39
|
+
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
40
|
+
req.options.on_data = handle_stream do |chunk|
|
41
|
+
accumulator.add chunk
|
42
|
+
block.call chunk
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
accumulator.to_message(response)
|
47
|
+
end
|
48
|
+
|
49
|
+
def handle_stream(&block)
|
50
|
+
buffer = +''
|
51
|
+
proc do |chunk, _bytes, env|
|
52
|
+
if env && env.status != 200
|
53
|
+
handle_failed_response(chunk, buffer, env)
|
54
|
+
else
|
55
|
+
process_chunk(chunk, &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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
|
+
# Provides methods to extract and process various types of content from the response data.
|
9
|
+
#
|
10
|
+
# Responsibilities:
|
11
|
+
# - Extracting content from different response formats
|
12
|
+
# - Processing JSON deltas and content blocks
|
13
|
+
# - Extracting metadata (tokens, model IDs, tool calls)
|
14
|
+
# - Handling different content structures (arrays, blocks, completions)
|
15
|
+
module ContentExtraction
|
16
|
+
def json_delta?(data)
|
17
|
+
data['type'] == 'content_block_delta' && data.dig('delta', 'type') == 'input_json_delta'
|
18
|
+
end
|
19
|
+
|
20
|
+
def extract_streaming_content(data)
|
21
|
+
return '' unless data.is_a?(Hash)
|
22
|
+
|
23
|
+
extract_content_by_type(data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def extract_tool_calls(data)
|
27
|
+
data.dig('message', 'tool_calls') || data['tool_calls']
|
28
|
+
end
|
29
|
+
|
30
|
+
def extract_model_id(data)
|
31
|
+
data.dig('message', 'model') || @model_id
|
32
|
+
end
|
33
|
+
|
34
|
+
def extract_input_tokens(data)
|
35
|
+
data.dig('message', 'usage', 'input_tokens')
|
36
|
+
end
|
37
|
+
|
38
|
+
def extract_output_tokens(data)
|
39
|
+
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def extract_content_by_type(data)
|
45
|
+
case data['type']
|
46
|
+
when 'content_block_start' then extract_block_start_content(data)
|
47
|
+
when 'content_block_delta' then extract_delta_content(data)
|
48
|
+
else ''
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def extract_block_start_content(data)
|
53
|
+
data.dig('content_block', 'text').to_s
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_delta_content(data)
|
57
|
+
data.dig('delta', 'text').to_s
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,79 @@
|
|
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
|
+
# Handles the core message processing logic, including validation and chunking.
|
9
|
+
#
|
10
|
+
# Responsibilities:
|
11
|
+
# - Processing incoming message chunks
|
12
|
+
# - Validating message structure and content
|
13
|
+
# - Managing message offsets and boundaries
|
14
|
+
# - Error handling during message processing
|
15
|
+
#
|
16
|
+
# @example Processing a message chunk
|
17
|
+
# offset = process_message(chunk, current_offset) do |processed_chunk|
|
18
|
+
# handle_processed_chunk(processed_chunk)
|
19
|
+
# end
|
20
|
+
module MessageProcessing
|
21
|
+
def process_chunk(chunk, &)
|
22
|
+
offset = 0
|
23
|
+
offset = process_message(chunk, offset, &) while offset < chunk.bytesize
|
24
|
+
rescue StandardError => e
|
25
|
+
RubyLLM.logger.debug "Error processing chunk: #{e.message}"
|
26
|
+
RubyLLM.logger.debug "Chunk size: #{chunk.bytesize}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def process_message(chunk, offset, &)
|
30
|
+
return chunk.bytesize unless can_read_prelude?(chunk, offset)
|
31
|
+
|
32
|
+
message_info = extract_message_info(chunk, offset)
|
33
|
+
return find_next_message(chunk, offset) unless message_info
|
34
|
+
|
35
|
+
process_valid_message(chunk, offset, message_info, &)
|
36
|
+
end
|
37
|
+
|
38
|
+
def process_valid_message(chunk, offset, message_info, &)
|
39
|
+
payload = extract_payload(chunk, message_info[:headers_end], message_info[:payload_end])
|
40
|
+
return find_next_message(chunk, offset) unless valid_payload?(payload)
|
41
|
+
|
42
|
+
process_payload(payload, &)
|
43
|
+
offset + message_info[:total_length]
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def extract_message_info(chunk, offset)
|
49
|
+
total_length, headers_length = read_prelude(chunk, offset)
|
50
|
+
return unless valid_lengths?(total_length, headers_length)
|
51
|
+
|
52
|
+
message_end = offset + total_length
|
53
|
+
return unless chunk.bytesize >= message_end
|
54
|
+
|
55
|
+
headers_end, payload_end = calculate_positions(offset, total_length, headers_length)
|
56
|
+
return unless valid_positions?(headers_end, payload_end, chunk.bytesize)
|
57
|
+
|
58
|
+
{ total_length:, headers_length:, headers_end:, payload_end: }
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_payload(chunk, headers_end, payload_end)
|
62
|
+
chunk[headers_end...payload_end]
|
63
|
+
end
|
64
|
+
|
65
|
+
def valid_payload?(payload)
|
66
|
+
return false if payload.nil? || payload.empty?
|
67
|
+
|
68
|
+
json_start = payload.index('{')
|
69
|
+
json_end = payload.rindex('}')
|
70
|
+
|
71
|
+
return false if json_start.nil? || json_end.nil? || json_start >= json_end
|
72
|
+
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,90 @@
|
|
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
|
+
# Handles JSON payload extraction, decoding, and chunk creation.
|
11
|
+
#
|
12
|
+
# Responsibilities:
|
13
|
+
# - Extracting and validating JSON payloads
|
14
|
+
# - Decoding Base64-encoded response data
|
15
|
+
# - Creating response chunks from processed data
|
16
|
+
# - Error handling for JSON parsing and processing
|
17
|
+
#
|
18
|
+
# @example Processing a payload
|
19
|
+
# process_payload(raw_payload) do |chunk|
|
20
|
+
# yield_chunk_to_client(chunk)
|
21
|
+
# end
|
22
|
+
module PayloadProcessing
|
23
|
+
def process_payload(payload, &)
|
24
|
+
json_payload = extract_json_payload(payload)
|
25
|
+
parse_and_process_json(json_payload, &)
|
26
|
+
rescue JSON::ParserError => e
|
27
|
+
log_json_parse_error(e, json_payload)
|
28
|
+
rescue StandardError => e
|
29
|
+
log_general_error(e)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def extract_json_payload(payload)
|
35
|
+
json_start = payload.index('{')
|
36
|
+
json_end = payload.rindex('}')
|
37
|
+
payload[json_start..json_end]
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_and_process_json(json_payload, &)
|
41
|
+
json_data = JSON.parse(json_payload)
|
42
|
+
process_json_data(json_data, &)
|
43
|
+
end
|
44
|
+
|
45
|
+
def process_json_data(json_data, &)
|
46
|
+
return unless json_data['bytes']
|
47
|
+
|
48
|
+
data = decode_and_parse_data(json_data)
|
49
|
+
create_and_yield_chunk(data, &)
|
50
|
+
end
|
51
|
+
|
52
|
+
def decode_and_parse_data(json_data)
|
53
|
+
decoded_bytes = Base64.strict_decode64(json_data['bytes'])
|
54
|
+
JSON.parse(decoded_bytes)
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_and_yield_chunk(data, &block)
|
58
|
+
block.call(build_chunk(data))
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_chunk(data)
|
62
|
+
Chunk.new(
|
63
|
+
**extract_chunk_attributes(data)
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
def extract_chunk_attributes(data)
|
68
|
+
{
|
69
|
+
role: :assistant,
|
70
|
+
model_id: extract_model_id(data),
|
71
|
+
content: extract_streaming_content(data),
|
72
|
+
input_tokens: extract_input_tokens(data),
|
73
|
+
output_tokens: extract_output_tokens(data),
|
74
|
+
tool_calls: extract_tool_calls(data)
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def log_json_parse_error(error, json_payload)
|
79
|
+
RubyLLM.logger.debug "Failed to parse payload as JSON: #{error.message}"
|
80
|
+
RubyLLM.logger.debug "Attempted JSON payload: #{json_payload.inspect}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def log_general_error(error)
|
84
|
+
RubyLLM.logger.debug "Error processing payload: #{error.message}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,91 @@
|
|
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
|
+
# Manages the parsing and validation of message headers and prelude data.
|
9
|
+
#
|
10
|
+
# Responsibilities:
|
11
|
+
# - Reading and validating message preludes
|
12
|
+
# - Calculating message positions and boundaries
|
13
|
+
# - Finding and validating prelude positions in chunks
|
14
|
+
# - Ensuring message integrity through length validation
|
15
|
+
#
|
16
|
+
# @example Reading a prelude
|
17
|
+
# if can_read_prelude?(chunk, offset)
|
18
|
+
# total_length, headers_length = read_prelude(chunk, offset)
|
19
|
+
# process_message_with_lengths(total_length, headers_length)
|
20
|
+
# end
|
21
|
+
module PreludeHandling
|
22
|
+
def can_read_prelude?(chunk, offset)
|
23
|
+
chunk.bytesize - offset >= 12
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_prelude(chunk, offset)
|
27
|
+
total_length = chunk[offset...(offset + 4)].unpack1('N')
|
28
|
+
headers_length = chunk[(offset + 4)...(offset + 8)].unpack1('N')
|
29
|
+
[total_length, headers_length]
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_lengths?(total_length, headers_length)
|
33
|
+
valid_length_constraints?(total_length, headers_length)
|
34
|
+
end
|
35
|
+
|
36
|
+
def calculate_positions(offset, total_length, headers_length)
|
37
|
+
headers_end = offset + 12 + headers_length
|
38
|
+
payload_end = offset + total_length - 4 # Subtract 4 bytes for message CRC
|
39
|
+
[headers_end, payload_end]
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid_positions?(headers_end, payload_end, chunk_size)
|
43
|
+
return false if headers_end >= payload_end
|
44
|
+
return false if headers_end >= chunk_size
|
45
|
+
return false if payload_end > chunk_size
|
46
|
+
|
47
|
+
true
|
48
|
+
end
|
49
|
+
|
50
|
+
def find_next_message(chunk, offset)
|
51
|
+
next_prelude = find_next_prelude(chunk, offset + 4)
|
52
|
+
next_prelude || chunk.bytesize
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_next_prelude(chunk, start_offset)
|
56
|
+
scan_range(chunk, start_offset).each do |pos|
|
57
|
+
return pos if valid_prelude_at_position?(chunk, pos)
|
58
|
+
end
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def scan_range(chunk, start_offset)
|
65
|
+
(start_offset...(chunk.bytesize - 8))
|
66
|
+
end
|
67
|
+
|
68
|
+
def valid_prelude_at_position?(chunk, pos)
|
69
|
+
lengths = extract_potential_lengths(chunk, pos)
|
70
|
+
valid_length_constraints?(*lengths)
|
71
|
+
end
|
72
|
+
|
73
|
+
def extract_potential_lengths(chunk, pos)
|
74
|
+
[
|
75
|
+
chunk[pos...(pos + 4)].unpack1('N'),
|
76
|
+
chunk[(pos + 4)...(pos + 8)].unpack1('N')
|
77
|
+
]
|
78
|
+
end
|
79
|
+
|
80
|
+
def valid_length_constraints?(total_length, headers_length)
|
81
|
+
return false if total_length.nil? || headers_length.nil?
|
82
|
+
return false if total_length <= 0 || total_length > 1_000_000
|
83
|
+
return false if headers_length <= 0 || headers_length >= total_length
|
84
|
+
|
85
|
+
true
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'streaming/base'
|
4
|
+
require_relative 'streaming/content_extraction'
|
5
|
+
require_relative 'streaming/message_processing'
|
6
|
+
require_relative 'streaming/payload_processing'
|
7
|
+
require_relative 'streaming/prelude_handling'
|
8
|
+
|
9
|
+
module RubyLLM
|
10
|
+
module Providers
|
11
|
+
class Bedrock
|
12
|
+
# Streaming implementation for the AWS Bedrock API.
|
13
|
+
# This module provides functionality for handling streaming responses from AWS Bedrock,
|
14
|
+
# including message processing, content extraction, and error handling.
|
15
|
+
#
|
16
|
+
# The implementation is split into several focused modules:
|
17
|
+
# - Base: Core streaming functionality and module coordination
|
18
|
+
# - ContentExtraction: Extracting content from response data
|
19
|
+
# - MessageProcessing: Processing streaming message chunks
|
20
|
+
# - PayloadProcessing: Handling JSON payloads and chunk creation
|
21
|
+
# - PreludeHandling: Managing message preludes and headers
|
22
|
+
#
|
23
|
+
# @example Using the streaming module
|
24
|
+
# class BedrockClient
|
25
|
+
# include RubyLLM::Providers::Bedrock::Streaming
|
26
|
+
#
|
27
|
+
# def stream_response(&block)
|
28
|
+
# handle_stream(&block)
|
29
|
+
# end
|
30
|
+
# end
|
31
|
+
module Streaming
|
32
|
+
include Base
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module RubyLLM
|
7
|
+
module Providers
|
8
|
+
# AWS Bedrock API integration. Handles chat completion and streaming
|
9
|
+
# for Claude models.
|
10
|
+
class Bedrock < Provider
|
11
|
+
include Bedrock::Chat
|
12
|
+
include Bedrock::Streaming
|
13
|
+
include Bedrock::Models
|
14
|
+
include Bedrock::Signing
|
15
|
+
include Bedrock::Media
|
16
|
+
include Anthropic::Tools
|
17
|
+
|
18
|
+
def api_base
|
19
|
+
"https://bedrock-runtime.#{@config.bedrock_region}.amazonaws.com"
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_error(response)
|
23
|
+
return if response.body.empty?
|
24
|
+
|
25
|
+
body = try_parse_json(response.body)
|
26
|
+
case body
|
27
|
+
when Hash
|
28
|
+
body['message']
|
29
|
+
when Array
|
30
|
+
body.map do |part|
|
31
|
+
part['message']
|
32
|
+
end.join('. ')
|
33
|
+
else
|
34
|
+
body
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def sign_request(url, method: :post, payload: nil)
|
39
|
+
signer = create_signer
|
40
|
+
request = build_request(url, method:, payload:)
|
41
|
+
signer.sign_request(request)
|
42
|
+
end
|
43
|
+
|
44
|
+
def create_signer
|
45
|
+
Signing::Signer.new({
|
46
|
+
access_key_id: @config.bedrock_api_key,
|
47
|
+
secret_access_key: @config.bedrock_secret_key,
|
48
|
+
session_token: @config.bedrock_session_token,
|
49
|
+
region: @config.bedrock_region,
|
50
|
+
service: 'bedrock'
|
51
|
+
})
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_request(url, method: :post, payload: nil)
|
55
|
+
{
|
56
|
+
connection: @connection,
|
57
|
+
http_method: method,
|
58
|
+
url: url || completion_url,
|
59
|
+
body: payload ? JSON.generate(payload, ascii_only: false) : nil
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def build_headers(signature_headers, streaming: false)
|
64
|
+
accept_header = streaming ? 'application/vnd.amazon.eventstream' : 'application/json'
|
65
|
+
|
66
|
+
signature_headers.merge(
|
67
|
+
'Content-Type' => 'application/json',
|
68
|
+
'Accept' => accept_header
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
def capabilities
|
74
|
+
Bedrock::Capabilities
|
75
|
+
end
|
76
|
+
|
77
|
+
def configuration_requirements
|
78
|
+
%i[bedrock_api_key bedrock_secret_key bedrock_region]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class DeepSeek
|
6
|
+
# Determines capabilities and pricing for DeepSeek models
|
7
|
+
module Capabilities
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def context_window_for(model_id)
|
11
|
+
case model_id
|
12
|
+
when /deepseek-(?:chat|reasoner)/ then 64_000
|
13
|
+
else 32_768 # Sensible default
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def max_tokens_for(model_id)
|
18
|
+
case model_id
|
19
|
+
when /deepseek-(?:chat|reasoner)/ then 8_192
|
20
|
+
else 4_096
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def input_price_for(model_id)
|
25
|
+
PRICES.dig(model_family(model_id), :input_miss) || default_input_price
|
26
|
+
end
|
27
|
+
|
28
|
+
def output_price_for(model_id)
|
29
|
+
PRICES.dig(model_family(model_id), :output) || default_output_price
|
30
|
+
end
|
31
|
+
|
32
|
+
def cache_hit_price_for(model_id)
|
33
|
+
PRICES.dig(model_family(model_id), :input_hit) || default_cache_hit_price
|
34
|
+
end
|
35
|
+
|
36
|
+
def supports_vision?(_model_id)
|
37
|
+
false
|
38
|
+
end
|
39
|
+
|
40
|
+
def supports_functions?(model_id)
|
41
|
+
model_id.match?(/deepseek-chat/)
|
42
|
+
end
|
43
|
+
|
44
|
+
def supports_json_mode?(_model_id)
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
def format_display_name(model_id)
|
49
|
+
case model_id
|
50
|
+
when 'deepseek-chat' then 'DeepSeek V3'
|
51
|
+
when 'deepseek-reasoner' then 'DeepSeek R1'
|
52
|
+
else
|
53
|
+
model_id.split('-')
|
54
|
+
.map(&:capitalize)
|
55
|
+
.join(' ')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def model_type(_model_id)
|
60
|
+
'chat'
|
61
|
+
end
|
62
|
+
|
63
|
+
def model_family(model_id)
|
64
|
+
case model_id
|
65
|
+
when /deepseek-reasoner/ then :reasoner
|
66
|
+
else :chat
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Pricing information for DeepSeek models (USD per 1M tokens)
|
71
|
+
PRICES = {
|
72
|
+
chat: {
|
73
|
+
input_hit: 0.07,
|
74
|
+
input_miss: 0.27,
|
75
|
+
output: 1.10
|
76
|
+
},
|
77
|
+
reasoner: {
|
78
|
+
input_hit: 0.14,
|
79
|
+
input_miss: 0.55,
|
80
|
+
output: 2.19
|
81
|
+
}
|
82
|
+
}.freeze
|
83
|
+
|
84
|
+
def default_input_price
|
85
|
+
0.27
|
86
|
+
end
|
87
|
+
|
88
|
+
def default_output_price
|
89
|
+
1.10
|
90
|
+
end
|
91
|
+
|
92
|
+
def default_cache_hit_price
|
93
|
+
0.07
|
94
|
+
end
|
95
|
+
|
96
|
+
def modalities_for(_model_id)
|
97
|
+
{
|
98
|
+
input: ['text'],
|
99
|
+
output: ['text']
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def capabilities_for(model_id)
|
104
|
+
capabilities = ['streaming']
|
105
|
+
|
106
|
+
capabilities << 'function_calling' if model_id.match?(/deepseek-chat/)
|
107
|
+
|
108
|
+
capabilities
|
109
|
+
end
|
110
|
+
|
111
|
+
def pricing_for(model_id)
|
112
|
+
family = model_family(model_id)
|
113
|
+
prices = PRICES.fetch(family, { input_miss: default_input_price, output: default_output_price })
|
114
|
+
|
115
|
+
standard_pricing = {
|
116
|
+
input_per_million: prices[:input_miss],
|
117
|
+
output_per_million: prices[:output]
|
118
|
+
}
|
119
|
+
|
120
|
+
standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
|
121
|
+
|
122
|
+
{
|
123
|
+
text_tokens: {
|
124
|
+
standard: standard_pricing
|
125
|
+
}
|
126
|
+
}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class DeepSeek
|
6
|
+
# Chat methods of the DeepSeek API integration
|
7
|
+
module Chat
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def format_role(role)
|
11
|
+
# DeepSeek doesn't use the new OpenAI convention for system prompts
|
12
|
+
role.to_s
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# DeepSeek API integration.
|
6
|
+
class DeepSeek < OpenAIBase
|
7
|
+
include DeepSeek::Chat
|
8
|
+
|
9
|
+
def api_base
|
10
|
+
'https://api.deepseek.com'
|
11
|
+
end
|
12
|
+
|
13
|
+
def headers
|
14
|
+
{
|
15
|
+
'Authorization' => "Bearer #{@config.deepseek_api_key}"
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def capabilities
|
21
|
+
DeepSeek::Capabilities
|
22
|
+
end
|
23
|
+
|
24
|
+
def configuration_requirements
|
25
|
+
%i[deepseek_api_key]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|