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,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# Mistral API integration.
|
6
|
+
class Mistral < OpenAI
|
7
|
+
include Mistral::Chat
|
8
|
+
include Mistral::Models
|
9
|
+
include Mistral::Embeddings
|
10
|
+
|
11
|
+
def api_base
|
12
|
+
'https://api.mistral.ai/v1'
|
13
|
+
end
|
14
|
+
|
15
|
+
def headers
|
16
|
+
{
|
17
|
+
'Authorization' => "Bearer #{@config.mistral_api_key}"
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def capabilities
|
23
|
+
Mistral::Capabilities
|
24
|
+
end
|
25
|
+
|
26
|
+
def configuration_requirements
|
27
|
+
%i[mistral_api_key]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class Ollama
|
6
|
+
# Chat methods of the Ollama API integration
|
7
|
+
module Chat
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def format_messages(messages)
|
11
|
+
messages.map do |msg|
|
12
|
+
{
|
13
|
+
role: format_role(msg.role),
|
14
|
+
content: Ollama::Media.format_content(msg.content),
|
15
|
+
tool_calls: format_tool_calls(msg.tool_calls),
|
16
|
+
tool_call_id: msg.tool_call_id
|
17
|
+
}.compact
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def format_role(role)
|
22
|
+
role.to_s
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class Ollama
|
6
|
+
# Handles formatting of media content (images, audio) for Ollama APIs
|
7
|
+
module Media
|
8
|
+
extend OpenAI::Media
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def format_content(content)
|
13
|
+
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
14
|
+
return content unless content.is_a?(Content)
|
15
|
+
|
16
|
+
parts = []
|
17
|
+
parts << format_text(content.text) if content.text
|
18
|
+
|
19
|
+
content.attachments.each do |attachment|
|
20
|
+
case attachment.type
|
21
|
+
when :image
|
22
|
+
parts << Ollama::Media.format_image(attachment)
|
23
|
+
when :text
|
24
|
+
parts << format_text_file(attachment)
|
25
|
+
else
|
26
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
parts
|
31
|
+
end
|
32
|
+
|
33
|
+
def format_image(image)
|
34
|
+
{
|
35
|
+
type: 'image_url',
|
36
|
+
image_url: {
|
37
|
+
url: image.for_llm,
|
38
|
+
detail: 'auto'
|
39
|
+
}
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class Ollama
|
6
|
+
# Models methods for the Ollama API integration
|
7
|
+
module Models
|
8
|
+
def models_url
|
9
|
+
'models'
|
10
|
+
end
|
11
|
+
|
12
|
+
def parse_list_models_response(response, slug, _capabilities)
|
13
|
+
data = response.body['data'] || []
|
14
|
+
data.map do |model|
|
15
|
+
Model::Info.new(
|
16
|
+
id: model['id'],
|
17
|
+
name: model['id'],
|
18
|
+
provider: slug,
|
19
|
+
family: 'ollama',
|
20
|
+
created_at: model['created'] ? Time.at(model['created']) : nil,
|
21
|
+
modalities: {
|
22
|
+
input: %w[text image],
|
23
|
+
output: %w[text]
|
24
|
+
},
|
25
|
+
capabilities: %w[streaming function_calling structured_output vision],
|
26
|
+
pricing: {},
|
27
|
+
metadata: {
|
28
|
+
owned_by: model['owned_by']
|
29
|
+
}
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# Ollama API integration.
|
6
|
+
class Ollama < OpenAI
|
7
|
+
include Ollama::Chat
|
8
|
+
include Ollama::Media
|
9
|
+
include Ollama::Models
|
10
|
+
|
11
|
+
def api_base
|
12
|
+
@config.ollama_api_base
|
13
|
+
end
|
14
|
+
|
15
|
+
def headers
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def configuration_requirements
|
21
|
+
%i[ollama_api_base]
|
22
|
+
end
|
23
|
+
|
24
|
+
def local?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,291 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class OpenAI
|
6
|
+
# Determines capabilities and pricing for OpenAI models
|
7
|
+
module Capabilities
|
8
|
+
module_function
|
9
|
+
|
10
|
+
MODEL_PATTERNS = {
|
11
|
+
dall_e: /^dall-e/,
|
12
|
+
chatgpt4o: /^chatgpt-4o/,
|
13
|
+
gpt41: /^gpt-4\.1(?!-(?:mini|nano))/,
|
14
|
+
gpt41_mini: /^gpt-4\.1-mini/,
|
15
|
+
gpt41_nano: /^gpt-4\.1-nano/,
|
16
|
+
gpt4: /^gpt-4(?:-\d{6})?$/,
|
17
|
+
gpt4_turbo: /^gpt-4(?:\.5)?-(?:\d{6}-)?(preview|turbo)/,
|
18
|
+
gpt35_turbo: /^gpt-3\.5-turbo/,
|
19
|
+
gpt4o: /^gpt-4o(?!-(?:mini|audio|realtime|transcribe|tts|search))/,
|
20
|
+
gpt4o_audio: /^gpt-4o-(?:audio)/,
|
21
|
+
gpt4o_mini: /^gpt-4o-mini(?!-(?:audio|realtime|transcribe|tts|search))/,
|
22
|
+
gpt4o_mini_audio: /^gpt-4o-mini-audio/,
|
23
|
+
gpt4o_mini_realtime: /^gpt-4o-mini-realtime/,
|
24
|
+
gpt4o_mini_transcribe: /^gpt-4o-mini-transcribe/,
|
25
|
+
gpt4o_mini_tts: /^gpt-4o-mini-tts/,
|
26
|
+
gpt4o_realtime: /^gpt-4o-realtime/,
|
27
|
+
gpt4o_search: /^gpt-4o-search/,
|
28
|
+
gpt4o_transcribe: /^gpt-4o-transcribe/,
|
29
|
+
o1: /^o1(?!-(?:mini|pro))/,
|
30
|
+
o1_mini: /^o1-mini/,
|
31
|
+
o1_pro: /^o1-pro/,
|
32
|
+
o3_mini: /^o3-mini/,
|
33
|
+
babbage: /^babbage/,
|
34
|
+
davinci: /^davinci/,
|
35
|
+
embedding3_large: /^text-embedding-3-large/,
|
36
|
+
embedding3_small: /^text-embedding-3-small/,
|
37
|
+
embedding_ada: /^text-embedding-ada/,
|
38
|
+
tts1: /^tts-1(?!-hd)/,
|
39
|
+
tts1_hd: /^tts-1-hd/,
|
40
|
+
whisper: /^whisper/,
|
41
|
+
moderation: /^(?:omni|text)-moderation/
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
def context_window_for(model_id)
|
45
|
+
case model_family(model_id)
|
46
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
|
47
|
+
when 'chatgpt4o', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
|
48
|
+
'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime',
|
49
|
+
'gpt4o_search', 'gpt4o_transcribe', 'gpt4o_mini_search', 'o1_mini' then 128_000
|
50
|
+
when 'gpt4' then 8_192
|
51
|
+
when 'gpt4o_mini_transcribe' then 16_000
|
52
|
+
when 'o1', 'o1_pro', 'o3_mini' then 200_000
|
53
|
+
when 'gpt35_turbo' then 16_385
|
54
|
+
when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
|
55
|
+
'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
|
56
|
+
else 4_096
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def max_tokens_for(model_id)
|
61
|
+
case model_family(model_id)
|
62
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
|
63
|
+
when 'chatgpt4o', 'gpt4o', 'gpt4o_mini', 'gpt4o_mini_search' then 16_384
|
64
|
+
when 'babbage', 'davinci' then 16_384 # rubocop:disable Lint/DuplicateBranch
|
65
|
+
when 'gpt4' then 8_192
|
66
|
+
when 'gpt35_turbo' then 4_096
|
67
|
+
when 'gpt4_turbo', 'gpt4o_realtime', 'gpt4o_mini_realtime' then 4_096 # rubocop:disable Lint/DuplicateBranch
|
68
|
+
when 'gpt4o_mini_transcribe' then 2_000
|
69
|
+
when 'o1', 'o1_pro', 'o3_mini' then 100_000
|
70
|
+
when 'o1_mini' then 65_536
|
71
|
+
when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
|
72
|
+
'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
|
73
|
+
else 16_384 # rubocop:disable Lint/DuplicateBranch
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def supports_vision?(model_id)
|
78
|
+
case model_family(model_id)
|
79
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4', 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1',
|
80
|
+
'o1_pro', 'moderation', 'gpt4o_search', 'gpt4o_mini_search' then true
|
81
|
+
else false
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def supports_functions?(model_id)
|
86
|
+
case model_family(model_id)
|
87
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4', 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro',
|
88
|
+
'o3_mini' then true
|
89
|
+
when 'chatgpt4o', 'gpt35_turbo', 'o1_mini', 'gpt4o_mini_tts',
|
90
|
+
'gpt4o_transcribe', 'gpt4o_search', 'gpt4o_mini_search' then false
|
91
|
+
else false # rubocop:disable Lint/DuplicateBranch
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def supports_structured_output?(model_id)
|
96
|
+
case model_family(model_id)
|
97
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro',
|
98
|
+
'o3_mini' then true
|
99
|
+
else false
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def supports_json_mode?(model_id)
|
104
|
+
supports_structured_output?(model_id)
|
105
|
+
end
|
106
|
+
|
107
|
+
PRICES = {
|
108
|
+
gpt41: { input: 2.0, output: 8.0, cached_input: 0.5 },
|
109
|
+
gpt41_mini: { input: 0.4, output: 1.6, cached_input: 0.1 },
|
110
|
+
gpt41_nano: { input: 0.1, output: 0.4 },
|
111
|
+
chatgpt4o: { input: 5.0, output: 15.0 },
|
112
|
+
gpt4: { input: 10.0, output: 30.0 },
|
113
|
+
gpt4_turbo: { input: 10.0, output: 30.0 },
|
114
|
+
gpt45: { input: 75.0, output: 150.0 },
|
115
|
+
gpt35_turbo: { input: 0.5, output: 1.5 },
|
116
|
+
gpt4o: { input: 2.5, output: 10.0 },
|
117
|
+
gpt4o_audio: { input: 2.5, output: 10.0, audio_input: 40.0, audio_output: 80.0 },
|
118
|
+
gpt4o_mini: { input: 0.15, output: 0.6 },
|
119
|
+
gpt4o_mini_audio: { input: 0.15, output: 0.6, audio_input: 10.0, audio_output: 20.0 },
|
120
|
+
gpt4o_mini_realtime: { input: 0.6, output: 2.4 },
|
121
|
+
gpt4o_mini_transcribe: { input: 1.25, output: 5.0, audio_input: 3.0 },
|
122
|
+
gpt4o_mini_tts: { input: 0.6, output: 12.0 },
|
123
|
+
gpt4o_realtime: { input: 5.0, output: 20.0 },
|
124
|
+
gpt4o_search: { input: 2.5, output: 10.0 },
|
125
|
+
gpt4o_transcribe: { input: 2.5, output: 10.0, audio_input: 6.0 },
|
126
|
+
o1: { input: 15.0, output: 60.0 },
|
127
|
+
o1_mini: { input: 1.1, output: 4.4 },
|
128
|
+
o1_pro: { input: 150.0, output: 600.0 },
|
129
|
+
o3_mini: { input: 1.1, output: 4.4 },
|
130
|
+
babbage: { input: 0.4, output: 0.4 },
|
131
|
+
davinci: { input: 2.0, output: 2.0 },
|
132
|
+
embedding3_large: { price: 0.13 },
|
133
|
+
embedding3_small: { price: 0.02 },
|
134
|
+
embedding_ada: { price: 0.10 },
|
135
|
+
tts1: { price: 15.0 },
|
136
|
+
tts1_hd: { price: 30.0 },
|
137
|
+
whisper: { price: 0.006 },
|
138
|
+
moderation: { price: 0.0 }
|
139
|
+
}.freeze
|
140
|
+
|
141
|
+
def model_family(model_id)
|
142
|
+
MODEL_PATTERNS.each do |family, pattern|
|
143
|
+
return family.to_s if model_id.match?(pattern)
|
144
|
+
end
|
145
|
+
'other'
|
146
|
+
end
|
147
|
+
|
148
|
+
def input_price_for(model_id)
|
149
|
+
family = model_family(model_id).to_sym
|
150
|
+
prices = PRICES.fetch(family, { input: default_input_price })
|
151
|
+
prices[:input] || prices[:price] || default_input_price
|
152
|
+
end
|
153
|
+
|
154
|
+
def cached_input_price_for(model_id)
|
155
|
+
family = model_family(model_id).to_sym
|
156
|
+
prices = PRICES.fetch(family, {})
|
157
|
+
prices[:cached_input]
|
158
|
+
end
|
159
|
+
|
160
|
+
def output_price_for(model_id)
|
161
|
+
family = model_family(model_id).to_sym
|
162
|
+
prices = PRICES.fetch(family, { output: default_output_price })
|
163
|
+
prices[:output] || prices[:price] || default_output_price
|
164
|
+
end
|
165
|
+
|
166
|
+
def model_type(model_id)
|
167
|
+
case model_family(model_id)
|
168
|
+
when /embedding/ then 'embedding'
|
169
|
+
when /^tts|whisper|gpt4o_(?:mini_)?(?:transcribe|tts)$/ then 'audio'
|
170
|
+
when 'moderation' then 'moderation'
|
171
|
+
when /dall/ then 'image'
|
172
|
+
else 'chat'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def default_input_price
|
177
|
+
0.50
|
178
|
+
end
|
179
|
+
|
180
|
+
def default_output_price
|
181
|
+
1.50
|
182
|
+
end
|
183
|
+
|
184
|
+
def format_display_name(model_id)
|
185
|
+
model_id.then { |id| humanize(id) }
|
186
|
+
.then { |name| apply_special_formatting(name) }
|
187
|
+
end
|
188
|
+
|
189
|
+
def humanize(id)
|
190
|
+
id.tr('-', ' ')
|
191
|
+
.split
|
192
|
+
.map(&:capitalize)
|
193
|
+
.join(' ')
|
194
|
+
end
|
195
|
+
|
196
|
+
def apply_special_formatting(name)
|
197
|
+
name
|
198
|
+
.gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1\2\3')
|
199
|
+
.gsub(/^(?:Gpt|Chatgpt|Tts|Dall E) /) { |m| special_prefix_format(m.strip) }
|
200
|
+
.gsub(/^O([13]) /, 'O\1-')
|
201
|
+
.gsub(/^O[13] Mini/, '\0'.tr(' ', '-'))
|
202
|
+
.gsub(/\d\.\d /, '\0'.sub(' ', '-'))
|
203
|
+
.gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-')
|
204
|
+
.gsub(/\bHd\b/, 'HD')
|
205
|
+
.gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-'))
|
206
|
+
.gsub('Text Embedding', 'text-embedding-')
|
207
|
+
end
|
208
|
+
|
209
|
+
def special_prefix_format(prefix)
|
210
|
+
case prefix # rubocop:disable Style/HashLikeCase
|
211
|
+
when 'Gpt' then 'GPT-'
|
212
|
+
when 'Chatgpt' then 'ChatGPT-'
|
213
|
+
when 'Tts' then 'TTS-'
|
214
|
+
when 'Dall E' then 'DALL-E-'
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.normalize_temperature(temperature, model_id)
|
219
|
+
if model_id.match?(/^(o\d|gpt-5)/)
|
220
|
+
RubyLLM.logger.debug "Model #{model_id} requires temperature=1.0, ignoring provided value"
|
221
|
+
1.0
|
222
|
+
elsif model_id.match?(/-search/)
|
223
|
+
RubyLLM.logger.debug "Model #{model_id} does not accept temperature parameter, removing"
|
224
|
+
nil
|
225
|
+
else
|
226
|
+
temperature
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def modalities_for(model_id)
|
231
|
+
modalities = {
|
232
|
+
input: ['text'],
|
233
|
+
output: ['text']
|
234
|
+
}
|
235
|
+
|
236
|
+
# Vision support
|
237
|
+
modalities[:input] << 'image' if supports_vision?(model_id)
|
238
|
+
modalities[:input] << 'audio' if model_id.match?(/whisper|audio|tts|transcribe/)
|
239
|
+
modalities[:input] << 'pdf' if supports_vision?(model_id)
|
240
|
+
modalities[:output] << 'audio' if model_id.match?(/tts|audio/)
|
241
|
+
modalities[:output] << 'image' if model_id.match?(/dall-e|image/)
|
242
|
+
modalities[:output] << 'embeddings' if model_id.match?(/embedding/)
|
243
|
+
modalities[:output] << 'moderation' if model_id.match?(/moderation/)
|
244
|
+
|
245
|
+
modalities
|
246
|
+
end
|
247
|
+
|
248
|
+
def capabilities_for(model_id) # rubocop:disable Metrics/PerceivedComplexity
|
249
|
+
capabilities = []
|
250
|
+
|
251
|
+
capabilities << 'streaming' unless model_id.match?(/moderation|embedding/)
|
252
|
+
capabilities << 'function_calling' if supports_functions?(model_id)
|
253
|
+
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
254
|
+
capabilities << 'batch' if model_id.match?(/embedding|batch/)
|
255
|
+
capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
|
256
|
+
|
257
|
+
if model_id.match?(/gpt-4-turbo|gpt-4o/)
|
258
|
+
capabilities << 'image_generation' if model_id.match?(/vision/)
|
259
|
+
capabilities << 'speech_generation' if model_id.match?(/audio/)
|
260
|
+
capabilities << 'transcription' if model_id.match?(/audio/)
|
261
|
+
end
|
262
|
+
|
263
|
+
capabilities
|
264
|
+
end
|
265
|
+
|
266
|
+
def pricing_for(model_id)
|
267
|
+
standard_pricing = {
|
268
|
+
input_per_million: input_price_for(model_id),
|
269
|
+
output_per_million: output_price_for(model_id)
|
270
|
+
}
|
271
|
+
|
272
|
+
if respond_to?(:cached_input_price_for)
|
273
|
+
cached_price = cached_input_price_for(model_id)
|
274
|
+
standard_pricing[:cached_input_per_million] = cached_price if cached_price
|
275
|
+
end
|
276
|
+
|
277
|
+
pricing = { text_tokens: { standard: standard_pricing } }
|
278
|
+
|
279
|
+
if model_id.match?(/embedding|batch/)
|
280
|
+
pricing[:text_tokens][:batch] = {
|
281
|
+
input_per_million: standard_pricing[:input_per_million] * 0.5,
|
282
|
+
output_per_million: standard_pricing[:output_per_million] * 0.5
|
283
|
+
}
|
284
|
+
end
|
285
|
+
|
286
|
+
pricing
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class OpenAI
|
6
|
+
# Chat methods of the OpenAI API integration
|
7
|
+
module Chat
|
8
|
+
def completion_url
|
9
|
+
'chat/completions'
|
10
|
+
end
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false, schema: nil) # rubocop:disable Metrics/ParameterLists
|
15
|
+
payload = {
|
16
|
+
model: model.id,
|
17
|
+
messages: format_messages(messages),
|
18
|
+
stream: stream
|
19
|
+
}
|
20
|
+
|
21
|
+
payload[:temperature] = temperature unless temperature.nil?
|
22
|
+
payload[:tools] = tools.map { |_, tool| tool_for(tool) } if tools.any?
|
23
|
+
|
24
|
+
if schema
|
25
|
+
strict = schema[:strict] != false
|
26
|
+
|
27
|
+
payload[:response_format] = {
|
28
|
+
type: 'json_schema',
|
29
|
+
json_schema: {
|
30
|
+
name: 'response',
|
31
|
+
schema: schema,
|
32
|
+
strict: strict
|
33
|
+
}
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
payload[:stream_options] = { include_usage: true } if stream
|
38
|
+
payload
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_completion_response(response)
|
42
|
+
data = response.body
|
43
|
+
return if data.empty?
|
44
|
+
|
45
|
+
raise Error.new(response, data.dig('error', 'message')) if data.dig('error', 'message')
|
46
|
+
|
47
|
+
message_data = data.dig('choices', 0, 'message')
|
48
|
+
return unless message_data
|
49
|
+
|
50
|
+
Message.new(
|
51
|
+
role: :assistant,
|
52
|
+
content: message_data['content'],
|
53
|
+
tool_calls: parse_tool_calls(message_data['tool_calls']),
|
54
|
+
input_tokens: data['usage']['prompt_tokens'],
|
55
|
+
output_tokens: data['usage']['completion_tokens'],
|
56
|
+
model_id: data['model'],
|
57
|
+
raw: response
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def format_messages(messages)
|
62
|
+
messages.map do |msg|
|
63
|
+
{
|
64
|
+
role: format_role(msg.role),
|
65
|
+
content: Media.format_content(msg.content),
|
66
|
+
tool_calls: format_tool_calls(msg.tool_calls),
|
67
|
+
tool_call_id: msg.tool_call_id
|
68
|
+
}.compact
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_role(role)
|
73
|
+
case role
|
74
|
+
when :system
|
75
|
+
@config.openai_use_system_role ? 'system' : 'developer'
|
76
|
+
else
|
77
|
+
role.to_s
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class OpenAI
|
6
|
+
# Embeddings methods of the OpenAI API integration
|
7
|
+
module Embeddings
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def embedding_url(...)
|
11
|
+
'embeddings'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_embedding_payload(text, model:, dimensions:)
|
15
|
+
{
|
16
|
+
model: model,
|
17
|
+
input: text,
|
18
|
+
dimensions: dimensions
|
19
|
+
}.compact
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_embedding_response(response, model:, text:)
|
23
|
+
data = response.body
|
24
|
+
input_tokens = data.dig('usage', 'prompt_tokens') || 0
|
25
|
+
vectors = data['data'].map { |d| d['embedding'] }
|
26
|
+
vectors = vectors.first if vectors.length == 1 && !text.is_a?(Array)
|
27
|
+
|
28
|
+
Embedding.new(vectors:, model:, input_tokens:)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class OpenAI
|
6
|
+
# Image generation methods for the OpenAI API integration
|
7
|
+
module Images
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def images_url
|
11
|
+
'images/generations'
|
12
|
+
end
|
13
|
+
|
14
|
+
def render_image_payload(prompt, model:, size:)
|
15
|
+
{
|
16
|
+
model: model,
|
17
|
+
prompt: prompt,
|
18
|
+
n: 1,
|
19
|
+
size: size
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def parse_image_response(response, model:)
|
24
|
+
data = response.body
|
25
|
+
image_data = data['data'].first
|
26
|
+
|
27
|
+
Image.new(
|
28
|
+
url: image_data['url'],
|
29
|
+
mime_type: 'image/png', # DALL-E typically returns PNGs
|
30
|
+
revised_prompt: image_data['revised_prompt'],
|
31
|
+
model_id: model,
|
32
|
+
data: image_data['b64_json']
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|