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
|
@@ -5,10 +5,27 @@ module RubyLLM
|
|
|
5
5
|
class Responses
|
|
6
6
|
# Chat methods of the OpenAI Responses API
|
|
7
7
|
module Chat
|
|
8
|
+
def native_tool_namespace?(_namespace) = true
|
|
9
|
+
|
|
10
|
+
# Tool search loads a deferred function declared outside any
|
|
11
|
+
# namespace as a namespace named after the function, and the model
|
|
12
|
+
# calls it under that namespace.
|
|
13
|
+
def searched_tool_namespace?(call, tools) # :nodoc:
|
|
14
|
+
function = tools[call.name.to_sym]
|
|
15
|
+
return false if function.nil? || function.namespace || call.namespace != call.name.to_s
|
|
16
|
+
|
|
17
|
+
tools.each_value.none? { |tool| tool.namespace == call.namespace }
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
def completion_url
|
|
9
21
|
'responses'
|
|
10
22
|
end
|
|
11
23
|
|
|
24
|
+
def wire_parameters
|
|
25
|
+
super.merge('reasoning' => :thinking, 'reasoning.effort' => :thinking,
|
|
26
|
+
'max_output_tokens' => :max_output_tokens)
|
|
27
|
+
end
|
|
28
|
+
|
|
12
29
|
OPENAI_INLINE_FILE_LIMIT = 50 * 1024 * 1024
|
|
13
30
|
PROMPT_CACHE_OPTIONS = %i[key ttl mode retention].freeze
|
|
14
31
|
|
|
@@ -23,8 +40,8 @@ module RubyLLM
|
|
|
23
40
|
# to ride back in the response. xAI only encrypts it when asked.
|
|
24
41
|
payload = {
|
|
25
42
|
model: model.id,
|
|
26
|
-
input:
|
|
27
|
-
instructions:
|
|
43
|
+
input: render_input(messages, caching:),
|
|
44
|
+
instructions: render_instructions(messages, caching:),
|
|
28
45
|
stream: stream,
|
|
29
46
|
store: false,
|
|
30
47
|
include: ['reasoning.encrypted_content']
|
|
@@ -34,8 +51,12 @@ module RubyLLM
|
|
|
34
51
|
payload[:max_output_tokens] = max_output_tokens unless max_output_tokens.nil?
|
|
35
52
|
|
|
36
53
|
if tools.any?
|
|
37
|
-
payload[:tools] = tools
|
|
38
|
-
payload[:
|
|
54
|
+
payload[:tools] = render_tools(tools)
|
|
55
|
+
payload[:input] = namespace_searched_calls(payload[:input], tools)
|
|
56
|
+
unless tool_prefs[:choice].nil?
|
|
57
|
+
Protocol::ToolNamespaces.validate_tool_choice(tools, tool_prefs[:choice])
|
|
58
|
+
payload[:tool_choice] = build_tool_choice(tool_prefs[:choice])
|
|
59
|
+
end
|
|
39
60
|
payload[:parallel_tool_calls] = tool_prefs[:calls] == :many unless tool_prefs[:calls].nil?
|
|
40
61
|
end
|
|
41
62
|
|
|
@@ -50,7 +71,8 @@ module RubyLLM
|
|
|
50
71
|
end
|
|
51
72
|
|
|
52
73
|
def parse_completion_body(data, raw:)
|
|
53
|
-
|
|
74
|
+
raise_failed_response(data) if data.dig('error', 'message')
|
|
75
|
+
raise Error.new('The provider reported a failed response', response: raw) if data['status'] == 'failed'
|
|
54
76
|
|
|
55
77
|
output = data['output'] || []
|
|
56
78
|
content = parse_output_text(output)
|
|
@@ -69,14 +91,19 @@ module RubyLLM
|
|
|
69
91
|
tool_calls: parse_pending_tool_calls(output, response: raw, finish_reason: finish_reason),
|
|
70
92
|
server_tool_calls: server_tool_calls,
|
|
71
93
|
raw_content: keep_raw_output?(output, server_tool_calls) ? output : nil,
|
|
94
|
+
phase: parse_phase(output),
|
|
72
95
|
model: data['model'],
|
|
73
96
|
raw: raw,
|
|
74
97
|
finish_reason: finish_reason,
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
content_filters: ChatCompletions::ContentFilters.parse_content_filters(data),
|
|
99
|
+
**parse_completion_usage(data)
|
|
77
100
|
)
|
|
78
101
|
end
|
|
79
102
|
|
|
103
|
+
def parse_completion_usage(data)
|
|
104
|
+
parse_usage(data['usage'] || {}).merge(server_tool_use: parse_server_tool_use(data))
|
|
105
|
+
end
|
|
106
|
+
|
|
80
107
|
# context_management is an OpenAI parameter that only OpenAI's own
|
|
81
108
|
# endpoints serve; the other services on this wire format reject it.
|
|
82
109
|
COMPACTION_PROVIDERS = %w[openai azure].freeze
|
|
@@ -109,8 +136,35 @@ module RubyLLM
|
|
|
109
136
|
|
|
110
137
|
PORTABLE_OUTPUT_ITEM_TYPES = (%w[message function_call] + TOOL_SEARCH_ITEM_TYPES).freeze
|
|
111
138
|
|
|
139
|
+
# A message carries one thinking signature and one phase, so output
|
|
140
|
+
# with more reasoning items or phases than that only replays whole
|
|
141
|
+
# from its raw items.
|
|
112
142
|
def keep_raw_output?(output, server_tool_calls)
|
|
113
|
-
server_tool_calls.any? || output.any? { |item| namespaced_call?(item) }
|
|
143
|
+
server_tool_calls.any? || output.any? { |item| namespaced_call?(item) } ||
|
|
144
|
+
output.count { |item| item['type'] == 'reasoning' } > 1 || message_phases(output).uniq.size > 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parse_phase(output)
|
|
148
|
+
message_phases(output).compact.last
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Structured output lives in the final_answer items, and a turn is
|
|
152
|
+
# read whole only when none of its items carries a phase, as in
|
|
153
|
+
# OpenAI's SDKs. Commentary, unphased text beside a final answer,
|
|
154
|
+
# and phases OpenAI adds later are not the answer. A turn whose
|
|
155
|
+
# items carry different phases keeps them in raw_content; otherwise
|
|
156
|
+
# its one phase tells.
|
|
157
|
+
def parse_answer_text(message)
|
|
158
|
+
output = message.raw_content
|
|
159
|
+
if output.is_a?(Array) && message_phases(output).any?
|
|
160
|
+
parse_output_text(output.select { |item| item['phase'] == 'final_answer' }) || ''
|
|
161
|
+
elsif !message.phase.nil? && message.phase != 'final_answer'
|
|
162
|
+
''
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def message_phases(output)
|
|
167
|
+
output.select { |item| item['type'] == 'message' }.map { |item| item['phase'] }
|
|
114
168
|
end
|
|
115
169
|
|
|
116
170
|
def namespaced_call?(item)
|
|
@@ -143,12 +197,26 @@ module RubyLLM
|
|
|
143
197
|
end
|
|
144
198
|
|
|
145
199
|
# Result payloads differ by item type; compaction items carry an
|
|
146
|
-
# opaque encrypted_content instead of a readable result
|
|
200
|
+
# opaque encrypted_content instead of a readable result, and a web
|
|
201
|
+
# search keeps the sources it found in its action.
|
|
147
202
|
SERVER_TOOL_RESULT_KEYS = %w[result results outputs output encrypted_content].freeze
|
|
148
203
|
|
|
149
204
|
def server_tool_result(item)
|
|
150
205
|
key = SERVER_TOOL_RESULT_KEYS.find { |candidate| item[candidate] }
|
|
151
|
-
item[key] if key
|
|
206
|
+
return item[key] if key
|
|
207
|
+
|
|
208
|
+
action = item['action']
|
|
209
|
+
action['sources'] if action.is_a?(Hash)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Only what a search returned. The address in an open_page or
|
|
213
|
+
# find_in_page action is the model's own choice and need not be a
|
|
214
|
+
# result, so text the model read could steer it there.
|
|
215
|
+
def parse_web_sources(message)
|
|
216
|
+
searches = message.server_tool_calls.select { |call| call.type == 'web_search_call' }
|
|
217
|
+
searches.flat_map { |call| Array(call.raw.dig('action', 'sources')) }.filter_map do |source|
|
|
218
|
+
WebSource.new(url: source['url'], title: source['title']) if source['url']
|
|
219
|
+
end
|
|
152
220
|
end
|
|
153
221
|
|
|
154
222
|
def parse_citations(data, output, content)
|
|
@@ -158,13 +226,15 @@ module RubyLLM
|
|
|
158
226
|
|
|
159
227
|
def parse_output_citations(output, content)
|
|
160
228
|
offset = 0
|
|
161
|
-
output.
|
|
229
|
+
text_messages(output).each_with_index.flat_map do |message, index|
|
|
230
|
+
offset += MESSAGE_SEPARATOR.length if index.positive?
|
|
162
231
|
Array(message['content']).flat_map do |part|
|
|
163
232
|
key = MESSAGE_TEXT_KEYS[part['type']]
|
|
164
233
|
next [] unless key
|
|
165
234
|
|
|
166
|
-
|
|
167
|
-
|
|
235
|
+
text, markers = CitationMarkers.strip(part[key].to_s)
|
|
236
|
+
citations = offset_citations(parse_annotations(part['annotations'], nil), offset, content, markers)
|
|
237
|
+
offset += text.length
|
|
168
238
|
citations
|
|
169
239
|
end
|
|
170
240
|
end
|
|
@@ -195,10 +265,10 @@ module RubyLLM
|
|
|
195
265
|
)
|
|
196
266
|
end
|
|
197
267
|
|
|
198
|
-
def offset_citations(citations, offset, content)
|
|
268
|
+
def offset_citations(citations, offset, content, markers = CitationMarkers.new)
|
|
199
269
|
citations.map do |citation|
|
|
200
|
-
start_index = citation.start_index && (citation.start_index + offset)
|
|
201
|
-
end_index = citation.end_index && (citation.end_index + offset)
|
|
270
|
+
start_index = citation.start_index && (markers.shift(citation.start_index) + offset)
|
|
271
|
+
end_index = citation.end_index && (markers.shift(citation.end_index) + offset)
|
|
202
272
|
|
|
203
273
|
Citation.new(citation.to_h.merge(
|
|
204
274
|
start_index: start_index,
|
|
@@ -229,10 +299,14 @@ module RubyLLM
|
|
|
229
299
|
|
|
230
300
|
{}.tap do |cache_options|
|
|
231
301
|
cache_options[:mode] = options[:mode] if options[:mode]
|
|
232
|
-
cache_options[:ttl] = ttl if ttl
|
|
302
|
+
cache_options[:ttl] = render_cache_lifetime(ttl) if ttl
|
|
233
303
|
end
|
|
234
304
|
end
|
|
235
305
|
|
|
306
|
+
def render_cache_lifetime(ttl)
|
|
307
|
+
Protocol::CacheLifetime.render(ttl)
|
|
308
|
+
end
|
|
309
|
+
|
|
236
310
|
def retention_ttl(retention)
|
|
237
311
|
return unless retention
|
|
238
312
|
|
|
@@ -252,6 +326,8 @@ module RubyLLM
|
|
|
252
326
|
"Responses prompt caching accepts :key, :ttl, and :mode, got #{format_cache_option_keys(unsupported)}"
|
|
253
327
|
end
|
|
254
328
|
|
|
329
|
+
def cache_option_names = PROMPT_CACHE_OPTIONS
|
|
330
|
+
|
|
255
331
|
def format_cache_option_keys(keys)
|
|
256
332
|
keys.map { |key| ":#{key}" }.join(', ')
|
|
257
333
|
end
|
|
@@ -291,31 +367,31 @@ module RubyLLM
|
|
|
291
367
|
# The +instructions+ parameter is a plain string and cannot carry a
|
|
292
368
|
# breakpoint marker or a file. When any system message needs one, every
|
|
293
369
|
# system message rides along as an input item, so their order holds.
|
|
294
|
-
def
|
|
370
|
+
def render_instructions(messages, caching: nil)
|
|
295
371
|
system = messages.select { |msg| msg.role == :system }
|
|
296
372
|
return if system.empty? || system_input_items?(system, caching:)
|
|
297
373
|
|
|
298
374
|
system.map { |msg| msg.content.to_s }.join("\n\n")
|
|
299
375
|
end
|
|
300
376
|
|
|
301
|
-
def
|
|
377
|
+
def render_input(messages, caching: nil)
|
|
302
378
|
system, conversation = messages.partition { |msg| msg.role == :system }
|
|
303
379
|
system_items = if system_input_items?(system, caching:)
|
|
304
|
-
system.flat_map { |msg| [
|
|
380
|
+
system.flat_map { |msg| [render_item(msg, caching:)].flatten(1) }
|
|
305
381
|
else
|
|
306
382
|
[]
|
|
307
383
|
end
|
|
308
384
|
|
|
309
|
-
system_items +
|
|
385
|
+
system_items + render_conversation(conversation, caching:)
|
|
310
386
|
end
|
|
311
387
|
|
|
312
|
-
def
|
|
388
|
+
def render_conversation(messages, caching: nil)
|
|
313
389
|
messages.each_with_object([]) do |message, input|
|
|
314
390
|
raw = message.raw_content
|
|
315
391
|
if raw.is_a?(Hash) && raw['object'] == 'response.compaction'
|
|
316
392
|
input.replace(raw.fetch('output'))
|
|
317
393
|
else
|
|
318
|
-
input.concat([
|
|
394
|
+
input.concat([render_item(message, caching:)].flatten(1))
|
|
319
395
|
end
|
|
320
396
|
end
|
|
321
397
|
end
|
|
@@ -328,17 +404,17 @@ module RubyLLM
|
|
|
328
404
|
msg.role == :system && ((caching != false && msg.cache_until_here?) || msg.attachments.any?)
|
|
329
405
|
end
|
|
330
406
|
|
|
331
|
-
def
|
|
407
|
+
def render_item(msg, caching: nil)
|
|
332
408
|
case msg.role
|
|
333
409
|
when :system
|
|
334
|
-
item = { role: 'system', content:
|
|
410
|
+
item = { role: 'system', content: render_content(msg.content, msg.attachments) }
|
|
335
411
|
caching != false && msg.cache_until_here? ? inject_cache_breakpoint(item) : item
|
|
336
412
|
when :tool
|
|
337
|
-
|
|
413
|
+
render_tool_items(msg)
|
|
338
414
|
when :assistant
|
|
339
|
-
|
|
415
|
+
render_assistant_items(msg)
|
|
340
416
|
else
|
|
341
|
-
item = { role: 'user', content:
|
|
417
|
+
item = { role: 'user', content: render_content(msg.content, msg.attachments) }
|
|
342
418
|
caching != false && msg.cache_until_here? ? inject_cache_breakpoint(item) : item
|
|
343
419
|
end
|
|
344
420
|
end
|
|
@@ -360,37 +436,44 @@ module RubyLLM
|
|
|
360
436
|
|
|
361
437
|
# Function call outputs are text-only on the wire, so tool attachments
|
|
362
438
|
# ride a user item spliced in right after the result.
|
|
363
|
-
def
|
|
439
|
+
def render_tool_items(msg)
|
|
364
440
|
return msg.raw_content if msg.raw_content
|
|
365
441
|
|
|
366
442
|
items = [{
|
|
367
443
|
type: 'function_call_output',
|
|
368
|
-
call_id: msg.
|
|
369
|
-
output:
|
|
444
|
+
call_id: msg.provider_call_id,
|
|
445
|
+
output: render_content(msg.content)
|
|
370
446
|
}]
|
|
371
447
|
|
|
372
448
|
if msg.attachments.any?
|
|
373
|
-
|
|
374
|
-
parts.concat(format_content(nil, msg.attachments))
|
|
375
|
-
items << { role: 'user', content: parts }
|
|
449
|
+
items << render_attachment_item("Attachments from tool call #{msg.provider_call_id}:", msg.attachments)
|
|
376
450
|
end
|
|
377
451
|
|
|
378
452
|
items
|
|
379
453
|
end
|
|
380
454
|
|
|
381
|
-
def
|
|
455
|
+
def render_attachment_item(label, attachments)
|
|
456
|
+
parts = [{ type: 'input_text', text: label }]
|
|
457
|
+
parts.concat(render_content(nil, attachments))
|
|
458
|
+
{ role: 'user', content: parts }
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Assistant output is text-only on the wire, so the attachments of a
|
|
462
|
+
# replayed assistant turn ride a user item spliced in after it.
|
|
463
|
+
def render_assistant_items(msg)
|
|
382
464
|
# Turns that used server tools replay their output items verbatim,
|
|
383
465
|
# reasoning and tool results included, as stateless chaining expects.
|
|
384
|
-
return msg
|
|
466
|
+
return replayed_output(msg) if msg.raw_content
|
|
385
467
|
|
|
386
468
|
items = []
|
|
387
|
-
items <<
|
|
388
|
-
items <<
|
|
389
|
-
items.concat(
|
|
469
|
+
items << render_reasoning_item(msg.thinking) if own_signature(msg)
|
|
470
|
+
items << render_output_item(msg) unless empty_content?(msg.content)
|
|
471
|
+
items.concat(render_function_call_items(msg.tool_calls)) if msg.tool_call?
|
|
472
|
+
items << render_attachment_item('Attachments from the assistant:', msg.attachments) if msg.attachments.any?
|
|
390
473
|
items
|
|
391
474
|
end
|
|
392
475
|
|
|
393
|
-
def
|
|
476
|
+
def render_reasoning_item(thinking)
|
|
394
477
|
{
|
|
395
478
|
type: 'reasoning',
|
|
396
479
|
summary: thinking.text ? [{ type: 'summary_text', text: thinking.text }] : [],
|
|
@@ -398,19 +481,25 @@ module RubyLLM
|
|
|
398
481
|
}
|
|
399
482
|
end
|
|
400
483
|
|
|
401
|
-
def
|
|
402
|
-
|
|
484
|
+
def replayed_output(msg)
|
|
485
|
+
Protocol::ReplayedArguments.blank(msg.raw_content, msg,
|
|
486
|
+
id: %w[call_id id], arguments: 'arguments', empty: '{}')
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def render_function_call_items(tool_calls)
|
|
490
|
+
tool_calls.map do |tc|
|
|
403
491
|
{
|
|
404
492
|
type: 'function_call',
|
|
405
|
-
call_id: tc.
|
|
493
|
+
call_id: tc.provider_call_id,
|
|
406
494
|
name: tc.name,
|
|
495
|
+
namespace: tc.namespace,
|
|
407
496
|
arguments: JSON.generate(tc.arguments)
|
|
408
|
-
}
|
|
497
|
+
}.compact
|
|
409
498
|
end
|
|
410
499
|
end
|
|
411
500
|
|
|
412
|
-
def
|
|
413
|
-
[{ type: 'output_text', text: msg.content }]
|
|
501
|
+
def render_output_item(msg)
|
|
502
|
+
{ role: 'assistant', content: [{ type: 'output_text', text: msg.content }], phase: msg.phase }.compact
|
|
414
503
|
end
|
|
415
504
|
|
|
416
505
|
def empty_content?(content)
|
|
@@ -418,53 +507,54 @@ module RubyLLM
|
|
|
418
507
|
end
|
|
419
508
|
|
|
420
509
|
MESSAGE_TEXT_KEYS = { 'output_text' => 'text', 'refusal' => 'refusal' }.freeze
|
|
510
|
+
MESSAGE_SEPARATOR = "\n\n"
|
|
421
511
|
|
|
422
512
|
def parse_output_text(output)
|
|
423
|
-
texts = output.
|
|
513
|
+
texts = text_messages(output).map do |message|
|
|
424
514
|
Array(message['content']).filter_map do |part|
|
|
425
515
|
key = MESSAGE_TEXT_KEYS[part['type']]
|
|
426
|
-
part[key] if key
|
|
427
|
-
end
|
|
516
|
+
CitationMarkers.strip(part[key]).first if key && part[key]
|
|
517
|
+
end.join
|
|
428
518
|
end
|
|
429
519
|
|
|
430
|
-
texts.empty? ? nil : texts.join
|
|
520
|
+
texts.empty? ? nil : texts.join(MESSAGE_SEPARATOR)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def text_messages(output)
|
|
524
|
+
output.select do |item|
|
|
525
|
+
item['type'] == 'message' && Array(item['content']).any? { |part| MESSAGE_TEXT_KEYS.key?(part['type']) }
|
|
526
|
+
end
|
|
431
527
|
end
|
|
432
528
|
|
|
433
529
|
FINISH_REASONS = {
|
|
434
|
-
'completed' => :stop, '
|
|
530
|
+
'completed' => :stop, 'function_call' => :tool_calls,
|
|
531
|
+
'max_output_tokens' => :max_tokens, 'content_filter' => :content_filter
|
|
435
532
|
}.freeze
|
|
436
533
|
|
|
437
534
|
def finish_reasons = FINISH_REASONS
|
|
438
535
|
|
|
439
536
|
def parse_finish_reason(data)
|
|
440
|
-
|
|
441
|
-
normalize_finish_reason(reason)
|
|
537
|
+
normalize_finish_reason(data.dig('incomplete_details', 'reason') || parse_completed_reason(data))
|
|
442
538
|
end
|
|
443
539
|
|
|
444
|
-
def
|
|
445
|
-
|
|
446
|
-
return nil if calls.empty?
|
|
540
|
+
def parse_completed_reason(data)
|
|
541
|
+
return unless data['status'] == 'completed'
|
|
447
542
|
|
|
448
|
-
|
|
449
|
-
arguments = call['arguments']
|
|
450
|
-
|
|
451
|
-
[
|
|
452
|
-
call['call_id'],
|
|
453
|
-
ToolCall.new(
|
|
454
|
-
id: call['call_id'],
|
|
455
|
-
name: call['name'],
|
|
456
|
-
arguments: parse_function_call_arguments(arguments, response: response, finish_reason: finish_reason)
|
|
457
|
-
)
|
|
458
|
-
]
|
|
459
|
-
end
|
|
543
|
+
Array(data['output']).any? { |item| item['type'] == 'function_call' } ? 'function_call' : 'completed'
|
|
460
544
|
end
|
|
461
545
|
|
|
462
|
-
def
|
|
463
|
-
|
|
546
|
+
def parse_function_calls(output, response: nil, finish_reason: nil)
|
|
547
|
+
calls = output.select { |item| item['type'] == 'function_call' }
|
|
548
|
+
return nil if calls.empty?
|
|
464
549
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
550
|
+
ToolCall.index(calls.map do |call|
|
|
551
|
+
ToolCall.new(
|
|
552
|
+
provider_call_id: call['call_id'],
|
|
553
|
+
name: call['name'],
|
|
554
|
+
namespace: call['namespace'],
|
|
555
|
+
**ToolCall.parse_arguments(call['arguments'], response:, finish_reason:)
|
|
556
|
+
)
|
|
557
|
+
end, by: :provider_call_id)
|
|
468
558
|
end
|
|
469
559
|
|
|
470
560
|
def parse_reasoning_summary(output)
|
|
@@ -472,14 +562,14 @@ module RubyLLM
|
|
|
472
562
|
Array(item['summary']).filter_map { |part| part['text'] }
|
|
473
563
|
end
|
|
474
564
|
|
|
475
|
-
texts.empty? ? nil : texts.join("\n")
|
|
565
|
+
texts.empty? ? nil : texts.join("\n\n")
|
|
476
566
|
end
|
|
477
567
|
|
|
478
568
|
def parse_reasoning_signature(output)
|
|
479
569
|
output.find { |item| item['type'] == 'reasoning' }&.dig('encrypted_content')
|
|
480
570
|
end
|
|
481
571
|
|
|
482
|
-
def
|
|
572
|
+
def provider_file_references?
|
|
483
573
|
true
|
|
484
574
|
end
|
|
485
575
|
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Responses
|
|
6
|
+
# Removes the citation markers OpenAI's models write into their text:
|
|
7
|
+
# U+E200, then "cite" and the sources and line locators it cites, each
|
|
8
|
+
# after a U+E202, then U+E201. Only OpenAI's own tools turn them into annotations, so
|
|
9
|
+
# a model that writes them without those tools leaves them in the
|
|
10
|
+
# answer. Model output is untrusted: each character is read once, and
|
|
11
|
+
# an unclosed marker holds back at most MAX_LENGTH characters. A space
|
|
12
|
+
# before a removed marker goes too when punctuation, a space, or the end
|
|
13
|
+
# of the text follows. Only a marker holds a space past the end of a
|
|
14
|
+
# stream chunk, so text keeps its order across content parts. #shift
|
|
15
|
+
# maps an index into the text as sent onto the text without markers.
|
|
16
|
+
class CitationMarkers # :nodoc:
|
|
17
|
+
OPEN = "\u{E200}"
|
|
18
|
+
CLOSE = "\u{E201}"
|
|
19
|
+
SEPARATOR = "\u{E202}"
|
|
20
|
+
MARKS = [OPEN, CLOSE, SEPARATOR].freeze
|
|
21
|
+
CITE = 'cite'
|
|
22
|
+
REFERENCE = /\A[\w-]*\z/
|
|
23
|
+
SPACE = ' '
|
|
24
|
+
BOUNDARY = /[[:space:].,;:!?)\]}]/
|
|
25
|
+
MAX_LENGTH = 256
|
|
26
|
+
|
|
27
|
+
def self.strip(text)
|
|
28
|
+
markers = new
|
|
29
|
+
["#{markers.feed(text)}#{markers.finish}", markers]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize
|
|
33
|
+
@held = nil
|
|
34
|
+
@seen = 0
|
|
35
|
+
@removed = []
|
|
36
|
+
@space_at = nil
|
|
37
|
+
@after_marker = false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def feed(text)
|
|
41
|
+
return text if text.nil? || text.empty?
|
|
42
|
+
|
|
43
|
+
output = +''
|
|
44
|
+
text.each_char { |char| read(char, output) }
|
|
45
|
+
settle_space(output, boundary: false) unless @held || @after_marker
|
|
46
|
+
output.empty? ? nil : output
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def finish
|
|
50
|
+
output = +''
|
|
51
|
+
held = take_held
|
|
52
|
+
if held && !citation_prefix?(held)
|
|
53
|
+
release(held, output)
|
|
54
|
+
else
|
|
55
|
+
drop(held) if held
|
|
56
|
+
settle_space(output, boundary: true)
|
|
57
|
+
end
|
|
58
|
+
output.empty? ? nil : output
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def shift(index)
|
|
62
|
+
return index if index.nil?
|
|
63
|
+
|
|
64
|
+
index - @removed.sum { |position, length| position < index ? length : 0 }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def read(char, output)
|
|
70
|
+
if @held
|
|
71
|
+
hold(char, output)
|
|
72
|
+
elsif char == OPEN
|
|
73
|
+
open_marker
|
|
74
|
+
elsif MARKS.include?(char)
|
|
75
|
+
@removed << [@seen, 1]
|
|
76
|
+
else
|
|
77
|
+
write(char, output)
|
|
78
|
+
end
|
|
79
|
+
@seen += 1
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def write(char, output)
|
|
83
|
+
settle_space(output, boundary: char.match?(BOUNDARY))
|
|
84
|
+
if char == SPACE
|
|
85
|
+
@space_at = @seen
|
|
86
|
+
else
|
|
87
|
+
output << char
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def settle_space(output, boundary:)
|
|
92
|
+
if @space_at && @after_marker && boundary
|
|
93
|
+
@removed << [@space_at, 1]
|
|
94
|
+
elsif @space_at
|
|
95
|
+
output << SPACE
|
|
96
|
+
end
|
|
97
|
+
@space_at = nil
|
|
98
|
+
@after_marker = false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def open_marker
|
|
102
|
+
@held = +OPEN
|
|
103
|
+
@held_at = @seen
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def hold(char, output)
|
|
107
|
+
if char == OPEN
|
|
108
|
+
release(@held, output)
|
|
109
|
+
open_marker
|
|
110
|
+
else
|
|
111
|
+
@held << char
|
|
112
|
+
return close_marker(output) if char == CLOSE
|
|
113
|
+
|
|
114
|
+
release(take_held, output) if @held.length > MAX_LENGTH
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def close_marker(output)
|
|
119
|
+
held = take_held
|
|
120
|
+
complete_marker?(held.delete_suffix(CLOSE)) ? drop(held) : release(held, output)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def citation_prefix?(held)
|
|
124
|
+
CITE.start_with?(held.delete_prefix(OPEN)) || marker?(held)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def complete_marker?(held)
|
|
128
|
+
marker?(held) && held.split(SEPARATOR, -1).drop(1).none?(&:empty?)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def marker?(held)
|
|
132
|
+
word, *references = held.delete_prefix(OPEN).split(SEPARATOR, -1)
|
|
133
|
+
return false unless word.to_s.end_with?(CITE) && word.each_char.all? { |char| alphanumeric?(char) }
|
|
134
|
+
|
|
135
|
+
references.all? { |reference| reference.match?(REFERENCE) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def alphanumeric?(char)
|
|
139
|
+
char.match?(/[[:alnum:]]/)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def take_held
|
|
143
|
+
held = @held
|
|
144
|
+
@held = nil
|
|
145
|
+
held
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def drop(held)
|
|
149
|
+
@removed << [@held_at, held.length]
|
|
150
|
+
@after_marker = true
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def release(held, output)
|
|
155
|
+
settle_space(output, boundary: false)
|
|
156
|
+
held.each_char.with_index do |char, index|
|
|
157
|
+
if MARKS.include?(char)
|
|
158
|
+
@removed << [@held_at + index, 1]
|
|
159
|
+
else
|
|
160
|
+
output << char
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -10,8 +10,8 @@ module RubyLLM
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def render_compaction_payload(messages)
|
|
13
|
-
{ model: model.id, input:
|
|
14
|
-
instructions:
|
|
13
|
+
{ model: model.id, input: render_input(messages, caching: false),
|
|
14
|
+
instructions: render_instructions(messages, caching: false) }.compact
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def parse_compaction_response(response)
|