ruby_llm 2.1.0 → 2.2.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 +162 -120
- data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
- data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
- data/app/assets/javascripts/ruby_llm/marked.js +71 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
- data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
- data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
- data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
- data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
- data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
- data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
- data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
- data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
- data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
- data/lib/ruby_llm/accounting/operation.rb +73 -0
- data/lib/ruby_llm/accounting/usage.rb +74 -24
- data/lib/ruby_llm/active_record/acts_as.rb +78 -11
- data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
- data/lib/ruby_llm/active_record/batch.rb +67 -9
- data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
- data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
- data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
- data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
- data/lib/ruby_llm/active_record/chat_file.rb +37 -0
- data/lib/ruby_llm/active_record/chat_files.rb +30 -0
- data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
- data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
- data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
- data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
- data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
- data/lib/ruby_llm/active_record/chat_run.rb +51 -0
- data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
- data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
- data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
- data/lib/ruby_llm/active_record/file_store.rb +269 -0
- data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
- data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
- data/lib/ruby_llm/active_record/message_methods.rb +83 -11
- data/lib/ruby_llm/active_record/model.rb +37 -9
- data/lib/ruby_llm/active_record/provider_file.rb +21 -4
- data/lib/ruby_llm/active_record/queued_message.rb +50 -0
- data/lib/ruby_llm/active_record/record.rb +3 -1
- data/lib/ruby_llm/active_record/run_claims.rb +608 -0
- data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
- data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
- data/lib/ruby_llm/active_record/tool_call.rb +55 -4
- data/lib/ruby_llm/active_record/usage.rb +79 -9
- data/lib/ruby_llm/agent/callbacks.rb +98 -0
- data/lib/ruby_llm/agent/context_budget.rb +26 -0
- data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
- data/lib/ruby_llm/agent/model_options.rb +56 -0
- data/lib/ruby_llm/agent/ownership.rb +22 -0
- data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
- data/lib/ruby_llm/agent.rb +620 -236
- data/lib/ruby_llm/agent_tool.rb +91 -0
- data/lib/ruby_llm/aliases.json +84 -91
- data/lib/ruby_llm/attachment.rb +101 -20
- data/lib/ruby_llm/batch.rb +132 -46
- data/lib/ruby_llm/cached_content.rb +4 -4
- data/lib/ruby_llm/chat/agent_run.rb +22 -0
- data/lib/ruby_llm/chat/context_budget.rb +119 -0
- data/lib/ruby_llm/chat/file_references.rb +259 -0
- data/lib/ruby_llm/chat/message_queue.rb +56 -0
- data/lib/ruby_llm/chat/ownership.rb +18 -0
- data/lib/ruby_llm/chat/rescuable.rb +48 -0
- data/lib/ruby_llm/chat/run.rb +32 -0
- data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
- data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
- data/lib/ruby_llm/chat/tool_search.rb +36 -1
- data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
- data/lib/ruby_llm/chat.rb +1598 -332
- data/lib/ruby_llm/chunk.rb +27 -1
- data/lib/ruby_llm/citation.rb +2 -1
- data/lib/ruby_llm/code_execution/language.rb +28 -0
- data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
- data/lib/ruby_llm/code_execution/packages.rb +241 -0
- data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
- data/lib/ruby_llm/code_execution/workspace.rb +314 -0
- data/lib/ruby_llm/code_execution.rb +781 -0
- data/lib/ruby_llm/configuration.rb +151 -4
- data/lib/ruby_llm/content_filter.rb +68 -0
- data/lib/ruby_llm/cost.rb +68 -41
- data/lib/ruby_llm/downloaded_file.rb +10 -9
- data/lib/ruby_llm/embedding.rb +30 -45
- data/lib/ruby_llm/embedding_request.rb +1 -1
- data/lib/ruby_llm/error.rb +167 -14
- data/lib/ruby_llm/evaluation/dataset.rb +5 -4
- data/lib/ruby_llm/evaluation/evidence.rb +5 -3
- data/lib/ruby_llm/evaluation/report.rb +7 -3
- data/lib/ruby_llm/evaluation/result.rb +1 -1
- data/lib/ruby_llm/evaluation.rb +2 -1
- data/lib/ruby_llm/fallback.rb +32 -4
- data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
- data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
- data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
- data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
- data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
- data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
- data/lib/ruby_llm/file_store/converters/office.rb +106 -0
- data/lib/ruby_llm/file_store/converters/text.rb +27 -0
- data/lib/ruby_llm/file_store/converters.rb +84 -0
- data/lib/ruby_llm/file_store/directory.rb +190 -0
- data/lib/ruby_llm/file_store/memory.rb +75 -0
- data/lib/ruby_llm/file_store/tools/base.rb +99 -0
- data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
- data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
- data/lib/ruby_llm/file_store/tools/list.rb +57 -0
- data/lib/ruby_llm/file_store/tools/read.rb +120 -0
- data/lib/ruby_llm/file_store/tools/search.rb +208 -0
- data/lib/ruby_llm/file_store/tools/write.rb +40 -0
- data/lib/ruby_llm/file_store/tools.rb +39 -0
- data/lib/ruby_llm/file_store.rb +338 -0
- data/lib/ruby_llm/files/saving.rb +14 -0
- data/lib/ruby_llm/image.rb +67 -45
- data/lib/ruby_llm/input_required_error.rb +28 -0
- data/lib/ruby_llm/judge/builder.rb +6 -1
- data/lib/ruby_llm/judge.rb +1 -1
- data/lib/ruby_llm/judgment.rb +6 -24
- data/lib/ruby_llm/mcp/client.rb +67 -28
- data/lib/ruby_llm/mcp/collection.rb +39 -2
- data/lib/ruby_llm/mcp/content.rb +7 -2
- data/lib/ruby_llm/mcp/error.rb +8 -2
- data/lib/ruby_llm/mcp/http.rb +117 -37
- data/lib/ruby_llm/mcp/input_request.rb +3 -112
- data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
- data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
- data/lib/ruby_llm/mcp/listener.rb +7 -1
- data/lib/ruby_llm/mcp/oauth.rb +102 -39
- data/lib/ruby_llm/mcp/outages.rb +172 -0
- data/lib/ruby_llm/mcp/resource.rb +9 -6
- data/lib/ruby_llm/mcp/resource_template.rb +3 -2
- data/lib/ruby_llm/mcp/session_expired.rb +10 -2
- data/lib/ruby_llm/mcp/stdio.rb +66 -18
- data/lib/ruby_llm/mcp/task.rb +13 -40
- data/lib/ruby_llm/mcp/tool.rb +34 -11
- data/lib/ruby_llm/mcp.rb +123 -59
- data/lib/ruby_llm/message.rb +217 -23
- data/lib/ruby_llm/model/modalities.rb +1 -1
- data/lib/ruby_llm/model/pricing.rb +26 -4
- data/lib/ruby_llm/model/pricing_category.rb +38 -15
- data/lib/ruby_llm/model/pricing_tier.rb +3 -2
- data/lib/ruby_llm/model.rb +24 -6
- data/lib/ruby_llm/models/lookup.rb +37 -4
- data/lib/ruby_llm/models/registry.rb +4 -19
- data/lib/ruby_llm/models.json +36186 -27832
- data/lib/ruby_llm/models.rb +44 -39
- data/lib/ruby_llm/moderation.rb +10 -30
- data/lib/ruby_llm/ocr.rb +5 -22
- data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
- data/lib/ruby_llm/open_telemetry/content.rb +105 -0
- data/lib/ruby_llm/open_telemetry.rb +18 -4
- data/lib/ruby_llm/prompt/context.rb +2 -2
- data/lib/ruby_llm/prompt.rb +7 -5
- data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
- data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
- data/lib/ruby_llm/protocol/native_content.rb +69 -0
- data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
- data/lib/ruby_llm/protocol/streaming.rb +105 -22
- data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
- data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
- data/lib/ruby_llm/protocol.rb +134 -109
- data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
- data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
- data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
- data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
- data/lib/ruby_llm/protocols/anthropic.rb +61 -6
- data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
- data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
- data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
- data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
- data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
- data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
- data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
- data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
- data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
- data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
- data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
- data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
- data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
- data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
- data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
- data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
- data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
- data/lib/ruby_llm/protocols/converse/media.rb +24 -22
- data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
- data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
- data/lib/ruby_llm/protocols/converse.rb +17 -1
- data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
- data/lib/ruby_llm/protocols/deepgram.rb +3 -2
- data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
- data/lib/ruby_llm/protocols/files.rb +27 -12
- data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
- data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
- data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
- data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
- data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
- data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
- data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
- data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
- data/lib/ruby_llm/protocols/gemini.rb +16 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
- data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
- data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
- data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
- data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
- data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
- data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
- data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
- data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
- data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
- data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
- data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
- data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
- data/lib/ruby_llm/protocols/minimax.rb +21 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
- data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
- data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
- data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
- data/lib/ruby_llm/protocols/openai/files.rb +13 -6
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
- data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
- data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
- data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
- data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
- data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
- data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
- data/lib/ruby_llm/protocols/responses/media.rb +17 -9
- data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
- data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
- data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
- data/lib/ruby_llm/protocols/responses.rb +21 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
- data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
- data/lib/ruby_llm/protocols/xai/files.rb +13 -6
- data/lib/ruby_llm/protocols/zai/files.rb +37 -0
- data/lib/ruby_llm/provider.rb +128 -31
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +17 -3
- data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
- data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
- data/lib/ruby_llm/providers/azure/media.rb +6 -6
- data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
- data/lib/ruby_llm/providers/azure/models.rb +16 -2
- data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
- data/lib/ruby_llm/providers/azure/responses.rb +9 -0
- data/lib/ruby_llm/providers/azure/videos.rb +2 -2
- data/lib/ruby_llm/providers/azure.rb +43 -1
- data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
- data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
- data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
- data/lib/ruby_llm/providers/bedrock.rb +51 -8
- data/lib/ruby_llm/providers/cohere.rb +5 -1
- data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
- data/lib/ruby_llm/providers/deepgram.rb +21 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
- data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
- data/lib/ruby_llm/providers/gemini.rb +13 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
- data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
- data/lib/ruby_llm/providers/gpustack.rb +1 -1
- data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
- data/lib/ruby_llm/providers/hetzner.rb +5 -1
- data/lib/ruby_llm/providers/meta/responses.rb +81 -0
- data/lib/ruby_llm/providers/meta.rb +37 -0
- data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
- data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
- data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
- data/lib/ruby_llm/providers/minimax.rb +104 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
- data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
- data/lib/ruby_llm/providers/mistral/media.rb +8 -8
- data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
- data/lib/ruby_llm/providers/mistral.rb +13 -1
- data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
- data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
- data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
- data/lib/ruby_llm/providers/moonshot.rb +54 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
- data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -6
- data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
- data/lib/ruby_llm/providers/ollama.rb +13 -1
- data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
- data/lib/ruby_llm/providers/openai/responses.rb +8 -0
- data/lib/ruby_llm/providers/openai.rb +7 -3
- data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
- data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
- data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +32 -5
- data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
- data/lib/ruby_llm/providers/perplexity.rb +13 -2
- data/lib/ruby_llm/providers/typesafe.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
- data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +56 -9
- data/lib/ruby_llm/providers/xai/chat.rb +3 -3
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/xai/images.rb +9 -4
- data/lib/ruby_llm/providers/xai/models.rb +2 -2
- data/lib/ruby_llm/providers/xai/responses.rb +10 -0
- data/lib/ruby_llm/providers/xai/speech.rb +2 -2
- data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
- data/lib/ruby_llm/providers/xai/videos.rb +1 -1
- data/lib/ruby_llm/providers/xai.rb +9 -1
- data/lib/ruby_llm/providers/zai/chat.rb +117 -0
- data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
- data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
- data/lib/ruby_llm/providers/zai/videos.rb +67 -0
- data/lib/ruby_llm/providers/zai.rb +66 -0
- data/lib/ruby_llm/question.rb +390 -0
- data/lib/ruby_llm/queued_message.rb +74 -0
- data/lib/ruby_llm/railtie.rb +43 -0
- data/lib/ruby_llm/request_shape/part.rb +2 -2
- data/lib/ruby_llm/request_shape/problem.rb +2 -2
- data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
- data/lib/ruby_llm/request_shape/turn.rb +2 -2
- data/lib/ruby_llm/request_shape.rb +1 -1
- data/lib/ruby_llm/rerank.rb +10 -37
- data/lib/ruby_llm/research_job.rb +26 -13
- data/lib/ruby_llm/sandbox/landlock.rb +78 -0
- data/lib/ruby_llm/sandbox/mxc.rb +137 -0
- data/lib/ruby_llm/sandbox.rb +141 -0
- data/lib/ruby_llm/search_results.rb +9 -4
- data/lib/ruby_llm/server_tool_call.rb +1 -1
- data/lib/ruby_llm/skill/loader.rb +131 -0
- data/lib/ruby_llm/skill.rb +263 -0
- data/lib/ruby_llm/speech.rb +25 -53
- data/lib/ruby_llm/stored_file.rb +94 -0
- data/lib/ruby_llm/support/cancellation.rb +4 -4
- data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
- data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
- data/lib/ruby_llm/support/inspectable.rb +1 -1
- data/lib/ruby_llm/support/instrumentation.rb +6 -5
- data/lib/ruby_llm/support/rescuable.rb +84 -0
- data/lib/ruby_llm/support/utils.rb +26 -3
- data/lib/ruby_llm/task.rb +126 -0
- data/lib/ruby_llm/test_helper.rb +129 -0
- data/lib/ruby_llm/thinking.rb +3 -3
- data/lib/ruby_llm/tokens.rb +52 -12
- data/lib/ruby_llm/tool.rb +339 -5
- data/lib/ruby_llm/tool_call/collection.rb +132 -0
- data/lib/ruby_llm/tool_call.rb +68 -6
- data/lib/ruby_llm/tools/provider_tools.rb +23 -0
- data/lib/ruby_llm/tools/search.rb +118 -0
- data/lib/ruby_llm/transcription/segment.rb +67 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
- data/lib/ruby_llm/transcription.rb +25 -49
- data/lib/ruby_llm/transcription_chunk.rb +9 -9
- data/lib/ruby_llm/transport/connection.rb +146 -33
- data/lib/ruby_llm/transport/error_middleware.rb +94 -49
- data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
- data/lib/ruby_llm/transport/json_response.rb +9 -3
- data/lib/ruby_llm/transport/log_formatter.rb +20 -0
- data/lib/ruby_llm/transport/release_request.rb +35 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
- data/lib/ruby_llm/uploaded_file.rb +15 -6
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +34 -10
- data/lib/ruby_llm/video_job.rb +55 -42
- data/lib/ruby_llm/web_source.rb +38 -0
- data/lib/ruby_llm/workflow.rb +2 -2
- data/lib/ruby_llm.rb +63 -25
- data/lib/tasks/ruby_llm/evaluations.rb +1 -1
- data/skills/rubyllm/SKILL.md +128 -54
- data/skills/rubyllm/references/cost-and-tracing.md +8 -0
- data/skills/rubyllm/references/files-code-and-context.md +6 -0
- data/skills/rubyllm/references/operations.md +8 -0
- data/skills/rubyllm/references/rails-chats.md +22 -0
- data/skills/rubyllm/references/tools-and-agents.md +11 -0
- metadata +219 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Transport
|
|
7
|
+
# Sits outside everything on a connection without retries, so an error
|
|
8
|
+
# leaves it without the credentials of the request it answers.
|
|
9
|
+
class ReleaseRequest < Faraday::Middleware # :nodoc:
|
|
10
|
+
# The response outlives the call as Message#raw, Error#response, and
|
|
11
|
+
# other results, which applications log and report. The request holds
|
|
12
|
+
# the API key in its headers, the whole serialized conversation in its
|
|
13
|
+
# body, and the provider and signer in its context, and a streaming
|
|
14
|
+
# callback closes over the payload it was built from.
|
|
15
|
+
def self.release(response)
|
|
16
|
+
response.delete(:request) if response.is_a?(Hash)
|
|
17
|
+
env = response.respond_to?(:env) ? response.env : response
|
|
18
|
+
return response unless env.is_a?(Faraday::Env)
|
|
19
|
+
|
|
20
|
+
env.request_body = nil
|
|
21
|
+
env.request_headers = Faraday::Utils::Headers.new
|
|
22
|
+
env.request.on_data = nil
|
|
23
|
+
env.request.context = nil
|
|
24
|
+
response
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(env)
|
|
28
|
+
@app.call(env)
|
|
29
|
+
rescue Error, Faraday::Error => e
|
|
30
|
+
self.class.release(e.response)
|
|
31
|
+
raise
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'socket'
|
|
4
4
|
require 'openssl'
|
|
5
5
|
require 'io/wait'
|
|
6
|
+
require 'timeout'
|
|
6
7
|
|
|
7
8
|
module RubyLLM
|
|
8
9
|
module Transport # :nodoc:
|
|
@@ -27,9 +28,11 @@ module RubyLLM
|
|
|
27
28
|
raise ArgumentError, 'WebSocket URL must not contain credentials or a fragment'
|
|
28
29
|
end
|
|
29
30
|
raise ArgumentError, 'WebSocket connections do not support HTTP proxies' if config.http_proxy
|
|
31
|
+
raise ArgumentError, 'WebSocket connections do not support TLS options' if config.ssl
|
|
30
32
|
|
|
31
33
|
@headers = headers
|
|
32
34
|
@timeout = config.request_timeout
|
|
35
|
+
@connect_timeout = config.connect_timeout || @timeout
|
|
33
36
|
@messages = []
|
|
34
37
|
@write_lock = Mutex.new
|
|
35
38
|
end
|
|
@@ -41,7 +44,7 @@ module RubyLLM
|
|
|
41
44
|
configure_driver
|
|
42
45
|
@driver.start
|
|
43
46
|
read_frame(deadline) until @opened || @closed || @error
|
|
44
|
-
raise @error if @error
|
|
47
|
+
raise @error if @error && !@opened
|
|
45
48
|
raise Error, 'WebSocket closed before opening' unless @opened
|
|
46
49
|
|
|
47
50
|
self
|
|
@@ -63,26 +66,13 @@ module RubyLLM
|
|
|
63
66
|
def read(timeout: @timeout)
|
|
64
67
|
deadline = monotonic_time + timeout
|
|
65
68
|
read_frame(deadline) while @messages.empty? && !@closed && !@error
|
|
66
|
-
|
|
69
|
+
raise_unread_error
|
|
67
70
|
|
|
68
71
|
@messages.shift
|
|
69
72
|
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
70
|
-
raise unless @closed
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def read_available
|
|
74
|
-
ensure_open
|
|
75
|
-
until @messages.any? || @closed || @error
|
|
76
|
-
break unless read_available_frame
|
|
77
|
-
end
|
|
78
|
-
ensure_open
|
|
73
|
+
raise unless @closed || @messages.any?
|
|
79
74
|
|
|
80
75
|
@messages.shift
|
|
81
|
-
rescue EOFError, Errno::ECONNRESET
|
|
82
|
-
@closed = true
|
|
83
|
-
raise Error, 'WebSocket connection ended without a close frame'
|
|
84
|
-
rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
|
|
85
|
-
raise unless @closed
|
|
86
76
|
end
|
|
87
77
|
|
|
88
78
|
def each_message(write:)
|
|
@@ -130,16 +120,6 @@ module RubyLLM
|
|
|
130
120
|
|
|
131
121
|
private
|
|
132
122
|
|
|
133
|
-
def read_available_frame
|
|
134
|
-
data = @socket.read_nonblock(16_384, exception: false)
|
|
135
|
-
return if %i[wait_readable wait_writable].include?(data)
|
|
136
|
-
|
|
137
|
-
raise EOFError unless data
|
|
138
|
-
|
|
139
|
-
@driver.parse(data)
|
|
140
|
-
data
|
|
141
|
-
end
|
|
142
|
-
|
|
143
123
|
def finish_writer(writer)
|
|
144
124
|
raise Error, 'WebSocket closed before outgoing messages finished' unless writer.join(1)
|
|
145
125
|
end
|
|
@@ -158,8 +138,20 @@ module RubyLLM
|
|
|
158
138
|
|
|
159
139
|
def open_socket(deadline)
|
|
160
140
|
port = @uri.port || (@uri.scheme == 'wss' ? 443 : 80)
|
|
161
|
-
|
|
162
|
-
|
|
141
|
+
opened_by = [monotonic_time + @connect_timeout, deadline].min
|
|
142
|
+
@socket = connect_socket(port, opened_by)
|
|
143
|
+
secure_socket(opened_by) if @uri.scheme == 'wss'
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Socket.tcp times the lookup and each address on their own, so only
|
|
147
|
+
# the outer timeout holds the whole connect to one deadline.
|
|
148
|
+
def connect_socket(port, opened_by)
|
|
149
|
+
remaining = opened_by - monotonic_time
|
|
150
|
+
raise Error, 'WebSocket operation timed out' unless remaining.positive?
|
|
151
|
+
|
|
152
|
+
Timeout.timeout(remaining, Error, 'WebSocket operation timed out') do
|
|
153
|
+
Socket.tcp(@uri.hostname, port, connect_timeout: remaining, resolv_timeout: remaining)
|
|
154
|
+
end
|
|
163
155
|
end
|
|
164
156
|
|
|
165
157
|
def close_socket
|
|
@@ -176,11 +168,15 @@ module RubyLLM
|
|
|
176
168
|
context = OpenSSL::SSL::SSLContext.new
|
|
177
169
|
context.set_params
|
|
178
170
|
socket = OpenSSL::SSL::SSLSocket.new(@socket, context)
|
|
179
|
-
socket.hostname = @uri.
|
|
171
|
+
socket.hostname = @uri.hostname
|
|
180
172
|
socket.sync_close = true
|
|
181
173
|
@socket = socket
|
|
182
174
|
socket_operation(deadline) { @socket.connect_nonblock }
|
|
183
|
-
@socket.post_connection_check(@uri.
|
|
175
|
+
@socket.post_connection_check(@uri.hostname)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def raise_unread_error
|
|
179
|
+
raise @error if @error && @messages.empty?
|
|
184
180
|
end
|
|
185
181
|
|
|
186
182
|
def ensure_open
|
|
@@ -41,7 +41,9 @@ module RubyLLM
|
|
|
41
41
|
# The provider-reported processing status of the file.
|
|
42
42
|
attr_reader :status
|
|
43
43
|
|
|
44
|
-
# The MIME type of the stored file.
|
|
44
|
+
# The MIME type of the stored file. OpenAI, Azure, DeepSeek, Meta, and
|
|
45
|
+
# xAI do not report one: ::upload returns the content type it sent, and
|
|
46
|
+
# ::find returns +nil+.
|
|
45
47
|
attr_reader :mime_type
|
|
46
48
|
|
|
47
49
|
# The purpose the file was uploaded for, such as <tt>"batch"</tt>, when
|
|
@@ -91,16 +93,23 @@ module RubyLLM
|
|
|
91
93
|
# RubyLLM::UploadedFile.upload(io, provider: :openai, purpose: "batch",
|
|
92
94
|
# filename: "batch.jsonl")
|
|
93
95
|
#
|
|
94
|
-
# OpenAI and Azure require +purpose:+. Cohere uses +purpose:+ for its
|
|
96
|
+
# OpenAI, Meta, and Azure require +purpose:+. Cohere uses +purpose:+ for its
|
|
95
97
|
# dataset type, such as <tt>"embed-input"</tt>.
|
|
96
98
|
# Pass +expires_in:+ as a number of
|
|
97
99
|
# seconds to have the provider delete the file automatically; OpenAI,
|
|
98
|
-
# xAI, and Mistral support it, and Mistral rounds up to
|
|
100
|
+
# Meta, Anthropic, xAI, and Mistral support it, and Mistral rounds up to
|
|
101
|
+
# whole hours.
|
|
99
102
|
# Storage-backed providers (Vertex AI and Bedrock) store the file at
|
|
100
103
|
# +uri:+ when given, otherwise at a generated object name under the
|
|
101
|
-
# configured bucket.
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
+
# configured bucket. Providers that cannot carry +purpose:+,
|
|
105
|
+
# +expires_in:+, or +uri:+ raise ArgumentError. +content_type:+
|
|
106
|
+
# overrides the MIME type RubyLLM detects. +provider_options:+
|
|
107
|
+
# carries the rest in the provider's own vocabulary: multipart form
|
|
108
|
+
# fields on most providers, such as +visibility:+ on Mistral, File
|
|
109
|
+
# resource fields on Gemini, such as +display_name:+, PutObject keywords
|
|
110
|
+
# on Bedrock, and +create_file+ keywords on Vertex AI. RubyLLM sends them
|
|
111
|
+
# as given, so the provider rejects what it does not recognize. They
|
|
112
|
+
# cannot replace the file itself.
|
|
104
113
|
def self.upload(file, provider: nil, context: nil, filename: nil, purpose: nil, expires_in: nil,
|
|
105
114
|
uri: nil, content_type: nil, provider_options: {})
|
|
106
115
|
options = { filename:, purpose:, expires_in:, uri:, content_type: }.compact
|
data/lib/ruby_llm/version.rb
CHANGED
data/lib/ruby_llm/video.rb
CHANGED
|
@@ -9,6 +9,8 @@ module RubyLLM
|
|
|
9
9
|
#
|
|
10
10
|
class Video
|
|
11
11
|
include Support::Inspectable
|
|
12
|
+
include Files::Saving
|
|
13
|
+
include Accounting::Usage::Result
|
|
12
14
|
|
|
13
15
|
# The URL of the hosted video, for providers that return one, or +nil+.
|
|
14
16
|
attr_reader :url
|
|
@@ -50,9 +52,9 @@ module RubyLLM
|
|
|
50
52
|
# the request as-is. +context:+ supplies a Context whose configuration
|
|
51
53
|
# replaces the global one. +metadata:+ is included in the
|
|
52
54
|
# instrumentation payload. +owner:+ attributes the usage to a record,
|
|
53
|
-
# such as a user, and wins over RubyLLM.
|
|
55
|
+
# such as a user, and wins over RubyLLM.with_owner.
|
|
54
56
|
#
|
|
55
|
-
# video = RubyLLM.animate("a hummingbird in slow motion", model: "
|
|
57
|
+
# video = RubyLLM.animate("a hummingbird in slow motion", model: "gemini-omni-1.2-flash")
|
|
56
58
|
#
|
|
57
59
|
# RubyLLM.animate(
|
|
58
60
|
# "Make the waterfall crash down",
|
|
@@ -72,6 +74,7 @@ module RubyLLM
|
|
|
72
74
|
metadata: nil,
|
|
73
75
|
owner: nil)
|
|
74
76
|
config = context&.config || RubyLLM.config
|
|
77
|
+
model ||= config.default_video_model
|
|
75
78
|
payload = { model:, prompt:, provider_options:, metadata: }
|
|
76
79
|
|
|
77
80
|
RubyLLM.instrument('video.ruby_llm', payload, config: config) do |event|
|
|
@@ -104,23 +107,44 @@ module RubyLLM
|
|
|
104
107
|
@raw = raw
|
|
105
108
|
end
|
|
106
109
|
|
|
107
|
-
# Returns the
|
|
108
|
-
#
|
|
110
|
+
# Returns the usage the provider reported for the job that rendered
|
|
111
|
+
# the video.
|
|
112
|
+
def tokens
|
|
113
|
+
return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
|
|
114
|
+
|
|
115
|
+
Tokens.new
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Returns the Cost of the job that rendered the video: the amount the
|
|
119
|
+
# provider reported, or its reported tokens at registry prices. Its total
|
|
120
|
+
# is +nil+ when the provider reports neither a price nor priced tokens.
|
|
109
121
|
#
|
|
110
|
-
#
|
|
122
|
+
# video = RubyLLM.animate("a paper boat")
|
|
123
|
+
# video.cost.total # => 0.05
|
|
111
124
|
#
|
|
112
|
-
def
|
|
113
|
-
|
|
125
|
+
def cost
|
|
126
|
+
return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
|
|
127
|
+
|
|
128
|
+
Cost.new(tokens:)
|
|
114
129
|
end
|
|
115
130
|
|
|
131
|
+
##
|
|
132
|
+
# :method: save
|
|
133
|
+
# :call-seq:
|
|
134
|
+
# save(path)
|
|
135
|
+
#
|
|
116
136
|
# Writes the binary video to +path+, expanding it first. Returns
|
|
117
137
|
# +path+ as given.
|
|
118
138
|
#
|
|
119
139
|
# video.save("clip.mp4")
|
|
140
|
+
|
|
141
|
+
# Returns the raw binary video bytes, downloading from #url when the
|
|
142
|
+
# provider returned a hosted video.
|
|
120
143
|
#
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
144
|
+
# video_bytes = video.to_blob
|
|
145
|
+
#
|
|
146
|
+
def to_blob
|
|
147
|
+
data || Transport::Connection.download(url, config)
|
|
124
148
|
end
|
|
125
149
|
|
|
126
150
|
private
|
data/lib/ruby_llm/video_job.rb
CHANGED
|
@@ -11,6 +11,7 @@ module RubyLLM
|
|
|
11
11
|
#
|
|
12
12
|
class VideoJob
|
|
13
13
|
include Support::Inspectable
|
|
14
|
+
include Support::DeprecatedJobPredicates
|
|
14
15
|
|
|
15
16
|
# The provider's id for the job.
|
|
16
17
|
attr_reader :id
|
|
@@ -28,23 +29,23 @@ module RubyLLM
|
|
|
28
29
|
attr_reader :raw
|
|
29
30
|
|
|
30
31
|
# The clip length in seconds the request asked for, or +nil+ when it
|
|
31
|
-
# left the length to the provider's default.
|
|
32
|
+
# left the length to the provider's default or the job came from ::find.
|
|
32
33
|
attr_reader :duration
|
|
33
34
|
|
|
34
35
|
# The resolution the request asked for, as the provider spells it,
|
|
35
36
|
# such as <tt>"720p"</tt> or <tt>"1280x720"</tt>, or +nil+ when it left
|
|
36
|
-
# the resolution to the provider's default.
|
|
37
|
+
# the resolution to the provider's default or the job came from ::find.
|
|
37
38
|
attr_reader :resolution
|
|
38
39
|
|
|
39
40
|
# Submits a video generation job and returns a VideoJob without
|
|
40
41
|
# waiting for the result. Most code calls this through
|
|
41
42
|
# RubyLLM.animate_later. Takes the same arguments as Video.animate.
|
|
42
43
|
# The job records its usage, attributed to +owner:+ or the owner of
|
|
43
|
-
# RubyLLM.
|
|
44
|
+
# RubyLLM.with_owner, once it finishes.
|
|
44
45
|
#
|
|
45
46
|
# job = RubyLLM.animate_later("a hummingbird in slow motion")
|
|
46
|
-
# job.id
|
|
47
|
-
# job.
|
|
47
|
+
# job.id # => "0eb6910f-a353-4699-9d1e-6a4f7a5b39e2"
|
|
48
|
+
# job.complete? # => false
|
|
48
49
|
#
|
|
49
50
|
def self.animate_later(prompt = nil,
|
|
50
51
|
model: nil,
|
|
@@ -56,31 +57,32 @@ module RubyLLM
|
|
|
56
57
|
provider_options: {},
|
|
57
58
|
metadata: nil,
|
|
58
59
|
owner: nil)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
provider: provider_instance.slug,
|
|
65
|
-
provider_class: provider_instance.name,
|
|
66
|
-
model: model.id,
|
|
67
|
-
model_info: model,
|
|
68
|
-
prompt: prompt,
|
|
69
|
-
provider_options: provider_options,
|
|
70
|
-
metadata: metadata
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
RubyLLM.instrument('video_job.ruby_llm', payload, config: config) do |event|
|
|
74
|
-
job = Accounting::Usage.owned_by(owner) do
|
|
75
|
-
provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
|
|
76
|
-
end
|
|
77
|
-
event[:job_id] = job.id
|
|
78
|
-
job
|
|
60
|
+
Accounting::Operation.new(
|
|
61
|
+
'video_job', model, default: :default_video_model, usage: false, provider:, assume_model_exists:, context:,
|
|
62
|
+
owner:, report: ->(event, job) { event[:job_id] = job.id }
|
|
63
|
+
).run(prompt:, provider_options:, metadata:) do |provider_instance, model|
|
|
64
|
+
provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
|
|
79
65
|
end
|
|
80
66
|
end
|
|
81
67
|
|
|
68
|
+
# Returns a VideoJob reflecting the provider's current state for +id+.
|
|
69
|
+
# Use it to pick a job back up from any process. Pass the #model that
|
|
70
|
+
# rendered it: it selects the provider and protocol, and names the
|
|
71
|
+
# usage entry. A job already finished reports its usage here, once per
|
|
72
|
+
# VideoJob object, attributed to the owner of RubyLLM.with_owner.
|
|
73
|
+
#
|
|
74
|
+
# job = RubyLLM::VideoJob.find(clip.video_job_id, model: clip.video_model, provider: :xai)
|
|
75
|
+
# job.video&.save("clip.mp4")
|
|
76
|
+
#
|
|
77
|
+
# Pass +context:+ to use a Context in place of the global configuration.
|
|
78
|
+
def self.find(id, model:, provider: nil, assume_model_exists: false, context: nil)
|
|
79
|
+
config = context&.config || RubyLLM.config
|
|
80
|
+
model, provider_instance = Models.resolve(model, provider:, assume_model_exists:, config:)
|
|
81
|
+
provider_instance.find_video_job(id, model:)
|
|
82
|
+
end
|
|
83
|
+
|
|
82
84
|
def initialize(id:, protocol:, model: nil, status: :pending, raw: nil, error: nil, # :nodoc:
|
|
83
|
-
reported_cost: nil, duration: nil, resolution: nil)
|
|
85
|
+
reported_cost: nil, tokens: nil, duration: nil, resolution: nil)
|
|
84
86
|
@id = id
|
|
85
87
|
@protocol = protocol
|
|
86
88
|
@model = model
|
|
@@ -88,9 +90,11 @@ module RubyLLM
|
|
|
88
90
|
@raw = raw
|
|
89
91
|
@error = error
|
|
90
92
|
@reported_cost = reported_cost
|
|
93
|
+
@tokens = tokens
|
|
91
94
|
@duration = duration
|
|
92
95
|
@resolution = resolution
|
|
93
96
|
@usage_owner = Accounting::Usage.owner
|
|
97
|
+
@usage_workflow = Accounting::Usage.workflow
|
|
94
98
|
record_usage
|
|
95
99
|
end
|
|
96
100
|
|
|
@@ -100,12 +104,12 @@ module RubyLLM
|
|
|
100
104
|
end
|
|
101
105
|
|
|
102
106
|
# Returns +true+ once the job finished, successfully or not.
|
|
103
|
-
def
|
|
107
|
+
def complete?
|
|
104
108
|
!pending?
|
|
105
109
|
end
|
|
106
110
|
|
|
107
111
|
# Returns +true+ when the job finished with a video.
|
|
108
|
-
def
|
|
112
|
+
def succeeded?
|
|
109
113
|
status == :completed
|
|
110
114
|
end
|
|
111
115
|
|
|
@@ -116,28 +120,29 @@ module RubyLLM
|
|
|
116
120
|
end
|
|
117
121
|
|
|
118
122
|
# Re-fetches the job from the provider, updating #status, #error, and
|
|
119
|
-
# #raw. Does nothing once the job is #
|
|
123
|
+
# #raw. Does nothing once the job is #complete?. Returns self.
|
|
120
124
|
def refresh
|
|
121
|
-
return self if
|
|
125
|
+
return self if complete?
|
|
122
126
|
|
|
123
127
|
state = @protocol.refresh_video_job(self)
|
|
124
128
|
@status = state.fetch(:status)
|
|
125
129
|
@raw = state[:raw]
|
|
126
130
|
@error = state[:error]
|
|
127
131
|
@reported_cost = state[:reported_cost]
|
|
132
|
+
@tokens = state[:tokens]
|
|
128
133
|
record_usage
|
|
129
134
|
self
|
|
130
135
|
end
|
|
131
136
|
|
|
132
|
-
# Returns the Cost the
|
|
133
|
-
# +nil+ while the job is
|
|
134
|
-
# price.
|
|
137
|
+
# Returns the Cost of the job: the amount the provider reported, or its
|
|
138
|
+
# reported tokens at registry prices. Its total is +nil+ while the job is
|
|
139
|
+
# pending, or when the provider reports neither a price nor priced tokens.
|
|
135
140
|
#
|
|
136
141
|
# job.wait
|
|
137
142
|
# job.cost.total # => 0.05
|
|
138
143
|
#
|
|
139
144
|
def cost
|
|
140
|
-
Cost.new(tokens:
|
|
145
|
+
Cost.new(tokens: usage_tokens, model: (@protocol.model if @tokens))
|
|
141
146
|
end
|
|
142
147
|
|
|
143
148
|
# Polls the job until it finishes, then returns self. Raises Error
|
|
@@ -149,7 +154,7 @@ module RubyLLM
|
|
|
149
154
|
interval ||= @protocol.config.video_generation_poll_interval
|
|
150
155
|
deadline = monotonic_time + timeout
|
|
151
156
|
|
|
152
|
-
until
|
|
157
|
+
until complete?
|
|
153
158
|
remaining = deadline - monotonic_time
|
|
154
159
|
raise Error, "Video generation timed out after #{timeout} seconds" unless remaining.positive?
|
|
155
160
|
|
|
@@ -166,20 +171,28 @@ module RubyLLM
|
|
|
166
171
|
# Error when #failed?.
|
|
167
172
|
def video
|
|
168
173
|
raise Error, "Video generation failed: #{error}" if failed?
|
|
169
|
-
return unless
|
|
174
|
+
return unless succeeded?
|
|
170
175
|
|
|
171
|
-
@video ||= @protocol.download_video(self).tap
|
|
176
|
+
@video ||= @protocol.download_video(self).tap do |video|
|
|
177
|
+
video.config = @protocol.config
|
|
178
|
+
video.ruby_llm_usage_entries = @usage_entry
|
|
179
|
+
end
|
|
172
180
|
end
|
|
173
181
|
|
|
174
182
|
private
|
|
175
183
|
|
|
176
184
|
def record_usage
|
|
177
|
-
return if pending? || @
|
|
185
|
+
return if pending? || @usage_entry
|
|
186
|
+
|
|
187
|
+
@usage_entry = Accounting::Usage::Entry.new(
|
|
188
|
+
operation: :video, provider: @protocol.provider.slug, model:, status: succeeded? ? :succeeded : :failed,
|
|
189
|
+
tokens: usage_tokens, cost:, owner: @usage_owner, workflow: @usage_workflow
|
|
190
|
+
)
|
|
191
|
+
Accounting::Usage.report(@usage_entry, config: @protocol.config)
|
|
192
|
+
end
|
|
178
193
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
status: completed? ? :succeeded : :failed, cost:, owner: @usage_owner)
|
|
182
|
-
Accounting::Usage.report(entry, config: @protocol.config)
|
|
194
|
+
def usage_tokens
|
|
195
|
+
@tokens || Tokens.new(reported_cost: @reported_cost)
|
|
183
196
|
end
|
|
184
197
|
|
|
185
198
|
def monotonic_time
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A WebSource is a page a hosted web search returned. They appear on
|
|
5
|
+
# Message#web_sources, normalized across providers. A Citation is different:
|
|
6
|
+
# it links a span of the answer to what supports it, so a search usually
|
|
7
|
+
# has more sources than citations.
|
|
8
|
+
#
|
|
9
|
+
# response = chat.with_provider_tools(:web_search).ask "What changed in Ruby 4.0?"
|
|
10
|
+
# response.web_sources.map(&:url) # => ["https://www.ruby-lang.org/en/news/", ...]
|
|
11
|
+
#
|
|
12
|
+
# An address here comes from the outside world. See Message#web_sources
|
|
13
|
+
# before treating one as safe to open.
|
|
14
|
+
#
|
|
15
|
+
class WebSource
|
|
16
|
+
include Support::Inspectable
|
|
17
|
+
|
|
18
|
+
# The address the provider reports for the page.
|
|
19
|
+
attr_reader :url
|
|
20
|
+
|
|
21
|
+
# The title the provider reports for the page, or +nil+.
|
|
22
|
+
attr_reader :title
|
|
23
|
+
|
|
24
|
+
def initialize(url:, title: nil) # :nodoc:
|
|
25
|
+
@url = url
|
|
26
|
+
@title = title
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the source as a Hash with symbol keys, omitting a +nil+ title.
|
|
30
|
+
def to_h
|
|
31
|
+
{ url: url, title: title }.compact
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect_attributes # :nodoc:
|
|
35
|
+
{ url: url, title: title }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/ruby_llm/workflow.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
# Adds workflow and step context to RubyLLM instrumentation
|
|
5
|
-
# orchestration in ordinary Ruby code.
|
|
4
|
+
# Adds workflow and step context to RubyLLM instrumentation and usage
|
|
5
|
+
# records while leaving orchestration in ordinary Ruby code.
|
|
6
6
|
#
|
|
7
7
|
# Create workflows through RubyLLM.workflow rather than constructing this
|
|
8
8
|
# class directly:
|