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
data/lib/ruby_llm/batch.rb
CHANGED
|
@@ -95,6 +95,11 @@ module RubyLLM
|
|
|
95
95
|
new(provider:, store: config.batch_store, **provider.find_batch(id))
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
def awaiting_response?(chat) # :nodoc:
|
|
99
|
+
last = chat.messages.reverse.find { |message| message.role != :system }
|
|
100
|
+
AWAITING_ROLES.include?(last&.role)
|
|
101
|
+
end
|
|
102
|
+
|
|
98
103
|
private
|
|
99
104
|
|
|
100
105
|
def submit_chats(records)
|
|
@@ -104,7 +109,7 @@ module RubyLLM
|
|
|
104
109
|
payload = { provider: provider.slug, provider_class: provider.name, requests: chats.size }
|
|
105
110
|
RubyLLM.instrument('batch.ruby_llm', payload, config: provider.config) do |event|
|
|
106
111
|
requests = chats.each_with_index.map do |chat, index|
|
|
107
|
-
{ custom_id: index.to_s, model: chat.model.id, payload: chat.render }
|
|
112
|
+
{ custom_id: index.to_s, model: chat.model.id, payload: chat.render, headers: chat.headers }
|
|
108
113
|
end
|
|
109
114
|
store = provider.config.batch_store
|
|
110
115
|
batch = new(provider:, chats:, store:, **provider.create_batch(requests))
|
|
@@ -153,7 +158,7 @@ module RubyLLM
|
|
|
153
158
|
end
|
|
154
159
|
|
|
155
160
|
def awaiting_model?(chat)
|
|
156
|
-
!chat.complete? &&
|
|
161
|
+
!chat.complete? && awaiting_response?(chat)
|
|
157
162
|
end
|
|
158
163
|
|
|
159
164
|
def shared_provider(chats)
|
|
@@ -161,19 +166,25 @@ module RubyLLM
|
|
|
161
166
|
raise ArgumentError, "A batch takes one provider per submission, got: #{slugs.join(', ')}" if slugs.size > 1
|
|
162
167
|
|
|
163
168
|
provider = chats.first.provider
|
|
169
|
+
unless chats.all? { |chat| chat.provider.same_credentials?(provider) }
|
|
170
|
+
raise ArgumentError, 'A batch takes one set of credentials per submission'
|
|
171
|
+
end
|
|
172
|
+
|
|
164
173
|
raise Error, "#{provider.slug} doesn't support batch requests" unless provider.batches?
|
|
165
174
|
|
|
166
175
|
provider
|
|
167
176
|
end
|
|
168
177
|
end
|
|
169
178
|
|
|
170
|
-
def initialize(provider:, chats: nil, requests: nil, batch_protocol: nil, store: nil,
|
|
179
|
+
def initialize(provider:, chats: nil, requests: nil, batch_protocol: nil, store: nil, delivered: [], # :nodoc:
|
|
180
|
+
delivery: nil, **attributes)
|
|
171
181
|
@provider = provider
|
|
172
182
|
@chats = chats
|
|
173
183
|
@requests = requests
|
|
174
184
|
@batch_protocol = protocol_name(batch_protocol)
|
|
175
185
|
@store = store
|
|
176
|
-
@delivered = {}
|
|
186
|
+
@delivered = delivered.to_h { |index| [index, true] }
|
|
187
|
+
@delivery = delivery
|
|
177
188
|
@statuses = []
|
|
178
189
|
apply(attributes)
|
|
179
190
|
end
|
|
@@ -241,8 +252,9 @@ module RubyLLM
|
|
|
241
252
|
def messages
|
|
242
253
|
return @messages if @messages
|
|
243
254
|
|
|
255
|
+
@deferred = false
|
|
244
256
|
collected = collect_results
|
|
245
|
-
@messages = collected if @completed
|
|
257
|
+
@messages = collected if @completed && !@deferred
|
|
246
258
|
collected
|
|
247
259
|
end
|
|
248
260
|
|
|
@@ -291,9 +303,11 @@ module RubyLLM
|
|
|
291
303
|
validate_result_indices(results)
|
|
292
304
|
slots = Array.new(result_slot_count(results))
|
|
293
305
|
|
|
294
|
-
results.each do |index, result, failure_status|
|
|
306
|
+
results.each do |index, result, failure_status, billed|
|
|
307
|
+
result = Chat::ToolCallIds.new(namespace: [@provider.slug, id, index]).assign(result) if result.is_a?(Message)
|
|
295
308
|
slots[index] = result
|
|
296
309
|
deliver(index, result, failure_status)
|
|
310
|
+
deliver_failed_usage(index, **billed) if billed
|
|
297
311
|
end
|
|
298
312
|
|
|
299
313
|
fill_missing_statuses(slots.size) if complete?
|
|
@@ -325,7 +339,9 @@ module RubyLLM
|
|
|
325
339
|
end
|
|
326
340
|
|
|
327
341
|
# Collecting early keeps reading fresh, so a result already delivered
|
|
328
|
-
# comes back on every later poll: hand each one over once.
|
|
342
|
+
# comes back on every later poll: hand each one over once. A result with
|
|
343
|
+
# no chat or request waits for completion, because a poller that finds
|
|
344
|
+
# the batch by id gets a new object each time.
|
|
329
345
|
def deliver(index, result, failure_status)
|
|
330
346
|
statuses[index] = result ? :succeeded : failure_status
|
|
331
347
|
return unless result
|
|
@@ -339,69 +355,119 @@ module RubyLLM
|
|
|
339
355
|
|
|
340
356
|
def deliver_embedding(index, embedding)
|
|
341
357
|
request = requests&.[](index)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
embedding,
|
|
345
|
-
operation: :embedding,
|
|
346
|
-
model: request&.model,
|
|
347
|
-
category: :embeddings,
|
|
348
|
-
instrument: !request.nil? && !delivered
|
|
349
|
-
)
|
|
350
|
-
return if delivered
|
|
358
|
+
entry = attach_batch_usage(embedding, operation: :embedding, model: request&.model, category: :embeddings)
|
|
359
|
+
return if settled?(index, request)
|
|
351
360
|
|
|
352
361
|
@delivered[index] = true
|
|
362
|
+
report_batch_usage(entry) if entry
|
|
353
363
|
request&.result = embedding
|
|
354
364
|
end
|
|
355
365
|
|
|
356
366
|
def deliver_message(index, message)
|
|
357
367
|
chat = chats&.[](index)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
entry = attach_batch_usage(message, operation: :chat, model: chat&.model, category: :text_tokens,
|
|
369
|
+
owner: chat&.owner)
|
|
370
|
+
return if settled?(index, chat) || (chat && already_in_chat?(chat, message))
|
|
371
|
+
|
|
372
|
+
delivered = exclusively(index) do
|
|
373
|
+
@delivered[index] = true
|
|
374
|
+
report_batch_usage(entry, chat:) if entry
|
|
375
|
+
chat&.add_completion(message, record_usage: true)
|
|
376
|
+
end
|
|
377
|
+
delivered ? @delivered[index] = true : @deferred = true
|
|
378
|
+
end
|
|
367
379
|
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
# A slot RubyLLM could not parse still cost what the provider reported.
|
|
381
|
+
def deliver_failed_usage(index, tokens:, model:)
|
|
382
|
+
chat = chats&.[](index)
|
|
383
|
+
entry = failed_usage_entry(tokens, model, chat)
|
|
384
|
+
return if entry.nil? || settled?(index, chat)
|
|
385
|
+
|
|
386
|
+
delivered = exclusively(index, failed: true) do
|
|
387
|
+
report_batch_usage(entry, chat:)
|
|
388
|
+
chat&.add_usage_entry(entry)
|
|
389
|
+
end
|
|
390
|
+
delivered ? @delivered[index] = true : @deferred = true
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def settled?(index, owner)
|
|
394
|
+
@delivered[index] || !(owner || complete?)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# A store may run this in its own turn, returning false when it could
|
|
398
|
+
# not, so the next poll delivers the result.
|
|
399
|
+
def exclusively(index, **, &deliver)
|
|
400
|
+
return @delivery.call(index, **, &deliver) if @delivery
|
|
401
|
+
|
|
402
|
+
deliver.call
|
|
403
|
+
true
|
|
370
404
|
end
|
|
371
405
|
|
|
372
|
-
def attach_batch_usage(result, operation:, model:, category:,
|
|
406
|
+
def attach_batch_usage(result, operation:, model:, category:, owner: nil)
|
|
373
407
|
return unless result.ruby_llm_usage_entries.empty?
|
|
374
408
|
|
|
375
|
-
model ||=
|
|
376
|
-
entry =
|
|
409
|
+
model ||= result_model(result.model)
|
|
410
|
+
entry = batch_usage_entry(
|
|
411
|
+
operation:, model_id: result.model || model.id, model:, status: :succeeded, tokens: result.tokens,
|
|
412
|
+
category:, message: result.is_a?(Message) ? result : nil, owner:
|
|
413
|
+
)
|
|
414
|
+
result.ruby_llm_usage_entries = [entry]
|
|
415
|
+
entry
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def failed_usage_entry(tokens, model_id, chat)
|
|
419
|
+
model = chat&.model || (result_model(model_id) if model_id)
|
|
420
|
+
model_id ||= model&.id
|
|
421
|
+
return unless model_id
|
|
422
|
+
|
|
423
|
+
batch_usage_entry(operation: :chat, model_id:, model:, status: :failed, tokens:, category: :text_tokens,
|
|
424
|
+
owner: chat&.owner)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def batch_usage_entry(operation:, model_id:, model:, status:, tokens:, category:, message: nil, owner: nil)
|
|
428
|
+
Accounting::Usage::Entry.new(
|
|
377
429
|
operation:,
|
|
378
430
|
provider: @provider.slug,
|
|
379
|
-
model:
|
|
380
|
-
status
|
|
381
|
-
tokens
|
|
382
|
-
cost: @provider.batch_cost(
|
|
383
|
-
message
|
|
384
|
-
owner: Accounting::Usage.owner
|
|
431
|
+
model: model_id,
|
|
432
|
+
status:,
|
|
433
|
+
tokens:,
|
|
434
|
+
cost: @provider.batch_cost(tokens, model:, category:),
|
|
435
|
+
message:,
|
|
436
|
+
owner: owner || Accounting::Usage.owner,
|
|
437
|
+
workflow: Accounting::Usage.workflow
|
|
385
438
|
)
|
|
386
|
-
|
|
387
|
-
return unless instrument
|
|
439
|
+
end
|
|
388
440
|
|
|
389
|
-
|
|
441
|
+
# A chat writes its own ledger row when it takes the answer. Anything
|
|
442
|
+
# else, such as a batch picked up by id, gets a row with no chat.
|
|
443
|
+
def report_batch_usage(entry, chat: nil)
|
|
444
|
+
if chat
|
|
390
445
|
Accounting::Usage.instrument(entry, config: @provider.config)
|
|
391
446
|
else
|
|
392
447
|
Accounting::Usage.report(entry, config: @provider.config)
|
|
393
448
|
end
|
|
394
449
|
end
|
|
395
450
|
|
|
396
|
-
|
|
451
|
+
def result_model(model_id)
|
|
452
|
+
RubyLLM.models.find(model_id, provider: @provider.slug, config: @provider.config)
|
|
453
|
+
rescue ModelNotFoundError
|
|
454
|
+
nil
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# A plain answer is the chat's last non-system message once it arrives. A tool-call
|
|
397
458
|
# answer is not: running its tools adds messages after it, so we match on its
|
|
398
459
|
# tool-call ids instead.
|
|
399
460
|
def already_in_chat?(chat, message)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
461
|
+
return !self.class.awaiting_response?(chat) unless message.tool_call?
|
|
462
|
+
|
|
463
|
+
tool_calls_delivered?(chat, message)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def tool_calls_delivered?(chat, message)
|
|
467
|
+
calls = chat.messages.flat_map { |existing| existing.tool_calls.to_a }
|
|
468
|
+
legacy_ids = calls.select { |call| call.provider_call_id == call.id }.map(&:id)
|
|
469
|
+
calls.map(&:id).intersect?(message.tool_calls.keys) ||
|
|
470
|
+
legacy_ids.intersect?(message.tool_calls.map(&:provider_call_id))
|
|
405
471
|
end
|
|
406
472
|
|
|
407
473
|
module Helpers # :nodoc:
|
|
@@ -416,6 +482,25 @@ module RubyLLM
|
|
|
416
482
|
status.to_s.match?(/cancel/i) ? :cancelled : :failed
|
|
417
483
|
end
|
|
418
484
|
|
|
485
|
+
# One unreadable answer fails its own slot, so the others still arrive.
|
|
486
|
+
# Pass a chat answer's +body+ to keep the usage the provider billed.
|
|
487
|
+
def parsed_batch_result(index, custom_id, body = nil, parser: self)
|
|
488
|
+
[index, yield]
|
|
489
|
+
rescue Error => e
|
|
490
|
+
[index, nil, batch_failure(custom_id, e.message), *billed_batch_usage(body, parser)]
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def billed_batch_usage(body, parser)
|
|
494
|
+
return [] unless body
|
|
495
|
+
|
|
496
|
+
tokens = Message.new(role: :assistant, content: nil, **parser.send(:parse_completion_usage, body)).tokens
|
|
497
|
+
tokens.to_h.any? ? [{ tokens:, model: body['model'] }] : []
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def batch_response_succeeded?(response)
|
|
501
|
+
response.is_a?(Hash) && response['status_code'].to_i.between?(200, 299)
|
|
502
|
+
end
|
|
503
|
+
|
|
419
504
|
def batch_error_message(line)
|
|
420
505
|
response = line['response']
|
|
421
506
|
body = response['body'] if response.is_a?(Hash)
|
|
@@ -425,7 +510,8 @@ module RubyLLM
|
|
|
425
510
|
batch_error_value(line['error']) ||
|
|
426
511
|
line['error_message'] ||
|
|
427
512
|
batch_error_value(body_error) ||
|
|
428
|
-
batch_error_value(response_error)
|
|
513
|
+
batch_error_value(response_error) ||
|
|
514
|
+
batch_error_value(body)
|
|
429
515
|
end
|
|
430
516
|
|
|
431
517
|
def batch_error_value(error)
|
|
@@ -60,11 +60,11 @@ module RubyLLM
|
|
|
60
60
|
self
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
-
# Extends the cache's lifetime to +ttl:+
|
|
64
|
-
#
|
|
63
|
+
# Extends the cache's lifetime to +ttl:+ from now, given as seconds, a
|
|
64
|
+
# Duration, or a provider duration string such as <tt>"600s"</tt>.
|
|
65
65
|
# Updates #expires_at and returns +self+.
|
|
66
66
|
#
|
|
67
|
-
# cache.renew(ttl:
|
|
67
|
+
# cache.renew(ttl: 1.hour)
|
|
68
68
|
#
|
|
69
69
|
def renew(ttl:)
|
|
70
70
|
refreshed = @provider_instance.extend_cache(name, ttl: ttl)
|
|
@@ -81,7 +81,7 @@ module RubyLLM
|
|
|
81
81
|
# +content+ is the text to cache; pass file attachments with +with:+
|
|
82
82
|
# the way Chat#ask accepts them. +instructions:+ caches a system
|
|
83
83
|
# prompt alongside the content, and +ttl:+ sets the cache lifetime in
|
|
84
|
-
# seconds
|
|
84
|
+
# seconds, as a Duration, or as a provider duration string such as
|
|
85
85
|
# <tt>"300s"</tt>. When +provider:+ is omitted, the model's default
|
|
86
86
|
# provider is used. The content must exceed the model's minimum
|
|
87
87
|
# cacheable token count.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Chat
|
|
5
|
+
# Wraps a run of a chat an Agent configured in one +agent.ruby_llm+
|
|
6
|
+
# event, whether it is reached through the chat, the agent, or a
|
|
7
|
+
# persisted record. Only the outermost operation of a run opens one, so
|
|
8
|
+
# #ask, #complete, and the moves they make report a single event.
|
|
9
|
+
module AgentRun # :nodoc:
|
|
10
|
+
OPERATIONS = %i[complete generate run_tools step].freeze
|
|
11
|
+
|
|
12
|
+
OPERATIONS.each do |operation|
|
|
13
|
+
define_method(operation) do |*args, **kwargs, &block|
|
|
14
|
+
chat = agent_run_chat
|
|
15
|
+
return super(*args, **kwargs, &block) unless chat
|
|
16
|
+
|
|
17
|
+
chat.running_agent { super(*args, **kwargs, &block) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Chat
|
|
5
|
+
# Fits tool results to shares of the model's context window, as set with
|
|
6
|
+
# Chat#with_context_budget. Counting tokens takes a request, so sizes go
|
|
7
|
+
# by characters, at two a token: JSON and HTML, which most large tool
|
|
8
|
+
# results are, run that dense.
|
|
9
|
+
class ContextBudget # :nodoc:
|
|
10
|
+
SHARES = %i[per_result per_request].freeze
|
|
11
|
+
CHARACTERS_PER_TOKEN = 2
|
|
12
|
+
# The cut moves in steps of this share of the request budget, so
|
|
13
|
+
# the request prefix, and the prompt cache, hold for many rounds.
|
|
14
|
+
STEP = 0.15
|
|
15
|
+
LEFT_OUT = '[This earlier tool result was left out to fit the context window. ' \
|
|
16
|
+
'Call the tool again if you need it.]'
|
|
17
|
+
TOO_LONG = '[This tool result was left out to fit the context window. ' \
|
|
18
|
+
'Ask for less, such as a narrower query or the next page.]'
|
|
19
|
+
NOTE_ROOM = 200
|
|
20
|
+
|
|
21
|
+
def self.normalize(budget, **shares)
|
|
22
|
+
return if budget.nil?
|
|
23
|
+
|
|
24
|
+
budget = budget.to_h.transform_keys(&:to_sym).merge(shares)
|
|
25
|
+
unknown = budget.keys - SHARES - [:window]
|
|
26
|
+
raise ArgumentError, "Unknown context budget option: #{unknown.join(', ')}" if unknown.any?
|
|
27
|
+
unless budget.keys.intersect?(SHARES)
|
|
28
|
+
raise ArgumentError, 'with_context_budget takes per_result:, per_request:, or both'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
budget.slice(*SHARES).each { |name, share| validate_share(name, share) }
|
|
32
|
+
validate_window(budget[:window]) if budget.key?(:window)
|
|
33
|
+
budget.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.validate_share(name, share)
|
|
37
|
+
return if share.is_a?(Numeric) && share.positive? && share <= 1
|
|
38
|
+
|
|
39
|
+
raise ArgumentError, "#{name} must be a share of the context window above 0 and at most 1"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.validate_window(window)
|
|
43
|
+
return if window.is_a?(Integer) && window.positive?
|
|
44
|
+
|
|
45
|
+
raise ArgumentError, 'window must be a positive whole number of tokens'
|
|
46
|
+
end
|
|
47
|
+
private_class_method :validate_share, :validate_window
|
|
48
|
+
|
|
49
|
+
def initialize(budget, context_window)
|
|
50
|
+
@budget = budget
|
|
51
|
+
window = budget[:window] || context_window
|
|
52
|
+
@window = window.to_i if window.to_i.positive?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def window?
|
|
56
|
+
!@window.nil?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cap(content)
|
|
60
|
+
limit = characters(:per_result)
|
|
61
|
+
return content unless limit && content.is_a?(String) && content.length > limit
|
|
62
|
+
|
|
63
|
+
shown = [limit - NOTE_ROOM, 0].max
|
|
64
|
+
"#{content[0, shown]}\n\n[Result truncated: #{shown} of #{content.length} characters shown. " \
|
|
65
|
+
'Ask for a narrower query or the next page.]'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fit(list)
|
|
69
|
+
limit = characters(:per_request)
|
|
70
|
+
return list unless limit
|
|
71
|
+
|
|
72
|
+
excess = size_of_all(list) - limit
|
|
73
|
+
return list unless excess.positive?
|
|
74
|
+
|
|
75
|
+
step = limit * STEP
|
|
76
|
+
latest = list.rindex { |message| message.role == :assistant } || -1
|
|
77
|
+
fitted = leave_out(list, (excess / step).ceil * step, LEFT_OUT) { |index| index < latest }
|
|
78
|
+
over = size_of_all(fitted) - limit
|
|
79
|
+
return fitted unless over.positive?
|
|
80
|
+
|
|
81
|
+
leave_out(fitted, over, TOO_LONG) { |index| index > latest }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Replaces the oldest results the block picks by index with +note+,
|
|
87
|
+
# until +characters+ are freed. A request stops here for older rounds,
|
|
88
|
+
# in steps. The latest round's results, which the model has not seen
|
|
89
|
+
# yet, are cut only as far as the request needs: left whole, a round
|
|
90
|
+
# over the budget would fail this request and every one after it.
|
|
91
|
+
def leave_out(list, characters, note)
|
|
92
|
+
list.each_with_index.map do |message, index|
|
|
93
|
+
next message unless characters.positive? && yield(index) && leavable?(message, note)
|
|
94
|
+
|
|
95
|
+
characters -= message.content.length - note.length
|
|
96
|
+
message.with(content: note)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def characters(share)
|
|
101
|
+
return unless @window && @budget[share]
|
|
102
|
+
|
|
103
|
+
(@window * @budget[share] * CHARACTERS_PER_TOKEN).to_i
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def leavable?(message, note)
|
|
107
|
+
message.tool_result? && message.content.is_a?(String) && message.content.length > note.length
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def size_of_all(list)
|
|
111
|
+
list.sum { |message| size_of(message) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def size_of(message)
|
|
115
|
+
message.content.to_s.length + message.tool_calls.to_h.sum { |_id, call| call.arguments.to_json.length }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|