ruby_llm 1.13.2 → 1.16.0
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 +4 -4
- data/README.md +16 -13
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +41 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +3 -3
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +5 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +6 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +125 -29
- data/lib/ruby_llm/active_record/chat_methods.rb +50 -26
- data/lib/ruby_llm/active_record/message_methods.rb +104 -4
- data/lib/ruby_llm/active_record/model_methods.rb +8 -10
- data/lib/ruby_llm/active_record/payload_helpers.rb +29 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +18 -0
- data/lib/ruby_llm/agent.rb +15 -2
- data/lib/ruby_llm/aliases.json +141 -89
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +44 -40
- data/lib/ruby_llm/chat.rb +229 -59
- data/lib/ruby_llm/configuration.rb +69 -75
- data/lib/ruby_llm/connection.rb +37 -10
- data/lib/ruby_llm/content.rb +15 -1
- data/lib/ruby_llm/cost.rb +224 -0
- data/lib/ruby_llm/deprecator.rb +24 -0
- data/lib/ruby_llm/embedding.rb +31 -1
- data/lib/ruby_llm/error.rb +16 -75
- data/lib/ruby_llm/error_middleware.rb +81 -0
- data/lib/ruby_llm/image.rb +39 -4
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/message.rb +20 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +67 -14
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_category.rb +13 -2
- data/lib/ruby_llm/model/pricing_tier.rb +20 -9
- data/lib/ruby_llm/model_registry.rb +39 -0
- data/lib/ruby_llm/models.json +21796 -20601
- data/lib/ruby_llm/models.rb +107 -34
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +25 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +4 -8
- data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
- data/lib/ruby_llm/providers/anthropic.rb +4 -0
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/azure.rb +4 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
- data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
- data/lib/ruby_llm/providers/bedrock.rb +5 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +28 -103
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
- data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
- data/lib/ruby_llm/providers/gpustack.rb +4 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
- data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
- data/lib/ruby_llm/providers/mistral/media.rb +55 -0
- data/lib/ruby_llm/providers/mistral/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral.rb +5 -1
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/ollama.rb +4 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +157 -195
- data/lib/ruby_llm/providers/openai/chat.rb +61 -7
- data/lib/ruby_llm/providers/openai/images.rb +58 -6
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
- data/lib/ruby_llm/providers/openai/tools.rb +2 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/openai.rb +10 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +3 -3
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +5 -1
- data/lib/ruby_llm/providers/vertexai.rb +8 -0
- data/lib/ruby_llm/providers/xai/chat.rb +9 -0
- data/lib/ruby_llm/providers/xai/models.rb +15 -27
- data/lib/ruby_llm/providers/xai.rb +5 -1
- data/lib/ruby_llm/railtie.rb +11 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tokens.rb +8 -0
- data/lib/ruby_llm/tool.rb +24 -7
- data/lib/ruby_llm/tool_concurrency.rb +105 -0
- data/lib/ruby_llm/transcription.rb +2 -1
- data/lib/ruby_llm/utils.rb +39 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +11 -6
- data/lib/tasks/models.rake +45 -16
- data/lib/tasks/release.rake +51 -24
- data/lib/tasks/ruby_llm.rake +6 -5
- data/lib/tasks/vcr.rake +1 -1
- metadata +81 -21
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Mistral
|
|
6
|
+
# Handles media content for Mistral Chat Completions.
|
|
7
|
+
module Media
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def format_content(content) # rubocop:disable Metrics/PerceivedComplexity
|
|
11
|
+
if content.is_a?(RubyLLM::Content::Raw)
|
|
12
|
+
value = content.value
|
|
13
|
+
return value.is_a?(Hash) ? value.to_json : value
|
|
14
|
+
end
|
|
15
|
+
return content.to_json if content.is_a?(Hash) || content.is_a?(Array)
|
|
16
|
+
return content unless content.is_a?(Content)
|
|
17
|
+
|
|
18
|
+
parts = []
|
|
19
|
+
parts << OpenAI::Media.format_text(content.text) if content.text
|
|
20
|
+
|
|
21
|
+
content.attachments.each do |attachment|
|
|
22
|
+
case attachment.type
|
|
23
|
+
when :image
|
|
24
|
+
parts << format_image(attachment)
|
|
25
|
+
when :audio
|
|
26
|
+
parts << OpenAI::Media.format_audio(attachment)
|
|
27
|
+
when :pdf, :document
|
|
28
|
+
parts << format_document(attachment)
|
|
29
|
+
when :text
|
|
30
|
+
parts << OpenAI::Media.format_text_file(attachment)
|
|
31
|
+
else
|
|
32
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
parts
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_image(image)
|
|
40
|
+
{
|
|
41
|
+
type: 'image_url',
|
|
42
|
+
image_url: image.url? ? image.source.to_s : image.for_llm
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def format_document(document)
|
|
47
|
+
{
|
|
48
|
+
type: 'document_url',
|
|
49
|
+
document_url: document.url? ? document.source.to_s : document.for_llm
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -9,7 +9,7 @@ module RubyLLM
|
|
|
9
9
|
include Mistral::Embeddings
|
|
10
10
|
|
|
11
11
|
def api_base
|
|
12
|
-
'https://api.mistral.ai/v1'
|
|
12
|
+
@config.mistral_api_base || 'https://api.mistral.ai/v1'
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def headers
|
|
@@ -23,6 +23,10 @@ module RubyLLM
|
|
|
23
23
|
Mistral::Capabilities
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def configuration_options
|
|
27
|
+
%i[mistral_api_key mistral_api_base]
|
|
28
|
+
end
|
|
29
|
+
|
|
26
30
|
def configuration_requirements
|
|
27
31
|
%i[mistral_api_key]
|
|
28
32
|
end
|
|
@@ -11,13 +11,20 @@ module RubyLLM
|
|
|
11
11
|
messages.map do |msg|
|
|
12
12
|
{
|
|
13
13
|
role: format_role(msg.role),
|
|
14
|
-
content:
|
|
14
|
+
content: format_message_content(msg),
|
|
15
15
|
tool_calls: format_tool_calls(msg.tool_calls),
|
|
16
16
|
tool_call_id: msg.tool_call_id
|
|
17
17
|
}.compact.merge(OpenAI::Chat.format_thinking(msg))
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def format_message_content(msg)
|
|
22
|
+
content = Ollama::Media.format_content(msg.content)
|
|
23
|
+
return '' if content.nil? && OpenAI::Chat.thinking_only_assistant_message?(msg)
|
|
24
|
+
|
|
25
|
+
content
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
def format_role(role)
|
|
22
29
|
role.to_s
|
|
23
30
|
end
|
|
@@ -3,13 +3,14 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module Providers
|
|
5
5
|
class OpenAI
|
|
6
|
-
#
|
|
6
|
+
# Provider-level capability checks and narrow registry fallbacks.
|
|
7
7
|
module Capabilities
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
MODEL_PATTERNS = {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
gpt_image15: /^gpt-image-1\.5/,
|
|
12
|
+
gpt_image_mini: /^gpt-image-1-mini/,
|
|
13
|
+
gpt_image: /^gpt-image-1(?:$|-)/,
|
|
13
14
|
gpt41: /^gpt-4\.1(?!-(?:mini|nano))/,
|
|
14
15
|
gpt41_mini: /^gpt-4\.1-mini/,
|
|
15
16
|
gpt41_nano: /^gpt-4\.1-nano/,
|
|
@@ -26,9 +27,9 @@ module RubyLLM
|
|
|
26
27
|
gpt4o_realtime: /^gpt-4o-realtime/,
|
|
27
28
|
gpt4o_search: /^gpt-4o-search/,
|
|
28
29
|
gpt4o_transcribe: /^gpt-4o-transcribe/,
|
|
29
|
-
gpt5: /^gpt-5/,
|
|
30
|
-
gpt5_mini: /^gpt-5
|
|
31
|
-
gpt5_nano: /^gpt-5
|
|
30
|
+
gpt5: /^gpt-5(?!.*(?:mini|nano))/,
|
|
31
|
+
gpt5_mini: /^gpt-5.*mini/,
|
|
32
|
+
gpt5_nano: /^gpt-5.*nano/,
|
|
32
33
|
o1: /^o1(?!-(?:mini|pro))/,
|
|
33
34
|
o1_mini: /^o1-mini/,
|
|
34
35
|
o1_pro: /^o1-pro/,
|
|
@@ -44,101 +45,38 @@ module RubyLLM
|
|
|
44
45
|
moderation: /^(?:omni|text)-moderation/
|
|
45
46
|
}.freeze
|
|
46
47
|
|
|
47
|
-
def context_window_for(model_id)
|
|
48
|
-
case model_family(model_id)
|
|
49
|
-
when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
|
|
50
|
-
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'chatgpt4o', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
|
|
51
|
-
'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime',
|
|
52
|
-
'gpt4o_search', 'gpt4o_transcribe', 'gpt4o_mini_search', 'o1_mini' then 128_000
|
|
53
|
-
when 'gpt4' then 8_192
|
|
54
|
-
when 'gpt4o_mini_transcribe' then 16_000
|
|
55
|
-
when 'o1', 'o1_pro', 'o3_mini' then 200_000
|
|
56
|
-
when 'gpt35_turbo' then 16_385
|
|
57
|
-
when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
|
|
58
|
-
'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
|
|
59
|
-
else 4_096
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def max_tokens_for(model_id)
|
|
64
|
-
case model_family(model_id)
|
|
65
|
-
when 'gpt5', 'gpt5_mini', 'gpt5_nano' then 400_000
|
|
66
|
-
when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
|
|
67
|
-
when 'chatgpt4o', 'gpt4o', 'gpt4o_mini', 'gpt4o_mini_search' then 16_384
|
|
68
|
-
when 'babbage', 'davinci' then 16_384 # rubocop:disable Lint/DuplicateBranch
|
|
69
|
-
when 'gpt4' then 8_192
|
|
70
|
-
when 'gpt35_turbo' then 4_096
|
|
71
|
-
when 'gpt4_turbo', 'gpt4o_realtime', 'gpt4o_mini_realtime' then 4_096 # rubocop:disable Lint/DuplicateBranch
|
|
72
|
-
when 'gpt4o_mini_transcribe' then 2_000
|
|
73
|
-
when 'o1', 'o1_pro', 'o3_mini' then 100_000
|
|
74
|
-
when 'o1_mini' then 65_536
|
|
75
|
-
when 'gpt4o_mini_tts', 'tts1', 'tts1_hd', 'whisper', 'moderation',
|
|
76
|
-
'embedding3_large', 'embedding3_small', 'embedding_ada' then nil
|
|
77
|
-
else 16_384 # rubocop:disable Lint/DuplicateBranch
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def supports_vision?(model_id)
|
|
82
|
-
case model_family(model_id)
|
|
83
|
-
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4',
|
|
84
|
-
'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'moderation', 'gpt4o_search',
|
|
85
|
-
'gpt4o_mini_search' then true
|
|
86
|
-
else false
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def supports_functions?(model_id)
|
|
91
|
-
case model_family(model_id)
|
|
92
|
-
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4', 'gpt4_turbo', 'gpt4o',
|
|
93
|
-
'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini' then true
|
|
94
|
-
when 'chatgpt4o', 'gpt35_turbo', 'o1_mini', 'gpt4o_mini_tts',
|
|
95
|
-
'gpt4o_transcribe', 'gpt4o_search', 'gpt4o_mini_search' then false
|
|
96
|
-
else false # rubocop:disable Lint/DuplicateBranch
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def supports_tool_choice?(_model_id)
|
|
101
|
-
true
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def supports_tool_parallel_control?(_model_id)
|
|
105
|
-
true
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def supports_structured_output?(model_id)
|
|
109
|
-
case model_family(model_id)
|
|
110
|
-
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'chatgpt4o', 'gpt4o',
|
|
111
|
-
'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini' then true
|
|
112
|
-
else false
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def supports_json_mode?(model_id)
|
|
117
|
-
supports_structured_output?(model_id)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
48
|
PRICES = {
|
|
49
|
+
gpt_image: {
|
|
50
|
+
text: { input: 5.0, cached_input: 1.25 },
|
|
51
|
+
images: { input: 10.0, cached_input: 2.5, output: 40.0 }
|
|
52
|
+
},
|
|
53
|
+
gpt_image_mini: {
|
|
54
|
+
text: { input: 2.0, cached_input: 0.2 },
|
|
55
|
+
images: { input: 2.5, cached_input: 0.25, output: 8.0 }
|
|
56
|
+
},
|
|
57
|
+
gpt_image15: {
|
|
58
|
+
text: { input: 5.0, cached_input: 1.25, output: 10.0 },
|
|
59
|
+
images: { input: 8.0, cached_input: 2.0, output: 32.0 }
|
|
60
|
+
},
|
|
121
61
|
gpt5: { input: 1.25, output: 10.0, cached_input: 0.125 },
|
|
122
62
|
gpt5_mini: { input: 0.25, output: 2.0, cached_input: 0.025 },
|
|
123
63
|
gpt5_nano: { input: 0.05, output: 0.4, cached_input: 0.005 },
|
|
124
64
|
gpt41: { input: 2.0, output: 8.0, cached_input: 0.5 },
|
|
125
65
|
gpt41_mini: { input: 0.4, output: 1.6, cached_input: 0.1 },
|
|
126
66
|
gpt41_nano: { input: 0.1, output: 0.4 },
|
|
127
|
-
chatgpt4o: { input: 5.0, output: 15.0 },
|
|
128
67
|
gpt4: { input: 10.0, output: 30.0 },
|
|
129
68
|
gpt4_turbo: { input: 10.0, output: 30.0 },
|
|
130
|
-
gpt45: { input: 75.0, output: 150.0 },
|
|
131
69
|
gpt35_turbo: { input: 0.5, output: 1.5 },
|
|
132
70
|
gpt4o: { input: 2.5, output: 10.0 },
|
|
133
|
-
gpt4o_audio: { input: 2.5, output: 10.0
|
|
71
|
+
gpt4o_audio: { input: 2.5, output: 10.0 },
|
|
134
72
|
gpt4o_mini: { input: 0.15, output: 0.6 },
|
|
135
|
-
gpt4o_mini_audio: { input: 0.15, output: 0.6
|
|
73
|
+
gpt4o_mini_audio: { input: 0.15, output: 0.6 },
|
|
136
74
|
gpt4o_mini_realtime: { input: 0.6, output: 2.4 },
|
|
137
|
-
gpt4o_mini_transcribe: { input: 1.25, output: 5.0
|
|
75
|
+
gpt4o_mini_transcribe: { input: 1.25, output: 5.0 },
|
|
138
76
|
gpt4o_mini_tts: { input: 0.6, output: 12.0 },
|
|
139
77
|
gpt4o_realtime: { input: 5.0, output: 20.0 },
|
|
140
78
|
gpt4o_search: { input: 2.5, output: 10.0 },
|
|
141
|
-
gpt4o_transcribe: { input: 2.5, output: 10.0
|
|
79
|
+
gpt4o_transcribe: { input: 2.5, output: 10.0 },
|
|
142
80
|
o1: { input: 15.0, output: 60.0 },
|
|
143
81
|
o1_mini: { input: 1.1, output: 4.4 },
|
|
144
82
|
o1_pro: { input: 150.0, output: 600.0 },
|
|
@@ -154,157 +92,181 @@ module RubyLLM
|
|
|
154
92
|
moderation: { price: 0.0 }
|
|
155
93
|
}.freeze
|
|
156
94
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
95
|
+
NIL_LIMIT_FAMILIES = %w[
|
|
96
|
+
gpt_image
|
|
97
|
+
gpt_image_mini
|
|
98
|
+
gpt_image15
|
|
99
|
+
gpt4o_mini_tts
|
|
100
|
+
tts1
|
|
101
|
+
tts1_hd
|
|
102
|
+
whisper
|
|
103
|
+
moderation
|
|
104
|
+
embedding3_large
|
|
105
|
+
embedding3_small
|
|
106
|
+
embedding_ada
|
|
107
|
+
].freeze
|
|
163
108
|
|
|
164
|
-
def
|
|
165
|
-
|
|
166
|
-
prices = PRICES.fetch(family, { input: default_input_price })
|
|
167
|
-
prices[:input] || prices[:price] || default_input_price
|
|
109
|
+
def supports_tool_choice?(_model_id)
|
|
110
|
+
true
|
|
168
111
|
end
|
|
169
112
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
prices = PRICES.fetch(family, {})
|
|
173
|
-
prices[:cached_input]
|
|
113
|
+
def supports_tool_parallel_control?(_model_id)
|
|
114
|
+
true
|
|
174
115
|
end
|
|
175
116
|
|
|
176
|
-
def
|
|
177
|
-
family = model_family(model_id)
|
|
178
|
-
|
|
179
|
-
prices[:output] || prices[:price] || default_output_price
|
|
180
|
-
end
|
|
117
|
+
def context_window_for(model_id)
|
|
118
|
+
family = model_family(model_id)
|
|
119
|
+
return nil if NIL_LIMIT_FAMILIES.include?(family)
|
|
181
120
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
when
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
when
|
|
188
|
-
|
|
121
|
+
case family
|
|
122
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 1_047_576
|
|
123
|
+
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt4_turbo', 'gpt4o', 'gpt4o_audio', 'gpt4o_mini',
|
|
124
|
+
'gpt4o_mini_audio', 'gpt4o_mini_realtime', 'gpt4o_realtime', 'gpt4o_search',
|
|
125
|
+
'gpt4o_transcribe', 'o1_mini' then 128_000
|
|
126
|
+
when 'gpt4' then 8_192
|
|
127
|
+
when 'gpt4o_mini_transcribe' then 16_000
|
|
128
|
+
when 'o1', 'o1_pro', 'o3_mini' then 200_000
|
|
129
|
+
when 'gpt35_turbo' then 16_385
|
|
130
|
+
else 4_096
|
|
189
131
|
end
|
|
190
132
|
end
|
|
191
133
|
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
134
|
+
def max_tokens_for(model_id)
|
|
135
|
+
family = model_family(model_id)
|
|
136
|
+
return nil if NIL_LIMIT_FAMILIES.include?(family)
|
|
195
137
|
|
|
196
|
-
|
|
197
|
-
|
|
138
|
+
case family
|
|
139
|
+
when 'gpt5', 'gpt5_mini', 'gpt5_nano' then 400_000
|
|
140
|
+
when 'gpt41', 'gpt41_mini', 'gpt41_nano' then 32_768
|
|
141
|
+
when 'gpt4' then 8_192
|
|
142
|
+
when 'gpt35_turbo' then 4_096
|
|
143
|
+
when 'gpt4o_mini_transcribe' then 2_000
|
|
144
|
+
when 'o1', 'o1_pro', 'o3_mini' then 100_000
|
|
145
|
+
when 'o1_mini' then 65_536
|
|
146
|
+
else 16_384
|
|
147
|
+
end
|
|
198
148
|
end
|
|
199
149
|
|
|
200
|
-
def
|
|
201
|
-
|
|
202
|
-
|
|
150
|
+
def critical_capabilities_for(model_id)
|
|
151
|
+
capabilities = []
|
|
152
|
+
capabilities << 'function_calling' if supports_functions?(model_id)
|
|
153
|
+
capabilities << 'structured_output' if supports_structured_output?(model_id)
|
|
154
|
+
capabilities << 'vision' if supports_vision?(model_id)
|
|
155
|
+
capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
|
|
156
|
+
capabilities
|
|
203
157
|
end
|
|
204
158
|
|
|
205
|
-
def
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
159
|
+
def pricing_for(model_id)
|
|
160
|
+
return image_pricing_for(model_id) if image_model?(model_id)
|
|
161
|
+
|
|
162
|
+
standard_pricing = {
|
|
163
|
+
input_per_million: input_price_for(model_id),
|
|
164
|
+
output_per_million: output_price_for(model_id)
|
|
165
|
+
}
|
|
211
166
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
.gsub(/^O([13]) /, 'O\1-')
|
|
217
|
-
.gsub(/^O[13] Mini/, '\0'.tr(' ', '-'))
|
|
218
|
-
.gsub(/\d\.\d /, '\0'.sub(' ', '-'))
|
|
219
|
-
.gsub(/4o (?=Mini|Preview|Turbo|Audio|Realtime|Transcribe|Tts)/, '4o-')
|
|
220
|
-
.gsub(/\bHd\b/, 'HD')
|
|
221
|
-
.gsub(/(?:Omni|Text) Moderation/, '\0'.tr(' ', '-'))
|
|
222
|
-
.gsub('Text Embedding', 'text-embedding-')
|
|
167
|
+
cached_price = cached_input_price_for(model_id)
|
|
168
|
+
standard_pricing[:cache_read_input_per_million] = cached_price if cached_price
|
|
169
|
+
|
|
170
|
+
{ text_tokens: { standard: standard_pricing } }
|
|
223
171
|
end
|
|
224
172
|
|
|
225
|
-
def
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
when 'Chatgpt' then 'ChatGPT-'
|
|
229
|
-
when 'Tts' then 'TTS-'
|
|
230
|
-
when 'Dall E' then 'DALL-E-'
|
|
173
|
+
def model_family(model_id)
|
|
174
|
+
MODEL_PATTERNS.each do |family, pattern|
|
|
175
|
+
return family.to_s if model_id.match?(pattern)
|
|
231
176
|
end
|
|
177
|
+
|
|
178
|
+
'other'
|
|
232
179
|
end
|
|
233
180
|
|
|
234
|
-
def
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
nil
|
|
181
|
+
def supports_vision?(model_id)
|
|
182
|
+
case model_family(model_id)
|
|
183
|
+
when 'gpt_image', 'gpt_image_mini', 'gpt_image15', 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini',
|
|
184
|
+
'gpt41_nano', 'gpt4', 'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'moderation',
|
|
185
|
+
'gpt4o_search'
|
|
186
|
+
true
|
|
241
187
|
else
|
|
242
|
-
|
|
188
|
+
false
|
|
243
189
|
end
|
|
244
190
|
end
|
|
245
191
|
|
|
246
|
-
def
|
|
247
|
-
(
|
|
192
|
+
def supports_functions?(model_id)
|
|
193
|
+
case model_family(model_id)
|
|
194
|
+
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4',
|
|
195
|
+
'gpt4_turbo', 'gpt4o', 'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini'
|
|
196
|
+
true
|
|
197
|
+
else
|
|
198
|
+
false
|
|
199
|
+
end
|
|
248
200
|
end
|
|
249
201
|
|
|
250
|
-
def
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
202
|
+
def supports_structured_output?(model_id)
|
|
203
|
+
case model_family(model_id)
|
|
204
|
+
when 'gpt5', 'gpt5_mini', 'gpt5_nano', 'gpt41', 'gpt41_mini', 'gpt41_nano', 'gpt4o',
|
|
205
|
+
'gpt4o_mini', 'o1', 'o1_pro', 'o3_mini'
|
|
206
|
+
true
|
|
207
|
+
else
|
|
208
|
+
false
|
|
209
|
+
end
|
|
210
|
+
end
|
|
255
211
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
modalities[:input] << 'audio' if model_id.match?(/whisper|audio|tts|transcribe/)
|
|
259
|
-
modalities[:input] << 'pdf' if supports_vision?(model_id)
|
|
260
|
-
modalities[:output] << 'audio' if model_id.match?(/tts|audio/)
|
|
261
|
-
modalities[:output] << 'image' if model_id.match?(/dall-e|image/)
|
|
262
|
-
modalities[:output] << 'embeddings' if model_id.match?(/embedding/)
|
|
263
|
-
modalities[:output] << 'moderation' if model_id.match?(/moderation/)
|
|
212
|
+
def input_price_for(model_id)
|
|
213
|
+
return family_prices(model_id).dig(:text, :input) if image_model?(model_id)
|
|
264
214
|
|
|
265
|
-
|
|
215
|
+
price_for(model_id, :input, 0.50)
|
|
266
216
|
end
|
|
267
217
|
|
|
268
|
-
def
|
|
269
|
-
|
|
218
|
+
def output_price_for(model_id)
|
|
219
|
+
return family_prices(model_id).dig(:text, :output) if image_model?(model_id)
|
|
270
220
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
capabilities << 'structured_output' if supports_json_mode?(model_id)
|
|
274
|
-
capabilities << 'batch' if model_id.match?(/embedding|batch/)
|
|
275
|
-
capabilities << 'reasoning' if model_id.match?(/o\d|gpt-5|codex/)
|
|
221
|
+
price_for(model_id, :output, 1.50)
|
|
222
|
+
end
|
|
276
223
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
capabilities << 'speech_generation' if model_id.match?(/audio/)
|
|
280
|
-
capabilities << 'transcription' if model_id.match?(/audio/)
|
|
281
|
-
end
|
|
224
|
+
def cached_input_price_for(model_id)
|
|
225
|
+
return family_prices(model_id).dig(:text, :cached_input) if image_model?(model_id)
|
|
282
226
|
|
|
283
|
-
|
|
227
|
+
family = model_family(model_id).to_sym
|
|
228
|
+
PRICES.fetch(family, {})[:cached_input]
|
|
284
229
|
end
|
|
285
230
|
|
|
286
|
-
def
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
231
|
+
def image_model?(model_id)
|
|
232
|
+
%w[gpt_image gpt_image_mini gpt_image15].include?(model_family(model_id))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def image_pricing_for(model_id)
|
|
236
|
+
text_pricing = {
|
|
237
|
+
input_per_million: input_price_for(model_id)
|
|
290
238
|
}
|
|
239
|
+
cached_text_price = cached_input_price_for(model_id)
|
|
240
|
+
text_pricing[:cache_read_input_per_million] = cached_text_price if cached_text_price
|
|
291
241
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
242
|
+
image_pricing = {
|
|
243
|
+
input_per_million: family_prices(model_id).dig(:images, :input),
|
|
244
|
+
output_per_million: family_prices(model_id).dig(:images, :output)
|
|
245
|
+
}
|
|
246
|
+
cached_image_price = family_prices(model_id).dig(:images, :cached_input)
|
|
247
|
+
image_pricing[:cache_read_input_per_million] = cached_image_price if cached_image_price
|
|
296
248
|
|
|
297
|
-
|
|
249
|
+
{
|
|
250
|
+
text_tokens: { standard: text_pricing },
|
|
251
|
+
images: { standard: image_pricing }
|
|
252
|
+
}
|
|
253
|
+
end
|
|
298
254
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
end
|
|
255
|
+
def price_for(model_id, key, fallback)
|
|
256
|
+
prices = family_prices(model_id)
|
|
257
|
+
prices = { key => fallback } if prices.empty?
|
|
258
|
+
prices[key] || prices[:price] || fallback
|
|
259
|
+
end
|
|
305
260
|
|
|
306
|
-
|
|
261
|
+
def family_prices(model_id)
|
|
262
|
+
family = model_family(model_id).to_sym
|
|
263
|
+
PRICES.fetch(family, {})
|
|
307
264
|
end
|
|
265
|
+
|
|
266
|
+
module_function :context_window_for, :max_tokens_for, :critical_capabilities_for, :pricing_for,
|
|
267
|
+
:model_family, :supports_vision?, :supports_functions?, :supports_structured_output?,
|
|
268
|
+
:input_price_for, :output_price_for, :cached_input_price_for, :image_model?,
|
|
269
|
+
:image_pricing_for, :price_for, :family_prices
|
|
308
270
|
end
|
|
309
271
|
end
|
|
310
272
|
end
|
|
@@ -61,8 +61,7 @@ module RubyLLM
|
|
|
61
61
|
return unless message_data
|
|
62
62
|
|
|
63
63
|
usage = data['usage'] || {}
|
|
64
|
-
|
|
65
|
-
thinking_tokens = usage.dig('completion_tokens_details', 'reasoning_tokens')
|
|
64
|
+
thinking_tokens = thinking_tokens(usage)
|
|
66
65
|
content, thinking_from_blocks = extract_content_and_thinking(message_data['content'])
|
|
67
66
|
thinking_text = thinking_from_blocks || extract_thinking_text(message_data)
|
|
68
67
|
thinking_signature = extract_thinking_signature(message_data)
|
|
@@ -72,27 +71,82 @@ module RubyLLM
|
|
|
72
71
|
content: content,
|
|
73
72
|
thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
|
|
74
73
|
tool_calls: parse_tool_calls(message_data['tool_calls']),
|
|
75
|
-
input_tokens: usage
|
|
76
|
-
output_tokens: usage
|
|
77
|
-
cached_tokens:
|
|
78
|
-
cache_creation_tokens:
|
|
74
|
+
input_tokens: input_tokens(usage),
|
|
75
|
+
output_tokens: output_tokens(usage),
|
|
76
|
+
cached_tokens: cache_read_tokens(usage),
|
|
77
|
+
cache_creation_tokens: cache_write_tokens(usage),
|
|
79
78
|
thinking_tokens: thinking_tokens,
|
|
80
79
|
model_id: data['model'],
|
|
81
80
|
raw: response
|
|
82
81
|
)
|
|
83
82
|
end
|
|
84
83
|
|
|
84
|
+
def input_tokens(usage)
|
|
85
|
+
return usage['prompt_cache_miss_tokens'] if usage['prompt_cache_miss_tokens']
|
|
86
|
+
|
|
87
|
+
prompt_tokens = usage['prompt_tokens']
|
|
88
|
+
return unless prompt_tokens
|
|
89
|
+
|
|
90
|
+
[prompt_tokens.to_i - cache_read_tokens(usage).to_i - cache_write_tokens(usage).to_i, 0].max
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def output_tokens(usage)
|
|
94
|
+
completion_tokens = usage['completion_tokens']
|
|
95
|
+
return unless completion_tokens
|
|
96
|
+
|
|
97
|
+
completion_tokens = completion_tokens.to_i
|
|
98
|
+
generated_tokens = generated_tokens_from_total(usage)
|
|
99
|
+
return completion_tokens unless generated_tokens && generated_tokens > completion_tokens
|
|
100
|
+
|
|
101
|
+
generated_tokens
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def generated_tokens_from_total(usage)
|
|
105
|
+
prompt_tokens = usage['prompt_tokens']
|
|
106
|
+
total_tokens = usage['total_tokens']
|
|
107
|
+
return unless prompt_tokens && total_tokens
|
|
108
|
+
|
|
109
|
+
[total_tokens.to_i - prompt_tokens.to_i, 0].max
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def cache_read_tokens(usage)
|
|
113
|
+
usage.dig('prompt_tokens_details', 'cached_tokens') || usage['prompt_cache_hit_tokens']
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def cache_write_tokens(usage)
|
|
117
|
+
usage.dig('prompt_tokens_details', 'cache_write_tokens') || 0
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def thinking_tokens(usage)
|
|
121
|
+
usage.dig('completion_tokens_details', 'reasoning_tokens') || usage['reasoning_tokens']
|
|
122
|
+
end
|
|
123
|
+
|
|
85
124
|
def format_messages(messages)
|
|
86
125
|
messages.map do |msg|
|
|
87
126
|
{
|
|
88
127
|
role: format_role(msg.role),
|
|
89
|
-
content:
|
|
128
|
+
content: format_message_content(msg),
|
|
90
129
|
tool_calls: format_tool_calls(msg.tool_calls),
|
|
91
130
|
tool_call_id: msg.tool_call_id
|
|
92
131
|
}.compact.merge(format_thinking(msg))
|
|
93
132
|
end
|
|
94
133
|
end
|
|
95
134
|
|
|
135
|
+
def format_message_content(msg)
|
|
136
|
+
content = format_content(msg.content)
|
|
137
|
+
return '' if content.nil? && thinking_only_assistant_message?(msg)
|
|
138
|
+
|
|
139
|
+
content
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def thinking_only_assistant_message?(msg)
|
|
143
|
+
msg.role == :assistant && msg.thinking && !msg.tool_call?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def format_content(content)
|
|
147
|
+
Media.format_content(content)
|
|
148
|
+
end
|
|
149
|
+
|
|
96
150
|
def format_role(role)
|
|
97
151
|
case role
|
|
98
152
|
when :system
|