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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Handles formatting of media content (images, audio) for OpenAI APIs
7
+ module Media
8
+ module_function
9
+
10
+ def format_content(content)
11
+ return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
12
+ return content unless content.is_a?(Content)
13
+
14
+ parts = []
15
+ parts << format_text(content.text) if content.text
16
+
17
+ content.attachments.each do |attachment|
18
+ case attachment.type
19
+ when :image
20
+ parts << format_image(attachment)
21
+ when :pdf
22
+ parts << format_pdf(attachment)
23
+ when :audio
24
+ parts << format_audio(attachment)
25
+ when :text
26
+ parts << format_text_file(attachment)
27
+ else
28
+ raise UnsupportedAttachmentError, attachment.type
29
+ end
30
+ end
31
+
32
+ parts
33
+ end
34
+
35
+ def format_image(image)
36
+ {
37
+ type: 'image_url',
38
+ image_url: {
39
+ url: image.url? ? image.source : image.for_llm
40
+ }
41
+ }
42
+ end
43
+
44
+ def format_pdf(pdf)
45
+ {
46
+ type: 'file',
47
+ file: {
48
+ filename: pdf.filename,
49
+ file_data: pdf.for_llm
50
+ }
51
+ }
52
+ end
53
+
54
+ def format_text_file(text_file)
55
+ {
56
+ type: 'text',
57
+ text: text_file.for_llm
58
+ }
59
+ end
60
+
61
+ def format_audio(audio)
62
+ {
63
+ type: 'input_audio',
64
+ input_audio: {
65
+ data: audio.encoded,
66
+ format: audio.format
67
+ }
68
+ }
69
+ end
70
+
71
+ def format_text(text)
72
+ {
73
+ type: 'text',
74
+ text: text
75
+ }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Models methods of the OpenAI API integration
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def parse_list_models_response(response, slug, capabilities)
15
+ Array(response.body['data']).map do |model_data|
16
+ model_id = model_data['id']
17
+
18
+ Model::Info.new(
19
+ id: model_id,
20
+ name: capabilities.format_display_name(model_id),
21
+ provider: slug,
22
+ family: capabilities.model_family(model_id),
23
+ created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
24
+ context_window: capabilities.context_window_for(model_id),
25
+ max_output_tokens: capabilities.max_tokens_for(model_id),
26
+ modalities: capabilities.modalities_for(model_id),
27
+ capabilities: capabilities.capabilities_for(model_id),
28
+ pricing: capabilities.pricing_for(model_id),
29
+ metadata: {
30
+ object: model_data['object'],
31
+ owned_by: model_data['owned_by']
32
+ }
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Streaming methods of the OpenAI API integration
7
+ module Streaming
8
+ module_function
9
+
10
+ def stream_url
11
+ completion_url
12
+ end
13
+
14
+ def build_chunk(data)
15
+ Chunk.new(
16
+ role: :assistant,
17
+ model_id: data['model'],
18
+ content: data.dig('choices', 0, 'delta', 'content'),
19
+ tool_calls: parse_tool_calls(data.dig('choices', 0, 'delta', 'tool_calls'), parse_arguments: false),
20
+ input_tokens: data.dig('usage', 'prompt_tokens'),
21
+ output_tokens: data.dig('usage', 'completion_tokens')
22
+ )
23
+ end
24
+
25
+ def parse_streaming_error(data)
26
+ error_data = JSON.parse(data)
27
+ return unless error_data['error']
28
+
29
+ case error_data.dig('error', 'type')
30
+ when 'server_error'
31
+ [500, error_data['error']['message']]
32
+ when 'rate_limit_exceeded', 'insufficient_quota'
33
+ [429, error_data['error']['message']]
34
+ else
35
+ [400, error_data['error']['message']]
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenAI
6
+ # Tools methods of the OpenAI API integration
7
+ module Tools
8
+ module_function
9
+
10
+ def tool_for(tool)
11
+ {
12
+ type: 'function',
13
+ function: {
14
+ name: tool.name,
15
+ description: tool.description,
16
+ parameters: {
17
+ type: 'object',
18
+ properties: tool.parameters.transform_values { |param| param_schema(param) },
19
+ required: tool.parameters.select { |_, p| p.required }.keys
20
+ }
21
+ }
22
+ }
23
+ end
24
+
25
+ def param_schema(param)
26
+ {
27
+ type: param.type,
28
+ description: param.description
29
+ }.compact
30
+ end
31
+
32
+ def format_tool_calls(tool_calls)
33
+ return nil unless tool_calls&.any?
34
+
35
+ tool_calls.map do |_, tc|
36
+ {
37
+ id: tc.id,
38
+ type: 'function',
39
+ function: {
40
+ name: tc.name,
41
+ arguments: JSON.generate(tc.arguments)
42
+ }
43
+ }
44
+ end
45
+ end
46
+
47
+ def parse_tool_call_arguments(tool_call)
48
+ arguments = tool_call.dig('function', 'arguments')
49
+
50
+ if arguments.nil? || arguments.empty?
51
+ {}
52
+ else
53
+ JSON.parse(arguments)
54
+ end
55
+ end
56
+
57
+ def parse_tool_calls(tool_calls, parse_arguments: true)
58
+ return nil unless tool_calls&.any?
59
+
60
+ tool_calls.to_h do |tc|
61
+ [
62
+ tc['id'],
63
+ ToolCall.new(
64
+ id: tc['id'],
65
+ name: tc.dig('function', 'name'),
66
+ arguments: if parse_arguments
67
+ parse_tool_call_arguments(tc)
68
+ else
69
+ tc.dig('function', 'arguments')
70
+ end
71
+ )
72
+ ]
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # OpenAI API integration.
6
+ class OpenAI < Provider
7
+ include OpenAI::Chat
8
+ include OpenAI::Embeddings
9
+ include OpenAI::Models
10
+ include OpenAI::Streaming
11
+ include OpenAI::Tools
12
+ include OpenAI::Images
13
+ include OpenAI::Media
14
+
15
+ def api_base
16
+ @config.openai_api_base || 'https://api.openai.com/v1'
17
+ end
18
+
19
+ def headers
20
+ {
21
+ 'Authorization' => "Bearer #{@config.openai_api_key}",
22
+ 'OpenAI-Organization' => @config.openai_organization_id,
23
+ 'OpenAI-Project' => @config.openai_project_id
24
+ }.compact
25
+ end
26
+
27
+ def maybe_normalize_temperature(temperature, model)
28
+ OpenAI::Capabilities.normalize_temperature(temperature, model.id)
29
+ end
30
+
31
+ class << self
32
+ def capabilities
33
+ OpenAI::Capabilities
34
+ end
35
+
36
+ def configuration_requirements
37
+ %i[openai_api_key]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class OpenRouter
6
+ # Models methods of the OpenRouter API integration
7
+ module Models
8
+ module_function
9
+
10
+ def models_url
11
+ 'models'
12
+ end
13
+
14
+ def parse_list_models_response(response, slug, _capabilities)
15
+ Array(response.body['data']).map do |model_data| # rubocop:disable Metrics/BlockLength
16
+ modalities = {
17
+ input: Array(model_data.dig('architecture', 'input_modalities')),
18
+ output: Array(model_data.dig('architecture', 'output_modalities'))
19
+ }
20
+
21
+ pricing = { text_tokens: { standard: {} } }
22
+
23
+ pricing_types = {
24
+ prompt: :input_per_million,
25
+ completion: :output_per_million,
26
+ input_cache_read: :cached_input_per_million,
27
+ internal_reasoning: :reasoning_output_per_million
28
+ }
29
+
30
+ pricing_types.each do |source_key, target_key|
31
+ value = model_data.dig('pricing', source_key.to_s).to_f
32
+ pricing[:text_tokens][:standard][target_key] = value * 1_000_000 if value.positive?
33
+ end
34
+
35
+ capabilities = supported_parameters_to_capabilities(model_data['supported_parameters'])
36
+
37
+ Model::Info.new(
38
+ id: model_data['id'],
39
+ name: model_data['name'],
40
+ provider: slug,
41
+ family: model_data['id'].split('/').first,
42
+ created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
43
+ context_window: model_data['context_length'],
44
+ max_output_tokens: model_data.dig('top_provider', 'max_completion_tokens'),
45
+ modalities: modalities,
46
+ capabilities: capabilities,
47
+ pricing: pricing,
48
+ metadata: {
49
+ description: model_data['description'],
50
+ architecture: model_data['architecture'],
51
+ top_provider: model_data['top_provider'],
52
+ per_request_limits: model_data['per_request_limits'],
53
+ supported_parameters: model_data['supported_parameters']
54
+ }
55
+ )
56
+ end
57
+ end
58
+
59
+ def supported_parameters_to_capabilities(params)
60
+ return [] unless params
61
+
62
+ capabilities = []
63
+ capabilities << 'streaming'
64
+ capabilities << 'function_calling' if params.include?('tools') || params.include?('tool_choice')
65
+ capabilities << 'structured_output' if params.include?('response_format')
66
+ capabilities << 'batch' if params.include?('batch')
67
+ capabilities << 'predicted_outputs' if params.include?('logit_bias') && params.include?('top_k')
68
+ capabilities
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # OpenRouter API integration.
6
+ class OpenRouter < OpenAI
7
+ include OpenRouter::Models
8
+
9
+ def api_base
10
+ 'https://openrouter.ai/api/v1'
11
+ end
12
+
13
+ def headers
14
+ {
15
+ 'Authorization' => "Bearer #{@config.openrouter_api_key}"
16
+ }
17
+ end
18
+
19
+ class << self
20
+ def configuration_requirements
21
+ %i[openrouter_api_key]
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Perplexity
6
+ # Determines capabilities and pricing for Perplexity models
7
+ module Capabilities
8
+ module_function
9
+
10
+ def context_window_for(model_id)
11
+ case model_id
12
+ when /sonar-pro/ then 200_000
13
+ else 128_000
14
+ end
15
+ end
16
+
17
+ def max_tokens_for(model_id)
18
+ case model_id
19
+ when /sonar-(?:pro|reasoning-pro)/ 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) || 1.0
26
+ end
27
+
28
+ def output_price_for(model_id)
29
+ PRICES.dig(model_family(model_id), :output) || 1.0
30
+ end
31
+
32
+ def supports_vision?(model_id)
33
+ case model_id
34
+ when /sonar-reasoning-pro/, /sonar-reasoning/, /sonar-pro/, /sonar/ then true
35
+ else false
36
+ end
37
+ end
38
+
39
+ def supports_functions?(_model_id)
40
+ false
41
+ end
42
+
43
+ def supports_json_mode?(_model_id)
44
+ true
45
+ end
46
+
47
+ def format_display_name(model_id)
48
+ case model_id
49
+ when 'sonar' then 'Sonar'
50
+ when 'sonar-pro' then 'Sonar Pro'
51
+ when 'sonar-reasoning' then 'Sonar Reasoning'
52
+ when 'sonar-reasoning-pro' then 'Sonar Reasoning Pro'
53
+ when 'sonar-deep-research' then 'Sonar Deep Research'
54
+ else
55
+ model_id.split('-')
56
+ .map(&:capitalize)
57
+ .join(' ')
58
+ end
59
+ end
60
+
61
+ def model_type(_model_id)
62
+ 'chat'
63
+ end
64
+
65
+ def model_family(model_id)
66
+ case model_id
67
+ when 'sonar' then :sonar
68
+ when 'sonar-pro' then :sonar_pro
69
+ when 'sonar-reasoning' then :sonar_reasoning
70
+ when 'sonar-reasoning-pro' then :sonar_reasoning_pro
71
+ when 'sonar-deep-research' then :sonar_deep_research
72
+ else :unknown
73
+ end
74
+ end
75
+
76
+ def modalities_for(_model_id)
77
+ {
78
+ input: ['text'],
79
+ output: ['text']
80
+ }
81
+ end
82
+
83
+ def capabilities_for(model_id)
84
+ capabilities = %w[streaming json_mode]
85
+ capabilities << 'vision' if supports_vision?(model_id)
86
+ capabilities
87
+ end
88
+
89
+ def pricing_for(model_id)
90
+ family = model_family(model_id)
91
+ prices = PRICES.fetch(family, { input: 1.0, output: 1.0 })
92
+
93
+ standard_pricing = {
94
+ input_per_million: prices[:input],
95
+ output_per_million: prices[:output]
96
+ }
97
+
98
+ standard_pricing[:citation_per_million] = prices[:citation] if prices[:citation]
99
+ standard_pricing[:reasoning_per_million] = prices[:reasoning] if prices[:reasoning]
100
+ standard_pricing[:search_per_thousand] = prices[:search_queries] if prices[:search_queries]
101
+
102
+ {
103
+ text_tokens: {
104
+ standard: standard_pricing
105
+ }
106
+ }
107
+ end
108
+
109
+ PRICES = {
110
+ sonar: {
111
+ input: 1.0,
112
+ output: 1.0
113
+ },
114
+ sonar_pro: {
115
+ input: 3.0,
116
+ output: 15.0
117
+ },
118
+ sonar_reasoning: {
119
+ input: 1.0,
120
+ output: 5.0
121
+ },
122
+ sonar_reasoning_pro: {
123
+ input: 2.0,
124
+ output: 8.0
125
+ },
126
+ sonar_deep_research: {
127
+ input: 2.0,
128
+ output: 8.0,
129
+ citation: 2.0,
130
+ reasoning: 3.0,
131
+ search_queries: 5.0
132
+ }
133
+ }.freeze
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Perplexity
6
+ # Chat formatting for Perplexity provider
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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class Perplexity
6
+ # Models methods of the Perplexity API integration
7
+ module Models
8
+ def list_models(**)
9
+ slug = 'perplexity'
10
+ capabilities = Perplexity::Capabilities
11
+ parse_list_models_response(nil, slug, capabilities)
12
+ end
13
+
14
+ def parse_list_models_response(_response, slug, capabilities)
15
+ [
16
+ create_model_info('sonar', slug, capabilities),
17
+ create_model_info('sonar-pro', slug, capabilities),
18
+ create_model_info('sonar-reasoning', slug, capabilities),
19
+ create_model_info('sonar-reasoning-pro', slug, capabilities),
20
+ create_model_info('sonar-deep-research', slug, capabilities)
21
+ ]
22
+ end
23
+
24
+ def create_model_info(id, slug, capabilities)
25
+ Model::Info.new(
26
+ id: id,
27
+ name: capabilities.format_display_name(id),
28
+ provider: slug,
29
+ family: capabilities.model_family(id).to_s,
30
+ created_at: Time.now,
31
+ context_window: capabilities.context_window_for(id),
32
+ max_output_tokens: capabilities.max_tokens_for(id),
33
+ modalities: capabilities.modalities_for(id),
34
+ capabilities: capabilities.capabilities_for(id),
35
+ pricing: capabilities.pricing_for(id),
36
+ metadata: {}
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ # Perplexity API integration.
6
+ class Perplexity < OpenAI
7
+ include Perplexity::Chat
8
+ include Perplexity::Models
9
+
10
+ def api_base
11
+ 'https://api.perplexity.ai'
12
+ end
13
+
14
+ def headers
15
+ {
16
+ 'Authorization' => "Bearer #{@config.perplexity_api_key}",
17
+ 'Content-Type' => 'application/json'
18
+ }
19
+ end
20
+
21
+ class << self
22
+ def capabilities
23
+ Perplexity::Capabilities
24
+ end
25
+
26
+ def configuration_requirements
27
+ %i[perplexity_api_key]
28
+ end
29
+ end
30
+
31
+ def parse_error(response)
32
+ body = response.body
33
+ return if body.empty?
34
+
35
+ # If response is HTML (Perplexity returns HTML for auth errors)
36
+ if body.include?('<html>') && body.include?('<title>')
37
+ title_match = body.match(%r{<title>(.+?)</title>})
38
+ if title_match
39
+ message = title_match[1]
40
+ message = message.sub(/^\d+\s+/, '')
41
+ return message
42
+ end
43
+ end
44
+ super
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class VertexAI
6
+ # Chat methods for the Vertex AI implementation
7
+ module Chat
8
+ def completion_url
9
+ "projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{@model}:generateContent" # rubocop:disable Layout/LineLength
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Providers
5
+ class VertexAI
6
+ # Embeddings methods for the Vertex AI implementation
7
+ module Embeddings
8
+ module_function
9
+
10
+ def embedding_url(model:)
11
+ "projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{model}:predict" # rubocop:disable Layout/LineLength
12
+ end
13
+
14
+ def render_embedding_payload(text, model:, dimensions:) # rubocop:disable Lint/UnusedMethodArgument
15
+ {
16
+ instances: [text].flatten.map { |t| { content: t.to_s } }
17
+ }.tap do |payload|
18
+ payload[:parameters] = { outputDimensionality: dimensions } if dimensions
19
+ end
20
+ end
21
+
22
+ def parse_embedding_response(response, model:, text:)
23
+ predictions = response.body['predictions']
24
+ vectors = predictions&.map { |p| p.dig('embeddings', 'values') }
25
+ vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
26
+
27
+ Embedding.new(vectors:, model:, input_tokens: 0)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end