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
|
@@ -6,6 +6,7 @@ require 'faraday/retry'
|
|
|
6
6
|
require 'ruby_llm/transport/error_middleware'
|
|
7
7
|
require 'ruby_llm/transport/signing_middleware'
|
|
8
8
|
require 'ruby_llm/transport/usage_middleware'
|
|
9
|
+
require 'time'
|
|
9
10
|
require 'timeout'
|
|
10
11
|
|
|
11
12
|
module RubyLLM
|
|
@@ -15,23 +16,37 @@ module RubyLLM
|
|
|
15
16
|
|
|
16
17
|
IDEMPOTENT_KEY = :ruby_llm_idempotent
|
|
17
18
|
STREAM_PROGRESS_KEY = :ruby_llm_stream_progress
|
|
19
|
+
FALLBACK_KEY = :ruby_llm_fallback_ready
|
|
20
|
+
|
|
21
|
+
# The longest wait, in seconds, a rate-limited or overloaded model's
|
|
22
|
+
# retry hint may ask for before a fallback takes the request instead.
|
|
23
|
+
# Providers often ask for a second or two, which costs less than
|
|
24
|
+
# moving to another model; waits of ten seconds or more are left to
|
|
25
|
+
# a fallback when one is configured.
|
|
26
|
+
FALLBACK_WAIT = 2
|
|
18
27
|
|
|
19
28
|
# The key a shared Faraday connection is cached under: the provider,
|
|
20
29
|
# whether it streams, and everything ::build reads.
|
|
21
30
|
Settings = Struct.new(
|
|
22
|
-
:provider, :stream, :api_base, :adapter, :timeout, :
|
|
23
|
-
:
|
|
31
|
+
:provider, :stream, :api_base, :adapter, :timeout, :connect_timeout, :idle_timeout, :proxy, :ssl, :logger,
|
|
32
|
+
:log_bodies, :log_regexp_timeout, :max_retries, :retry_interval, :retry_max_interval,
|
|
33
|
+
:retry_interval_randomness, :retry_backoff_factor,
|
|
24
34
|
keyword_init: true
|
|
25
35
|
)
|
|
26
36
|
|
|
27
37
|
CACHE = Support::ProcessCache.new
|
|
38
|
+
REDIRECTS = [301, 302, 303, 307, 308].freeze
|
|
39
|
+
REDIRECT_LIMIT = 5
|
|
28
40
|
|
|
29
41
|
attr_reader :provider, :config
|
|
30
42
|
|
|
31
43
|
def self.basic(config = RubyLLM.config, &)
|
|
32
44
|
Faraday.new do |f|
|
|
45
|
+
f.use ReleaseRequest
|
|
33
46
|
f.options.timeout = config.request_timeout
|
|
47
|
+
f.options.open_timeout = config.connect_timeout
|
|
34
48
|
f.proxy = config.http_proxy if config.http_proxy
|
|
49
|
+
f.ssl.update(ssl_snapshot(config.ssl)) if config.ssl
|
|
35
50
|
f.response :logger,
|
|
36
51
|
RubyLLM.logger,
|
|
37
52
|
bodies: false,
|
|
@@ -43,10 +58,56 @@ module RubyLLM
|
|
|
43
58
|
end
|
|
44
59
|
end
|
|
45
60
|
|
|
61
|
+
# Fetches the body at +url+ with no credentials, so a redirect to
|
|
62
|
+
# another host carries none.
|
|
63
|
+
def self.download(url, config = RubyLLM.config)
|
|
64
|
+
connection = basic(config)
|
|
65
|
+
uri = URI(url.to_s)
|
|
66
|
+
response = connection.get(uri)
|
|
67
|
+
REDIRECT_LIMIT.times do
|
|
68
|
+
break unless REDIRECTS.include?(response.status) && response.headers['location']
|
|
69
|
+
|
|
70
|
+
uri = redirect_target(uri, response.headers['location'])
|
|
71
|
+
response = connection.get(uri)
|
|
72
|
+
end
|
|
73
|
+
return response.body if response.success?
|
|
74
|
+
|
|
75
|
+
raise Error.new("Download from #{uri.host} failed with status #{response.status}", response:)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.redirect_target(from, location)
|
|
79
|
+
target = URI.join(from, location)
|
|
80
|
+
return target if target.scheme == 'https' || (target.scheme == 'http' && from.scheme == 'http')
|
|
81
|
+
|
|
82
|
+
raise Error, "Download from #{from.host} redirected to an unsafe #{target.scheme} URL"
|
|
83
|
+
rescue URI::InvalidURIError
|
|
84
|
+
raise Error, "Download from #{from.host} redirected to an invalid URL"
|
|
85
|
+
end
|
|
86
|
+
private_class_method :redirect_target
|
|
87
|
+
|
|
46
88
|
def self.cache
|
|
47
89
|
CACHE
|
|
48
90
|
end
|
|
49
91
|
|
|
92
|
+
# Runs the block with a fallback model ready to take over its
|
|
93
|
+
# requests: +ready+ answers whether an error moves on to it. Until the
|
|
94
|
+
# block returns, a timeout or a failed connection is not retried on
|
|
95
|
+
# the same model, nor an error whose retry hint asks for a wait
|
|
96
|
+
# longer than FALLBACK_WAIT, when +ready+ accepts the error.
|
|
97
|
+
def self.with_fallback_ready(ready)
|
|
98
|
+
previous = Fiber[FALLBACK_KEY]
|
|
99
|
+
Fiber[FALLBACK_KEY] = ready
|
|
100
|
+
yield
|
|
101
|
+
ensure
|
|
102
|
+
Fiber[FALLBACK_KEY] = previous
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A frozen copy of +ssl+, so a value changed in place afterwards
|
|
106
|
+
# reaches neither a cache key nor a connection already built.
|
|
107
|
+
def self.ssl_snapshot(ssl)
|
|
108
|
+
ssl.to_h.transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze
|
|
109
|
+
end
|
|
110
|
+
|
|
50
111
|
# The result is shared across threads and contexts, so the middleware
|
|
51
112
|
# stack is built now rather than lazily, without a lock, on the first
|
|
52
113
|
# request, and the defaults every request copies are frozen.
|
|
@@ -57,6 +118,7 @@ module RubyLLM
|
|
|
57
118
|
setup_retry(faraday, settings)
|
|
58
119
|
setup_middleware(faraday, settings)
|
|
59
120
|
setup_http_proxy(faraday, settings)
|
|
121
|
+
setup_ssl(faraday, settings)
|
|
60
122
|
end
|
|
61
123
|
[connection.headers, connection.params, connection.options].each(&:freeze)
|
|
62
124
|
connection.app
|
|
@@ -68,36 +130,37 @@ module RubyLLM
|
|
|
68
130
|
@config = config
|
|
69
131
|
@headers = headers
|
|
70
132
|
@signer = signer
|
|
71
|
-
@
|
|
72
|
-
@stream_settings = @settings.dup.tap { |settings| settings.stream = true }.freeze
|
|
133
|
+
@api_base = api_base
|
|
73
134
|
connection
|
|
74
135
|
end
|
|
75
136
|
|
|
76
137
|
# The Faraday connection shared by every request with these settings in
|
|
77
138
|
# this process. Looked up per request, so an object that outlives a fork
|
|
78
|
-
# never reaches the parent's sockets
|
|
79
|
-
#
|
|
139
|
+
# never reaches the parent's sockets, and a request goes to the endpoint
|
|
140
|
+
# configured for the credentials it carries. Streaming requests get
|
|
141
|
+
# their own: some adapters, such as httpx, only stream if their first
|
|
142
|
+
# request did.
|
|
80
143
|
def connection(stream: false)
|
|
81
|
-
settings = stream
|
|
144
|
+
settings = settings_for(stream)
|
|
82
145
|
CACHE.fetch(settings) { self.class.build(settings) }
|
|
83
146
|
end
|
|
84
147
|
|
|
85
148
|
def post(url, payload, usage: nil, idempotent: true, stream: false, &)
|
|
86
149
|
instrument_request(:post, url) do
|
|
87
|
-
|
|
150
|
+
connection(stream:).post url, payload do |req|
|
|
88
151
|
prepare(req)
|
|
89
152
|
set_usage_tracker(req, usage) if usage
|
|
90
153
|
mark_non_idempotent(req) unless idempotent
|
|
91
154
|
yield req if block_given?
|
|
92
155
|
end
|
|
93
|
-
release_request(response)
|
|
94
156
|
end
|
|
95
157
|
end
|
|
96
158
|
|
|
97
|
-
def get(url, &)
|
|
159
|
+
def get(url, raw: false, &)
|
|
98
160
|
instrument_request(:get, url) do
|
|
99
161
|
connection.get url do |req|
|
|
100
162
|
prepare(req)
|
|
163
|
+
keep_raw_body(req) if raw
|
|
101
164
|
yield req if block_given?
|
|
102
165
|
end
|
|
103
166
|
end
|
|
@@ -105,11 +168,10 @@ module RubyLLM
|
|
|
105
168
|
|
|
106
169
|
def patch(url, payload, &)
|
|
107
170
|
instrument_request(:patch, url) do
|
|
108
|
-
|
|
171
|
+
connection.patch url, payload do |req|
|
|
109
172
|
prepare(req)
|
|
110
173
|
yield req if block_given?
|
|
111
174
|
end
|
|
112
|
-
release_request(response)
|
|
113
175
|
end
|
|
114
176
|
end
|
|
115
177
|
|
|
@@ -132,32 +194,29 @@ module RubyLLM
|
|
|
132
194
|
}
|
|
133
195
|
|
|
134
196
|
RubyLLM.instrument('request.ruby_llm', payload, config: @config) do |event|
|
|
135
|
-
response = yield
|
|
197
|
+
response = ReleaseRequest.release(yield)
|
|
136
198
|
event[:status] = response.status if response.respond_to?(:status)
|
|
137
199
|
response
|
|
200
|
+
rescue RubyLLM::Error => e
|
|
201
|
+
ReleaseRequest.release(e.response)
|
|
202
|
+
raise
|
|
138
203
|
end
|
|
139
204
|
end
|
|
140
205
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
# callback closes over the payload it was built from.
|
|
144
|
-
def release_request(response)
|
|
145
|
-
response.env.request_body = nil
|
|
146
|
-
response.env.request.on_data = nil
|
|
147
|
-
response.env.request.context&.delete(ErrorMiddleware::STREAM_RESET_KEY)
|
|
148
|
-
response
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def settings_for(api_base)
|
|
206
|
+
def settings_for(stream)
|
|
207
|
+
logger = @config.logger || RubyLLM.logger
|
|
152
208
|
Settings.new(
|
|
153
209
|
provider: @provider.class,
|
|
154
|
-
stream:
|
|
155
|
-
api_base: api_base,
|
|
210
|
+
stream: stream,
|
|
211
|
+
api_base: @api_base || @provider.api_base,
|
|
156
212
|
adapter: @config.faraday_adapter,
|
|
157
213
|
timeout: @config.request_timeout,
|
|
214
|
+
connect_timeout: @config.connect_timeout,
|
|
215
|
+
idle_timeout: (@config.stream_idle_timeout if stream),
|
|
158
216
|
proxy: @config.http_proxy,
|
|
159
|
-
|
|
160
|
-
|
|
217
|
+
ssl: (self.class.ssl_snapshot(@config.ssl) if @config.ssl),
|
|
218
|
+
logger: logger,
|
|
219
|
+
log_bodies: logger.debug?,
|
|
161
220
|
log_regexp_timeout: @config.log_regexp_timeout,
|
|
162
221
|
max_retries: @config.max_retries,
|
|
163
222
|
retry_interval: @config.retry_interval,
|
|
@@ -177,8 +236,12 @@ module RubyLLM
|
|
|
177
236
|
context[SigningMiddleware::CONTEXT_KEY] = @signer if @signer
|
|
178
237
|
end
|
|
179
238
|
|
|
239
|
+
# Adapters time each read of a response with +read_timeout+, so on a
|
|
240
|
+
# streaming connection it is the longest wait for the next bytes.
|
|
180
241
|
def self.setup_timeout(faraday, settings)
|
|
181
242
|
faraday.options.timeout = settings.timeout
|
|
243
|
+
faraday.options.open_timeout = settings.connect_timeout
|
|
244
|
+
faraday.options.read_timeout = settings.idle_timeout if settings.idle_timeout
|
|
182
245
|
end
|
|
183
246
|
|
|
184
247
|
def self.setup_logging(faraday, settings)
|
|
@@ -187,7 +250,8 @@ module RubyLLM
|
|
|
187
250
|
bodies: settings.log_bodies,
|
|
188
251
|
errors: true,
|
|
189
252
|
headers: false,
|
|
190
|
-
log_level: :debug
|
|
253
|
+
log_level: :debug,
|
|
254
|
+
formatter: LogFormatter do |logger|
|
|
191
255
|
logger.filter(logging_regexp('[A-Za-z0-9+/=]{100,}', settings), '[BASE64 DATA]')
|
|
192
256
|
logger.filter(logging_regexp('[-\\d.e,\\s]{100,}', settings), '[EMBEDDINGS ARRAY]')
|
|
193
257
|
end
|
|
@@ -207,8 +271,10 @@ module RubyLLM
|
|
|
207
271
|
interval_randomness: settings.retry_interval_randomness,
|
|
208
272
|
backoff_factor: settings.retry_backoff_factor,
|
|
209
273
|
methods: Faraday::Retry::Middleware::IDEMPOTENT_METHODS,
|
|
210
|
-
retry_if: lambda { |env,
|
|
211
|
-
|
|
274
|
+
retry_if: lambda { |env, exception|
|
|
275
|
+
Support::Cancellation.check
|
|
276
|
+
env[:method] == :post && idempotent?(env) && !stream_delivered?(env) &&
|
|
277
|
+
!fall_back_instead?(env, exception)
|
|
212
278
|
},
|
|
213
279
|
exceptions: retry_exceptions
|
|
214
280
|
}
|
|
@@ -223,21 +289,61 @@ module RubyLLM
|
|
|
223
289
|
env[:request]&.context&.dig(IDEMPOTENT_KEY) != false
|
|
224
290
|
end
|
|
225
291
|
|
|
292
|
+
# Whether a fallback set up by ::with_fallback_ready takes this
|
|
293
|
+
# failure instead of a retry on the same model.
|
|
294
|
+
def self.fall_back_instead?(env, exception)
|
|
295
|
+
ready = Fiber[FALLBACK_KEY]
|
|
296
|
+
return false unless ready && exception
|
|
297
|
+
|
|
298
|
+
error = exception.is_a?(RubyLLM::Error) ? exception : RubyLLM::ConnectionError.new(exception.message)
|
|
299
|
+
return false unless ready.call(error)
|
|
300
|
+
|
|
301
|
+
error.is_a?(RubyLLM::ConnectionError) || retry_wait(env, error) > FALLBACK_WAIT
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# The wait in seconds the response's retry hints ask for, read as the
|
|
305
|
+
# retry middleware reads them, or 0 without one.
|
|
306
|
+
def self.retry_wait(env, error)
|
|
307
|
+
headers = env[:response_headers] || error.response&.headers || {}
|
|
308
|
+
%w[Retry-After RateLimit-Reset].filter_map { |name| header_seconds(headers[name]) }.max || 0
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def self.header_seconds(value)
|
|
312
|
+
text = value.to_s.strip
|
|
313
|
+
Float(text, exception: false) || (Time.httpdate(text) - Time.now)
|
|
314
|
+
rescue ArgumentError
|
|
315
|
+
nil
|
|
316
|
+
end
|
|
317
|
+
|
|
226
318
|
def self.setup_middleware(faraday, settings)
|
|
227
319
|
faraday.request :multipart
|
|
228
320
|
faraday.request :json
|
|
229
321
|
faraday.use :llm_signing
|
|
230
322
|
faraday.use JsonResponse
|
|
231
|
-
faraday.adapter(settings.adapter)
|
|
323
|
+
faraday.adapter(settings.adapter, **adapter_options(settings))
|
|
232
324
|
faraday.use :llm_errors
|
|
233
325
|
end
|
|
234
326
|
|
|
327
|
+
# async-http-faraday reads its timeout for each read from its own
|
|
328
|
+
# options rather than the request's.
|
|
329
|
+
def self.adapter_options(settings)
|
|
330
|
+
return {} unless settings.adapter == :async_http && settings.idle_timeout
|
|
331
|
+
|
|
332
|
+
{ read_timeout: settings.idle_timeout }
|
|
333
|
+
end
|
|
334
|
+
|
|
235
335
|
def self.setup_http_proxy(faraday, settings)
|
|
236
336
|
return unless settings.proxy
|
|
237
337
|
|
|
238
338
|
faraday.proxy = settings.proxy
|
|
239
339
|
end
|
|
240
340
|
|
|
341
|
+
def self.setup_ssl(faraday, settings)
|
|
342
|
+
return unless settings.ssl
|
|
343
|
+
|
|
344
|
+
faraday.ssl.update(settings.ssl)
|
|
345
|
+
end
|
|
346
|
+
|
|
241
347
|
def self.retry_exceptions
|
|
242
348
|
[
|
|
243
349
|
Errno::ETIMEDOUT,
|
|
@@ -246,6 +352,7 @@ module RubyLLM
|
|
|
246
352
|
Faraday::ConnectionFailed,
|
|
247
353
|
Faraday::SSLError,
|
|
248
354
|
Faraday::RetriableResponse,
|
|
355
|
+
RubyLLM::ConnectionError,
|
|
249
356
|
RubyLLM::RateLimitError,
|
|
250
357
|
RubyLLM::ServerError,
|
|
251
358
|
RubyLLM::ServiceUnavailableError,
|
|
@@ -254,7 +361,8 @@ module RubyLLM
|
|
|
254
361
|
end
|
|
255
362
|
|
|
256
363
|
private_class_method :setup_timeout, :setup_logging, :logging_regexp, :setup_retry, :stream_delivered?,
|
|
257
|
-
:idempotent?, :
|
|
364
|
+
:idempotent?, :fall_back_instead?, :retry_wait, :header_seconds, :setup_middleware,
|
|
365
|
+
:adapter_options, :setup_http_proxy, :setup_ssl, :retry_exceptions
|
|
258
366
|
|
|
259
367
|
def set_usage_tracker(request, tracker)
|
|
260
368
|
context = request.options.context ||= {}
|
|
@@ -268,6 +376,11 @@ module RubyLLM
|
|
|
268
376
|
context[IDEMPOTENT_KEY] = false
|
|
269
377
|
end
|
|
270
378
|
|
|
379
|
+
def keep_raw_body(request)
|
|
380
|
+
context = request.options.context ||= {}
|
|
381
|
+
context[JsonResponse::RAW_KEY] = true
|
|
382
|
+
end
|
|
383
|
+
|
|
271
384
|
def inspect_attributes # :nodoc:
|
|
272
385
|
{ provider: @provider.slug }
|
|
273
386
|
end
|
|
@@ -8,6 +8,8 @@ module RubyLLM
|
|
|
8
8
|
class ErrorMiddleware < Faraday::Middleware # :nodoc: all
|
|
9
9
|
PROVIDER_KEY = :ruby_llm_provider
|
|
10
10
|
STREAM_RESET_KEY = :ruby_llm_stream_reset
|
|
11
|
+
STREAM_FAILURE_KEY = :ruby_llm_stream_failure
|
|
12
|
+
NETWORK_FAILURES = [Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError].freeze
|
|
11
13
|
|
|
12
14
|
def initialize(app, options = {})
|
|
13
15
|
super(app)
|
|
@@ -16,25 +18,59 @@ module RubyLLM
|
|
|
16
18
|
|
|
17
19
|
# Sits directly above the adapter, inside the retry middleware, so this
|
|
18
20
|
# runs once per attempt: streaming state stored on the env by a previous
|
|
19
|
-
# attempt must not leak into the next one
|
|
21
|
+
# attempt must not leak into the next one, and a body the previous
|
|
22
|
+
# attempt read to its end is sent again from its start.
|
|
20
23
|
def call(env)
|
|
24
|
+
context = env[:request]&.context
|
|
25
|
+
provider = context&.[](PROVIDER_KEY) || @provider
|
|
26
|
+
attempt(env, context, provider)
|
|
27
|
+
rescue *NETWORK_FAILURES => e
|
|
28
|
+
raise if context&.dig(Connection::STREAM_PROGRESS_KEY, :callback_error).equal?(e)
|
|
29
|
+
|
|
30
|
+
raise ConnectionError.new(e.message, provider: (provider.slug if provider.respond_to?(:slug)))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def attempt(env, context, provider)
|
|
21
36
|
env[:streaming_error_response] = nil
|
|
22
37
|
env[:streaming_state] = nil
|
|
23
|
-
|
|
38
|
+
env[:body].rewind if env[:body].respond_to?(:rewind)
|
|
24
39
|
context&.[](STREAM_RESET_KEY)&.call
|
|
25
|
-
provider = context&.[](PROVIDER_KEY) || @provider
|
|
26
40
|
@app.call(env).on_complete do |response|
|
|
41
|
+
context&.[](STREAM_FAILURE_KEY)&.call(response)
|
|
27
42
|
apply_retry_delay(response, provider)
|
|
28
43
|
self.class.parse_error(provider:, response: streaming_error_response(response))
|
|
29
44
|
end
|
|
45
|
+
rescue RubyLLM::Error => e
|
|
46
|
+
apply_raised_retry_delay(env, provider, e)
|
|
47
|
+
raise
|
|
30
48
|
end
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
# A rate limit reported inside a stream raises with no failed HTTP
|
|
51
|
+
# status, and without an env on Faraday 1, so its hint is only on the
|
|
52
|
+
# error's own response.
|
|
53
|
+
def apply_raised_retry_delay(env, provider, error)
|
|
54
|
+
return apply_retry_delay(env, provider) unless rate_limit_report?(error)
|
|
55
|
+
|
|
56
|
+
env[:streaming_error_response] ||= error.response
|
|
57
|
+
env[:response_headers] ||= Faraday::Utils::Headers.new
|
|
58
|
+
apply_retry_delay(env, provider, status: 429)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def rate_limit_report?(error)
|
|
62
|
+
return true if error.is_a?(RateLimitError)
|
|
63
|
+
|
|
64
|
+
[error.response&.body].flatten.grep(Hash).any? do |part|
|
|
65
|
+
part['error'].is_a?(Hash) && part['error']['code'] == 429
|
|
66
|
+
end
|
|
67
|
+
end
|
|
33
68
|
|
|
34
69
|
# The retry middleware only reads the standard Retry-After header, so
|
|
35
|
-
# other retry hints are normalized into seconds here.
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
# other retry hints are normalized into seconds here. A failed stream
|
|
71
|
+
# usually raises while it is read, before on_complete, and keeps its
|
|
72
|
+
# body in the stored streaming error rather than on the response.
|
|
73
|
+
def apply_retry_delay(response, provider, status: response_status(response))
|
|
38
74
|
return unless status && status >= 400
|
|
39
75
|
|
|
40
76
|
headers = response[:response_headers]
|
|
@@ -44,8 +80,12 @@ module RubyLLM
|
|
|
44
80
|
headers['Retry-After'] = delay.to_s if delay
|
|
45
81
|
end
|
|
46
82
|
|
|
83
|
+
def response_status(response)
|
|
84
|
+
response.respond_to?(:status) ? response.status : response[:status]
|
|
85
|
+
end
|
|
86
|
+
|
|
47
87
|
def provider_retry_delay(provider, response, status)
|
|
48
|
-
provider&.retry_delay(response) if status == 429
|
|
88
|
+
provider&.retry_delay(streaming_error_response(response)) if status == 429
|
|
49
89
|
end
|
|
50
90
|
|
|
51
91
|
def millisecond_retry_delay(headers)
|
|
@@ -78,7 +118,11 @@ module RubyLLM
|
|
|
78
118
|
/input or output tokens? must be reduced/i,
|
|
79
119
|
/reduce the length of messages/i,
|
|
80
120
|
/prompt is too long/i,
|
|
81
|
-
/context limit/i
|
|
121
|
+
/context limit/i,
|
|
122
|
+
/exceeded model token limit/i,
|
|
123
|
+
/prompt exceeds max length/i,
|
|
124
|
+
/input is too long/i,
|
|
125
|
+
/longer than the maximum model length/i
|
|
82
126
|
].freeze
|
|
83
127
|
|
|
84
128
|
RATE_LIMIT_PATTERNS = [
|
|
@@ -105,59 +149,60 @@ module RubyLLM
|
|
|
105
149
|
].freeze
|
|
106
150
|
|
|
107
151
|
def parse_error(provider:, response:)
|
|
108
|
-
|
|
152
|
+
status = failure_status(provider, response)
|
|
153
|
+
return if (200..399).cover?(status)
|
|
109
154
|
|
|
110
155
|
message = provider&.parse_error(response)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
when 400
|
|
114
|
-
raise_bad_request(message, response)
|
|
115
|
-
when 401
|
|
116
|
-
raise UnauthorizedError.new(message, response:)
|
|
117
|
-
when 402
|
|
118
|
-
raise PaymentRequiredError.new(message, response:)
|
|
119
|
-
when 403
|
|
120
|
-
raise_forbidden(message, response)
|
|
121
|
-
when 413
|
|
122
|
-
raise_too_large(message, response)
|
|
123
|
-
when 429
|
|
124
|
-
raise RateLimitError.new(message, response:) if rate_limited?(message)
|
|
125
|
-
raise ContextLengthExceededError.new(message, response:) if context_length_exceeded?(message)
|
|
126
|
-
|
|
127
|
-
raise RateLimitError.new(message, response:)
|
|
128
|
-
when 500
|
|
129
|
-
raise ServerError.new(message, response:)
|
|
130
|
-
when 502..504
|
|
131
|
-
raise ServiceUnavailableError.new(message, response:)
|
|
132
|
-
when 529
|
|
133
|
-
raise OverloadedError.new(message, response:)
|
|
134
|
-
else
|
|
135
|
-
raise Error.new(message, response:)
|
|
136
|
-
end
|
|
156
|
+
raise error_class(message, status).new(response:, provider_message: message,
|
|
157
|
+
**provider_details(provider, response))
|
|
137
158
|
end
|
|
138
159
|
|
|
139
160
|
private
|
|
140
161
|
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
raise PaymentRequiredError.new(message, response:) if payment_required?(message)
|
|
162
|
+
def failure_status(provider, response)
|
|
163
|
+
return response.status unless (200..299).cover?(response.status)
|
|
164
|
+
|
|
165
|
+
provider&.error_status(response) || response.status
|
|
166
|
+
end
|
|
147
167
|
|
|
148
|
-
|
|
168
|
+
def provider_details(provider, response)
|
|
169
|
+
{
|
|
170
|
+
provider: (provider.slug if provider.respond_to?(:slug)),
|
|
171
|
+
error_code: (provider.parse_error_code(response) if provider.respond_to?(:parse_error_code)),
|
|
172
|
+
redact: (provider.config&.redact_error_messages if provider.respond_to?(:config))
|
|
173
|
+
}
|
|
149
174
|
end
|
|
150
175
|
|
|
151
|
-
def
|
|
152
|
-
|
|
176
|
+
def error_class(message, status)
|
|
177
|
+
case status
|
|
178
|
+
when 400 then bad_request_class(message)
|
|
179
|
+
when 401 then UnauthorizedError
|
|
180
|
+
when 402 then PaymentRequiredError
|
|
181
|
+
when 403 then authentication_failed?(message) ? UnauthorizedError : ForbiddenError
|
|
182
|
+
when 413 then context_length_exceeded?(message) ? ContextLengthExceededError : Error
|
|
183
|
+
when 429 then rate_limit_class(message)
|
|
184
|
+
when 500 then ServerError
|
|
185
|
+
when 502..504 then ServiceUnavailableError
|
|
186
|
+
when 529 then OverloadedError
|
|
187
|
+
else Error
|
|
188
|
+
end
|
|
189
|
+
end
|
|
153
190
|
|
|
154
|
-
|
|
191
|
+
def bad_request_class(message)
|
|
192
|
+
if authentication_failed?(message) then UnauthorizedError
|
|
193
|
+
elsif context_length_exceeded?(message) then ContextLengthExceededError
|
|
194
|
+
elsif rate_limited?(message) then RateLimitError
|
|
195
|
+
elsif overloaded?(message) then OverloadedError
|
|
196
|
+
elsif payment_required?(message) then PaymentRequiredError
|
|
197
|
+
else BadRequestError
|
|
198
|
+
end
|
|
155
199
|
end
|
|
156
200
|
|
|
157
|
-
def
|
|
158
|
-
|
|
201
|
+
def rate_limit_class(message)
|
|
202
|
+
return RateLimitError if rate_limited?(message)
|
|
203
|
+
return ContextLengthExceededError if context_length_exceeded?(message)
|
|
159
204
|
|
|
160
|
-
|
|
205
|
+
RateLimitError
|
|
161
206
|
end
|
|
162
207
|
|
|
163
208
|
def context_length_exceeded?(message)
|
|
@@ -6,6 +6,8 @@ module RubyLLM
|
|
|
6
6
|
# whatever pieces the network delivers. Each dispatched event is yielded
|
|
7
7
|
# as its type, data, and last event ID. The last event ID and the
|
|
8
8
|
# reconnection time the server asked for stay readable between events.
|
|
9
|
+
# The standard ignores a line that names no known field; +on_unknown_line+
|
|
10
|
+
# receives each one, undecoded, for a caller that wants to look at it.
|
|
9
11
|
class EventStreamParser # :nodoc:
|
|
10
12
|
BOM = "\xEF\xBB\xBF".b.freeze
|
|
11
13
|
LINE_BREAK = /[\r\n]/
|
|
@@ -16,7 +18,8 @@ module RubyLLM
|
|
|
16
18
|
|
|
17
19
|
attr_reader :last_event_id, :reconnection_time
|
|
18
20
|
|
|
19
|
-
def initialize
|
|
21
|
+
def initialize(on_unknown_line: nil)
|
|
22
|
+
@on_unknown_line = on_unknown_line
|
|
20
23
|
@buffer = String.new(encoding: Encoding::BINARY)
|
|
21
24
|
@position = 0
|
|
22
25
|
@searched = 0
|
|
@@ -43,6 +46,16 @@ module RubyLLM
|
|
|
43
46
|
@searched = @buffer.bytesize
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
# Reads a last line the stream ended without a line break. It can't
|
|
50
|
+
# complete an event, which needs a blank line after it.
|
|
51
|
+
def finish
|
|
52
|
+
return if @position >= @buffer.bytesize
|
|
53
|
+
|
|
54
|
+
line = @buffer.byteslice(@position, @buffer.bytesize - @position)
|
|
55
|
+
@position = @searched = @buffer.bytesize
|
|
56
|
+
process_line(line)
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
private
|
|
47
60
|
|
|
48
61
|
def compact
|
|
@@ -85,18 +98,19 @@ module RubyLLM
|
|
|
85
98
|
return if line.start_with?(':')
|
|
86
99
|
|
|
87
100
|
colon = line.index(':')
|
|
88
|
-
return process_field(line, +'') unless colon
|
|
101
|
+
return process_field(line, +'', line) unless colon
|
|
89
102
|
|
|
90
103
|
offset = line.getbyte(colon + 1) == SPACE ? colon + 2 : colon + 1
|
|
91
|
-
process_field(line.byteslice(0, colon), line.byteslice(offset, line.bytesize - offset))
|
|
104
|
+
process_field(line.byteslice(0, colon), line.byteslice(offset, line.bytesize - offset), line)
|
|
92
105
|
end
|
|
93
106
|
|
|
94
|
-
def process_field(field, value)
|
|
107
|
+
def process_field(field, value, line)
|
|
95
108
|
case field
|
|
96
109
|
when 'event' then @event_type = decode(value)
|
|
97
110
|
when 'data' then @data << decode(value) << "\n"
|
|
98
111
|
when 'id' then @id_buffer = decode(value) unless value.include?("\0")
|
|
99
112
|
when 'retry' then @reconnection_time = value.to_i if value.match?(DIGITS)
|
|
113
|
+
else @on_unknown_line&.call(line)
|
|
100
114
|
end
|
|
101
115
|
end
|
|
102
116
|
|
|
@@ -6,6 +6,8 @@ require 'json'
|
|
|
6
6
|
module RubyLLM
|
|
7
7
|
module Transport
|
|
8
8
|
class JsonResponse < Faraday::Middleware # :nodoc:
|
|
9
|
+
RAW_KEY = :ruby_llm_raw_body
|
|
10
|
+
|
|
9
11
|
def initialize(app, parser_options: {}, preserve_raw: false)
|
|
10
12
|
super(app)
|
|
11
13
|
@parser_options = parser_options || {}
|
|
@@ -14,16 +16,20 @@ module RubyLLM
|
|
|
14
16
|
|
|
15
17
|
def on_complete(env)
|
|
16
18
|
body = env[:body]
|
|
17
|
-
return unless body.respond_to?(:to_str) && json_response?(env)
|
|
19
|
+
return unless body.respond_to?(:to_str) && json_response?(env) && !raw?(env)
|
|
18
20
|
|
|
19
21
|
env[:raw_body] = body if @preserve_raw
|
|
20
22
|
env[:body] = body.strip.empty? ? nil : JSON.parse(body, **@parser_options)
|
|
21
|
-
rescue StandardError, SyntaxError
|
|
22
|
-
raise
|
|
23
|
+
rescue StandardError, SyntaxError
|
|
24
|
+
raise Error.new('The response body is not valid JSON', response: env[:response])
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
private
|
|
26
28
|
|
|
29
|
+
def raw?(env)
|
|
30
|
+
env[:request]&.context&.dig(RAW_KEY)
|
|
31
|
+
end
|
|
32
|
+
|
|
27
33
|
def json_response?(env)
|
|
28
34
|
content_type = env[:response_headers]['content-type'].to_s.split(';', 2).first
|
|
29
35
|
content_type&.match?(/\bjson$/)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'faraday/logging/formatter'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Transport
|
|
8
|
+
# A request that succeeded and was billed must not fail because a debug
|
|
9
|
+
# log filter ran out of time on a large body.
|
|
10
|
+
class LogFormatter < Faraday::Logging::Formatter # :nodoc:
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def apply_filters(output)
|
|
14
|
+
super
|
|
15
|
+
rescue Regexp::TimeoutError
|
|
16
|
+
'[NOT LOGGED: FILTER TIMED OUT]'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|