ruby_llm 2.1.1 → 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 +160 -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 +515 -274
- 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 +1593 -331
- 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 +50 -24
- 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 +127 -106
- 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 +15 -15
- 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 +1 -7
- 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 +5 -14
- 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 +213 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Protocols
|
|
8
|
+
module Bedrock
|
|
9
|
+
# AWS event stream framing for Bedrock's streaming APIs, ConverseStream
|
|
10
|
+
# and InvokeModelWithResponseStream. Each frame carries one JSON event,
|
|
11
|
+
# bare or base64-encoded under "bytes", or an error.
|
|
12
|
+
module EventStream
|
|
13
|
+
ErrorResponse = Struct.new(:body, :status)
|
|
14
|
+
|
|
15
|
+
EXCEPTION_STATUSES = {
|
|
16
|
+
'throttlingException' => 429,
|
|
17
|
+
'validationException' => 400,
|
|
18
|
+
'accessDeniedException' => 401,
|
|
19
|
+
'unrecognizedClientException' => 401,
|
|
20
|
+
'serviceUnavailableException' => 503
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Posts +body+ as an event stream request, yielding each decoded
|
|
26
|
+
# event Hash and raising the errors the stream reports. Returns the
|
|
27
|
+
# Faraday response.
|
|
28
|
+
def stream_events(url, body, additional_headers = {}, &handler)
|
|
29
|
+
decoder = event_stream_decoder
|
|
30
|
+
progress = {}
|
|
31
|
+
|
|
32
|
+
on_data = RubyLLM::Protocol::Streaming::FaradayHandlers.build(
|
|
33
|
+
faraday_v1: faraday_1?,
|
|
34
|
+
on_chunk: ->(chunk, _env) { parse_stream_chunk(decoder, chunk, progress, &handler) },
|
|
35
|
+
on_failed_response: ->(chunk, env) { handle_failed_stream(chunk, env) }
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
on_reset = -> { decoder = event_stream_decoder }
|
|
39
|
+
post_stream(url, body, on_data:, progress:, on_reset:) do |req|
|
|
40
|
+
req.headers.merge!(additional_headers) unless additional_headers.empty?
|
|
41
|
+
req.headers['Accept'] = 'application/vnd.amazon.eventstream'
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def event_stream_decoder
|
|
46
|
+
require 'aws-eventstream'
|
|
47
|
+
Aws::EventStream::Decoder.new
|
|
48
|
+
rescue LoadError
|
|
49
|
+
raise Error,
|
|
50
|
+
'The aws-eventstream gem is required for Bedrock streaming. ' \
|
|
51
|
+
'Please add it to your Gemfile: gem "aws-eventstream"'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The error body arrives in as many reads as the adapter hands out, so
|
|
55
|
+
# it accumulates on the env, which ErrorMiddleware clears per attempt.
|
|
56
|
+
def handle_failed_stream(chunk, env)
|
|
57
|
+
buffer = failed_stream_buffer(env)
|
|
58
|
+
buffer << chunk
|
|
59
|
+
error_response = env.merge(body: JSON.parse(buffer))
|
|
60
|
+
Transport::ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
61
|
+
rescue JSON::ParserError
|
|
62
|
+
RubyLLM.logger.debug { "Accumulating Bedrock stream error chunk: #{chunk}" }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def failed_stream_buffer(env)
|
|
66
|
+
(env[:streaming_state] ||= RubyLLM::Protocol::Streaming::StreamState.new).buffer
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_stream_chunk(decoder, raw_chunk, progress)
|
|
70
|
+
handle_non_eventstream_error_chunk(raw_chunk)
|
|
71
|
+
|
|
72
|
+
decode_events(decoder, raw_chunk).each do |event|
|
|
73
|
+
raise_stream_error(event) if stream_error_event?(event)
|
|
74
|
+
|
|
75
|
+
progress[:started] = true
|
|
76
|
+
yield event
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def handle_non_eventstream_error_chunk(raw_chunk)
|
|
81
|
+
text = raw_chunk.to_s
|
|
82
|
+
|
|
83
|
+
if text.start_with?('event: error')
|
|
84
|
+
payload = text.lines.find { |line| line.start_with?('data:') }&.delete_prefix('data:')&.strip
|
|
85
|
+
raise_streaming_chunk_error(payload) if payload
|
|
86
|
+
return
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
return unless text.lstrip.start_with?('{') && text.include?('"error"')
|
|
90
|
+
|
|
91
|
+
raise_streaming_chunk_error(text)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def raise_streaming_chunk_error(payload)
|
|
95
|
+
message = chunk_error_message(JSON.parse(payload)) || 'Bedrock streaming error'
|
|
96
|
+
response = ErrorResponse.new({ 'message' => message }, 500)
|
|
97
|
+
Transport::ErrorMiddleware.parse_error(provider: self, response: response)
|
|
98
|
+
rescue JSON::ParserError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def chunk_error_message(parsed)
|
|
103
|
+
return unless parsed.is_a?(Hash)
|
|
104
|
+
|
|
105
|
+
error = parsed['error']
|
|
106
|
+
(error.is_a?(Hash) ? error['message'] : error) || parsed['message']
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def decode_events(decoder, raw_chunk)
|
|
110
|
+
events = []
|
|
111
|
+
message, eof = decoder.decode_chunk(raw_chunk)
|
|
112
|
+
|
|
113
|
+
while message
|
|
114
|
+
event = decode_event(message)
|
|
115
|
+
if event && RubyLLM.config.log_stream_debug
|
|
116
|
+
RubyLLM.logger.debug do
|
|
117
|
+
"Bedrock stream event keys: #{event.keys}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
events << event if event
|
|
121
|
+
break if eof
|
|
122
|
+
|
|
123
|
+
message, eof = decoder.decode_chunk
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
events
|
|
127
|
+
rescue Aws::EventStream::Errors::PreludeChecksumError
|
|
128
|
+
raise_streaming_chunk_error(raw_chunk)
|
|
129
|
+
raise Error, 'Bedrock returned an unreadable event stream'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Modeled errors arrive as a frame after the 200, with the type in the
|
|
133
|
+
# frame headers and a bare message as the payload. Rewrap it under the
|
|
134
|
+
# exception name so raise_stream_error classifies it instead of the
|
|
135
|
+
# frame passing for an empty event.
|
|
136
|
+
def decode_event(message)
|
|
137
|
+
return decode_error_frame(message.headers) if stream_error_frame?(message)
|
|
138
|
+
|
|
139
|
+
exception_type = stream_exception_type(message)
|
|
140
|
+
payload = message.payload.read
|
|
141
|
+
event = decode_event_payload(payload)
|
|
142
|
+
return event unless exception_type
|
|
143
|
+
|
|
144
|
+
{ exception_type => event || { 'message' => payload.to_s } }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Failures the API does not model arrive as an error frame instead,
|
|
148
|
+
# with the code and message in the headers and nothing in the payload.
|
|
149
|
+
def stream_error_frame?(message)
|
|
150
|
+
event_header(message.headers, ':message-type') == 'error'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def decode_error_frame(headers)
|
|
154
|
+
detail = [event_header(headers, ':error-code'), event_header(headers, ':error-message')].compact.join(': ')
|
|
155
|
+
event = { 'type' => 'error' }
|
|
156
|
+
event['error'] = { 'message' => detail } unless detail.empty?
|
|
157
|
+
event
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def stream_exception_type(message)
|
|
161
|
+
headers = message.headers
|
|
162
|
+
return nil unless event_header(headers, ':message-type') == 'exception'
|
|
163
|
+
|
|
164
|
+
event_header(headers, ':exception-type') || 'streamingException'
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def event_header(headers, name)
|
|
168
|
+
header = headers[name]
|
|
169
|
+
header.respond_to?(:value) ? header.value : header
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def decode_event_payload(payload)
|
|
173
|
+
outer = JSON.parse(payload)
|
|
174
|
+
|
|
175
|
+
if outer['bytes'].is_a?(String)
|
|
176
|
+
JSON.parse(Base64.decode64(outer['bytes']))
|
|
177
|
+
else
|
|
178
|
+
outer
|
|
179
|
+
end
|
|
180
|
+
rescue JSON::ParserError => e
|
|
181
|
+
RubyLLM.logger.debug { "Failed to decode Bedrock stream event payload: #{e.message}" }
|
|
182
|
+
nil
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def stream_error_event?(event)
|
|
186
|
+
event.keys.any? { |key| key.end_with?('Exception') } || event['type'] == 'error'
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def raise_stream_error(event)
|
|
190
|
+
if event['type'] == 'error'
|
|
191
|
+
message = event.dig('error', 'message') || 'Bedrock streaming error'
|
|
192
|
+
response = ErrorResponse.new({ 'message' => message }, 500)
|
|
193
|
+
Transport::ErrorMiddleware.parse_error(provider: self, response: response)
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
key = event.keys.find { |candidate| candidate.end_with?('Exception') }
|
|
198
|
+
message = event[key]['message'] || key
|
|
199
|
+
response = ErrorResponse.new({ 'message' => message }, EXCEPTION_STATUSES.fetch(key, 500))
|
|
200
|
+
Transport::ErrorMiddleware.parse_error(provider: self, response: response)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -5,16 +5,25 @@ module RubyLLM
|
|
|
5
5
|
module Bedrock
|
|
6
6
|
# S3-backed file storage for Bedrock batch input and output.
|
|
7
7
|
class Files < Protocols::Files
|
|
8
|
+
# S3 object keys allow spaces and other characters URI() rejects or
|
|
9
|
+
# reads as a query or fragment.
|
|
10
|
+
S3_URI = %r{\As3://([^/]+)/?(.*)\z}m
|
|
11
|
+
|
|
12
|
+
def rejected_options(operation)
|
|
13
|
+
operation == :upload ? %i[purpose expires_in] : super
|
|
14
|
+
end
|
|
15
|
+
|
|
8
16
|
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
9
17
|
def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
|
|
10
18
|
provider_options: {})
|
|
11
19
|
attachment = file_attachment(file, filename:)
|
|
12
20
|
target_uri = uri || storage_uri_for(attachment)
|
|
13
21
|
bucket, key = parse_s3_uri(target_uri)
|
|
22
|
+
mime_type = content_type || file_content_type(attachment)
|
|
14
23
|
|
|
15
24
|
with_file_body(attachment) do |body|
|
|
16
|
-
s3_client.put_object(
|
|
17
|
-
|
|
25
|
+
s3_client.put_object(**provider_options.transform_keys(&:to_sym), bucket:, key:, body:,
|
|
26
|
+
content_type: mime_type)
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
uploaded_file(
|
|
@@ -23,7 +32,7 @@ module RubyLLM
|
|
|
23
32
|
uri: target_uri,
|
|
24
33
|
filename: attachment.filename,
|
|
25
34
|
byte_size: file_size(attachment),
|
|
26
|
-
mime_type:
|
|
35
|
+
mime_type:
|
|
27
36
|
)
|
|
28
37
|
end
|
|
29
38
|
|
|
@@ -99,10 +108,10 @@ module RubyLLM
|
|
|
99
108
|
end
|
|
100
109
|
|
|
101
110
|
def parse_s3_uri(uri)
|
|
102
|
-
|
|
103
|
-
raise ArgumentError, "Expected an s3:// URI, got: #{uri}" unless
|
|
111
|
+
match = S3_URI.match(uri.to_s)
|
|
112
|
+
raise ArgumentError, "Expected an s3:// URI, got: #{uri}" unless match
|
|
104
113
|
|
|
105
|
-
[
|
|
114
|
+
[match[1], match[2]]
|
|
106
115
|
end
|
|
107
116
|
end
|
|
108
117
|
end
|
|
@@ -46,7 +46,7 @@ module RubyLLM
|
|
|
46
46
|
def parse_moderation_responses(responses)
|
|
47
47
|
RubyLLM::Moderation.new(
|
|
48
48
|
id: responses.one? ? responses.first.first.headers['x-amzn-requestid'] : nil,
|
|
49
|
-
model:
|
|
49
|
+
model: guardrail_model_id,
|
|
50
50
|
results: responses.map(&:last),
|
|
51
51
|
raw: responses.one? ? responses.first.first.body : responses.map { |response, _result| response.body }
|
|
52
52
|
)
|
|
@@ -71,6 +71,8 @@ module RubyLLM
|
|
|
71
71
|
RubyLLM::Rerank::Result.new(index:, document: documents[index], score: item['relevanceScore'])
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
|
+
raise Error, 'Bedrock reranking returned a duplicate document index' unless results.uniq(&:index) == results
|
|
75
|
+
|
|
74
76
|
RubyLLM::Rerank.new(results:, model:, raw: pages.length == 1 ? pages.first : pages)
|
|
75
77
|
end
|
|
76
78
|
end
|
|
@@ -13,10 +13,41 @@ module RubyLLM
|
|
|
13
13
|
OPENAI_INLINE_FILE_LIMIT = 50 * 1024 * 1024
|
|
14
14
|
PROMPT_CACHE_OPTIONS = %i[key ttl mode retention].freeze
|
|
15
15
|
|
|
16
|
+
UNSUPPORTED_PARAMETER_CODES = %w[unsupported_parameter unsupported_value unknown_parameter].freeze
|
|
17
|
+
# The Responses API reports some of these, such as a temperature,
|
|
18
|
+
# with a +param+ and no +code+.
|
|
19
|
+
UNSUPPORTED_PARAMETER_MESSAGE = /\AUnsupported (?:parameter|value): /
|
|
20
|
+
PARAMETERS = {
|
|
21
|
+
'temperature' => :temperature, 'reasoning_effort' => :thinking,
|
|
22
|
+
'max_tokens' => :max_output_tokens, 'max_completion_tokens' => :max_output_tokens
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
16
25
|
def completion_url
|
|
17
26
|
'chat/completions'
|
|
18
27
|
end
|
|
19
28
|
|
|
29
|
+
# OpenAI names the rejected parameter in +param+ and why in +code+.
|
|
30
|
+
def parse_unsupported_parameter(response)
|
|
31
|
+
body = response.body
|
|
32
|
+
body = JSON.parse(body) if body.is_a?(String)
|
|
33
|
+
error = body['error'] if body.is_a?(Hash)
|
|
34
|
+
return unless error.is_a?(Hash) && unsupported_parameter_error?(error)
|
|
35
|
+
return unless error['param'].is_a?(String) && !error['param'].empty?
|
|
36
|
+
|
|
37
|
+
wire_parameters.fetch(error['param'], error['param'])
|
|
38
|
+
rescue JSON::ParserError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def unsupported_parameter_error?(error)
|
|
43
|
+
return UNSUPPORTED_PARAMETER_CODES.include?(error['code']) if error['code']
|
|
44
|
+
|
|
45
|
+
error['message'].to_s.match?(UNSUPPORTED_PARAMETER_MESSAGE)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The wire parameters that carry RubyLLM settings, by name.
|
|
49
|
+
def wire_parameters = PARAMETERS
|
|
50
|
+
|
|
20
51
|
module_function
|
|
21
52
|
|
|
22
53
|
def finish_reasons = FINISH_REASONS
|
|
@@ -24,7 +55,7 @@ module RubyLLM
|
|
|
24
55
|
def normalize_finish_reason(reason)
|
|
25
56
|
return nil if reason.nil?
|
|
26
57
|
|
|
27
|
-
finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
|
|
58
|
+
finish_reasons.fetch(reason.to_s) { reason.to_s.downcase.to_sym }
|
|
28
59
|
end
|
|
29
60
|
|
|
30
61
|
# OpenAI strict mode rejects an object whose properties are not all
|
|
@@ -35,14 +66,15 @@ module RubyLLM
|
|
|
35
66
|
end
|
|
36
67
|
|
|
37
68
|
def strict_schema?(node)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return false if optional_properties?(node)
|
|
69
|
+
return true unless node.is_a?(Hash)
|
|
70
|
+
return false if optional_properties?(node)
|
|
41
71
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
72
|
+
subschemas(node).all? { |schema| strict_schema?(schema) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def subschemas(node)
|
|
76
|
+
keyed = node.values_at(:properties, :$defs, :definitions).grep(Hash).flat_map(&:values)
|
|
77
|
+
keyed + node.values_at(:items, :prefixItems, :anyOf, :oneOf, :allOf, :additionalProperties).flatten(1)
|
|
46
78
|
end
|
|
47
79
|
|
|
48
80
|
def optional_properties?(node)
|
|
@@ -59,7 +91,7 @@ module RubyLLM
|
|
|
59
91
|
tool_prefs ||= {}
|
|
60
92
|
payload = {
|
|
61
93
|
model: model.id,
|
|
62
|
-
messages:
|
|
94
|
+
messages: render_messages(messages, caching: caching),
|
|
63
95
|
stream: stream
|
|
64
96
|
}
|
|
65
97
|
|
|
@@ -110,32 +142,41 @@ module RubyLLM
|
|
|
110
142
|
message_data = data.dig('choices', 0, 'message')
|
|
111
143
|
raise no_completion_message_error(data, raw) unless message_data
|
|
112
144
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
145
|
+
content, thinking_from_blocks =
|
|
146
|
+
parse_content_and_thinking(message_data['content'] || message_data['refusal'])
|
|
147
|
+
thinking_text = thinking_from_blocks || parse_thinking_text(message_data)
|
|
148
|
+
thinking_signature = parse_thinking_signature(message_data)
|
|
149
|
+
answer, thinking_text, offset = parse_think_tags(content, thinking_text)
|
|
118
150
|
|
|
119
151
|
finish_reason = normalize_finish_reason(data.dig('choices', 0, 'finish_reason'))
|
|
120
152
|
|
|
121
153
|
Message.new(
|
|
122
154
|
role: :assistant,
|
|
123
|
-
content:
|
|
124
|
-
citations: extract_citations(message_data, data, content),
|
|
155
|
+
content: answer,
|
|
156
|
+
citations: offset_citations(extract_citations(message_data, data, content), offset),
|
|
125
157
|
thinking: Thinking.build(text: thinking_text, signature: thinking_signature),
|
|
126
|
-
raw_reasoning:
|
|
158
|
+
raw_reasoning: parse_raw_reasoning(message_data),
|
|
127
159
|
tool_calls: parse_tool_calls(message_data['tool_calls'], response: raw, finish_reason: finish_reason),
|
|
160
|
+
finish_reason: finish_reason,
|
|
161
|
+
content_filters: ContentFilters.parse_content_filters(data),
|
|
162
|
+
model: data['model'],
|
|
163
|
+
raw: raw,
|
|
164
|
+
**parse_completion_usage(data)
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_completion_usage(data)
|
|
169
|
+
usage = data['usage'] || {}
|
|
170
|
+
|
|
171
|
+
{
|
|
128
172
|
input_tokens: input_tokens(usage),
|
|
129
173
|
output_tokens: output_tokens(usage),
|
|
130
174
|
cache_read_tokens: cache_read_tokens(usage),
|
|
131
175
|
cache_write_tokens: cache_write_tokens(usage),
|
|
132
|
-
thinking_tokens: thinking_tokens,
|
|
176
|
+
thinking_tokens: thinking_tokens(usage),
|
|
133
177
|
server_tool_use: server_tool_use(usage),
|
|
134
|
-
reported_cost: reported_cost(usage)
|
|
135
|
-
|
|
136
|
-
model: data['model'],
|
|
137
|
-
raw: raw
|
|
138
|
-
)
|
|
178
|
+
reported_cost: reported_cost(usage)
|
|
179
|
+
}
|
|
139
180
|
end
|
|
140
181
|
|
|
141
182
|
def reported_cost(_usage)
|
|
@@ -189,7 +230,10 @@ module RubyLLM
|
|
|
189
230
|
end
|
|
190
231
|
|
|
191
232
|
def cache_write_tokens(usage)
|
|
233
|
+
return if usage.empty?
|
|
234
|
+
|
|
192
235
|
usage.dig('prompt_tokens_details', 'cache_write_tokens') ||
|
|
236
|
+
usage.dig('prompt_tokens_details', 'created_cache_tokens') ||
|
|
193
237
|
usage.dig('input_tokens_details', 'cache_write_tokens') ||
|
|
194
238
|
0
|
|
195
239
|
end
|
|
@@ -277,10 +321,14 @@ module RubyLLM
|
|
|
277
321
|
|
|
278
322
|
{}.tap do |cache_options|
|
|
279
323
|
cache_options[:mode] = options[:mode] if options[:mode]
|
|
280
|
-
cache_options[:ttl] = ttl if ttl
|
|
324
|
+
cache_options[:ttl] = render_cache_lifetime(ttl) if ttl
|
|
281
325
|
end
|
|
282
326
|
end
|
|
283
327
|
|
|
328
|
+
def render_cache_lifetime(ttl)
|
|
329
|
+
Protocol::CacheLifetime.render(ttl)
|
|
330
|
+
end
|
|
331
|
+
|
|
284
332
|
def retention_ttl(retention)
|
|
285
333
|
return unless retention
|
|
286
334
|
|
|
@@ -301,39 +349,66 @@ module RubyLLM
|
|
|
301
349
|
"got #{format_cache_option_keys(unsupported)}"
|
|
302
350
|
end
|
|
303
351
|
|
|
352
|
+
def cache_option_names = PROMPT_CACHE_OPTIONS
|
|
353
|
+
|
|
304
354
|
def format_cache_option_keys(keys)
|
|
305
355
|
keys.map { |key| ":#{key}" }.join(', ')
|
|
306
356
|
end
|
|
307
357
|
|
|
308
|
-
def
|
|
358
|
+
def render_messages(messages, caching: nil)
|
|
309
359
|
messages_for_provider(messages)
|
|
310
|
-
.chunk_while { |previous, current|
|
|
311
|
-
.flat_map { |group|
|
|
360
|
+
.chunk_while { |previous, current| current.tool_result? && (previous.tool_result? || carrier?(previous)) }
|
|
361
|
+
.flat_map { |group| render_message_group(group, caching: caching) }
|
|
312
362
|
end
|
|
313
363
|
|
|
314
364
|
# An assistant turn's tool results must stay consecutive, so the
|
|
315
|
-
# attachment carriers of a
|
|
316
|
-
def
|
|
317
|
-
formatted = group.
|
|
318
|
-
|
|
365
|
+
# attachment carriers of a round follow the whole run.
|
|
366
|
+
def render_message_group(group, caching: nil)
|
|
367
|
+
formatted = group.filter_map do |msg|
|
|
368
|
+
next if attachments_only_assistant_message?(msg)
|
|
369
|
+
|
|
370
|
+
render_message(msg.role == :assistant ? msg.with_attachments([]) : msg, caching: caching)
|
|
371
|
+
end
|
|
372
|
+
carriers = group.select { |msg| carrier?(msg) }.map do |msg|
|
|
373
|
+
msg.tool_result? ? tool_attachment_message(msg) : assistant_attachment_message(msg)
|
|
374
|
+
end
|
|
319
375
|
|
|
320
|
-
formatted + carriers
|
|
376
|
+
formatted + carriers
|
|
321
377
|
end
|
|
322
378
|
|
|
323
|
-
def
|
|
379
|
+
def carrier?(msg)
|
|
380
|
+
(msg.tool_result? || msg.role == :assistant) && msg.attachments.any?
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def attachments_only_assistant_message?(msg)
|
|
384
|
+
msg.role == :assistant && msg.attachments.any? && !msg.tool_call? && msg.thinking.nil? &&
|
|
385
|
+
msg.content.to_s.empty?
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def render_message(msg, caching: nil)
|
|
324
389
|
{
|
|
325
|
-
role:
|
|
326
|
-
content:
|
|
327
|
-
tool_calls:
|
|
328
|
-
tool_call_id: msg.
|
|
329
|
-
}.compact.merge(
|
|
390
|
+
role: render_role(msg.role),
|
|
391
|
+
content: render_message_content(msg, caching: caching),
|
|
392
|
+
tool_calls: render_tool_calls(msg.tool_calls),
|
|
393
|
+
tool_call_id: msg.provider_call_id
|
|
394
|
+
}.compact.merge(render_thinking(msg))
|
|
330
395
|
end
|
|
331
396
|
|
|
332
397
|
# Chat Completions tool messages are text-only on the wire, so tool
|
|
333
398
|
# attachments ride a user message spliced in after the results.
|
|
334
399
|
def tool_attachment_message(msg)
|
|
335
|
-
|
|
336
|
-
|
|
400
|
+
attachment_message("Attachments from tool call #{msg.provider_call_id}:", msg.attachments)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Assistant messages take only text, so a replayed turn's attachments
|
|
404
|
+
# ride a user message after it, as on Responses.
|
|
405
|
+
def assistant_attachment_message(msg)
|
|
406
|
+
attachment_message('Attachments from the assistant:', msg.attachments)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def attachment_message(label, attachments)
|
|
410
|
+
parts = [Media.render_text(label)]
|
|
411
|
+
parts.concat(render_content(nil, attachments))
|
|
337
412
|
{ role: 'user', content: parts }
|
|
338
413
|
end
|
|
339
414
|
|
|
@@ -342,8 +417,8 @@ module RubyLLM
|
|
|
342
417
|
system_messages + other_messages
|
|
343
418
|
end
|
|
344
419
|
|
|
345
|
-
def
|
|
346
|
-
content =
|
|
420
|
+
def render_message_content(msg, caching: nil, **)
|
|
421
|
+
content = render_content(msg.content, msg.tool_result? ? [] : msg.attachments)
|
|
347
422
|
return '' if content.nil? && thinking_only_assistant_message?(msg)
|
|
348
423
|
return inject_cache_breakpoint(content) if caching != false && msg.cache_until_here? && openai_prompt_caching?
|
|
349
424
|
|
|
@@ -361,7 +436,7 @@ module RubyLLM
|
|
|
361
436
|
def cache_breakpoint_parts(content)
|
|
362
437
|
case content
|
|
363
438
|
when Array then content.dup
|
|
364
|
-
when String then [Media.
|
|
439
|
+
when String then [Media.render_text(content)] unless content.empty?
|
|
365
440
|
end
|
|
366
441
|
end
|
|
367
442
|
|
|
@@ -369,11 +444,11 @@ module RubyLLM
|
|
|
369
444
|
msg.role == :assistant && msg.thinking && !msg.tool_call?
|
|
370
445
|
end
|
|
371
446
|
|
|
372
|
-
def
|
|
373
|
-
Media.
|
|
447
|
+
def render_content(content, attachments = [])
|
|
448
|
+
Media.render_content(content, attachments)
|
|
374
449
|
end
|
|
375
450
|
|
|
376
|
-
def
|
|
451
|
+
def render_role(role)
|
|
377
452
|
case role
|
|
378
453
|
when :system
|
|
379
454
|
@config.openai_use_system_role ? 'system' : 'developer'
|
|
@@ -398,7 +473,7 @@ module RubyLLM
|
|
|
398
473
|
payload.merge(safety_identifier: identifier)
|
|
399
474
|
end
|
|
400
475
|
|
|
401
|
-
def
|
|
476
|
+
def provider_file_references?
|
|
402
477
|
@provider.slug == 'openai'
|
|
403
478
|
end
|
|
404
479
|
|
|
@@ -414,7 +489,7 @@ module RubyLLM
|
|
|
414
489
|
{ purpose: 'user_data' }
|
|
415
490
|
end
|
|
416
491
|
|
|
417
|
-
def
|
|
492
|
+
def render_thinking(msg)
|
|
418
493
|
return {} unless msg.role == :assistant
|
|
419
494
|
|
|
420
495
|
thinking = msg.thinking
|
|
@@ -429,44 +504,69 @@ module RubyLLM
|
|
|
429
504
|
payload
|
|
430
505
|
end
|
|
431
506
|
|
|
432
|
-
def
|
|
507
|
+
def parse_thinking_text(message_data)
|
|
433
508
|
candidate = message_data['reasoning_content'] || message_data['reasoning'] || message_data['thinking']
|
|
434
509
|
candidate.is_a?(String) ? candidate : nil
|
|
435
510
|
end
|
|
436
511
|
|
|
437
|
-
def
|
|
512
|
+
def parse_thinking_signature(message_data)
|
|
438
513
|
candidate = message_data['reasoning_signature'] || message_data['signature']
|
|
439
514
|
candidate.is_a?(String) ? candidate : nil
|
|
440
515
|
end
|
|
441
516
|
|
|
442
|
-
def
|
|
517
|
+
def parse_think_tags(content, thinking_text)
|
|
518
|
+
return [content, thinking_text, 0] unless split_think_tags? && thinking_text.nil?
|
|
519
|
+
return [content, nil, 0] unless content.is_a?(String) && !content.empty?
|
|
520
|
+
|
|
521
|
+
ThinkTags.split(content)
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Annotation indices count from the start of the content the server
|
|
525
|
+
# sent, before the think block was removed.
|
|
526
|
+
def offset_citations(citations, offset)
|
|
527
|
+
return citations if offset.zero?
|
|
528
|
+
|
|
529
|
+
citations.map do |citation|
|
|
530
|
+
next citation unless citation.start_index && citation.end_index
|
|
531
|
+
|
|
532
|
+
start_index = citation.start_index - offset
|
|
533
|
+
end_index = citation.end_index - offset if start_index >= 0
|
|
534
|
+
Citation.new(**citation.to_h, start_index: end_index && start_index, end_index:)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def split_think_tags?
|
|
539
|
+
@config&.split_think_tags
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def parse_raw_reasoning(_message_data)
|
|
443
543
|
nil
|
|
444
544
|
end
|
|
445
545
|
|
|
446
|
-
def
|
|
546
|
+
def parse_content_and_thinking(content)
|
|
447
547
|
return [content, nil] unless content.is_a?(Array)
|
|
448
548
|
|
|
449
|
-
text =
|
|
450
|
-
thinking =
|
|
549
|
+
text = parse_text_from_blocks(content)
|
|
550
|
+
thinking = parse_thinking_from_blocks(content)
|
|
451
551
|
|
|
452
552
|
[text.empty? ? nil : text, thinking.empty? ? nil : thinking]
|
|
453
553
|
end
|
|
454
554
|
|
|
455
|
-
def
|
|
555
|
+
def parse_text_from_blocks(blocks)
|
|
456
556
|
blocks.filter_map do |block|
|
|
457
557
|
block['text'] if block['type'] == 'text' && block['text'].is_a?(String)
|
|
458
558
|
end.join
|
|
459
559
|
end
|
|
460
560
|
|
|
461
|
-
def
|
|
561
|
+
def parse_thinking_from_blocks(blocks)
|
|
462
562
|
blocks.filter_map do |block|
|
|
463
563
|
next unless block['type'] == 'thinking'
|
|
464
564
|
|
|
465
|
-
|
|
565
|
+
parse_thinking_text_from_block(block)
|
|
466
566
|
end.join
|
|
467
567
|
end
|
|
468
568
|
|
|
469
|
-
def
|
|
569
|
+
def parse_thinking_text_from_block(block)
|
|
470
570
|
thinking_block = block['thinking']
|
|
471
571
|
return thinking_block if thinking_block.is_a?(String)
|
|
472
572
|
|