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,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Generators
|
|
7
|
+
# Generator for sandbox classes that code execution runs model code in.
|
|
8
|
+
class SandboxGenerator < Rails::Generators::NamedBase
|
|
9
|
+
INITIALIZER = 'config/initializers/ruby_llm.rb'
|
|
10
|
+
CONFIGURE_BLOCK = /^RubyLLM\.configure do \|config\|\n/
|
|
11
|
+
MXC_REQUIREMENT = '>= 0.4.1'
|
|
12
|
+
|
|
13
|
+
source_root File.expand_path('templates', __dir__)
|
|
14
|
+
|
|
15
|
+
namespace 'ruby_llm:sandbox'
|
|
16
|
+
|
|
17
|
+
desc 'Creates a sandbox class for code execution in app/sandboxes'
|
|
18
|
+
|
|
19
|
+
def check_sandbox_class_collision
|
|
20
|
+
class_collisions sandbox_class_name
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def create_sandbox_file
|
|
24
|
+
template 'sandbox.rb.tt', File.join('app/sandboxes', class_path, "#{sandbox_file_name}.rb")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def add_mxc_gem
|
|
28
|
+
gemfile = File.join(destination_root, 'Gemfile')
|
|
29
|
+
return unless File.exist?(gemfile)
|
|
30
|
+
|
|
31
|
+
existing = File.read(gemfile)[/^\s*gem\s+["']mxc["'].*$/]
|
|
32
|
+
return warn_about_mxc_constraint(existing) if existing
|
|
33
|
+
|
|
34
|
+
gem 'mxc', MXC_REQUIREMENT
|
|
35
|
+
say "\nRun bundle install" if behavior == :invoke
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def default_to_new_sandbox
|
|
39
|
+
initializer = File.join(destination_root, INITIALIZER)
|
|
40
|
+
return unless File.exist?(initializer)
|
|
41
|
+
return if behavior == :invoke && File.read(initializer).match?(/^\s*config\.sandbox\s*(\|\|)?=/)
|
|
42
|
+
|
|
43
|
+
unless File.read(initializer).match?(CONFIGURE_BLOCK)
|
|
44
|
+
return say %(Set config.sandbox = "#{sandbox_class_name}" where you configure RubyLLM), :yellow
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
inject_into_file INITIALIZER, %( config.sandbox = "#{sandbox_class_name}"\n), after: CONFIGURE_BLOCK
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def warn_about_mxc_constraint(line)
|
|
53
|
+
requirement = line[/["']mxc["']\s*,\s*["']([^"']+)["']/, 1]
|
|
54
|
+
return if requirement.nil? || Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new('0.4.1'))
|
|
55
|
+
|
|
56
|
+
say %(Change the mxc requirement in your Gemfile to "#{MXC_REQUIREMENT}"), :yellow
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def sandbox_class_name
|
|
60
|
+
class_name.end_with?('Sandbox') ? class_name : "#{class_name}Sandbox"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def sandbox_file_name
|
|
64
|
+
sandbox_class_name.demodulize.underscore
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'rails/generators'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
module Generators
|
|
9
|
+
# Generator for skills in the open Agent Skills format.
|
|
10
|
+
class SkillGenerator < Rails::Generators::Base
|
|
11
|
+
namespace 'ruby_llm:skill'
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
argument :skill_name, type: :string, banner: 'name'
|
|
18
|
+
argument :description, type: :string, banner: '"What it does and when to use it"'
|
|
19
|
+
|
|
20
|
+
desc 'Creates a skill folder with its SKILL.md in app/skills'
|
|
21
|
+
|
|
22
|
+
def check_skill
|
|
23
|
+
Dir.mktmpdir do |root|
|
|
24
|
+
folder = File.join(root, skill_name)
|
|
25
|
+
FileUtils.mkdir_p(folder)
|
|
26
|
+
File.write(File.join(folder, 'SKILL.md'), skill_markdown)
|
|
27
|
+
RubyLLM::Skill.new(folder)
|
|
28
|
+
end
|
|
29
|
+
rescue RubyLLM::InvalidSkillError => e
|
|
30
|
+
raise Thor::Error, e.message.sub(%r{\A\S*/SKILL\.md }, '')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_skill_file
|
|
34
|
+
create_file File.join(skill_path, 'SKILL.md'), skill_markdown
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def skill_path
|
|
40
|
+
File.join('app/skills', skill_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def skill_markdown
|
|
44
|
+
"---\nname: #{skill_name}\ndescription: #{description.to_json}\n---\n"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div id="<%= message_variable_name %>_tool_call_<%%= tool_call.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
|
|
2
2
|
<div>
|
|
3
3
|
<span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
|
|
4
|
-
<%=
|
|
4
|
+
<%= tool_partial_name.humanize %> call
|
|
5
5
|
</span>
|
|
6
6
|
</div>
|
|
7
7
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<div id="<%= message_variable_name %>_<%%= tool.id %>" class="w-full sm:w-auto my-5 space-y-3 rounded-md px-3 py-2 bg-gray-50 border border-gray-200">
|
|
9
9
|
<div>
|
|
10
10
|
<span class="inline-block rounded px-2 py-0.5 text-xs font-medium bg-gray-200 text-gray-700">
|
|
11
|
-
<%=
|
|
11
|
+
<%= tool_partial_name.humanize %> result
|
|
12
12
|
</span>
|
|
13
13
|
</div>
|
|
14
14
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div id="<%= message_variable_name %>_tool_call_<%%= tool_call.id %>" class="<%= message_variable_name %>"
|
|
2
2
|
style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
|
|
3
|
-
<div style="font-weight: bold; margin-bottom: 5px;"><%=
|
|
3
|
+
<div style="font-weight: bold; margin-bottom: 5px;"><%= tool_partial_name.humanize %> call</div>
|
|
4
4
|
<pre style="white-space: pre-wrap; margin: 0;"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
|
|
5
5
|
<div style="font-size: 0.85em; color: #666; margin-top: 5px;">
|
|
6
6
|
<%%= message.created_at&.strftime("%I:%M %p") %>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<%% else %>
|
|
8
8
|
<div id="<%= message_variable_name %>_<%%= tool.id %>" class="<%= message_variable_name %>"
|
|
9
9
|
style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
|
|
10
|
-
<div style="font-weight: bold; margin-bottom: 5px;"><%=
|
|
10
|
+
<div style="font-weight: bold; margin-bottom: 5px;"><%= tool_partial_name.humanize %> result</div>
|
|
11
11
|
<pre style="white-space: pre-wrap; margin: 0;"><%%= tool.content.presence || "(no output)" %></pre>
|
|
12
12
|
<div style="font-size: 0.85em; color: #666; margin-top: 5px;">
|
|
13
13
|
<%%= tool.created_at&.strftime("%I:%M %p") %>
|
|
@@ -49,13 +49,9 @@ module RubyLLM
|
|
|
49
49
|
tool_class_name.demodulize.underscore
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def tool_display_name
|
|
53
|
-
tool_class_name.demodulize.delete_suffix('Tool')
|
|
54
|
-
end
|
|
55
|
-
|
|
56
52
|
# Mirrors RubyLLM::Tool.tool_name, the key the runtime partial lookup uses.
|
|
57
53
|
def tool_partial_name
|
|
58
|
-
|
|
54
|
+
RubyLLM::Support::Utils.underscore(tool_class_name).delete_suffix('_tool').gsub('::', '__')
|
|
59
55
|
end
|
|
60
56
|
end
|
|
61
57
|
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
class <%= 'upgrade_ruby_llm_to_2_2'.camelize %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
unless column_exists?(:ruby_llm_tool_calls, :provider_call_id)
|
|
4
|
+
if column_exists?(:ruby_llm_tool_calls, :provider_id)
|
|
5
|
+
rename_column :ruby_llm_tool_calls, :provider_id, :provider_call_id
|
|
6
|
+
else
|
|
7
|
+
add_column :ruby_llm_tool_calls, :provider_call_id, :string
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
add_column :ruby_llm_tool_calls, :namespace, :string unless column_exists?(:ruby_llm_tool_calls, :namespace)
|
|
12
|
+
|
|
13
|
+
unless column_exists?(:<%= chat_table_name %>, :claim_token)
|
|
14
|
+
add_column :<%= chat_table_name %>, :claim_token, :string
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless column_exists?(:<%= chat_table_name %>, :claimed_until)
|
|
18
|
+
add_column :<%= chat_table_name %>, :claimed_until, :datetime
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
unless index_exists?(:<%= chat_table_name %>, :claimed_until)
|
|
22
|
+
add_index :<%= chat_table_name %>, :claimed_until<%= ', where: "claim_token IS NOT NULL"' unless mysql? %>
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
unless column_exists?(:ruby_llm_tool_calls, :claimed_until)
|
|
26
|
+
add_column :ruby_llm_tool_calls, :claimed_until, :datetime
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
unless column_exists?(:<%= message_table_name %>, :phase)
|
|
30
|
+
add_column :<%= message_table_name %>, :phase, :string
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
unless column_exists?(:<%= message_table_name %>, :continuation)
|
|
34
|
+
add_column :<%= message_table_name %>, :continuation, :text<%= ', size: :long' if mysql? %>
|
|
35
|
+
end
|
|
36
|
+
<% if mysql? -%>
|
|
37
|
+
|
|
38
|
+
message_columns = %i[content thinking_text thinking_signature].select do |column|
|
|
39
|
+
column_exists?(:<%= message_table_name %>, column)
|
|
40
|
+
end
|
|
41
|
+
if message_columns.any?
|
|
42
|
+
change_table :<%= message_table_name %>, bulk: true do |t|
|
|
43
|
+
message_columns.each { |column| t.change column, :text, size: :long }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
change_column :ruby_llm_tool_calls, :thought_signature, :text, size: :long
|
|
47
|
+
<% end -%>
|
|
48
|
+
|
|
49
|
+
%i[workflow_id workflow_name workflow_step_id workflow_step_name].each do |column|
|
|
50
|
+
add_column :ruby_llm_usages, column, :string unless column_exists?(:ruby_llm_usages, column)
|
|
51
|
+
end
|
|
52
|
+
add_index :ruby_llm_usages, :workflow_id unless index_exists?(:ruby_llm_usages, :workflow_id)
|
|
53
|
+
|
|
54
|
+
add_column :ruby_llm_usages, :characters, :integer unless column_exists?(:ruby_llm_usages, :characters)
|
|
55
|
+
unless column_exists?(:ruby_llm_usages, :audio_seconds)
|
|
56
|
+
add_column :ruby_llm_usages, :audio_seconds, :decimal, precision: 16, scale: 6
|
|
57
|
+
end
|
|
58
|
+
add_column :ruby_llm_usages, :search_units, :integer unless column_exists?(:ruby_llm_usages, :search_units)
|
|
59
|
+
%i[characters_cost audio_seconds_cost search_units_cost].each do |column|
|
|
60
|
+
unless column_exists?(:ruby_llm_usages, column)
|
|
61
|
+
add_column :ruby_llm_usages, column, :decimal, precision: 16, scale: 10
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
unless table_exists?(:ruby_llm_queued_messages)
|
|
66
|
+
create_table :ruby_llm_queued_messages, id: :<%= reference_type %> do |t|
|
|
67
|
+
t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
|
|
68
|
+
t.references :sender, polymorphic: true, type: :<%= reference_type %>, index: false
|
|
69
|
+
t.text :content<%= ', size: :long' if mysql? %>
|
|
70
|
+
t.boolean :steer, default: true, null: false
|
|
71
|
+
t.timestamps
|
|
72
|
+
|
|
73
|
+
t.index [ :chat_type, :chat_id ]
|
|
74
|
+
t.index [ :sender_type, :sender_id ]
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless table_exists?(:ruby_llm_chat_files)
|
|
79
|
+
create_table :ruby_llm_chat_files, id: :<%= reference_type %> do |t|
|
|
80
|
+
t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
|
|
81
|
+
t.references :owner, polymorphic: true, type: :<%= reference_type %>, index: false
|
|
82
|
+
t.string :path, null: false
|
|
83
|
+
t.string :content_type
|
|
84
|
+
t.bigint :byte_size
|
|
85
|
+
t.string :digest
|
|
86
|
+
t.text :text<%= ', size: :long' if mysql? %>
|
|
87
|
+
t.string :read_digest
|
|
88
|
+
t.timestamps
|
|
89
|
+
|
|
90
|
+
t.index [ :chat_type, :chat_id, :path ]
|
|
91
|
+
t.index [ :owner_type, :owner_id ]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
unless column_exists?(:ruby_llm_chat_files, :read_digest)
|
|
95
|
+
add_column :ruby_llm_chat_files, :read_digest, :string
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
allow_code_execution_usage unless usage_accepts_code_execution?
|
|
99
|
+
# Asks again: a check of the application's may still refuse the row.
|
|
100
|
+
warn_about_code_execution_usage unless usage_accepts_code_execution?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def down
|
|
104
|
+
drop_table :ruby_llm_chat_files, if_exists: true
|
|
105
|
+
drop_table :ruby_llm_queued_messages, if_exists: true
|
|
106
|
+
remove_column :<%= message_table_name %>, :continuation
|
|
107
|
+
%i[search_units_cost audio_seconds_cost characters_cost search_units audio_seconds characters].each do |column|
|
|
108
|
+
remove_column :ruby_llm_usages, column
|
|
109
|
+
end
|
|
110
|
+
remove_index :ruby_llm_usages, :workflow_id, if_exists: true
|
|
111
|
+
remove_column :ruby_llm_usages, :workflow_step_name
|
|
112
|
+
remove_column :ruby_llm_usages, :workflow_step_id
|
|
113
|
+
remove_column :ruby_llm_usages, :workflow_name
|
|
114
|
+
remove_column :ruby_llm_usages, :workflow_id
|
|
115
|
+
remove_column :<%= message_table_name %>, :phase
|
|
116
|
+
remove_column :ruby_llm_tool_calls, :namespace
|
|
117
|
+
remove_column :ruby_llm_tool_calls, :claimed_until
|
|
118
|
+
remove_index :<%= chat_table_name %>, :claimed_until, if_exists: true
|
|
119
|
+
remove_column :<%= chat_table_name %>, :claimed_until
|
|
120
|
+
remove_column :<%= chat_table_name %>, :claim_token
|
|
121
|
+
remove_column :ruby_llm_tool_calls, :provider_call_id
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Asks the database, since each one prints a check differently: a check
|
|
127
|
+
# refusing the row leaves code runs unmetered, with only a warning at run time.
|
|
128
|
+
def usage_accepts_code_execution?
|
|
129
|
+
connection.transaction(requires_new: true) do
|
|
130
|
+
execute <<~SQL.squish
|
|
131
|
+
INSERT INTO ruby_llm_usages (operation, provider, model, status, created_at, updated_at)
|
|
132
|
+
VALUES ('code_execution', 'sandbox', 'upgrade check', 'succeeded', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
|
133
|
+
SQL
|
|
134
|
+
raise ActiveRecord::Rollback
|
|
135
|
+
end
|
|
136
|
+
true
|
|
137
|
+
rescue ActiveRecord::StatementInvalid
|
|
138
|
+
false
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def allow_code_execution_usage
|
|
142
|
+
lists = check_constraints(:ruby_llm_usages).select { |check| operation_list?(check.expression) }
|
|
143
|
+
lists.each { |check| remove_check_constraint :ruby_llm_usages, name: check.name }
|
|
144
|
+
return if lists.empty?
|
|
145
|
+
|
|
146
|
+
add_check_constraint :ruby_llm_usages, "operation<%= '::text' if postgresql? %> IN (<%= usage_operations_sql %>)"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The list the install generator wrote, as each database prints it. A check
|
|
150
|
+
# with other conditions is the application's, and stays.
|
|
151
|
+
def operation_list?(expression)
|
|
152
|
+
names = expression.scan(/'(\w+)'/).flatten
|
|
153
|
+
expression.match?(/\A\(*[`"]?operation[`"]?\)*(?:::\w+)?\s*(?:in\b|=\s*any\b)/i) &&
|
|
154
|
+
!expression.match?(/\b(?:and|or|not)\b|<>|!=/i) && names.size > 1 && (names - %w[<%= ::RubyLLM::Accounting::Usage::Entry::OPERATIONS.join(' ') %>]).empty?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def warn_about_code_execution_usage
|
|
158
|
+
checks = check_constraints(:ruby_llm_usages).select { |check| check.expression.match?(/\boperation\b/) }
|
|
159
|
+
.map(&:name).join(', ')
|
|
160
|
+
say "RubyLLM: ruby_llm_usages refuses 'code_execution' rows, so code runs will go unmetered with only a " \
|
|
161
|
+
"warning in the log. Check these constraints, or any NOT NULL column you added: #{checks}. Allow the " \
|
|
162
|
+
'value in a new migration: see "Code Runs Write Usage Rows" in the upgrading guide.'
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -6,7 +6,7 @@ require_relative '../generator_helpers'
|
|
|
6
6
|
|
|
7
7
|
module RubyLLM
|
|
8
8
|
module Generators
|
|
9
|
-
# Upgrades the schema of an application on RubyLLM 2.
|
|
9
|
+
# Upgrades the schema of an application on RubyLLM 2.1 to 2.2.
|
|
10
10
|
class UpgradeGenerator < Rails::Generators::Base
|
|
11
11
|
include Rails::Generators::Migration
|
|
12
12
|
include RubyLLM::Generators::GeneratorHelpers
|
|
@@ -16,7 +16,7 @@ module RubyLLM
|
|
|
16
16
|
|
|
17
17
|
argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName'
|
|
18
18
|
|
|
19
|
-
desc
|
|
19
|
+
desc "Upgrades a RubyLLM 2.1 Rails schema to 2.2\n" \
|
|
20
20
|
'Usage: bin/rails g ruby_llm:upgrade [chat:ChatName] [message:MessageName]'
|
|
21
21
|
|
|
22
22
|
def self.next_migration_number(dirname)
|
|
@@ -24,7 +24,45 @@ module RubyLLM
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def create_migration_file
|
|
27
|
-
migration_template '
|
|
27
|
+
migration_template 'upgrade_ruby_llm_to_2_2.rb.tt', 'db/migrate/upgrade_ruby_llm_to_2_2.rb'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def create_application_agent_file
|
|
31
|
+
create_application_agent
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def remove_generated_message_code_from_message_model
|
|
35
|
+
return if chat_ui_uses_generated_broadcasting?
|
|
36
|
+
|
|
37
|
+
enable_message_broadcasting if remove_generated_message_code
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def install_active_storage
|
|
41
|
+
return if table_exists?('active_storage_blobs')
|
|
42
|
+
|
|
43
|
+
rails_command 'active_storage:install'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def show_next_step
|
|
47
|
+
say "\nRun bin/rails db:migrate" if behavior == :invoke
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def chat_ui_uses_generated_broadcasting?
|
|
53
|
+
return false unless behavior == :invoke
|
|
54
|
+
|
|
55
|
+
model = File.join(destination_root, "app/models/#{message_model_name.underscore}.rb")
|
|
56
|
+
return false unless File.exist?(model)
|
|
57
|
+
|
|
58
|
+
source = File.read(model)
|
|
59
|
+
return false unless generated_broadcasting_blocks.any? { |block| source.include?(block) }
|
|
60
|
+
|
|
61
|
+
stream = %("#{chat_variable_name}_\#{)
|
|
62
|
+
Dir.glob(File.join(destination_root, 'app/**/*.{rb,erb}')).without(model).any? do |path|
|
|
63
|
+
source = File.read(path)
|
|
64
|
+
source.include?('broadcast_append_chunk') || source.include?(stream)
|
|
65
|
+
end
|
|
28
66
|
end
|
|
29
67
|
end
|
|
30
68
|
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Accounting
|
|
5
|
+
# The body the standalone operations share: resolve the model, open the
|
|
6
|
+
# instrumentation event, attribute the provider call to its owner, and
|
|
7
|
+
# report the result on the event. +run+ takes the event fields particular
|
|
8
|
+
# to the operation, in event order, and yields the provider and the
|
|
9
|
+
# resolved model.
|
|
10
|
+
#
|
|
11
|
+
# +details:+ adds fields after the response model. +report:+ replaces the
|
|
12
|
+
# reporting of an operation whose result does not read like the others.
|
|
13
|
+
# +usage: false+ leaves tokens and cost off the event. +resolved:+ takes
|
|
14
|
+
# a Model already resolved, such as a fallback given as one, and skips
|
|
15
|
+
# the lookup.
|
|
16
|
+
class Operation # :nodoc: all
|
|
17
|
+
def initialize(name, model, owner:, context: nil, provider: nil, assume_model_exists: false,
|
|
18
|
+
default: nil, operation: nil, category: nil, usage: true, details: nil, report: nil,
|
|
19
|
+
resolved: nil)
|
|
20
|
+
@name = name
|
|
21
|
+
@owner = owner
|
|
22
|
+
@category = category
|
|
23
|
+
@usage = usage
|
|
24
|
+
@details = details
|
|
25
|
+
@report = report
|
|
26
|
+
@config = context&.config || RubyLLM.config
|
|
27
|
+
@model, @provider = if resolved
|
|
28
|
+
[resolved, Provider.resolve!(resolved.provider).new(@config)]
|
|
29
|
+
else
|
|
30
|
+
resolve(model, default, operation, provider:, assume_model_exists:)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def run(**fields)
|
|
35
|
+
RubyLLM.instrument("#{@name}.ruby_llm", payload(fields), config: @config) do |event|
|
|
36
|
+
result = Usage.owned_by(@owner) { yield @provider, @model }
|
|
37
|
+
report(event, result)
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def resolve(model, default, operation, **options)
|
|
45
|
+
default_model = default && @config.public_send(default)
|
|
46
|
+
options[:config] = @config
|
|
47
|
+
return Models.resolve(model, **options, operation:, default_model:) if operation
|
|
48
|
+
|
|
49
|
+
Models.resolve(model || default_model, **options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def payload(fields)
|
|
53
|
+
common = { provider: @provider.slug, provider_class: @provider.name, model: @model&.id, model_info: @model }
|
|
54
|
+
return common.merge(fields) unless @usage
|
|
55
|
+
|
|
56
|
+
tokens = Tokens.new
|
|
57
|
+
common.merge(fields, tokens:, cost: Cost.new(tokens:, model: @model, **{ category: @category }.compact))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def report(event, result)
|
|
61
|
+
return @report.call(event, result) if @report
|
|
62
|
+
|
|
63
|
+
event[:result] = result
|
|
64
|
+
event[:response_model] = result.model
|
|
65
|
+
@details&.call(event, result)
|
|
66
|
+
return unless @usage
|
|
67
|
+
|
|
68
|
+
event[:tokens] = result.tokens
|
|
69
|
+
event[:cost] = result.cost
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -31,6 +31,10 @@ module RubyLLM
|
|
|
31
31
|
owner.nil? ? yield : with_owner(owner, &)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
def workflow
|
|
35
|
+
Support::Instrumentation.current_workflow
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
def record(entry)
|
|
35
39
|
ledger&.record(entry)
|
|
36
40
|
end
|
|
@@ -40,20 +44,24 @@ module RubyLLM
|
|
|
40
44
|
instrument(entry, config:)
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
# An attempt reports the workflow it started in, even when it
|
|
48
|
+
# finishes outside that block, such as a polled video job.
|
|
43
49
|
def instrument(entry, config:)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
Support::Instrumentation.with_workflow(entry.workflow || workflow) do
|
|
51
|
+
RubyLLM.instrument(
|
|
52
|
+
'usage.ruby_llm',
|
|
53
|
+
{
|
|
54
|
+
operation: entry.operation,
|
|
55
|
+
provider: entry.provider,
|
|
56
|
+
model: entry.model,
|
|
57
|
+
status: entry.status,
|
|
58
|
+
tokens: entry.tokens,
|
|
59
|
+
cost: entry.cost,
|
|
60
|
+
owner: entry.owner
|
|
61
|
+
},
|
|
62
|
+
config:
|
|
63
|
+
)
|
|
64
|
+
end
|
|
57
65
|
end
|
|
58
66
|
end
|
|
59
67
|
|
|
@@ -82,12 +90,26 @@ module RubyLLM
|
|
|
82
90
|
end
|
|
83
91
|
end
|
|
84
92
|
|
|
93
|
+
# The registry Model of a result that names one. The usage tracker
|
|
94
|
+
# assigns it; a result built by hand looks up #model.
|
|
95
|
+
module ModelInfo
|
|
96
|
+
attr_writer :model_info # :nodoc:
|
|
97
|
+
|
|
98
|
+
def model_info # :nodoc:
|
|
99
|
+
@model_info ||= RubyLLM.models.find(model)
|
|
100
|
+
rescue ModelNotFoundError
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
85
105
|
# One physical provider request attempt and its accounting facts.
|
|
86
106
|
class Entry
|
|
87
|
-
OPERATIONS = %i[
|
|
107
|
+
OPERATIONS = %i[
|
|
108
|
+
chat embedding moderation image speech transcription ocr rerank judgment video research code_execution
|
|
109
|
+
].freeze
|
|
88
110
|
STATUSES = %i[pending succeeded failed cancelled].freeze
|
|
89
111
|
|
|
90
|
-
attr_reader :operation, :provider, :model, :status, :tokens, :owner
|
|
112
|
+
attr_reader :operation, :provider, :model, :status, :tokens, :owner, :workflow
|
|
91
113
|
attr_accessor :message
|
|
92
114
|
|
|
93
115
|
include Support::Inspectable
|
|
@@ -97,7 +119,7 @@ module RubyLLM
|
|
|
97
119
|
end
|
|
98
120
|
|
|
99
121
|
def initialize(operation:, provider:, model:, status: :pending, tokens: nil, cost: nil, message: nil,
|
|
100
|
-
owner: nil)
|
|
122
|
+
owner: nil, workflow: nil)
|
|
101
123
|
@operation = operation.to_sym
|
|
102
124
|
@provider = provider.to_s
|
|
103
125
|
@model = model&.to_s
|
|
@@ -106,6 +128,7 @@ module RubyLLM
|
|
|
106
128
|
@cost = cost
|
|
107
129
|
@message = message
|
|
108
130
|
@owner = owner
|
|
131
|
+
@workflow = workflow
|
|
109
132
|
validate!
|
|
110
133
|
end
|
|
111
134
|
|
|
@@ -161,6 +184,11 @@ module RubyLLM
|
|
|
161
184
|
judgment: :text_tokens
|
|
162
185
|
}.freeze
|
|
163
186
|
|
|
187
|
+
CONNECT_FAILURES = [
|
|
188
|
+
Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, SocketError, Net::OpenTimeout
|
|
189
|
+
].freeze
|
|
190
|
+
TLS_HANDSHAKE_FAILURE = /\ASSL_connect|does not match the server certificate/
|
|
191
|
+
|
|
164
192
|
attr_reader :entries
|
|
165
193
|
|
|
166
194
|
def initialize(operation:, provider:, model:, config:, on_finish: nil, model_id: nil)
|
|
@@ -176,13 +204,14 @@ module RubyLLM
|
|
|
176
204
|
|
|
177
205
|
def start
|
|
178
206
|
entry = Entry.new(operation: @operation, provider: @provider.slug, model: @model_id || @model_info&.id,
|
|
179
|
-
owner: Usage.owner)
|
|
207
|
+
owner: Usage.owner, workflow: Usage.workflow)
|
|
180
208
|
@entries << entry
|
|
181
209
|
@pending << entry
|
|
182
210
|
entry
|
|
183
211
|
end
|
|
184
212
|
|
|
185
213
|
def observe(chunk)
|
|
214
|
+
@answered = @pending.last
|
|
186
215
|
observe_tokens(chunk.tokens) if chunk.respond_to?(:tokens)
|
|
187
216
|
end
|
|
188
217
|
|
|
@@ -207,6 +236,12 @@ module RubyLLM
|
|
|
207
236
|
@pending.dup.each { |entry| fail_attempt(entry, error) }
|
|
208
237
|
end
|
|
209
238
|
|
|
239
|
+
# Finishes the attempts an exception outside StandardError
|
|
240
|
+
# interrupted, such as a stopped task or a shutdown.
|
|
241
|
+
def cancel_pending
|
|
242
|
+
@pending.dup.each { |entry| finish(entry, status: :cancelled, tokens: entry.tokens) }
|
|
243
|
+
end
|
|
244
|
+
|
|
210
245
|
def succeed(result)
|
|
211
246
|
# A request that produced several results, like a multi-image
|
|
212
247
|
# generation, is billed once: the first result carries the call.
|
|
@@ -246,23 +281,38 @@ module RubyLLM
|
|
|
246
281
|
end
|
|
247
282
|
|
|
248
283
|
# A request that never reached the provider, or that the provider
|
|
249
|
-
#
|
|
250
|
-
# failures keep their usage unknown.
|
|
284
|
+
# answered with an error before streaming anything, cannot have been
|
|
285
|
+
# billed; possibly-billed failures keep their usage unknown.
|
|
251
286
|
def failure_tokens(entry, error)
|
|
252
287
|
return entry.tokens unless @model_info
|
|
253
|
-
return entry.tokens if entry.usage_available? || (!never_sent?(error) && !refused?(error))
|
|
288
|
+
return entry.tokens if entry.usage_available? || (!never_sent?(entry, error) && !refused?(entry, error))
|
|
254
289
|
|
|
255
290
|
Tokens.new(input: 0, output: 0)
|
|
256
291
|
end
|
|
257
292
|
|
|
258
|
-
|
|
259
|
-
|
|
293
|
+
# Faraday also raises its connection errors for a connection that
|
|
294
|
+
# drops after the request was sent, so only the wrapped cause tells
|
|
295
|
+
# whether the connection was ever established.
|
|
296
|
+
def never_sent?(entry, error)
|
|
297
|
+
return false if entry.equal?(@answered)
|
|
298
|
+
|
|
299
|
+
error = transport_error(error)
|
|
300
|
+
return false unless error.is_a?(Faraday::ConnectionFailed) || error.is_a?(Faraday::SSLError)
|
|
301
|
+
|
|
302
|
+
cause = error.wrapped_exception || error.cause
|
|
303
|
+
CONNECT_FAILURES.any? { |failure| cause.is_a?(failure) } ||
|
|
304
|
+
(cause.is_a?(OpenSSL::SSL::SSLError) && cause.message.match?(TLS_HANDSHAKE_FAILURE))
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def transport_error(error)
|
|
308
|
+
error.is_a?(ConnectionError) ? error.cause : error
|
|
260
309
|
end
|
|
261
310
|
|
|
262
|
-
def refused?(error)
|
|
311
|
+
def refused?(entry, error)
|
|
263
312
|
return false unless error.is_a?(Error) && error.response.respond_to?(:status)
|
|
264
313
|
|
|
265
|
-
|
|
314
|
+
status = error.response.status
|
|
315
|
+
(400..499).cover?(status) || ((500..599).cover?(status) && !entry.equal?(@answered))
|
|
266
316
|
end
|
|
267
317
|
|
|
268
318
|
def finish(entry, status:, tokens:, **details)
|