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
|
@@ -24,10 +24,12 @@ module RubyLLM
|
|
|
24
24
|
|
|
25
25
|
def finish_reasons = FINISH_REASONS
|
|
26
26
|
|
|
27
|
+
def native_tool_namespace?(namespace) = Tools::CLIENT_TOOLSETS.key?(namespace)
|
|
28
|
+
|
|
27
29
|
def normalize_finish_reason(reason)
|
|
28
30
|
return nil if reason.nil?
|
|
29
31
|
|
|
30
|
-
finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
|
|
32
|
+
finish_reasons.fetch(reason.to_s) { reason.to_s.downcase.to_sym }
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def completion_url
|
|
@@ -64,7 +66,9 @@ module RubyLLM
|
|
|
64
66
|
thinking: thinking,
|
|
65
67
|
citations: citations,
|
|
66
68
|
caching: caching
|
|
67
|
-
).then { |payload| Tools.apply_tool_search(payload) }.slice(*COUNT_TOKENS_KEYS)
|
|
69
|
+
).then { |payload| Tools.apply_tool_search(payload) }.slice(*COUNT_TOKENS_KEYS).tap do |payload|
|
|
70
|
+
payload[:thinking]&.delete(:block_binding)
|
|
71
|
+
end
|
|
68
72
|
end
|
|
69
73
|
|
|
70
74
|
def parse_count_tokens_response(response)
|
|
@@ -83,13 +87,11 @@ module RubyLLM
|
|
|
83
87
|
end
|
|
84
88
|
|
|
85
89
|
def build_system_content(system_messages, caching: nil)
|
|
86
|
-
return [] if system_messages.empty?
|
|
87
|
-
|
|
88
90
|
# Anthropic's `system` parameter accepts an array of text content blocks
|
|
89
91
|
# (each optionally with cache_control); each :system message becomes its
|
|
90
92
|
# own block in the resulting array.
|
|
91
93
|
system_messages.flat_map do |msg|
|
|
92
|
-
blocks = Media.
|
|
94
|
+
blocks = Media.render_content(msg.content, msg.attachments).dup
|
|
93
95
|
cache_boundary?(msg, caching:) ? inject_cache_control(blocks, caching:, ttl: msg.cache_ttl) : blocks
|
|
94
96
|
end
|
|
95
97
|
end
|
|
@@ -98,23 +100,41 @@ module RubyLLM
|
|
|
98
100
|
max_output_tokens: nil)
|
|
99
101
|
payload = {
|
|
100
102
|
model: model.id,
|
|
101
|
-
messages:
|
|
103
|
+
messages: render_messages(chat_messages, thinking:, citations:, caching:),
|
|
102
104
|
stream: stream,
|
|
103
105
|
max_tokens: max_output_tokens || model.max_output_tokens || DEFAULT_MAX_OUTPUT_TOKENS
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
add_thinking_fields(payload, thinking, model)
|
|
109
|
+
bind_thinking(payload, model)
|
|
107
110
|
|
|
108
111
|
payload
|
|
109
112
|
end
|
|
110
113
|
|
|
111
|
-
|
|
114
|
+
# The binding goes on every request, not only once thinking appears
|
|
115
|
+
# in the history: a thinking setting that changes mid-conversation
|
|
116
|
+
# invalidates the prompt cache.
|
|
117
|
+
def bind_thinking(payload, model)
|
|
118
|
+
return unless thinking_block_binding? && BlockBinding.binds?(model.id)
|
|
119
|
+
|
|
120
|
+
bound = BlockBinding.apply(payload[:thinking])
|
|
121
|
+
payload[:thinking] = bound if bound
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Hosts that don't take the binding's beta override this.
|
|
125
|
+
def thinking_block_binding?
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def render_messages(messages, thinking: nil, citations: false, caching: nil)
|
|
112
130
|
rendered = []
|
|
113
131
|
tool_result_blocks = []
|
|
132
|
+
calls = {}
|
|
114
133
|
|
|
115
134
|
messages.each do |msg|
|
|
135
|
+
calls.merge!(msg.tool_calls.to_h)
|
|
116
136
|
if msg.tool_result?
|
|
117
|
-
tool_result_blocks << Tools.
|
|
137
|
+
tool_result_blocks << Tools.render_tool_result_block(msg, citations:, tool_call: calls[msg.tool_call_id])
|
|
118
138
|
inject_cache_control(tool_result_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
119
139
|
next
|
|
120
140
|
end
|
|
@@ -124,7 +144,7 @@ module RubyLLM
|
|
|
124
144
|
tool_result_blocks = []
|
|
125
145
|
end
|
|
126
146
|
|
|
127
|
-
formatted =
|
|
147
|
+
formatted = render_message(msg, thinking:, citations:, caching:)
|
|
128
148
|
rendered << formatted unless formatted[:content].empty?
|
|
129
149
|
end
|
|
130
150
|
|
|
@@ -134,8 +154,9 @@ module RubyLLM
|
|
|
134
154
|
|
|
135
155
|
def add_optional_fields(payload, system_content:, tools:, tool_prefs:, temperature:, schema: nil)
|
|
136
156
|
if tools.any?
|
|
137
|
-
payload[:tools] =
|
|
157
|
+
payload[:tools] = Tools.render_tools(tools)
|
|
138
158
|
unless tool_prefs[:choice].nil? && tool_prefs[:calls].nil?
|
|
159
|
+
Protocol::ToolNamespaces.validate_tool_choice(tools, tool_prefs[:choice])
|
|
139
160
|
payload[:tool_choice] = Tools.build_tool_choice(tool_prefs)
|
|
140
161
|
end
|
|
141
162
|
end
|
|
@@ -165,7 +186,13 @@ module RubyLLM
|
|
|
165
186
|
end
|
|
166
187
|
|
|
167
188
|
def apply_compaction_headers(headers, _compaction)
|
|
168
|
-
|
|
189
|
+
add_beta(headers, COMPACTION_BETA)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def apply_binding_beta(headers, payload)
|
|
193
|
+
return headers unless payload[:thinking]&.key?(:block_binding)
|
|
194
|
+
|
|
195
|
+
add_beta(headers, BlockBinding::BETA)
|
|
169
196
|
end
|
|
170
197
|
|
|
171
198
|
# The Files API is still a beta, so a request that references an
|
|
@@ -173,22 +200,56 @@ module RubyLLM
|
|
|
173
200
|
def apply_files_beta(headers, payload)
|
|
174
201
|
return headers unless provider_file_source?(payload)
|
|
175
202
|
|
|
176
|
-
|
|
203
|
+
add_beta(headers, Files::BETA_HEADER)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def apply_mcp_beta(headers)
|
|
207
|
+
add_beta(headers, Anthropic::MCP_BETA)
|
|
177
208
|
end
|
|
178
209
|
|
|
179
210
|
def provider_file_source?(payload)
|
|
180
|
-
blocks = Array(payload[:messages]).flat_map { |message| Array(message
|
|
211
|
+
blocks = Array(payload[:messages]).flat_map { |message| Array(block_value(message, :content)) }
|
|
181
212
|
blocks.concat(Array(payload[:system]))
|
|
182
|
-
|
|
213
|
+
file_source?(blocks)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def file_source?(blocks)
|
|
217
|
+
blocks.any? do |block|
|
|
218
|
+
next false unless block.is_a?(Hash)
|
|
219
|
+
|
|
220
|
+
source = block_value(block, :source)
|
|
221
|
+
(source.is_a?(Hash) && block_value(source, :type) == 'file') ||
|
|
222
|
+
file_source?(Array(block_value(block, :content)))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# A before_request hook can add blocks parsed from JSON.
|
|
227
|
+
def block_value(block, key)
|
|
228
|
+
block.fetch(key) { block[key.to_s] }
|
|
183
229
|
end
|
|
184
230
|
|
|
185
231
|
# Anthropic takes several betas as one comma-separated header, so an
|
|
186
232
|
# added beta joins whatever a server tool or with_headers already set.
|
|
187
233
|
def join_betas(existing, beta)
|
|
188
|
-
|
|
234
|
+
[existing, beta].flat_map { |betas| betas.to_s.split(',') }.map(&:strip).reject(&:empty?).uniq.join(',')
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def add_beta(headers, beta)
|
|
238
|
+
headers = canonical_beta_headers(headers)
|
|
239
|
+
headers.merge(BETA_HEADER => join_betas(headers[BETA_HEADER], beta))
|
|
189
240
|
end
|
|
190
241
|
|
|
191
|
-
|
|
242
|
+
# HTTP header names ignore case but Hash keys do not, so every
|
|
243
|
+
# spelling a caller used folds into BETA_HEADER before a beta joins it.
|
|
244
|
+
def canonical_beta_headers(headers)
|
|
245
|
+
names = headers.keys.select { |name| name.to_s.casecmp?(BETA_HEADER) }
|
|
246
|
+
return headers if names.empty?
|
|
247
|
+
|
|
248
|
+
betas = headers.values_at(*names).reduce { |joined, beta| join_betas(joined, beta) }
|
|
249
|
+
headers.except(*names).merge(BETA_HEADER => betas)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def provider_file_references?
|
|
192
253
|
true
|
|
193
254
|
end
|
|
194
255
|
|
|
@@ -210,11 +271,11 @@ module RubyLLM
|
|
|
210
271
|
def parse_completion_body(data, raw:)
|
|
211
272
|
content_blocks = data['content'] || []
|
|
212
273
|
|
|
213
|
-
text_content, citations =
|
|
214
|
-
thinking_content =
|
|
215
|
-
thinking_signature =
|
|
274
|
+
text_content, citations = parse_text_and_citations(content_blocks)
|
|
275
|
+
thinking_content = parse_thinking_content(content_blocks)
|
|
276
|
+
thinking_signature = parse_thinking_signature(content_blocks)
|
|
216
277
|
tool_use_blocks = Tools.find_tool_uses(content_blocks)
|
|
217
|
-
server_tool_calls =
|
|
278
|
+
server_tool_calls = parse_server_tool_calls(content_blocks)
|
|
218
279
|
|
|
219
280
|
build_message(data, content: text_content, citations:, thinking: thinking_content,
|
|
220
281
|
thinking_signature:, tool_use_blocks:, server_tool_calls:,
|
|
@@ -230,7 +291,7 @@ module RubyLLM
|
|
|
230
291
|
type.end_with?('_tool_result')
|
|
231
292
|
end
|
|
232
293
|
|
|
233
|
-
def
|
|
294
|
+
def parse_server_tool_calls(blocks)
|
|
234
295
|
blocks.select { |block| server_tool_block?(block) }.map do |block|
|
|
235
296
|
ServerToolCall.new(
|
|
236
297
|
type: block['type'],
|
|
@@ -243,7 +304,15 @@ module RubyLLM
|
|
|
243
304
|
end
|
|
244
305
|
end
|
|
245
306
|
|
|
246
|
-
|
|
307
|
+
# A failed search holds one error object instead of a list of results.
|
|
308
|
+
def parse_web_sources(message)
|
|
309
|
+
blocks = message.server_tool_calls.select { |call| call.type == 'web_search_tool_result' }
|
|
310
|
+
blocks.flat_map { |call| call.result.is_a?(Array) ? call.result : [] }.filter_map do |result|
|
|
311
|
+
WebSource.new(url: result['url'], title: result['title']) if result['type'] == 'web_search_result'
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def parse_text_and_citations(blocks)
|
|
247
316
|
text = +''
|
|
248
317
|
citations = []
|
|
249
318
|
|
|
@@ -272,6 +341,7 @@ module RubyLLM
|
|
|
272
341
|
text: text,
|
|
273
342
|
start_index: start_index,
|
|
274
343
|
end_index: end_index,
|
|
344
|
+
source_id: data['source'],
|
|
275
345
|
source_index: data['document_index'] || data['search_result_index'],
|
|
276
346
|
start_page: data['start_page_number'],
|
|
277
347
|
end_page: end_page && (end_page - 1)
|
|
@@ -286,14 +356,14 @@ module RubyLLM
|
|
|
286
356
|
|
|
287
357
|
# An empty thinking text is a real block whose display was omitted;
|
|
288
358
|
# collapsing it to nil would replay the signature as redacted data.
|
|
289
|
-
def
|
|
359
|
+
def parse_thinking_content(blocks)
|
|
290
360
|
thinking_blocks = blocks.select { |c| c['type'] == 'thinking' }
|
|
291
361
|
return nil if thinking_blocks.empty?
|
|
292
362
|
|
|
293
363
|
thinking_blocks.map { |c| c['thinking'] || c['text'] }.join
|
|
294
364
|
end
|
|
295
365
|
|
|
296
|
-
def
|
|
366
|
+
def parse_thinking_signature(blocks)
|
|
297
367
|
thinking_block = blocks.find { |c| c['type'] == 'thinking' } ||
|
|
298
368
|
blocks.find { |c| c['type'] == 'redacted_thinking' }
|
|
299
369
|
thinking_block&.dig('signature') || thinking_block&.dig('data')
|
|
@@ -307,10 +377,6 @@ module RubyLLM
|
|
|
307
377
|
def build_message(data, content:, citations:, thinking:, thinking_signature:, tool_use_blocks:, raw:,
|
|
308
378
|
server_tool_calls: [], raw_content: nil)
|
|
309
379
|
usage = aggregate_usage(data['usage'])
|
|
310
|
-
thinking_tokens = usage.dig('output_tokens_details', 'thinking_tokens') ||
|
|
311
|
-
usage.dig('output_tokens_details', 'reasoning_tokens') ||
|
|
312
|
-
usage['thinking_tokens'] ||
|
|
313
|
-
usage['reasoning_tokens']
|
|
314
380
|
|
|
315
381
|
Message.new(
|
|
316
382
|
role: :assistant,
|
|
@@ -323,29 +389,33 @@ module RubyLLM
|
|
|
323
389
|
raw_content: raw_content,
|
|
324
390
|
input_tokens: usage['input_tokens'],
|
|
325
391
|
output_tokens: usage['output_tokens'],
|
|
326
|
-
cache_read_tokens:
|
|
327
|
-
cache_write_tokens:
|
|
328
|
-
cache_write_tokens_by_ttl:
|
|
329
|
-
thinking_tokens:
|
|
392
|
+
cache_read_tokens: parse_cache_read_tokens(data),
|
|
393
|
+
cache_write_tokens: parse_cache_write_tokens(data),
|
|
394
|
+
cache_write_tokens_by_ttl: parse_cache_write_by_ttl(data),
|
|
395
|
+
thinking_tokens: usage_thinking_tokens(usage),
|
|
330
396
|
server_tool_use: usage['server_tool_use'],
|
|
331
397
|
finish_reason: normalize_finish_reason(data['stop_reason']),
|
|
332
|
-
model: data
|
|
398
|
+
model: parse_response_model_id(data),
|
|
333
399
|
raw: raw
|
|
334
400
|
)
|
|
335
401
|
end
|
|
336
402
|
|
|
403
|
+
def parse_response_model_id(data)
|
|
404
|
+
data['model']
|
|
405
|
+
end
|
|
406
|
+
|
|
337
407
|
# Stored thinking blocks replay whether or not this request asks for
|
|
338
408
|
# thinking: Claude emits them on its own and requires them back on
|
|
339
409
|
# tool-use turns.
|
|
340
|
-
def
|
|
410
|
+
def render_message(msg, citations: false, caching: nil, **)
|
|
341
411
|
if msg.role == :assistant && msg.raw_content
|
|
342
|
-
|
|
412
|
+
render_raw_assistant_message(msg, caching:)
|
|
343
413
|
elsif msg.tool_call?
|
|
344
|
-
|
|
414
|
+
render_tool_call_with_thinking(msg, caching:)
|
|
345
415
|
elsif msg.tool_result?
|
|
346
|
-
Tools.
|
|
416
|
+
Tools.render_tool_result(msg, citations:)
|
|
347
417
|
else
|
|
348
|
-
|
|
418
|
+
render_basic_message_with_thinking(msg, citations: citations, caching:)
|
|
349
419
|
end
|
|
350
420
|
end
|
|
351
421
|
|
|
@@ -360,15 +430,15 @@ module RubyLLM
|
|
|
360
430
|
# Turns that used server tools replay their provider-shaped blocks
|
|
361
431
|
# verbatim: the API requires the tool_use/result blocks and their
|
|
362
432
|
# citations back exactly as returned.
|
|
363
|
-
def
|
|
364
|
-
blocks = msg.raw_content.dup
|
|
433
|
+
def render_raw_assistant_message(msg, caching: nil)
|
|
434
|
+
blocks = Protocol::ReplayedArguments.blank(msg.raw_content.dup, msg, id: 'id', arguments: 'input', empty: {})
|
|
365
435
|
inject_cache_control(blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
366
436
|
|
|
367
437
|
{ role: 'assistant', content: blocks }
|
|
368
438
|
end
|
|
369
439
|
|
|
370
|
-
def
|
|
371
|
-
content_blocks = msg.role == :assistant ?
|
|
440
|
+
def render_basic_message_with_thinking(msg, citations: false, caching: nil)
|
|
441
|
+
content_blocks = msg.role == :assistant ? render_thinking_blocks(msg) : []
|
|
372
442
|
|
|
373
443
|
append_formatted_content(content_blocks, msg, citations: citations)
|
|
374
444
|
inject_cache_control(content_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
@@ -379,17 +449,18 @@ module RubyLLM
|
|
|
379
449
|
}
|
|
380
450
|
end
|
|
381
451
|
|
|
382
|
-
def
|
|
452
|
+
def render_tool_call_with_thinking(msg, caching: nil)
|
|
383
453
|
content_blocks = prepend_thinking_blocks([], msg)
|
|
384
454
|
append_formatted_content(content_blocks, msg) unless msg.content.nil? || msg.content.empty?
|
|
385
455
|
|
|
386
|
-
msg.tool_calls.
|
|
456
|
+
msg.tool_calls.each do |tool_call|
|
|
387
457
|
content_blocks << {
|
|
388
458
|
type: 'tool_use',
|
|
389
|
-
id: tool_call.
|
|
459
|
+
id: tool_call.provider_call_id,
|
|
390
460
|
name: tool_call.name,
|
|
391
|
-
input: tool_call.arguments
|
|
392
|
-
|
|
461
|
+
input: tool_call.arguments,
|
|
462
|
+
toolset_name: tool_call.namespace
|
|
463
|
+
}.compact
|
|
393
464
|
end
|
|
394
465
|
inject_cache_control(content_blocks, caching:, ttl: msg.cache_ttl) if cache_boundary?(msg, caching:)
|
|
395
466
|
|
|
@@ -400,12 +471,12 @@ module RubyLLM
|
|
|
400
471
|
end
|
|
401
472
|
|
|
402
473
|
def prepend_thinking_blocks(content_blocks, msg)
|
|
403
|
-
content_blocks.unshift(*
|
|
474
|
+
content_blocks.unshift(*render_thinking_blocks(msg))
|
|
404
475
|
|
|
405
476
|
content_blocks
|
|
406
477
|
end
|
|
407
478
|
|
|
408
|
-
def
|
|
479
|
+
def render_thinking_blocks(msg)
|
|
409
480
|
blocks = msg.raw_reasoning['anthropic'] if msg.raw_reasoning.is_a?(Hash)
|
|
410
481
|
return Support::Utils.deep_dup(blocks) if blocks
|
|
411
482
|
|
|
@@ -422,7 +493,7 @@ module RubyLLM
|
|
|
422
493
|
end
|
|
423
494
|
|
|
424
495
|
def append_formatted_content(content_blocks, msg, citations: false)
|
|
425
|
-
content_blocks.concat(Media.
|
|
496
|
+
content_blocks.concat(Media.render_content(msg.content, msg.attachments, citations: citations))
|
|
426
497
|
end
|
|
427
498
|
|
|
428
499
|
def cache_boundary?(message, caching: nil)
|
|
@@ -430,11 +501,9 @@ module RubyLLM
|
|
|
430
501
|
end
|
|
431
502
|
|
|
432
503
|
def inject_cache_control(blocks, caching: nil, ttl: nil)
|
|
433
|
-
return blocks if blocks.empty?
|
|
434
|
-
|
|
435
504
|
last = blocks.last
|
|
436
|
-
return blocks if last.is_a?(Hash) && (last[:cache_control] || last['cache_control'])
|
|
437
505
|
return blocks unless last.is_a?(Hash)
|
|
506
|
+
return blocks if last[:cache_control] || last['cache_control']
|
|
438
507
|
|
|
439
508
|
blocks[-1] = last.merge(cache_control: prompt_cache_control(caching, ttl:))
|
|
440
509
|
blocks
|
|
@@ -445,13 +514,11 @@ module RubyLLM
|
|
|
445
514
|
ttl ||= options[:ttl]
|
|
446
515
|
|
|
447
516
|
{ type: CACHE_CONTROL_TYPE }.tap do |control|
|
|
448
|
-
control[:ttl] = ttl if ttl
|
|
517
|
+
control[:ttl] = Protocol::CacheLifetime.render(ttl) if ttl
|
|
449
518
|
end
|
|
450
519
|
end
|
|
451
520
|
|
|
452
521
|
def prompt_cache_options(caching)
|
|
453
|
-
return {} unless caching
|
|
454
|
-
|
|
455
522
|
options = caching.to_h.transform_keys(&:to_sym)
|
|
456
523
|
unsupported = options.keys - PROMPT_CACHE_OPTIONS
|
|
457
524
|
return options if unsupported.empty?
|
|
@@ -460,6 +527,8 @@ module RubyLLM
|
|
|
460
527
|
"Anthropic prompt caching accepts :ttl, got #{format_cache_option_keys(unsupported)}"
|
|
461
528
|
end
|
|
462
529
|
|
|
530
|
+
def cache_option_names = PROMPT_CACHE_OPTIONS
|
|
531
|
+
|
|
463
532
|
def format_cache_option_keys(keys)
|
|
464
533
|
keys.map { |key| ":#{key}" }.join(', ')
|
|
465
534
|
end
|
|
@@ -499,9 +568,10 @@ module RubyLLM
|
|
|
499
568
|
# Effort alone never turns thinking on; Claude needs a thinking block.
|
|
500
569
|
# Generations that take a budget get one sized from the effort with
|
|
501
570
|
# the levels Bedrock publishes. Generations without a budget option
|
|
502
|
-
# think adaptively
|
|
571
|
+
# think adaptively, and so does a bare switch, except on generations
|
|
572
|
+
# that only take a budget, which get the smallest one.
|
|
503
573
|
def thinking_mode(thinking, model, effort, max_tokens)
|
|
504
|
-
return
|
|
574
|
+
return toggle_mode(model) if thinking.enabled == true
|
|
505
575
|
|
|
506
576
|
budget = thinking.budget || effort_budget(effort, model, max_tokens)
|
|
507
577
|
mode = if budget
|
|
@@ -515,6 +585,13 @@ module RubyLLM
|
|
|
515
585
|
|
|
516
586
|
EFFORT_BUDGETS = { 'low' => 1024, 'medium' => 40_000, 'high' => 63_999 }.freeze
|
|
517
587
|
|
|
588
|
+
def toggle_mode(model)
|
|
589
|
+
budget = model.reasoning_option(:budget_tokens)
|
|
590
|
+
return { type: 'adaptive' } unless budget && !model.reasoning_option(:effort)
|
|
591
|
+
|
|
592
|
+
{ type: 'enabled', budget_tokens: [budget[:min].to_i, EFFORT_BUDGETS['low']].max }
|
|
593
|
+
end
|
|
594
|
+
|
|
518
595
|
def adaptive_thinking?(thinking, effort, model)
|
|
519
596
|
return true if thinking.display
|
|
520
597
|
return false unless effort
|
|
@@ -527,8 +604,6 @@ module RubyLLM
|
|
|
527
604
|
|
|
528
605
|
budget = EFFORT_BUDGETS.fetch(effort, EFFORT_BUDGETS['high'])
|
|
529
606
|
minimum = [model.reasoning_option(:budget_tokens)[:min].to_i, 1].max
|
|
530
|
-
return [budget, minimum].max unless max_tokens
|
|
531
|
-
|
|
532
607
|
budget.clamp(minimum, [max_tokens - 1, minimum].max)
|
|
533
608
|
end
|
|
534
609
|
|
|
@@ -7,12 +7,20 @@ module RubyLLM
|
|
|
7
7
|
class Files < Protocols::Files
|
|
8
8
|
BETA_HEADER = 'files-api-2025-04-14'
|
|
9
9
|
|
|
10
|
+
def rejected_options(operation)
|
|
11
|
+
operation == :upload ? %i[purpose uri] : super
|
|
12
|
+
end
|
|
13
|
+
|
|
10
14
|
private
|
|
11
15
|
|
|
12
16
|
def files_url
|
|
13
17
|
'v1/files'
|
|
14
18
|
end
|
|
15
19
|
|
|
20
|
+
def render_upload_fields(_attachment, expires_in: nil, **)
|
|
21
|
+
{ expires_in_seconds: expires_in }
|
|
22
|
+
end
|
|
23
|
+
|
|
16
24
|
def upload_headers(request)
|
|
17
25
|
file_headers(request)
|
|
18
26
|
end
|
|
@@ -28,6 +36,7 @@ module RubyLLM
|
|
|
28
36
|
filename: data['filename'],
|
|
29
37
|
byte_size: data['size_bytes'],
|
|
30
38
|
created_at: timestamp(data['created_at']),
|
|
39
|
+
expires_at: timestamp(data['expires_at']),
|
|
31
40
|
mime_type: data['mime_type'],
|
|
32
41
|
downloadable: data['downloadable']
|
|
33
42
|
)
|
|
@@ -11,39 +11,39 @@ module RubyLLM
|
|
|
11
11
|
attachment.provider_file? || %i[image pdf text].include?(attachment.type)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def
|
|
14
|
+
def render_content(content, attachments = [], citations: false)
|
|
15
15
|
parts = []
|
|
16
|
-
parts <<
|
|
16
|
+
parts << render_text(content) unless content.nil? || content.empty?
|
|
17
17
|
|
|
18
18
|
attachments.each do |attachment|
|
|
19
19
|
raise UnsupportedAttachmentError, attachment.mime_type unless supported_attachment?(attachment)
|
|
20
20
|
|
|
21
21
|
if attachment.provider_file?
|
|
22
|
-
parts <<
|
|
22
|
+
parts << render_provider_file(attachment, citations: citations)
|
|
23
23
|
next
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
case attachment.type
|
|
27
27
|
when :image
|
|
28
|
-
parts <<
|
|
28
|
+
parts << render_image(attachment)
|
|
29
29
|
when :pdf
|
|
30
|
-
parts <<
|
|
30
|
+
parts << render_pdf(attachment, citations: citations)
|
|
31
31
|
when :text
|
|
32
|
-
parts << (citations ?
|
|
32
|
+
parts << (citations ? render_text_document(attachment) : render_text_file(attachment))
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
parts
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def render_text(text)
|
|
40
40
|
{
|
|
41
41
|
type: 'text',
|
|
42
42
|
text: text
|
|
43
43
|
}
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
def
|
|
46
|
+
def render_image(image)
|
|
47
47
|
if image.url?
|
|
48
48
|
{
|
|
49
49
|
type: 'image',
|
|
@@ -64,8 +64,8 @@ module RubyLLM
|
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def
|
|
68
|
-
return
|
|
67
|
+
def render_provider_file(file, citations: false)
|
|
68
|
+
return render_provider_image(file) if file.image?
|
|
69
69
|
|
|
70
70
|
document = {
|
|
71
71
|
type: 'document',
|
|
@@ -78,7 +78,7 @@ module RubyLLM
|
|
|
78
78
|
document
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
def
|
|
81
|
+
def render_provider_image(image)
|
|
82
82
|
{
|
|
83
83
|
type: 'image',
|
|
84
84
|
source: {
|
|
@@ -88,7 +88,7 @@ module RubyLLM
|
|
|
88
88
|
}
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
def
|
|
91
|
+
def render_pdf(pdf, citations: false)
|
|
92
92
|
document = {
|
|
93
93
|
type: 'document',
|
|
94
94
|
source: pdf_source(pdf)
|
|
@@ -113,7 +113,7 @@ module RubyLLM
|
|
|
113
113
|
end
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
-
def
|
|
116
|
+
def render_text_file(text_file)
|
|
117
117
|
{
|
|
118
118
|
type: 'text',
|
|
119
119
|
text: text_file.for_llm
|
|
@@ -121,7 +121,7 @@ module RubyLLM
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
# Text attachments become citable documents when citations are enabled.
|
|
124
|
-
def
|
|
124
|
+
def render_text_document(text_file)
|
|
125
125
|
document = {
|
|
126
126
|
type: 'document',
|
|
127
127
|
source: {
|
|
@@ -70,28 +70,34 @@ module RubyLLM
|
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
-
def
|
|
73
|
+
def parse_model_id(data)
|
|
74
74
|
data.dig('message', 'model')
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
def
|
|
78
|
-
message_usage(data)['input_tokens']
|
|
77
|
+
def parse_input_tokens(data)
|
|
78
|
+
message_usage(data)['input_tokens'] || delta_usage(data)['input_tokens']
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
def
|
|
81
|
+
def parse_output_tokens(data)
|
|
82
82
|
message_usage(data)['output_tokens'] || delta_usage(data)['output_tokens']
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
-
def
|
|
86
|
-
message_usage(data)
|
|
87
|
-
delta_usage(data).dig('output_tokens_details', 'thinking_tokens')
|
|
85
|
+
def parse_thinking_tokens(data)
|
|
86
|
+
usage_thinking_tokens(message_usage(data)) || usage_thinking_tokens(delta_usage(data))
|
|
88
87
|
end
|
|
89
88
|
|
|
90
|
-
def
|
|
89
|
+
def usage_thinking_tokens(usage)
|
|
90
|
+
usage.dig('output_tokens_details', 'thinking_tokens') ||
|
|
91
|
+
usage.dig('output_tokens_details', 'reasoning_tokens') ||
|
|
92
|
+
usage['thinking_tokens'] ||
|
|
93
|
+
usage['reasoning_tokens']
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_cache_read_tokens(data)
|
|
91
97
|
message_usage(data)['cache_read_input_tokens'] || delta_usage(data)['cache_read_input_tokens']
|
|
92
98
|
end
|
|
93
99
|
|
|
94
|
-
def
|
|
100
|
+
def parse_cache_write_tokens(data)
|
|
95
101
|
direct = message_usage(data)['cache_creation_input_tokens'] ||
|
|
96
102
|
delta_usage(data)['cache_creation_input_tokens']
|
|
97
103
|
return direct if direct
|
|
@@ -102,7 +108,7 @@ module RubyLLM
|
|
|
102
108
|
breakdown.values.compact.sum
|
|
103
109
|
end
|
|
104
110
|
|
|
105
|
-
def
|
|
111
|
+
def parse_cache_write_by_ttl(data)
|
|
106
112
|
breakdown = message_usage(data)['cache_creation'] || delta_usage(data)['cache_creation']
|
|
107
113
|
return unless breakdown.is_a?(Hash)
|
|
108
114
|
|