ruby_llm 2.0.0 → 2.1.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 +65 -8
- data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
- data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
- data/lib/ruby_llm/accounting/usage.rb +86 -49
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
- data/lib/ruby_llm/active_record/batch.rb +1 -1
- data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
- data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
- data/lib/ruby_llm/active_record/message_methods.rb +32 -7
- data/lib/ruby_llm/active_record/model.rb +1 -1
- data/lib/ruby_llm/active_record/provider_file.rb +64 -0
- data/lib/ruby_llm/active_record/record.rb +19 -0
- data/lib/ruby_llm/active_record/tool_call.rb +1 -1
- data/lib/ruby_llm/active_record/usage.rb +53 -3
- data/lib/ruby_llm/agent.rb +198 -33
- data/lib/ruby_llm/aliases.json +66 -8
- data/lib/ruby_llm/attachment.rb +54 -10
- data/lib/ruby_llm/batch.rb +26 -3
- data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
- data/lib/ruby_llm/chat/tool_search.rb +26 -0
- data/lib/ruby_llm/chat.rb +488 -82
- data/lib/ruby_llm/choice.rb +38 -0
- data/lib/ruby_llm/configuration.rb +36 -11
- data/lib/ruby_llm/context.rb +45 -32
- data/lib/ruby_llm/cost.rb +42 -1
- data/lib/ruby_llm/embedding.rb +9 -2
- data/lib/ruby_llm/error.rb +34 -2
- data/lib/ruby_llm/evaluation/assertions.rb +40 -0
- data/lib/ruby_llm/evaluation/case.rb +52 -0
- data/lib/ruby_llm/evaluation/dataset.rb +65 -0
- data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
- data/lib/ruby_llm/evaluation/evidence.rb +125 -0
- data/lib/ruby_llm/evaluation/minitest.rb +26 -0
- data/lib/ruby_llm/evaluation/report.rb +103 -0
- data/lib/ruby_llm/evaluation/result.rb +75 -0
- data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
- data/lib/ruby_llm/evaluation/rspec.rb +23 -0
- data/lib/ruby_llm/evaluation/runner.rb +93 -0
- data/lib/ruby_llm/evaluation/trial.rb +101 -0
- data/lib/ruby_llm/evaluation.rb +289 -0
- data/lib/ruby_llm/image.rb +12 -4
- data/lib/ruby_llm/judge/builder.rb +51 -0
- data/lib/ruby_llm/judge/data.rb +43 -0
- data/lib/ruby_llm/judge/question.rb +101 -0
- data/lib/ruby_llm/judge.rb +179 -0
- data/lib/ruby_llm/judgment.rb +125 -0
- data/lib/ruby_llm/mcp/apps.rb +25 -0
- data/lib/ruby_llm/mcp/client.rb +213 -0
- data/lib/ruby_llm/mcp/collection.rb +56 -0
- data/lib/ruby_llm/mcp/content.rb +44 -0
- data/lib/ruby_llm/mcp/error.rb +27 -0
- data/lib/ruby_llm/mcp/http.rb +379 -0
- data/lib/ruby_llm/mcp/input_request.rb +118 -0
- data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
- data/lib/ruby_llm/mcp/listener.rb +168 -0
- data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
- data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
- data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
- data/lib/ruby_llm/mcp/oauth.rb +653 -0
- data/lib/ruby_llm/mcp/param_headers.rb +83 -0
- data/lib/ruby_llm/mcp/prompt.rb +71 -0
- data/lib/ruby_llm/mcp/resource.rb +84 -0
- data/lib/ruby_llm/mcp/resource_template.rb +99 -0
- data/lib/ruby_llm/mcp/result.rb +76 -0
- data/lib/ruby_llm/mcp/session_expired.rb +10 -0
- data/lib/ruby_llm/mcp/stdio.rb +250 -0
- data/lib/ruby_llm/mcp/task.rb +151 -0
- data/lib/ruby_llm/mcp/tool.rb +130 -0
- data/lib/ruby_llm/mcp.rb +1101 -0
- data/lib/ruby_llm/message.rb +41 -6
- data/lib/ruby_llm/model.rb +2 -1
- data/lib/ruby_llm/models/aliases.rb +5 -2
- data/lib/ruby_llm/models/lookup.rb +80 -0
- data/lib/ruby_llm/models/registry.rb +2 -2
- data/lib/ruby_llm/models/schema.rb +1 -1
- data/lib/ruby_llm/models.json +23082 -10158
- data/lib/ruby_llm/models.rb +20 -69
- data/lib/ruby_llm/moderation.rb +7 -2
- data/lib/ruby_llm/ocr.rb +7 -2
- data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
- data/lib/ruby_llm/open_telemetry.rb +82 -0
- data/lib/ruby_llm/probability.rb +30 -0
- data/lib/ruby_llm/progress.rb +44 -0
- data/lib/ruby_llm/prompt/context.rb +55 -0
- data/lib/ruby_llm/prompt.rb +1 -1
- data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
- data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
- data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
- data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
- data/lib/ruby_llm/protocol/streaming.rb +38 -31
- data/lib/ruby_llm/protocol.rb +192 -40
- data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
- data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
- data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
- data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
- data/lib/ruby_llm/protocols/anthropic.rb +20 -1
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
- data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
- data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
- data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
- data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
- data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
- data/lib/ruby_llm/protocols/cohere.rb +1 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
- data/lib/ruby_llm/protocols/converse/media.rb +16 -2
- data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
- data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
- data/lib/ruby_llm/protocols/converse.rb +3 -5
- data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
- data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
- data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
- data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
- data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
- data/lib/ruby_llm/protocols/gemini.rb +1 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
- data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
- data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
- data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
- data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
- data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
- data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
- data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
- data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
- data/lib/ruby_llm/protocols/openai/files.rb +0 -7
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
- data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
- data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
- data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
- data/lib/ruby_llm/protocols/responses/media.rb +11 -7
- data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
- data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
- data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
- data/lib/ruby_llm/protocols/responses.rb +1 -0
- data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
- data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
- data/lib/ruby_llm/protocols/system_one.rb +27 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
- data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
- data/lib/ruby_llm/provider.rb +87 -15
- data/lib/ruby_llm/providers/anthropic.rb +23 -0
- data/lib/ruby_llm/providers/azure/chat.rb +4 -0
- data/lib/ruby_llm/providers/azure/images.rb +2 -2
- data/lib/ruby_llm/providers/azure/media.rb +8 -3
- data/lib/ruby_llm/providers/azure/videos.rb +7 -2
- data/lib/ruby_llm/providers/azure.rb +24 -4
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
- data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
- data/lib/ruby_llm/providers/bedrock.rb +38 -6
- data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
- data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
- data/lib/ruby_llm/providers/hetzner.rb +41 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
- data/lib/ruby_llm/providers/mistral/media.rb +8 -4
- data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
- data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -2
- data/lib/ruby_llm/providers/ollama/models.rb +2 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
- data/lib/ruby_llm/providers/openai.rb +14 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
- data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
- data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
- data/lib/ruby_llm/providers/perplexity.rb +11 -0
- data/lib/ruby_llm/providers/typesafe.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/vertexai.rb +27 -28
- data/lib/ruby_llm/providers/xai/chat.rb +6 -0
- data/lib/ruby_llm/providers/xai/images.rb +9 -10
- data/lib/ruby_llm/providers/xai/responses.rb +10 -8
- data/lib/ruby_llm/providers/xai/videos.rb +11 -9
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/railtie.rb +11 -0
- data/lib/ruby_llm/request_shape/part.rb +117 -0
- data/lib/ruby_llm/request_shape/problem.rb +63 -0
- data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
- data/lib/ruby_llm/request_shape/turn.rb +54 -0
- data/lib/ruby_llm/request_shape.rb +160 -0
- data/lib/ruby_llm/rerank.rb +9 -2
- data/lib/ruby_llm/research_job.rb +21 -5
- data/lib/ruby_llm/score.rb +42 -0
- data/lib/ruby_llm/search_results.rb +2 -2
- data/lib/ruby_llm/server_tool_call.rb +10 -2
- data/lib/ruby_llm/speech.rb +8 -1
- data/lib/ruby_llm/support/cancellation.rb +39 -0
- data/lib/ruby_llm/support/inspectable.rb +3 -2
- data/lib/ruby_llm/support/instrumentation.rb +38 -1
- data/lib/ruby_llm/support/process_cache.rb +60 -0
- data/lib/ruby_llm/support/progress_reporter.rb +33 -0
- data/lib/ruby_llm/thinking.rb +2 -1
- data/lib/ruby_llm/tokens.rb +57 -14
- data/lib/ruby_llm/tool.rb +50 -3
- data/lib/ruby_llm/tool_call.rb +17 -2
- data/lib/ruby_llm/transcription.rb +10 -3
- data/lib/ruby_llm/transport/connection.rb +127 -44
- data/lib/ruby_llm/transport/error_middleware.rb +90 -24
- data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
- data/lib/ruby_llm/transport/json_response.rb +33 -0
- data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +5 -3
- data/lib/ruby_llm/video_job.rb +48 -5
- data/lib/ruby_llm.rb +106 -5
- data/lib/tasks/ruby_llm/evaluations.rb +43 -0
- data/lib/tasks/ruby_llm.rake +6 -9
- data/skills/rubyllm/SKILL.md +33 -12
- metadata +95 -37
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
|
@@ -46,6 +46,13 @@ module RubyLLM
|
|
|
46
46
|
keys.map { |key| ":#{key}" }.join(', ')
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
# extra_content carries Gemini's thought signatures on Google's
|
|
50
|
+
# OpenAI-compatible endpoints, and Mistral refuses any field it does
|
|
51
|
+
# not define.
|
|
52
|
+
def format_tool_calls(tool_calls)
|
|
53
|
+
super&.map { |call| call.except(:extra_content) }
|
|
54
|
+
end
|
|
55
|
+
|
|
49
56
|
def build_tool_choice(tool_choice)
|
|
50
57
|
return 'any' if tool_choice == :required
|
|
51
58
|
|
|
@@ -70,7 +77,7 @@ module RubyLLM
|
|
|
70
77
|
formatted_content = super
|
|
71
78
|
return formatted_content unless msg.role == :assistant && msg.thinking
|
|
72
79
|
|
|
73
|
-
content_blocks = build_thinking_blocks(msg
|
|
80
|
+
content_blocks = build_thinking_blocks(msg)
|
|
74
81
|
append_formatted_content(content_blocks, formatted_content)
|
|
75
82
|
|
|
76
83
|
content_blocks
|
|
@@ -80,17 +87,14 @@ module RubyLLM
|
|
|
80
87
|
{}
|
|
81
88
|
end
|
|
82
89
|
|
|
83
|
-
def build_thinking_blocks(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}.compact]
|
|
92
|
-
elsif thinking.signature
|
|
93
|
-
[{ type: 'thinking', signature: thinking.signature }]
|
|
90
|
+
def build_thinking_blocks(msg)
|
|
91
|
+
text = msg.thinking&.text
|
|
92
|
+
signature = own_signature(msg)
|
|
93
|
+
|
|
94
|
+
if text
|
|
95
|
+
[{ type: 'thinking', thinking: [{ type: 'text', text: }], signature: }.compact]
|
|
96
|
+
elsif signature
|
|
97
|
+
[{ type: 'thinking', signature: }]
|
|
94
98
|
else
|
|
95
99
|
[]
|
|
96
100
|
end
|
|
@@ -7,8 +7,14 @@ module RubyLLM
|
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
def supported_attachment?(attachment)
|
|
11
|
+
%i[image audio pdf document text].include?(attachment.type)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def format_content(content, attachments = [])
|
|
11
15
|
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
17
|
+
|
|
12
18
|
case attachment.type
|
|
13
19
|
when :image
|
|
14
20
|
format_image(attachment)
|
|
@@ -18,8 +24,6 @@ module RubyLLM
|
|
|
18
24
|
format_document(attachment)
|
|
19
25
|
when :text
|
|
20
26
|
Protocols::ChatCompletions::Media.format_text_file(attachment)
|
|
21
|
-
else
|
|
22
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
23
27
|
end
|
|
24
28
|
end
|
|
25
29
|
end
|
|
@@ -27,14 +31,14 @@ module RubyLLM
|
|
|
27
31
|
def format_image(image)
|
|
28
32
|
{
|
|
29
33
|
type: 'image_url',
|
|
30
|
-
image_url: image.
|
|
34
|
+
image_url: image.url_or_data_uri
|
|
31
35
|
}
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
def format_document(document)
|
|
35
39
|
{
|
|
36
40
|
type: 'document_url',
|
|
37
|
-
document_url: document.
|
|
41
|
+
document_url: document.url_or_data_uri
|
|
38
42
|
}
|
|
39
43
|
end
|
|
40
44
|
end
|
|
@@ -22,7 +22,7 @@ module RubyLLM
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def ocr_document_part(attachment)
|
|
25
|
-
reference = attachment.
|
|
25
|
+
reference = attachment.url_or_data_uri
|
|
26
26
|
|
|
27
27
|
if attachment.image?
|
|
28
28
|
{ type: 'image_url', image_url: reference }
|
|
@@ -31,10 +31,6 @@ module RubyLLM
|
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def ocr_data_uri(attachment)
|
|
35
|
-
"data:#{attachment.mime_type};base64,#{attachment.encoded}"
|
|
36
|
-
end
|
|
37
|
-
|
|
38
34
|
def parse_ocr_response(response, model:)
|
|
39
35
|
data = response.body
|
|
40
36
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class Ollama
|
|
6
|
+
# Ollama's dialect of the System One API, which accepts images for multimodal decision models.
|
|
7
|
+
module Judgments
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def judgment_url
|
|
11
|
+
'systemone'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def render_judgment_payload(input, questions:, model:, with: [], provider_options: {})
|
|
15
|
+
images = with.map do |attachment|
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
|
|
17
|
+
|
|
18
|
+
attachment.encoded
|
|
19
|
+
end
|
|
20
|
+
payload = super(input, questions:, model:, provider_options:)
|
|
21
|
+
images.empty? ? payload : payload.merge(images:)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -7,8 +7,14 @@ module RubyLLM
|
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
def supported_attachment?(attachment)
|
|
11
|
+
%i[image audio text].include?(attachment.type)
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
def format_content(content, attachments = [])
|
|
11
15
|
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
17
|
+
|
|
12
18
|
case attachment.type
|
|
13
19
|
when :image
|
|
14
20
|
format_image(attachment)
|
|
@@ -16,8 +22,6 @@ module RubyLLM
|
|
|
16
22
|
Protocols::ChatCompletions::Media.format_audio(attachment)
|
|
17
23
|
when :text
|
|
18
24
|
Protocols::ChatCompletions::Media.format_text_file(attachment)
|
|
19
|
-
else
|
|
20
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
21
25
|
end
|
|
22
26
|
end
|
|
23
27
|
end
|
|
@@ -59,6 +59,7 @@ module RubyLLM
|
|
|
59
59
|
|
|
60
60
|
def build_capabilities(reported)
|
|
61
61
|
return [] if reported.include?('embedding')
|
|
62
|
+
return %w[judgment] if reported.include?('decision')
|
|
62
63
|
|
|
63
64
|
derived = CAPABILITY_MAP.filter_map { |native, capability| capability if reported.include?(native) }
|
|
64
65
|
model_capabilities + derived
|
|
@@ -66,6 +67,7 @@ module RubyLLM
|
|
|
66
67
|
|
|
67
68
|
def build_modalities(reported)
|
|
68
69
|
return { input: %w[text], output: %w[embeddings] } if reported.include?('embedding')
|
|
70
|
+
return { input: %w[text], output: %w[judgment] } if reported.include?('decision')
|
|
69
71
|
|
|
70
72
|
input = %w[text]
|
|
71
73
|
input << 'image' if reported.include?('vision')
|
|
@@ -11,12 +11,23 @@ module RubyLLM
|
|
|
11
11
|
include Ollama::Models
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
# Ollama's dialect of the System One API for decision models such as Clef.
|
|
15
|
+
class SystemOne < Protocols::SystemOne
|
|
16
|
+
include Ollama::Judgments
|
|
17
|
+
end
|
|
18
|
+
|
|
14
19
|
protocol :chat_completions, ChatCompletions
|
|
20
|
+
protocol :system_one, SystemOne
|
|
15
21
|
|
|
16
22
|
def api_base
|
|
17
23
|
@config.ollama_api_base
|
|
18
24
|
end
|
|
19
25
|
|
|
26
|
+
def resolve_protocol(name, model, operation: nil, **options)
|
|
27
|
+
name ||= :system_one if operation == :judge
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
20
31
|
def headers
|
|
21
32
|
return {} unless @config.ollama_api_key
|
|
22
33
|
|
|
@@ -46,13 +46,15 @@ module RubyLLM
|
|
|
46
46
|
gpt-4o-transcribe-diarize
|
|
47
47
|
whisper-1
|
|
48
48
|
].freeze
|
|
49
|
+
JUDGMENT_MODELS = %w[gpt-6-luna].freeze
|
|
49
50
|
EXPLICIT_CAPABILITIES = {
|
|
50
51
|
'function_calling' => (CHAT_MODELS + CODEX_MODELS).freeze,
|
|
51
52
|
'structured_output' => (CHAT_MODELS + CODEX_MODELS + SEARCH_MODELS).freeze,
|
|
52
53
|
'vision' => (CHAT_MODELS + CODEX_MODELS + DEEP_RESEARCH_MODELS + MODERATION_MODELS).freeze,
|
|
53
54
|
'reasoning' => (CODEX_MODELS + DEEP_RESEARCH_MODELS).freeze,
|
|
54
55
|
'transcription' => TRANSCRIPTION_MODELS,
|
|
55
|
-
'citations' => SEARCH_MODELS
|
|
56
|
+
'citations' => SEARCH_MODELS,
|
|
57
|
+
'judgment' => JUDGMENT_MODELS
|
|
56
58
|
}.freeze
|
|
57
59
|
|
|
58
60
|
def self.augment(capabilities, model_id:, **)
|
|
@@ -9,14 +9,24 @@ module RubyLLM
|
|
|
9
9
|
protocol :embeddings, Protocols::ChatCompletions,
|
|
10
10
|
batches: Protocols::ChatCompletions::EmbeddingBatches
|
|
11
11
|
protocol :files, Protocols::OpenAI::Files
|
|
12
|
+
protocol :decisions, Protocols::OpenAI::Decisions
|
|
12
13
|
|
|
13
14
|
RATE_LIMIT_RESET_HEADERS = %w[x-ratelimit-reset-requests x-ratelimit-reset-tokens].freeze
|
|
14
15
|
RESET_DURATION_UNITS = { 'h' => 3600, 'm' => 60, 's' => 1, 'ms' => 0.001 }.freeze
|
|
15
16
|
|
|
17
|
+
def original_image_detail? # :nodoc:
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
16
21
|
def api_base
|
|
17
22
|
@config.openai_api_base || 'https://api.openai.com/v1'
|
|
18
23
|
end
|
|
19
24
|
|
|
25
|
+
def resolve_protocol(name, model, operation: nil, **options)
|
|
26
|
+
name ||= :decisions if operation == :judge
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
20
30
|
# Audio, realtime, and dedicated search models only exist on Chat Completions.
|
|
21
31
|
def protocol_for(model, **)
|
|
22
32
|
return protocols[:chat_completions] if Capabilities::SEARCH_MODELS.include?(model.id)
|
|
@@ -32,6 +42,10 @@ module RubyLLM
|
|
|
32
42
|
}.compact
|
|
33
43
|
end
|
|
34
44
|
|
|
45
|
+
def account_identity
|
|
46
|
+
account_digest(api_base, @config.openai_api_key, @config.openai_organization_id, @config.openai_project_id)
|
|
47
|
+
end
|
|
48
|
+
|
|
35
49
|
# OpenAI reports when each rate limit resets in its own headers,
|
|
36
50
|
# as durations like "6m0s", "7.66s" or "76ms".
|
|
37
51
|
def retry_delay(response)
|
|
@@ -6,7 +6,6 @@ module RubyLLM
|
|
|
6
6
|
# Chat methods of the OpenRouter API integration
|
|
7
7
|
module Chat
|
|
8
8
|
OPENROUTER_INLINE_FILE_THRESHOLD = 50 * 1024 * 1024
|
|
9
|
-
OPENROUTER_FILE_UPLOAD_LIMIT = 100 * 1024 * 1024
|
|
10
9
|
CACHE_CONTROL_TYPE = 'ephemeral'
|
|
11
10
|
PROMPT_CACHE_OPTIONS = %i[ttl].freeze
|
|
12
11
|
COMPACTION_PLUGIN_ID = 'context-compression'
|
|
@@ -35,6 +34,10 @@ module RubyLLM
|
|
|
35
34
|
end
|
|
36
35
|
end
|
|
37
36
|
|
|
37
|
+
def supported_attachment?(attachment)
|
|
38
|
+
OpenRouter::Media.supported_attachment?(attachment)
|
|
39
|
+
end
|
|
40
|
+
|
|
38
41
|
def format_content(content, attachments = [])
|
|
39
42
|
OpenRouter::Media.format_content(content, attachments)
|
|
40
43
|
end
|
|
@@ -86,32 +89,28 @@ module RubyLLM
|
|
|
86
89
|
return {} unless msg.role == :assistant
|
|
87
90
|
return { reasoning_details: msg.raw_reasoning } if msg.raw_reasoning.is_a?(Array)
|
|
88
91
|
|
|
89
|
-
thinking
|
|
90
|
-
|
|
92
|
+
return {} unless msg.thinking && producer_slug(msg) == @provider.slug
|
|
93
|
+
|
|
94
|
+
detail = reasoning_detail(msg.thinking)
|
|
95
|
+
detail ? { reasoning_details: [detail] } : {}
|
|
96
|
+
end
|
|
91
97
|
|
|
92
|
-
|
|
98
|
+
def reasoning_detail(thinking)
|
|
93
99
|
if thinking.text
|
|
94
|
-
|
|
95
|
-
type: 'reasoning.text',
|
|
96
|
-
text: thinking.text,
|
|
97
|
-
signature: thinking.signature
|
|
98
|
-
}.compact
|
|
100
|
+
{ type: 'reasoning.text', text: thinking.text, signature: thinking.signature }.compact
|
|
99
101
|
elsif thinking.signature
|
|
100
|
-
|
|
101
|
-
type: 'reasoning.encrypted',
|
|
102
|
-
data: thinking.signature
|
|
103
|
-
}
|
|
102
|
+
{ type: 'reasoning.encrypted', data: thinking.signature }
|
|
104
103
|
end
|
|
105
|
-
|
|
106
|
-
details.empty? ? {} : { reasoning_details: details }
|
|
107
104
|
end
|
|
108
105
|
|
|
109
106
|
def format_message_content(msg, caching: nil)
|
|
110
107
|
content = super
|
|
111
|
-
caching != false && msg.cache_until_here?
|
|
108
|
+
return content unless caching != false && msg.cache_until_here?
|
|
109
|
+
|
|
110
|
+
inject_cache_control(content, caching:, ttl: msg.cache_ttl)
|
|
112
111
|
end
|
|
113
112
|
|
|
114
|
-
def inject_cache_control(content, caching: nil)
|
|
113
|
+
def inject_cache_control(content, caching: nil, ttl: nil)
|
|
115
114
|
blocks = content.is_a?(Array) ? content.dup : [{ type: 'text', text: content }]
|
|
116
115
|
return blocks if blocks.empty?
|
|
117
116
|
|
|
@@ -119,15 +118,16 @@ module RubyLLM
|
|
|
119
118
|
return blocks unless last.is_a?(Hash)
|
|
120
119
|
return blocks if last[:cache_control] || last['cache_control']
|
|
121
120
|
|
|
122
|
-
blocks[-1] = last.merge(cache_control: prompt_cache_control(caching))
|
|
121
|
+
blocks[-1] = last.merge(cache_control: prompt_cache_control(caching, ttl:))
|
|
123
122
|
blocks
|
|
124
123
|
end
|
|
125
124
|
|
|
126
|
-
def prompt_cache_control(caching = nil)
|
|
125
|
+
def prompt_cache_control(caching = nil, ttl: nil)
|
|
127
126
|
options = prompt_cache_options(caching)
|
|
127
|
+
ttl ||= options[:ttl]
|
|
128
128
|
|
|
129
129
|
{ type: CACHE_CONTROL_TYPE }.tap do |control|
|
|
130
|
-
control[:ttl] =
|
|
130
|
+
control[:ttl] = ttl if ttl
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
@@ -166,10 +166,6 @@ module RubyLLM
|
|
|
166
166
|
OPENROUTER_INLINE_FILE_THRESHOLD
|
|
167
167
|
end
|
|
168
168
|
|
|
169
|
-
def provider_file_upload_limit
|
|
170
|
-
OPENROUTER_FILE_UPLOAD_LIMIT
|
|
171
|
-
end
|
|
172
|
-
|
|
173
169
|
def provider_file_attachable?(attachment)
|
|
174
170
|
attachment.pdf?
|
|
175
171
|
end
|
|
@@ -39,6 +39,8 @@ module RubyLLM
|
|
|
39
39
|
|
|
40
40
|
if (type = EMBEDDING_MEDIA_TYPES[attachment.type])
|
|
41
41
|
{ type: type.to_s, type => { data: attachment.for_llm, format: attachment.format } }
|
|
42
|
+
elsif attachment.type == :image
|
|
43
|
+
Protocols::ChatCompletions::Media.format_image(attachment)
|
|
42
44
|
else
|
|
43
45
|
Protocols::ChatCompletions::Media.format_attachment(
|
|
44
46
|
attachment, document_attachments: :none, image_attachments: true, audio_attachments: false
|
|
@@ -8,6 +8,10 @@ module RubyLLM
|
|
|
8
8
|
module Media
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
+
def supported_attachment?(attachment)
|
|
12
|
+
attachment.video? || Protocols::ChatCompletions::Media.supported_attachment?(attachment)
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def format_content(content, attachments = [])
|
|
12
16
|
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
13
17
|
if attachment.type == :video
|
|
@@ -24,7 +28,7 @@ module RubyLLM
|
|
|
24
28
|
{
|
|
25
29
|
type: 'video_url',
|
|
26
30
|
video_url: {
|
|
27
|
-
url: video.
|
|
31
|
+
url: video.url_or_data_uri
|
|
28
32
|
}
|
|
29
33
|
}
|
|
30
34
|
end
|
|
@@ -17,6 +17,7 @@ module RubyLLM
|
|
|
17
17
|
role: :assistant,
|
|
18
18
|
model: data['model'],
|
|
19
19
|
content: delta['content'],
|
|
20
|
+
citations: extract_chunk_citations(delta, data),
|
|
20
21
|
thinking: Thinking.build(
|
|
21
22
|
text: extract_thinking_text(delta),
|
|
22
23
|
signature: extract_thinking_signature(delta)
|
|
@@ -28,6 +29,7 @@ module RubyLLM
|
|
|
28
29
|
cache_read_tokens: cache_read_tokens(usage),
|
|
29
30
|
cache_write_tokens: cache_write_tokens(usage),
|
|
30
31
|
thinking_tokens: thinking_tokens(usage),
|
|
32
|
+
server_tool_use: server_tool_use(usage),
|
|
31
33
|
reported_cost: reported_cost(usage),
|
|
32
34
|
finish_reason: normalize_finish_reason(data.dig('choices', 0, 'finish_reason'))
|
|
33
35
|
)
|
|
@@ -47,7 +49,8 @@ module RubyLLM
|
|
|
47
49
|
|
|
48
50
|
detail.each do |key, value|
|
|
49
51
|
if value.is_a?(String) && target[key].is_a?(String) && ACCUMULATED_REASONING_KEYS.include?(key)
|
|
50
|
-
target[key]
|
|
52
|
+
target[key] = +target[key]
|
|
53
|
+
target[key] << value
|
|
51
54
|
elsif target[key].nil?
|
|
52
55
|
target[key] = value
|
|
53
56
|
end
|
|
@@ -20,11 +20,16 @@ module RubyLLM
|
|
|
20
20
|
payload.merge(provider_options)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def parse_video_job(response, model
|
|
23
|
+
def parse_video_job(response, model:, **request)
|
|
24
24
|
id = response.body['id']
|
|
25
25
|
raise Error.new('OpenRouter did not return a video job', response:) unless id
|
|
26
26
|
|
|
27
|
-
VideoJob.new(id: id, protocol: self, model: model, status: job_status(response.body), raw: response.body
|
|
27
|
+
VideoJob.new(id: id, protocol: self, model: model, status: job_status(response.body), raw: response.body,
|
|
28
|
+
**request)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def video_request_settings(request)
|
|
32
|
+
{ duration: request[:duration], resolution: request[:resolution] || request[:size] }
|
|
28
33
|
end
|
|
29
34
|
|
|
30
35
|
def video_job_url(job)
|
|
@@ -34,9 +39,9 @@ module RubyLLM
|
|
|
34
39
|
def parse_video_job_status(response, job:) # rubocop:disable Lint/UnusedMethodArgument
|
|
35
40
|
body = response.body
|
|
36
41
|
status = job_status(body)
|
|
37
|
-
state = { status: status, raw: body }
|
|
42
|
+
state = { status: status, raw: body, reported_cost: reported_cost(body['usage'] || {}) }
|
|
38
43
|
state[:error] = body['error'] || body['status'] if status == :failed
|
|
39
|
-
state
|
|
44
|
+
state.compact
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
def download_video(job)
|
|
@@ -71,7 +76,7 @@ module RubyLLM
|
|
|
71
76
|
|
|
72
77
|
def frame_images(attachments)
|
|
73
78
|
attachments.zip(FRAME_TYPES).map do |attachment, frame_type|
|
|
74
|
-
url = attachment.
|
|
79
|
+
url = attachment.url_or_data_uri
|
|
75
80
|
{ type: 'image_url', image_url: { url: url }, frame_type: frame_type }
|
|
76
81
|
end
|
|
77
82
|
end
|
|
@@ -11,6 +11,10 @@ module RubyLLM
|
|
|
11
11
|
role.to_s
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def supported_attachment?(attachment)
|
|
15
|
+
Perplexity::Media.supported_attachment?(attachment)
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
def format_content(content, attachments = [])
|
|
15
19
|
Perplexity::Media.format_content(content, attachments)
|
|
16
20
|
end
|
|
@@ -7,10 +7,14 @@ module RubyLLM
|
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
def supported_attachment?(attachment)
|
|
11
|
+
%i[image pdf document text].include?(attachment.type)
|
|
12
|
+
end
|
|
11
13
|
|
|
12
14
|
def format_content(content, attachments = [])
|
|
13
15
|
Protocols::ChatCompletions::Media.format_parts(content, attachments) do |attachment|
|
|
16
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
17
|
+
|
|
14
18
|
case attachment.type
|
|
15
19
|
when :image
|
|
16
20
|
Protocols::ChatCompletions::Media.format_image(attachment)
|
|
@@ -18,15 +22,11 @@ module RubyLLM
|
|
|
18
22
|
format_document(attachment)
|
|
19
23
|
when :text
|
|
20
24
|
Protocols::ChatCompletions::Media.format_text_file(attachment)
|
|
21
|
-
else
|
|
22
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def format_document(attachment)
|
|
28
|
-
raise UnsupportedAttachmentError, attachment.mime_type unless supported_file?(attachment)
|
|
29
|
-
|
|
30
30
|
{
|
|
31
31
|
type: 'file_url',
|
|
32
32
|
file_url: {
|
|
@@ -34,12 +34,6 @@ module RubyLLM
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
end
|
|
37
|
-
|
|
38
|
-
def supported_file?(attachment)
|
|
39
|
-
return true if attachment.pdf?
|
|
40
|
-
|
|
41
|
-
SUPPORTED_DOCUMENT_EXTENSIONS.include?(attachment.extension)
|
|
42
|
-
end
|
|
43
37
|
end
|
|
44
38
|
end
|
|
45
39
|
end
|
|
@@ -11,12 +11,16 @@ module RubyLLM
|
|
|
11
11
|
sonar-reasoning-pro
|
|
12
12
|
sonar-deep-research
|
|
13
13
|
].freeze
|
|
14
|
+
PRESET_MODEL_IDS = Protocols::Perplexity::Agent::PRESETS
|
|
14
15
|
EMBEDDING_MODEL_IDS = %w[
|
|
15
16
|
pplx-embed-v1-0.6b
|
|
16
17
|
pplx-embed-v1-4b
|
|
17
18
|
].freeze
|
|
18
|
-
STATIC_MODEL_IDS = (SEARCH_MODEL_IDS + EMBEDDING_MODEL_IDS).freeze
|
|
19
|
-
|
|
19
|
+
STATIC_MODEL_IDS = (SEARCH_MODEL_IDS + PRESET_MODEL_IDS + EMBEDDING_MODEL_IDS).freeze
|
|
20
|
+
PRESET_MODEL_DATA = PRESET_MODEL_IDS.to_h do |id|
|
|
21
|
+
[id, { capabilities: %w[streaming structured_output citations function_calling] }]
|
|
22
|
+
end
|
|
23
|
+
STATIC_MODEL_DATA = PRESET_MODEL_DATA.merge(
|
|
20
24
|
'sonar' => {
|
|
21
25
|
context_window: 128_000, input_price: 1.0, output_price: 1.0,
|
|
22
26
|
capabilities: %w[streaming structured_output citations]
|
|
@@ -35,7 +39,7 @@ module RubyLLM
|
|
|
35
39
|
},
|
|
36
40
|
'pplx-embed-v1-0.6b' => { context_window: 32_768, input_price: 0.004 },
|
|
37
41
|
'pplx-embed-v1-4b' => { context_window: 32_768, input_price: 0.03 }
|
|
38
|
-
|
|
42
|
+
).freeze
|
|
39
43
|
|
|
40
44
|
def models_url
|
|
41
45
|
'v1/models'
|
|
@@ -49,7 +53,7 @@ module RubyLLM
|
|
|
49
53
|
end
|
|
50
54
|
|
|
51
55
|
# The models endpoint lists the multi-model catalog but not the search
|
|
52
|
-
# or embedding models, so those ride along statically.
|
|
56
|
+
# models, presets, or embedding models, so those ride along statically.
|
|
53
57
|
def parse_list_models_response(response, slug)
|
|
54
58
|
listed = Array(response.body['data']).map do |model_data|
|
|
55
59
|
create_model_info(model_data['id'], slug, pricing: endpoint_pricing(model_data['pricing']))
|
|
@@ -14,11 +14,22 @@ module RubyLLM
|
|
|
14
14
|
protocol :chat_completions, ChatCompletions
|
|
15
15
|
protocol :router_chat_completions, Protocols::Perplexity::Router
|
|
16
16
|
protocol :files, Protocols::Perplexity::Files
|
|
17
|
+
protocol :agent_responses, Protocols::Perplexity::Agent
|
|
17
18
|
|
|
18
19
|
def api_base
|
|
19
20
|
@config.perplexity_api_base || 'https://api.perplexity.ai'
|
|
20
21
|
end
|
|
21
22
|
|
|
23
|
+
# Chat runs on the Agent API. Embeddings and the model listing stay on
|
|
24
|
+
# the endpoints the Chat Completions dialect knows.
|
|
25
|
+
def protocol_for(model, operation: nil, **)
|
|
26
|
+
operation ? super : protocols[:agent_responses]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def agent_url # :nodoc:
|
|
30
|
+
"#{api_base.delete_suffix('/').delete_suffix('/v1')}/v1/agent"
|
|
31
|
+
end
|
|
32
|
+
|
|
22
33
|
def router_url(operation) # :nodoc:
|
|
23
34
|
"#{api_base.delete_suffix('/').delete_suffix('/router/v1')}/router/v1/#{operation}"
|
|
24
35
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# Connects TypeSafe's judgment models to RubyLLM.
|
|
6
|
+
class TypeSafe < Provider
|
|
7
|
+
protocol :system_one, Protocols::SystemOne
|
|
8
|
+
|
|
9
|
+
def api_base
|
|
10
|
+
@config.typesafe_api_base || 'https://api.typesafe.ai'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def headers
|
|
14
|
+
{ 'Authorization' => "Bearer #{@config.typesafe_api_key}" }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def parse_error(response)
|
|
18
|
+
protocols.fetch(:system_one).new(self).parse_error_response(response) || super
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def configuration_options
|
|
23
|
+
%i[typesafe_api_key typesafe_api_base]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configuration_requirements
|
|
27
|
+
%i[typesafe_api_key]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'stringio'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Providers
|
|
8
|
+
class VertexAI
|
|
9
|
+
# Google credentials shared by every Vertex AI provider in the process.
|
|
10
|
+
# googleauth keeps the access token on the credentials object and
|
|
11
|
+
# refreshes it a minute before it expires, so sharing the object shares
|
|
12
|
+
# the token.
|
|
13
|
+
class Credentials # :nodoc:
|
|
14
|
+
CACHE = Support::ProcessCache.new
|
|
15
|
+
|
|
16
|
+
attr_reader :authorizer
|
|
17
|
+
|
|
18
|
+
def self.for(config)
|
|
19
|
+
key = config.vertexai_service_account_key
|
|
20
|
+
CACHE.fetch([source(key), SCOPES]) { new(key) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.cache
|
|
24
|
+
CACHE
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.source(key)
|
|
28
|
+
return Digest::SHA256.hexdigest(key) if key
|
|
29
|
+
|
|
30
|
+
[:application_default, ENV.fetch('GOOGLE_APPLICATION_CREDENTIALS', nil)]
|
|
31
|
+
end
|
|
32
|
+
private_class_method :source
|
|
33
|
+
|
|
34
|
+
def initialize(service_account_key)
|
|
35
|
+
@authorizer = build(service_account_key)
|
|
36
|
+
@lock = Mutex.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# One caller fetches an expiring token while the others wait for it.
|
|
40
|
+
def headers
|
|
41
|
+
@lock.synchronize { @authorizer.apply({}) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build(service_account_key)
|
|
47
|
+
require 'googleauth'
|
|
48
|
+
if service_account_key
|
|
49
|
+
::Google::Auth::ServiceAccountCredentials.make_creds(
|
|
50
|
+
json_key_io: StringIO.new(service_account_key),
|
|
51
|
+
scope: SCOPES
|
|
52
|
+
)
|
|
53
|
+
else
|
|
54
|
+
::Google::Auth.get_application_default(SCOPES)
|
|
55
|
+
end
|
|
56
|
+
rescue LoadError
|
|
57
|
+
raise Error,
|
|
58
|
+
'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|