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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +22 -0
  3. data/README.md +172 -0
  4. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +108 -0
  5. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  6. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +8 -0
  7. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +15 -0
  8. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +14 -0
  9. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -0
  10. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +3 -0
  11. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  12. data/lib/generators/ruby_llm/install_generator.rb +121 -0
  13. data/lib/ruby_llm/active_record/acts_as.rb +382 -0
  14. data/lib/ruby_llm/aliases.json +217 -0
  15. data/lib/ruby_llm/aliases.rb +56 -0
  16. data/lib/ruby_llm/attachment.rb +164 -0
  17. data/lib/ruby_llm/chat.rb +219 -0
  18. data/lib/ruby_llm/chunk.rb +6 -0
  19. data/lib/ruby_llm/configuration.rb +75 -0
  20. data/lib/ruby_llm/connection.rb +126 -0
  21. data/lib/ruby_llm/content.rb +52 -0
  22. data/lib/ruby_llm/context.rb +29 -0
  23. data/lib/ruby_llm/embedding.rb +30 -0
  24. data/lib/ruby_llm/error.rb +84 -0
  25. data/lib/ruby_llm/image.rb +53 -0
  26. data/lib/ruby_llm/message.rb +76 -0
  27. data/lib/ruby_llm/mime_type.rb +67 -0
  28. data/lib/ruby_llm/model/info.rb +101 -0
  29. data/lib/ruby_llm/model/modalities.rb +22 -0
  30. data/lib/ruby_llm/model/pricing.rb +51 -0
  31. data/lib/ruby_llm/model/pricing_category.rb +48 -0
  32. data/lib/ruby_llm/model/pricing_tier.rb +34 -0
  33. data/lib/ruby_llm/model.rb +7 -0
  34. data/lib/ruby_llm/models.json +29924 -0
  35. data/lib/ruby_llm/models.rb +218 -0
  36. data/lib/ruby_llm/models_schema.json +168 -0
  37. data/lib/ruby_llm/provider.rb +219 -0
  38. data/lib/ruby_llm/providers/anthropic/capabilities.rb +179 -0
  39. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  40. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  41. data/lib/ruby_llm/providers/anthropic/media.rb +92 -0
  42. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  43. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  44. data/lib/ruby_llm/providers/anthropic/tools.rb +108 -0
  45. data/lib/ruby_llm/providers/anthropic.rb +37 -0
  46. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  47. data/lib/ruby_llm/providers/bedrock/chat.rb +65 -0
  48. data/lib/ruby_llm/providers/bedrock/media.rb +61 -0
  49. data/lib/ruby_llm/providers/bedrock/models.rb +82 -0
  50. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  51. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +63 -0
  52. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +63 -0
  53. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +79 -0
  54. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +90 -0
  55. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +91 -0
  56. data/lib/ruby_llm/providers/bedrock/streaming.rb +36 -0
  57. data/lib/ruby_llm/providers/bedrock.rb +83 -0
  58. data/lib/ruby_llm/providers/deepseek/capabilities.rb +131 -0
  59. data/lib/ruby_llm/providers/deepseek/chat.rb +17 -0
  60. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  61. data/lib/ruby_llm/providers/gemini/capabilities.rb +351 -0
  62. data/lib/ruby_llm/providers/gemini/chat.rb +139 -0
  63. data/lib/ruby_llm/providers/gemini/embeddings.rb +39 -0
  64. data/lib/ruby_llm/providers/gemini/images.rb +48 -0
  65. data/lib/ruby_llm/providers/gemini/media.rb +55 -0
  66. data/lib/ruby_llm/providers/gemini/models.rb +41 -0
  67. data/lib/ruby_llm/providers/gemini/streaming.rb +58 -0
  68. data/lib/ruby_llm/providers/gemini/tools.rb +82 -0
  69. data/lib/ruby_llm/providers/gemini.rb +36 -0
  70. data/lib/ruby_llm/providers/gpustack/chat.rb +17 -0
  71. data/lib/ruby_llm/providers/gpustack/models.rb +55 -0
  72. data/lib/ruby_llm/providers/gpustack.rb +33 -0
  73. data/lib/ruby_llm/providers/mistral/capabilities.rb +163 -0
  74. data/lib/ruby_llm/providers/mistral/chat.rb +26 -0
  75. data/lib/ruby_llm/providers/mistral/embeddings.rb +36 -0
  76. data/lib/ruby_llm/providers/mistral/models.rb +49 -0
  77. data/lib/ruby_llm/providers/mistral.rb +32 -0
  78. data/lib/ruby_llm/providers/ollama/chat.rb +28 -0
  79. data/lib/ruby_llm/providers/ollama/media.rb +50 -0
  80. data/lib/ruby_llm/providers/ollama.rb +29 -0
  81. data/lib/ruby_llm/providers/openai/capabilities.rb +306 -0
  82. data/lib/ruby_llm/providers/openai/chat.rb +86 -0
  83. data/lib/ruby_llm/providers/openai/embeddings.rb +36 -0
  84. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  85. data/lib/ruby_llm/providers/openai/media.rb +81 -0
  86. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  87. data/lib/ruby_llm/providers/openai/response.rb +115 -0
  88. data/lib/ruby_llm/providers/openai/response_media.rb +76 -0
  89. data/lib/ruby_llm/providers/openai/streaming.rb +190 -0
  90. data/lib/ruby_llm/providers/openai/tools.rb +100 -0
  91. data/lib/ruby_llm/providers/openai.rb +44 -0
  92. data/lib/ruby_llm/providers/openai_base.rb +44 -0
  93. data/lib/ruby_llm/providers/openrouter/models.rb +88 -0
  94. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  95. data/lib/ruby_llm/providers/perplexity/capabilities.rb +138 -0
  96. data/lib/ruby_llm/providers/perplexity/chat.rb +17 -0
  97. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  98. data/lib/ruby_llm/providers/perplexity.rb +52 -0
  99. data/lib/ruby_llm/railtie.rb +17 -0
  100. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  101. data/lib/ruby_llm/streaming.rb +162 -0
  102. data/lib/ruby_llm/tool.rb +100 -0
  103. data/lib/ruby_llm/tool_call.rb +31 -0
  104. data/lib/ruby_llm/utils.rb +49 -0
  105. data/lib/ruby_llm/version.rb +5 -0
  106. data/lib/ruby_llm.rb +98 -0
  107. data/lib/tasks/aliases.rake +235 -0
  108. data/lib/tasks/models_docs.rake +224 -0
  109. data/lib/tasks/models_update.rake +108 -0
  110. data/lib/tasks/release.rake +32 -0
  111. data/lib/tasks/vcr.rake +99 -0
  112. 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