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,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,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
|