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.
Files changed (129) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +157 -0
  4. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +3 -0
  5. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +8 -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_legacy_migration.rb.tt +16 -0
  8. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +16 -0
  9. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
  10. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +15 -0
  11. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +9 -0
  12. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -0
  13. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  14. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +3 -0
  15. data/lib/generators/ruby_llm/install_generator.rb +184 -0
  16. data/lib/generators/ruby_llm/migrate_model_fields/templates/migration.rb.tt +142 -0
  17. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +84 -0
  18. data/lib/ruby_llm/active_record/acts_as.rb +137 -0
  19. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  20. data/lib/ruby_llm/active_record/chat_methods.rb +315 -0
  21. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  22. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  23. data/lib/ruby_llm/aliases.json +274 -0
  24. data/lib/ruby_llm/aliases.rb +38 -0
  25. data/lib/ruby_llm/attachment.rb +191 -0
  26. data/lib/ruby_llm/chat.rb +212 -0
  27. data/lib/ruby_llm/chunk.rb +6 -0
  28. data/lib/ruby_llm/configuration.rb +69 -0
  29. data/lib/ruby_llm/connection.rb +137 -0
  30. data/lib/ruby_llm/content.rb +50 -0
  31. data/lib/ruby_llm/context.rb +29 -0
  32. data/lib/ruby_llm/embedding.rb +29 -0
  33. data/lib/ruby_llm/error.rb +76 -0
  34. data/lib/ruby_llm/image.rb +49 -0
  35. data/lib/ruby_llm/message.rb +76 -0
  36. data/lib/ruby_llm/mime_type.rb +67 -0
  37. data/lib/ruby_llm/model/info.rb +103 -0
  38. data/lib/ruby_llm/model/modalities.rb +22 -0
  39. data/lib/ruby_llm/model/pricing.rb +48 -0
  40. data/lib/ruby_llm/model/pricing_category.rb +46 -0
  41. data/lib/ruby_llm/model/pricing_tier.rb +33 -0
  42. data/lib/ruby_llm/model.rb +7 -0
  43. data/lib/ruby_llm/models.json +31418 -0
  44. data/lib/ruby_llm/models.rb +235 -0
  45. data/lib/ruby_llm/models_schema.json +168 -0
  46. data/lib/ruby_llm/provider.rb +215 -0
  47. data/lib/ruby_llm/providers/anthropic/capabilities.rb +134 -0
  48. data/lib/ruby_llm/providers/anthropic/chat.rb +106 -0
  49. data/lib/ruby_llm/providers/anthropic/embeddings.rb +20 -0
  50. data/lib/ruby_llm/providers/anthropic/media.rb +91 -0
  51. data/lib/ruby_llm/providers/anthropic/models.rb +48 -0
  52. data/lib/ruby_llm/providers/anthropic/streaming.rb +43 -0
  53. data/lib/ruby_llm/providers/anthropic/tools.rb +107 -0
  54. data/lib/ruby_llm/providers/anthropic.rb +36 -0
  55. data/lib/ruby_llm/providers/bedrock/capabilities.rb +167 -0
  56. data/lib/ruby_llm/providers/bedrock/chat.rb +63 -0
  57. data/lib/ruby_llm/providers/bedrock/media.rb +60 -0
  58. data/lib/ruby_llm/providers/bedrock/models.rb +98 -0
  59. data/lib/ruby_llm/providers/bedrock/signing.rb +831 -0
  60. data/lib/ruby_llm/providers/bedrock/streaming/base.rb +51 -0
  61. data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +56 -0
  62. data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +67 -0
  63. data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +78 -0
  64. data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +78 -0
  65. data/lib/ruby_llm/providers/bedrock/streaming.rb +18 -0
  66. data/lib/ruby_llm/providers/bedrock.rb +82 -0
  67. data/lib/ruby_llm/providers/deepseek/capabilities.rb +130 -0
  68. data/lib/ruby_llm/providers/deepseek/chat.rb +16 -0
  69. data/lib/ruby_llm/providers/deepseek.rb +30 -0
  70. data/lib/ruby_llm/providers/dify/capabilities.rb +16 -0
  71. data/lib/ruby_llm/providers/dify/chat.rb +59 -0
  72. data/lib/ruby_llm/providers/dify/media.rb +37 -0
  73. data/lib/ruby_llm/providers/dify/streaming.rb +28 -0
  74. data/lib/ruby_llm/providers/dify.rb +48 -0
  75. data/lib/ruby_llm/providers/gemini/capabilities.rb +276 -0
  76. data/lib/ruby_llm/providers/gemini/chat.rb +171 -0
  77. data/lib/ruby_llm/providers/gemini/embeddings.rb +37 -0
  78. data/lib/ruby_llm/providers/gemini/images.rb +47 -0
  79. data/lib/ruby_llm/providers/gemini/media.rb +54 -0
  80. data/lib/ruby_llm/providers/gemini/models.rb +40 -0
  81. data/lib/ruby_llm/providers/gemini/streaming.rb +61 -0
  82. data/lib/ruby_llm/providers/gemini/tools.rb +77 -0
  83. data/lib/ruby_llm/providers/gemini.rb +36 -0
  84. data/lib/ruby_llm/providers/gpustack/chat.rb +27 -0
  85. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  86. data/lib/ruby_llm/providers/gpustack/models.rb +90 -0
  87. data/lib/ruby_llm/providers/gpustack.rb +34 -0
  88. data/lib/ruby_llm/providers/mistral/capabilities.rb +155 -0
  89. data/lib/ruby_llm/providers/mistral/chat.rb +24 -0
  90. data/lib/ruby_llm/providers/mistral/embeddings.rb +33 -0
  91. data/lib/ruby_llm/providers/mistral/models.rb +48 -0
  92. data/lib/ruby_llm/providers/mistral.rb +32 -0
  93. data/lib/ruby_llm/providers/ollama/chat.rb +27 -0
  94. data/lib/ruby_llm/providers/ollama/media.rb +45 -0
  95. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  96. data/lib/ruby_llm/providers/ollama.rb +30 -0
  97. data/lib/ruby_llm/providers/openai/capabilities.rb +291 -0
  98. data/lib/ruby_llm/providers/openai/chat.rb +83 -0
  99. data/lib/ruby_llm/providers/openai/embeddings.rb +33 -0
  100. data/lib/ruby_llm/providers/openai/images.rb +38 -0
  101. data/lib/ruby_llm/providers/openai/media.rb +80 -0
  102. data/lib/ruby_llm/providers/openai/models.rb +39 -0
  103. data/lib/ruby_llm/providers/openai/streaming.rb +41 -0
  104. data/lib/ruby_llm/providers/openai/tools.rb +78 -0
  105. data/lib/ruby_llm/providers/openai.rb +42 -0
  106. data/lib/ruby_llm/providers/openrouter/models.rb +73 -0
  107. data/lib/ruby_llm/providers/openrouter.rb +26 -0
  108. data/lib/ruby_llm/providers/perplexity/capabilities.rb +137 -0
  109. data/lib/ruby_llm/providers/perplexity/chat.rb +16 -0
  110. data/lib/ruby_llm/providers/perplexity/models.rb +42 -0
  111. data/lib/ruby_llm/providers/perplexity.rb +48 -0
  112. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  113. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  114. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  115. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  116. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  117. data/lib/ruby_llm/railtie.rb +41 -0
  118. data/lib/ruby_llm/stream_accumulator.rb +97 -0
  119. data/lib/ruby_llm/streaming.rb +153 -0
  120. data/lib/ruby_llm/tool.rb +83 -0
  121. data/lib/ruby_llm/tool_call.rb +22 -0
  122. data/lib/ruby_llm/utils.rb +45 -0
  123. data/lib/ruby_llm/version.rb +5 -0
  124. data/lib/ruby_llm.rb +97 -0
  125. data/lib/tasks/models.rake +525 -0
  126. data/lib/tasks/release.rake +67 -0
  127. data/lib/tasks/ruby_llm.rake +15 -0
  128. data/lib/tasks/vcr.rake +92 -0
  129. 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,16 @@
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
+ role.to_s
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Dify
6
+ module Capabilities
7
+ module_function
8
+
9
+ def capabilities_for(model_id)
10
+ capabilities = ['streaming']
11
+ capabilities
12
+ end
13
+ end
14
+ end
15
+ end
16
+ 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