dify_llm 1.6.4
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +157 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/model_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 +184 -0
- data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
- data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
- data/lib/ruby_llm/active_record/acts_as.rb +137 -0
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +274 -0
- data/lib/ruby_llm/aliases.rb +38 -0
- data/lib/ruby_llm/attachment.rb +191 -0
- data/lib/ruby_llm/chat.rb +212 -0
- data/lib/ruby_llm/chunk.rb +6 -0
- data/lib/ruby_llm/configuration.rb +69 -0
- data/lib/ruby_llm/connection.rb +137 -0
- data/lib/ruby_llm/content.rb +50 -0
- data/lib/ruby_llm/context.rb +29 -0
- data/lib/ruby_llm/embedding.rb +29 -0
- data/lib/ruby_llm/error.rb +76 -0
- data/lib/ruby_llm/image.rb +49 -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 +103 -0
- data/lib/ruby_llm/model/modalities.rb +22 -0
- data/lib/ruby_llm/model/pricing.rb +48 -0
- data/lib/ruby_llm/model/pricing_category.rb +46 -0
- data/lib/ruby_llm/model/pricing_tier.rb +33 -0
- data/lib/ruby_llm/model.rb +7 -0
- data/lib/ruby_llm/models.json +31418 -0
- data/lib/ruby_llm/models.rb +235 -0
- data/lib/ruby_llm/models_schema.json +168 -0
- data/lib/ruby_llm/provider.rb +215 -0
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -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 +91 -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 +107 -0
- data/lib/ruby_llm/providers/anthropic.rb +36 -0
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
- data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
- data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
- data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
- data/lib/ruby_llm/providers/bedrock.rb +82 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
- data/lib/ruby_llm/providers/deepseek.rb +30 -0
- data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/dify/chat.rb +59 -0
- data/lib/ruby_llm/providers/dify/media.rb +37 -0
- data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
- data/lib/ruby_llm/providers/dify.rb +48 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
- data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
- data/lib/ruby_llm/providers/gemini/images.rb +47 -0
- data/lib/ruby_llm/providers/gemini/media.rb +54 -0
- data/lib/ruby_llm/providers/gemini/models.rb +40 -0
- data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
- data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
- data/lib/ruby_llm/providers/gemini.rb +36 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
- data/lib/ruby_llm/providers/gpustack.rb +34 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/mistral/models.rb +48 -0
- data/lib/ruby_llm/providers/mistral.rb +32 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
- data/lib/ruby_llm/providers/ollama/media.rb +45 -0
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +30 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
- data/lib/ruby_llm/providers/openai/chat.rb +83 -0
- data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
- data/lib/ruby_llm/providers/openai/images.rb +38 -0
- data/lib/ruby_llm/providers/openai/media.rb +80 -0
- data/lib/ruby_llm/providers/openai/models.rb +39 -0
- data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
- data/lib/ruby_llm/providers/openai/tools.rb +78 -0
- data/lib/ruby_llm/providers/openai.rb +42 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
- data/lib/ruby_llm/providers/openrouter.rb +26 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
- data/lib/ruby_llm/providers/perplexity.rb +48 -0
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +41 -0
- data/lib/ruby_llm/stream_accumulator.rb +97 -0
- data/lib/ruby_llm/streaming.rb +153 -0
- data/lib/ruby_llm/tool.rb +83 -0
- data/lib/ruby_llm/tool_call.rb +22 -0
- data/lib/ruby_llm/utils.rb +45 -0
- data/lib/ruby_llm/version.rb +5 -0
- data/lib/ruby_llm.rb +97 -0
- data/lib/tasks/models.rake +525 -0
- data/lib/tasks/release.rake +67 -0
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +92 -0
- metadata +291 -0
@@ -0,0 +1,51 @@
|
|
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
|
@@ -0,0 +1,56 @@
|
|
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_tool_calls(data)
|
20
|
+
data.dig('message', 'tool_calls') || data['tool_calls']
|
21
|
+
end
|
22
|
+
|
23
|
+
def extract_model_id(data)
|
24
|
+
data.dig('message', 'model') || @model_id
|
25
|
+
end
|
26
|
+
|
27
|
+
def extract_input_tokens(data)
|
28
|
+
data.dig('message', 'usage', 'input_tokens')
|
29
|
+
end
|
30
|
+
|
31
|
+
def extract_output_tokens(data)
|
32
|
+
data.dig('message', 'usage', 'output_tokens') || data.dig('usage', 'output_tokens')
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def extract_content_by_type(data)
|
38
|
+
case data['type']
|
39
|
+
when 'content_block_start' then extract_block_start_content(data)
|
40
|
+
when 'content_block_delta' then extract_delta_content(data)
|
41
|
+
else ''
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_block_start_content(data)
|
46
|
+
data.dig('content_block', 'text').to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def extract_delta_content(data)
|
50
|
+
data.dig('delta', 'text').to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
@@ -0,0 +1,78 @@
|
|
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
|
+
input_tokens: extract_input_tokens(data),
|
61
|
+
output_tokens: extract_output_tokens(data),
|
62
|
+
tool_calls: extract_tool_calls(data)
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_json_parse_error(error, json_payload)
|
67
|
+
RubyLLM.logger.debug "Failed to parse payload as JSON: #{error.message}"
|
68
|
+
RubyLLM.logger.debug "Attempted JSON payload: #{json_payload.inspect}"
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_general_error(error)
|
72
|
+
RubyLLM.logger.debug "Error processing payload: #{error.message}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,78 @@
|
|
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
|
@@ -0,0 +1,18 @@
|
|
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
|
+
module Streaming
|
14
|
+
include Base
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module RubyLLM
|
7
|
+
module Providers
|
8
|
+
# AWS Bedrock API integration.
|
9
|
+
class Bedrock < Provider
|
10
|
+
include Bedrock::Chat
|
11
|
+
include Bedrock::Streaming
|
12
|
+
include Bedrock::Models
|
13
|
+
include Bedrock::Signing
|
14
|
+
include Bedrock::Media
|
15
|
+
include Anthropic::Tools
|
16
|
+
|
17
|
+
def api_base
|
18
|
+
"https://bedrock-runtime.#{@config.bedrock_region}.amazonaws.com"
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse_error(response)
|
22
|
+
return if response.body.empty?
|
23
|
+
|
24
|
+
body = try_parse_json(response.body)
|
25
|
+
case body
|
26
|
+
when Hash
|
27
|
+
body['message']
|
28
|
+
when Array
|
29
|
+
body.map do |part|
|
30
|
+
part['message']
|
31
|
+
end.join('. ')
|
32
|
+
else
|
33
|
+
body
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def sign_request(url, method: :post, payload: nil)
|
38
|
+
signer = create_signer
|
39
|
+
request = build_request(url, method:, payload:)
|
40
|
+
signer.sign_request(request)
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_signer
|
44
|
+
Signing::Signer.new({
|
45
|
+
access_key_id: @config.bedrock_api_key,
|
46
|
+
secret_access_key: @config.bedrock_secret_key,
|
47
|
+
session_token: @config.bedrock_session_token,
|
48
|
+
region: @config.bedrock_region,
|
49
|
+
service: 'bedrock'
|
50
|
+
})
|
51
|
+
end
|
52
|
+
|
53
|
+
def build_request(url, method: :post, payload: nil)
|
54
|
+
{
|
55
|
+
connection: @connection,
|
56
|
+
http_method: method,
|
57
|
+
url: url || completion_url,
|
58
|
+
body: payload ? JSON.generate(payload, ascii_only: false) : nil
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
def build_headers(signature_headers, streaming: false)
|
63
|
+
accept_header = streaming ? 'application/vnd.amazon.eventstream' : 'application/json'
|
64
|
+
|
65
|
+
signature_headers.merge(
|
66
|
+
'Content-Type' => 'application/json',
|
67
|
+
'Accept' => accept_header
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
class << self
|
72
|
+
def capabilities
|
73
|
+
Bedrock::Capabilities
|
74
|
+
end
|
75
|
+
|
76
|
+
def configuration_requirements
|
77
|
+
%i[bedrock_api_key bedrock_secret_key bedrock_region]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,130 @@
|
|
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
|
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
|
+
PRICES = {
|
71
|
+
chat: {
|
72
|
+
input_hit: 0.07,
|
73
|
+
input_miss: 0.27,
|
74
|
+
output: 1.10
|
75
|
+
},
|
76
|
+
reasoner: {
|
77
|
+
input_hit: 0.14,
|
78
|
+
input_miss: 0.55,
|
79
|
+
output: 2.19
|
80
|
+
}
|
81
|
+
}.freeze
|
82
|
+
|
83
|
+
def default_input_price
|
84
|
+
0.27
|
85
|
+
end
|
86
|
+
|
87
|
+
def default_output_price
|
88
|
+
1.10
|
89
|
+
end
|
90
|
+
|
91
|
+
def default_cache_hit_price
|
92
|
+
0.07
|
93
|
+
end
|
94
|
+
|
95
|
+
def modalities_for(_model_id)
|
96
|
+
{
|
97
|
+
input: ['text'],
|
98
|
+
output: ['text']
|
99
|
+
}
|
100
|
+
end
|
101
|
+
|
102
|
+
def capabilities_for(model_id)
|
103
|
+
capabilities = ['streaming']
|
104
|
+
|
105
|
+
capabilities << 'function_calling' if model_id.match?(/deepseek-chat/)
|
106
|
+
|
107
|
+
capabilities
|
108
|
+
end
|
109
|
+
|
110
|
+
def pricing_for(model_id)
|
111
|
+
family = model_family(model_id)
|
112
|
+
prices = PRICES.fetch(family, { input_miss: default_input_price, output: default_output_price })
|
113
|
+
|
114
|
+
standard_pricing = {
|
115
|
+
input_per_million: prices[:input_miss],
|
116
|
+
output_per_million: prices[:output]
|
117
|
+
}
|
118
|
+
|
119
|
+
standard_pricing[:cached_input_per_million] = prices[:input_hit] if prices[:input_hit]
|
120
|
+
|
121
|
+
{
|
122
|
+
text_tokens: {
|
123
|
+
standard: standard_pricing
|
124
|
+
}
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# DeepSeek API integration.
|
6
|
+
class DeepSeek < OpenAI
|
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
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class Dify
|
6
|
+
# Chat methods of the Dify API integration
|
7
|
+
module Chat
|
8
|
+
def upload_document(document_path, original_filename = nil)
|
9
|
+
pn = Pathname.new(document_path)
|
10
|
+
mime_type = RubyLLM::MimeType.for pn
|
11
|
+
original_filename ||= document_path.is_a?(String) ? pn.basename : (document_path.is_a?(Tempfile) ? File.basename(document_path) : document_path.original_filename)
|
12
|
+
payload = {
|
13
|
+
file: Faraday::Multipart::FilePart.new(document_path, mime_type, original_filename),
|
14
|
+
user: config.dify_user || 'dify-user'
|
15
|
+
}
|
16
|
+
@connection.upload('v1/files/upload', payload)
|
17
|
+
end
|
18
|
+
|
19
|
+
module_function
|
20
|
+
|
21
|
+
def completion_url
|
22
|
+
'v1/chat-messages'
|
23
|
+
end
|
24
|
+
|
25
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Lint/UnusedMethodArgument
|
26
|
+
current_message = messages[-1]
|
27
|
+
current_message_content = current_message.content # dify using conversation_id to trace message history
|
28
|
+
|
29
|
+
# Find the latest non-nil conversation_id from all messages
|
30
|
+
latest_conversation_id = messages.reverse.find { |msg| msg.conversation_id }&.conversation_id
|
31
|
+
|
32
|
+
{
|
33
|
+
inputs: {},
|
34
|
+
query: current_message_content.is_a?(Content) ? current_message_content.text : current_message_content,
|
35
|
+
response_mode: (stream ? 'streaming' : 'blocking'),
|
36
|
+
conversation_id: latest_conversation_id,
|
37
|
+
user: config.dify_user || 'dify-user',
|
38
|
+
files: format_files(current_message_content)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse_completion_response(response)
|
43
|
+
data = response.body
|
44
|
+
|
45
|
+
Message.new(
|
46
|
+
role: :assistant,
|
47
|
+
content: data['answer'],
|
48
|
+
tool_calls: nil,
|
49
|
+
input_tokens: data.dig('metadata', 'usage', 'prompt_tokens'),
|
50
|
+
output_tokens: data.dig('metadata', 'usage', 'completion_tokens'),
|
51
|
+
conversation_id: data['conversation_id'],
|
52
|
+
model_id: 'dify-model',
|
53
|
+
raw: response
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|