ruby_llm 1.16.0 → 2.0.0.rc2
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/.rdoc_options +25 -0
- data/README.md +85 -32
- data/exe/ruby_llm +8 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
- 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.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
- 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.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
- data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
- data/lib/generators/ruby_llm/provider/cli.rb +175 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
- data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
- data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
- data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
- data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
- data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
- data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
- data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
- data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
- data/lib/ruby_llm/accounting/usage.rb +245 -0
- data/lib/ruby_llm/active_record/acts_as.rb +94 -111
- data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
- data/lib/ruby_llm/active_record/message_methods.rb +113 -136
- data/lib/ruby_llm/active_record/model.rb +135 -0
- data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
- data/lib/ruby_llm/active_record/tool_call.rb +33 -0
- data/lib/ruby_llm/active_record/usage.rb +61 -0
- data/lib/ruby_llm/agent.rb +1065 -151
- data/lib/ruby_llm/aliases.json +268 -100
- data/lib/ruby_llm/attachment.rb +187 -48
- data/lib/ruby_llm/batch.rb +432 -0
- data/lib/ruby_llm/cached_content.rb +112 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
- data/lib/ruby_llm/chat.rb +1127 -198
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +261 -24
- data/lib/ruby_llm/context.rb +128 -6
- data/lib/ruby_llm/cost.rb +217 -80
- data/lib/ruby_llm/downloaded_file.rb +33 -0
- data/lib/ruby_llm/embedding.rb +121 -17
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +159 -23
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +153 -32
- data/lib/ruby_llm/message.rb +233 -54
- data/lib/ruby_llm/model/modalities.rb +17 -4
- data/lib/ruby_llm/model/pricing.rb +24 -5
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +55 -15
- data/lib/ruby_llm/model.rb +244 -2
- data/lib/ruby_llm/models/aliases.rb +41 -0
- data/lib/ruby_llm/models/registry.rb +165 -0
- data/lib/ruby_llm/models/schema.rb +99 -0
- data/lib/ruby_llm/models.json +73261 -33733
- data/lib/ruby_llm/models.rb +477 -215
- data/lib/ruby_llm/moderation.rb +139 -26
- data/lib/ruby_llm/ocr.rb +112 -0
- data/lib/ruby_llm/prompt.rb +79 -0
- data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
- data/lib/ruby_llm/protocol/streaming.rb +230 -0
- data/lib/ruby_llm/protocol.rb +662 -0
- data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
- data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
- data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
- data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
- data/lib/ruby_llm/protocols/anthropic.rb +101 -0
- data/lib/ruby_llm/protocols/azure/files.rb +16 -0
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
- data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
- data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
- data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
- data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
- data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
- data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
- data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
- data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
- data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
- data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
- data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
- data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
- data/lib/ruby_llm/protocols/cohere.rb +21 -0
- data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
- data/lib/ruby_llm/protocols/converse.rb +54 -0
- data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
- data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
- data/lib/ruby_llm/protocols/deepgram.rb +19 -0
- data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
- data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
- data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
- data/lib/ruby_llm/protocols/files.rb +119 -0
- data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
- data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
- data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
- data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
- data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
- data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
- data/lib/ruby_llm/protocols/gemini.rb +35 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
- data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
- data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
- data/lib/ruby_llm/protocols/interactions.rb +29 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
- data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
- data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
- data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
- data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
- data/lib/ruby_llm/protocols/openai/files.rb +42 -0
- data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
- data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
- data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
- data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
- data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
- data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
- data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
- data/lib/ruby_llm/protocols/responses/media.rb +61 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
- data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
- data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
- data/lib/ruby_llm/protocols/responses.rb +35 -0
- data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
- data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
- data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
- data/lib/ruby_llm/protocols/xai/files.rb +30 -0
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
- data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
- data/lib/ruby_llm/provider.rb +560 -128
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
- data/lib/ruby_llm/providers/anthropic.rb +4 -6
- data/lib/ruby_llm/providers/azure/audio.rb +18 -0
- data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/azure/chat.rb +2 -9
- data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
- data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
- data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
- data/lib/ruby_llm/providers/azure/images.rb +22 -0
- data/lib/ruby_llm/providers/azure/media.rb +5 -14
- data/lib/ruby_llm/providers/azure/models.rb +35 -0
- data/lib/ruby_llm/providers/azure/responses.rb +26 -0
- data/lib/ruby_llm/providers/azure/videos.rb +64 -0
- data/lib/ruby_llm/providers/azure.rb +77 -78
- data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
- data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
- data/lib/ruby_llm/providers/bedrock.rb +216 -45
- data/lib/ruby_llm/providers/cohere.rb +31 -0
- data/lib/ruby_llm/providers/deepgram.rb +37 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
- data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
- data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
- data/lib/ruby_llm/providers/deepseek.rb +9 -2
- data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
- data/lib/ruby_llm/providers/gemini.rb +15 -8
- data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
- data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
- data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
- data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
- data/lib/ruby_llm/providers/gpustack.rb +35 -11
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
- data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
- data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
- data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
- data/lib/ruby_llm/providers/mistral/media.rb +6 -18
- data/lib/ruby_llm/providers/mistral/models.rb +57 -23
- data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
- data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
- data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
- data/lib/ruby_llm/providers/mistral.rb +16 -4
- data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
- data/lib/ruby_llm/providers/ollama/media.rb +6 -15
- data/lib/ruby_llm/providers/ollama/models.rb +50 -9
- data/lib/ruby_llm/providers/ollama.rb +9 -8
- data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
- data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
- data/lib/ruby_llm/providers/openai/models.rb +23 -23
- data/lib/ruby_llm/providers/openai/responses.rb +13 -0
- data/lib/ruby_llm/providers/openai.rb +92 -11
- data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
- data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
- data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
- data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
- data/lib/ruby_llm/providers/openrouter.rb +78 -20
- data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +28 -20
- data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
- data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
- data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
- data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
- data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
- data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
- data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
- data/lib/ruby_llm/providers/vertexai.rb +160 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +3 -2
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
- data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
- data/lib/ruby_llm/providers/xai/images.rb +91 -0
- data/lib/ruby_llm/providers/xai/models.rb +32 -36
- data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
- data/lib/ruby_llm/providers/xai/responses.rb +52 -0
- data/lib/ruby_llm/providers/xai/speech.rb +45 -0
- data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
- data/lib/ruby_llm/providers/xai/videos.rb +87 -0
- data/lib/ruby_llm/providers/xai.rb +15 -5
- data/lib/ruby_llm/railtie.rb +7 -16
- data/lib/ruby_llm/rerank.rb +105 -0
- data/lib/ruby_llm/research_job.rb +241 -0
- data/lib/ruby_llm/search_results.rb +68 -0
- data/lib/ruby_llm/server_tool_call.rb +73 -0
- data/lib/ruby_llm/speech.rb +159 -0
- data/lib/ruby_llm/speech_chunk.rb +33 -0
- data/lib/ruby_llm/support/deprecator.rb +22 -0
- data/lib/ruby_llm/support/inspectable.rb +49 -0
- data/lib/ruby_llm/support/instrumentation.rb +41 -0
- data/lib/ruby_llm/support/utils.rb +147 -0
- data/lib/ruby_llm/thinking.rb +127 -20
- data/lib/ruby_llm/tokenization.rb +59 -0
- data/lib/ruby_llm/tokens.rb +103 -33
- data/lib/ruby_llm/tool.rb +266 -91
- data/lib/ruby_llm/tool_call.rb +36 -3
- data/lib/ruby_llm/tools/server_tools.rb +109 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
- data/lib/ruby_llm/transcription.rb +138 -13
- data/lib/ruby_llm/transcription_chunk.rb +68 -0
- data/lib/ruby_llm/transport/connection.rb +193 -0
- data/lib/ruby_llm/transport/error_middleware.rb +131 -0
- data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
- data/lib/ruby_llm/uploaded_file.rb +144 -0
- data/lib/ruby_llm/version.rb +2 -1
- data/lib/ruby_llm/video.rb +136 -0
- data/lib/ruby_llm/video_job.rb +150 -0
- data/lib/ruby_llm/workflow.rb +91 -0
- data/lib/ruby_llm.rb +380 -6
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +339 -97
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
- data/lib/ruby_llm/active_record/model_methods.rb +0 -82
- data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
- data/lib/ruby_llm/aliases.rb +0 -41
- data/lib/ruby_llm/connection.rb +0 -159
- data/lib/ruby_llm/content.rb +0 -91
- data/lib/ruby_llm/deprecator.rb +0 -24
- data/lib/ruby_llm/error_middleware.rb +0 -81
- data/lib/ruby_llm/instrumentation.rb +0 -36
- data/lib/ruby_llm/mime_type.rb +0 -96
- data/lib/ruby_llm/model/info.rb +0 -164
- data/lib/ruby_llm/model_registry.rb +0 -39
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
- data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
- data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
- data/lib/ruby_llm/providers/gemini/images.rb +0 -47
- data/lib/ruby_llm/providers/gemini/models.rb +0 -38
- data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/openai/chat.rb +0 -236
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
- data/lib/ruby_llm/providers/openai/images.rb +0 -90
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
- data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
- data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
- data/lib/ruby_llm/stream_accumulator.rb +0 -218
- data/lib/ruby_llm/streaming.rb +0 -179
- data/lib/ruby_llm/tool_concurrency.rb +0 -105
- data/lib/ruby_llm/utils.rb +0 -130
- data/lib/tasks/models.rake +0 -593
- data/lib/tasks/release.rake +0 -94
- data/lib/tasks/vcr.rake +0 -124
data/lib/ruby_llm/moderation.rb
CHANGED
|
@@ -1,56 +1,169 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# A Moderation holds the result of screening text or images for
|
|
5
|
+
# potentially harmful content. Most code obtains one through
|
|
6
|
+
# RubyLLM.moderate.
|
|
7
|
+
#
|
|
8
|
+
# result = RubyLLM.moderate("This is a safe message about Ruby programming")
|
|
9
|
+
# result.flagged? # => false
|
|
10
|
+
#
|
|
11
|
+
# RubyLLM.moderate(with: "profile.png").flagged?
|
|
12
|
+
#
|
|
6
13
|
class Moderation
|
|
7
|
-
|
|
14
|
+
include Support::Inspectable
|
|
15
|
+
include Accounting::Usage::Result
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
# A Result is the verdict for a single moderated input. Providers return
|
|
18
|
+
# results in different shapes. RubyLLM normalizes all of them into Result
|
|
19
|
+
# objects on Moderation#results.
|
|
20
|
+
#
|
|
21
|
+
# result = RubyLLM.moderate("Some user input").results.first
|
|
22
|
+
# result.flagged? # => false
|
|
23
|
+
# result.categories # => []
|
|
24
|
+
# result.category_scores["violence"] # => 0.0004
|
|
25
|
+
#
|
|
26
|
+
class Result
|
|
27
|
+
# The names of the categories flagged for this input, as an array of
|
|
28
|
+
# strings, empty when nothing was flagged.
|
|
29
|
+
attr_reader :categories
|
|
30
|
+
|
|
31
|
+
# The confidence scores for this input, as a hash of category name to
|
|
32
|
+
# a score between 0.0 and 1.0. Empty when the provider reports none.
|
|
33
|
+
attr_reader :category_scores
|
|
34
|
+
|
|
35
|
+
def initialize(flagged:, categories:, category_scores:) # :nodoc:
|
|
36
|
+
@flagged = flagged
|
|
37
|
+
@categories = categories
|
|
38
|
+
@category_scores = category_scores
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.from_h(data) # :nodoc:
|
|
42
|
+
flagged_names = (data['categories'] || {}).select { |_category, flagged| flagged }.keys
|
|
43
|
+
new(
|
|
44
|
+
flagged: data.fetch('flagged', flagged_names.any?),
|
|
45
|
+
categories: flagged_names,
|
|
46
|
+
category_scores: data['category_scores'] || {}
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns +true+ if this input was flagged as potentially harmful,
|
|
51
|
+
# +false+ otherwise.
|
|
52
|
+
def flagged?
|
|
53
|
+
@flagged
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The provider-assigned identifier of the moderation request.
|
|
58
|
+
attr_reader :id
|
|
59
|
+
|
|
60
|
+
# The id of the model that performed the moderation, or +nil+ for an
|
|
61
|
+
# operation that does not select a model.
|
|
62
|
+
attr_reader :model
|
|
63
|
+
|
|
64
|
+
# The per-input verdicts, as an array of Result objects, one per
|
|
65
|
+
# moderated input.
|
|
66
|
+
attr_reader :results
|
|
67
|
+
|
|
68
|
+
# The original provider response, or an array of responses when each
|
|
69
|
+
# input requires a separate request.
|
|
70
|
+
attr_reader :raw
|
|
71
|
+
|
|
72
|
+
def initialize(id:, model:, results:, raw: nil) # :nodoc:
|
|
10
73
|
@id = id
|
|
11
74
|
@model = model
|
|
12
75
|
@results = results
|
|
76
|
+
@raw = raw
|
|
13
77
|
end
|
|
14
78
|
|
|
15
|
-
|
|
79
|
+
# Screens +input+ and optional image attachments and returns a Moderation with the
|
|
80
|
+
# provider's verdict. Uses the configured default moderation model when
|
|
81
|
+
# +model+ is not given. Pass +provider:+ and <tt>assume_model_exists: true</tt>
|
|
82
|
+
# to use a model that is not in the registry. An explicitly selected
|
|
83
|
+
# provider may instead use a configured resource without a model.
|
|
84
|
+
#
|
|
85
|
+
# RubyLLM.moderate("User message")
|
|
86
|
+
# RubyLLM.moderate(["First comment", "Second comment"]).results
|
|
87
|
+
# RubyLLM.moderate("Caption", with: "screenshot.png")
|
|
88
|
+
#
|
|
89
|
+
def self.moderate(input = nil,
|
|
16
90
|
model: nil,
|
|
91
|
+
with: nil,
|
|
17
92
|
provider: nil,
|
|
18
93
|
assume_model_exists: false,
|
|
19
|
-
context: nil
|
|
94
|
+
context: nil,
|
|
95
|
+
provider_options: {},
|
|
96
|
+
metadata: nil)
|
|
97
|
+
attachments = Attachment.wrap(with)
|
|
98
|
+
raise ArgumentError, 'must provide input text, image attachment, or both' if input.nil? && attachments.empty?
|
|
99
|
+
|
|
20
100
|
config = context&.config || RubyLLM.config
|
|
21
|
-
model
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
101
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
102
|
+
config: config, operation: :moderate,
|
|
103
|
+
default_model: config.default_moderation_model)
|
|
104
|
+
empty_tokens = Tokens.new
|
|
105
|
+
payload = {
|
|
106
|
+
provider: provider_instance.slug,
|
|
107
|
+
provider_class: provider_instance.class.display_name,
|
|
108
|
+
model: model&.id,
|
|
109
|
+
model_info: model,
|
|
110
|
+
input: input,
|
|
111
|
+
attachment_count: attachments.size,
|
|
112
|
+
provider_options: provider_options,
|
|
113
|
+
metadata: metadata,
|
|
114
|
+
tokens: empty_tokens,
|
|
115
|
+
cost: Cost.new(tokens: empty_tokens, model:)
|
|
116
|
+
}
|
|
25
117
|
|
|
26
|
-
|
|
118
|
+
RubyLLM.instrument('moderation.ruby_llm', payload, config: config) do |event|
|
|
119
|
+
result = provider_instance.moderate(input, model:, with: attachments, provider_options:)
|
|
120
|
+
event[:result] = result
|
|
121
|
+
event[:flagged] = result.flagged?
|
|
122
|
+
event[:tokens] = result.tokens
|
|
123
|
+
event[:cost] = result.cost
|
|
124
|
+
result
|
|
125
|
+
end
|
|
27
126
|
end
|
|
28
127
|
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
128
|
+
# Returns +true+ if any input was flagged as potentially harmful,
|
|
129
|
+
# +false+ otherwise.
|
|
130
|
+
def flagged?
|
|
131
|
+
results.any?(&:flagged?)
|
|
32
132
|
end
|
|
33
133
|
|
|
34
|
-
#
|
|
35
|
-
|
|
36
|
-
|
|
134
|
+
# Returns provider-reported usage across every attempt. Its fields are
|
|
135
|
+
# +nil+ when the provider did not report any.
|
|
136
|
+
def tokens
|
|
137
|
+
ruby_llm_usage_tokens
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Returns the moderation cost across every provider attempt.
|
|
141
|
+
def cost
|
|
142
|
+
ruby_llm_usage_cost
|
|
37
143
|
end
|
|
38
144
|
|
|
39
|
-
#
|
|
145
|
+
# Returns the unique names of the categories flagged across all results.
|
|
146
|
+
#
|
|
147
|
+
# result.flagged_categories # => ["harassment", "violence"]
|
|
148
|
+
#
|
|
40
149
|
def flagged_categories
|
|
41
|
-
results.flat_map
|
|
42
|
-
result['categories']&.select { |_category, flagged| flagged }&.keys || []
|
|
43
|
-
end.uniq
|
|
150
|
+
results.flat_map(&:categories).uniq
|
|
44
151
|
end
|
|
45
152
|
|
|
46
|
-
#
|
|
153
|
+
# Returns the confidence scores across all results, as a hash of category
|
|
154
|
+
# name to a score between 0.0 and 1.0. Keeps the highest score per
|
|
155
|
+
# category when there are multiple results.
|
|
156
|
+
#
|
|
157
|
+
# result.category_scores["violence"] # => 0.0001
|
|
158
|
+
#
|
|
47
159
|
def category_scores
|
|
48
|
-
results.
|
|
160
|
+
results.map(&:category_scores).reduce({}) do |merged, scores|
|
|
161
|
+
merged.merge(scores) { |_category, left, right| [left, right].max }
|
|
162
|
+
end
|
|
49
163
|
end
|
|
50
164
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
results.first&.dig('categories') || {}
|
|
165
|
+
def inspect_attributes # :nodoc:
|
|
166
|
+
{ id: id, model: model, flagged: flagged? }
|
|
54
167
|
end
|
|
55
168
|
end
|
|
56
169
|
end
|
data/lib/ruby_llm/ocr.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# An OCR is the text a document AI model extracted from a file.
|
|
5
|
+
# RubyLLM.ocr returns one. Each page carries the extracted markdown along
|
|
6
|
+
# with any images and tables the provider reports.
|
|
7
|
+
#
|
|
8
|
+
# ocr = RubyLLM.ocr("contract.pdf")
|
|
9
|
+
# ocr.markdown # => "# Contract\n\n..."
|
|
10
|
+
# ocr.pages.first.markdown # => "# Contract\n..."
|
|
11
|
+
#
|
|
12
|
+
class OCR
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
include Accounting::Usage::Result
|
|
15
|
+
|
|
16
|
+
# One page of an OCR result: the zero-based page +index+, the extracted
|
|
17
|
+
# +markdown+, and the +images+ and +tables+ the provider reports for the
|
|
18
|
+
# page, or +nil+ when it reports none. +raw+ holds the provider's
|
|
19
|
+
# unmodified page hash, including any fields beyond these.
|
|
20
|
+
Page = Struct.new(:index, :markdown, :images, :tables, :raw, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
# The id of the model that performed the OCR.
|
|
23
|
+
attr_reader :model
|
|
24
|
+
|
|
25
|
+
# The provider's usage block for the request, such as the number of
|
|
26
|
+
# pages processed, or +nil+ when the provider does not report one.
|
|
27
|
+
attr_reader :usage # :nodoc:
|
|
28
|
+
|
|
29
|
+
# The provider's raw response hash.
|
|
30
|
+
attr_reader :raw
|
|
31
|
+
|
|
32
|
+
# Extracts the text of +file+ and returns an OCR result. Most code calls
|
|
33
|
+
# this through RubyLLM.ocr. The file may be a path, URL, IO object, or
|
|
34
|
+
# Attachment; accepted document and image formats depend on the provider.
|
|
35
|
+
#
|
|
36
|
+
# +model:+ selects the OCR model and defaults to the configured
|
|
37
|
+
# +default_ocr_model+. +provider:+ forces a specific provider, and
|
|
38
|
+
# +assume_model_exists:+ skips the registry lookup. +context:+ supplies
|
|
39
|
+
# a Context whose configuration replaces the global one. +metadata:+ is
|
|
40
|
+
# included in the instrumentation payload. +pages:+ limits the read to
|
|
41
|
+
# the given zero-based page indexes. +provider_options:+ merges options
|
|
42
|
+
# into the request in the provider's own vocabulary, such as Mistral's
|
|
43
|
+
# +include_image_base64:+ or +table_format:+.
|
|
44
|
+
#
|
|
45
|
+
# RubyLLM.ocr("report.pdf")
|
|
46
|
+
# RubyLLM.ocr("https://example.com/scan.png")
|
|
47
|
+
# RubyLLM.ocr("report.pdf", pages: [0, 1], provider_options: { table_format: "html" })
|
|
48
|
+
#
|
|
49
|
+
# Raises RubyLLM::ModelNotFoundError if +model:+ is not in the registry,
|
|
50
|
+
# and RubyLLM::Error when the provider has no OCR support.
|
|
51
|
+
def self.ocr(file,
|
|
52
|
+
model: nil,
|
|
53
|
+
provider: nil,
|
|
54
|
+
assume_model_exists: false,
|
|
55
|
+
context: nil,
|
|
56
|
+
pages: nil,
|
|
57
|
+
provider_options: {},
|
|
58
|
+
metadata: nil)
|
|
59
|
+
config = context&.config || RubyLLM.config
|
|
60
|
+
model ||= config.default_ocr_model
|
|
61
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
62
|
+
config: config)
|
|
63
|
+
payload = {
|
|
64
|
+
provider: provider_instance.slug,
|
|
65
|
+
provider_class: provider_instance.class.display_name,
|
|
66
|
+
model: model.id,
|
|
67
|
+
model_info: model,
|
|
68
|
+
pages: pages,
|
|
69
|
+
provider_options: provider_options,
|
|
70
|
+
metadata: metadata
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
RubyLLM.instrument('ocr.ruby_llm', payload, config: config) do |event|
|
|
74
|
+
result = provider_instance.ocr(file, model:, pages:, provider_options:)
|
|
75
|
+
event[:result] = result
|
|
76
|
+
event[:response_model] = result.model
|
|
77
|
+
result
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def initialize(pages:, model:, usage: nil, raw: nil) # :nodoc:
|
|
82
|
+
@page_hashes = Array(pages)
|
|
83
|
+
@model = model
|
|
84
|
+
@usage = usage
|
|
85
|
+
@raw = raw
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returns the pages of the document as an array of Page structs.
|
|
89
|
+
def pages
|
|
90
|
+
@pages ||= @page_hashes.map.with_index do |page, position|
|
|
91
|
+
next page if page.is_a?(Page)
|
|
92
|
+
|
|
93
|
+
Page.new(
|
|
94
|
+
index: page['index'] || position,
|
|
95
|
+
markdown: page['markdown'],
|
|
96
|
+
images: page['images'],
|
|
97
|
+
tables: page['tables'],
|
|
98
|
+
raw: page
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns the markdown of every page, joined with blank lines.
|
|
104
|
+
def markdown
|
|
105
|
+
pages.filter_map(&:markdown).join("\n\n")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def inspect_attributes # :nodoc:
|
|
109
|
+
{ model: model, pages: pages.length }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
# Resolves prompt templates from the application and registered engine directories.
|
|
8
|
+
class Prompt
|
|
9
|
+
# The ordered directories searched for prompt files. The application's
|
|
10
|
+
# prompt directory is always first, so an application can override any
|
|
11
|
+
# prompt an engine ships by placing a file at the same relative path.
|
|
12
|
+
# Engines append their own directory in an initializer:
|
|
13
|
+
#
|
|
14
|
+
# initializer "my_engine.prompts" do
|
|
15
|
+
# RubyLLM::Prompt.roots << MyEngine::Engine.root.join("app/prompts")
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
class Roots
|
|
19
|
+
include Enumerable
|
|
20
|
+
|
|
21
|
+
def initialize(&default) # :nodoc:
|
|
22
|
+
@default = default
|
|
23
|
+
@registered = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Appends a prompt directory and returns +self+.
|
|
27
|
+
def <<(path)
|
|
28
|
+
@registered << Pathname.new(path)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
alias push <<
|
|
32
|
+
|
|
33
|
+
# Yields each prompt directory in lookup order. Returns an Enumerator without a block.
|
|
34
|
+
def each(&)
|
|
35
|
+
[@default.call, *@registered].each(&)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
attr_reader :name # :nodoc:
|
|
40
|
+
|
|
41
|
+
def initialize(name) # :nodoc:
|
|
42
|
+
@name = name.to_s
|
|
43
|
+
@filename = @name.end_with?('.txt.erb') ? @name : "#{@name}.txt.erb"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def path # :nodoc:
|
|
47
|
+
@path ||= candidates.find { |candidate| File.exist?(candidate) } || candidates.first
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render(**locals) # :nodoc:
|
|
51
|
+
raise PromptNotFoundError, "Prompt file not found: #{path}" unless File.exist?(path)
|
|
52
|
+
|
|
53
|
+
ERB.new(File.read(path)).result_with_hash(locals)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.render(name, **locals) # :nodoc:
|
|
57
|
+
new(name).render(**locals)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the ordered prompt directories. Append engine directories with +<<+.
|
|
61
|
+
def self.roots
|
|
62
|
+
@roots ||= Roots.new { root }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.root # :nodoc:
|
|
66
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
67
|
+
Rails.root.join('app/prompts')
|
|
68
|
+
else
|
|
69
|
+
Pathname.new(Dir.pwd).join('app/prompts')
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def candidates
|
|
76
|
+
self.class.roots.map { |root| Pathname.new(root).join(@filename) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Protocol
|
|
5
|
+
module BinaryStreaming # :nodoc: all
|
|
6
|
+
def stream_binary(url, payload, &)
|
|
7
|
+
progress = {}
|
|
8
|
+
fallback = Streaming::StreamState.new
|
|
9
|
+
on_data = binary_on_data(fallback, progress, &)
|
|
10
|
+
|
|
11
|
+
response = @connection.post(url, payload, usage: @usage_tracker) do |request|
|
|
12
|
+
(request.options.context ||= {})[Transport::Connection::STREAM_PROGRESS_KEY] = progress
|
|
13
|
+
if faraday_1?
|
|
14
|
+
request.options[:on_data] = on_data
|
|
15
|
+
else
|
|
16
|
+
request.options.on_data = on_data
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
state = response.env[:streaming_state] || fallback
|
|
20
|
+
validate_binary_response(response, state.buffer)
|
|
21
|
+
yield state.buffer.dup unless progress[:started] || state.buffer.empty?
|
|
22
|
+
response.env.body = state.buffer.b
|
|
23
|
+
response
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def binary_on_data(fallback, progress)
|
|
29
|
+
lambda do |chunk, bytes, env = nil|
|
|
30
|
+
next if chunk.empty?
|
|
31
|
+
|
|
32
|
+
state = stream_state(env, fallback)
|
|
33
|
+
state.buffer.clear if env.nil? && bytes && bytes <= state.buffer.bytesize
|
|
34
|
+
if failed_http_status(env)
|
|
35
|
+
handle_failed_response(chunk, state.buffer, env)
|
|
36
|
+
else
|
|
37
|
+
state.buffer << chunk.b
|
|
38
|
+
next unless binary_response?(env)
|
|
39
|
+
|
|
40
|
+
progress[:started] = true
|
|
41
|
+
yield chunk.b
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def binary_response?(env)
|
|
47
|
+
return false unless env&.status&.between?(200, 299)
|
|
48
|
+
|
|
49
|
+
content_type = env.response_headers&.fetch('content-type', '').to_s.split(';').first
|
|
50
|
+
content_type&.start_with?('audio/') || content_type == 'application/octet-stream'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_binary_response(response, data)
|
|
54
|
+
return if binary_response?(response.env)
|
|
55
|
+
|
|
56
|
+
begin
|
|
57
|
+
raise_stream_error(data, JSON.parse(data), response.env)
|
|
58
|
+
rescue JSON::ParserError
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
raise Error.new('Expected an audio response from the speech endpoint', response: response)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'securerandom'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
class Protocol
|
|
8
|
+
class StreamAccumulator # :nodoc:
|
|
9
|
+
attr_reader :content, :model, :tool_calls
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
@content = +''
|
|
13
|
+
@citations = []
|
|
14
|
+
@thinking_text = nil
|
|
15
|
+
@thinking_signature = nil
|
|
16
|
+
@tool_calls = {}
|
|
17
|
+
@input_tokens = nil
|
|
18
|
+
@output_tokens = nil
|
|
19
|
+
@cache_read_tokens = nil
|
|
20
|
+
@cache_write_tokens = nil
|
|
21
|
+
@thinking_tokens = nil
|
|
22
|
+
@server_tool_use = nil
|
|
23
|
+
@reported_cost = nil
|
|
24
|
+
@server_tool_calls = []
|
|
25
|
+
@raw_content = nil
|
|
26
|
+
@raw_reasoning = nil
|
|
27
|
+
@finish_reason = nil
|
|
28
|
+
@latest_tool_call_id = nil
|
|
29
|
+
@tool_call_ids_by_index = {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def add(chunk)
|
|
33
|
+
RubyLLM.logger.debug { chunk.inspect } if RubyLLM.config.log_stream_debug
|
|
34
|
+
@model = chunk.model if @model.to_s.empty?
|
|
35
|
+
|
|
36
|
+
handle_chunk_content(chunk)
|
|
37
|
+
accumulate_citations(chunk.citations)
|
|
38
|
+
append_thinking_from_chunk(chunk)
|
|
39
|
+
accumulate_server_tool_calls(chunk.server_tool_calls)
|
|
40
|
+
@raw_content = chunk.raw_content if chunk.raw_content
|
|
41
|
+
@raw_reasoning = chunk.raw_reasoning if chunk.raw_reasoning
|
|
42
|
+
@finish_reason = chunk.finish_reason if chunk.finish_reason
|
|
43
|
+
count_tokens chunk
|
|
44
|
+
RubyLLM.logger.debug { inspect } if RubyLLM.config.log_stream_debug
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_message(response)
|
|
48
|
+
Message.new(
|
|
49
|
+
role: :assistant,
|
|
50
|
+
content: content.empty? ? nil : content,
|
|
51
|
+
citations: resolved_citations,
|
|
52
|
+
thinking: Thinking.build(
|
|
53
|
+
text: @thinking_text,
|
|
54
|
+
signature: @thinking_signature
|
|
55
|
+
),
|
|
56
|
+
tokens: Tokens.new(
|
|
57
|
+
input: @input_tokens,
|
|
58
|
+
output: @output_tokens,
|
|
59
|
+
cache_read: @cache_read_tokens,
|
|
60
|
+
cache_write: @cache_write_tokens,
|
|
61
|
+
thinking: @thinking_tokens,
|
|
62
|
+
server_tool_use: @server_tool_use,
|
|
63
|
+
reported_cost: @reported_cost
|
|
64
|
+
),
|
|
65
|
+
server_tool_calls: @server_tool_calls,
|
|
66
|
+
raw_content: @raw_content,
|
|
67
|
+
raw_reasoning: @raw_reasoning,
|
|
68
|
+
finish_reason: @finish_reason,
|
|
69
|
+
model: model,
|
|
70
|
+
tool_calls: tool_calls_from_stream(response),
|
|
71
|
+
raw: response
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# Providers like Perplexity repeat the full citation list on every chunk.
|
|
78
|
+
def accumulate_citations(new_citations)
|
|
79
|
+
new_citations.each do |citation|
|
|
80
|
+
@citations << citation unless @citations.include?(citation)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def accumulate_server_tool_calls(calls)
|
|
85
|
+
calls.each do |call|
|
|
86
|
+
index = @server_tool_calls.index do |existing|
|
|
87
|
+
call.id && existing.id == call.id && existing.type == call.type
|
|
88
|
+
end
|
|
89
|
+
if index
|
|
90
|
+
@server_tool_calls[index] = call
|
|
91
|
+
else
|
|
92
|
+
@server_tool_calls << call
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolved_citations
|
|
98
|
+
@citations.map { |citation| resolve_citation_text(citation) }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def resolve_citation_text(citation)
|
|
102
|
+
return citation if citation.text || citation.start_index.nil? || citation.end_index.nil?
|
|
103
|
+
|
|
104
|
+
span = content[citation.start_index...citation.end_index]
|
|
105
|
+
span ? Citation.new(citation.to_h.merge(text: span)) : citation
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def tool_calls_from_stream(response)
|
|
109
|
+
tool_calls.transform_values do |tc|
|
|
110
|
+
arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
|
|
111
|
+
parse_tool_call_arguments(tc.arguments, response)
|
|
112
|
+
elsif tc.arguments.is_a?(String)
|
|
113
|
+
{}
|
|
114
|
+
else
|
|
115
|
+
tc.arguments
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
ToolCall.new(
|
|
119
|
+
id: tc.id,
|
|
120
|
+
name: tc.name,
|
|
121
|
+
arguments: arguments,
|
|
122
|
+
thought_signature: tc.thought_signature,
|
|
123
|
+
remote: tc.remote?
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def parse_tool_call_arguments(arguments, response)
|
|
129
|
+
JSON.parse(arguments)
|
|
130
|
+
rescue JSON::ParserError => e
|
|
131
|
+
raise ToolCallParseError.new(response: response, finish_reason: @finish_reason), cause: e
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def accumulate_tool_calls(new_tool_calls)
|
|
135
|
+
RubyLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if RubyLLM.config.log_stream_debug
|
|
136
|
+
new_tool_calls.each do |stream_key, tool_call|
|
|
137
|
+
if tool_call.id
|
|
138
|
+
start_tool_call(stream_key, tool_call)
|
|
139
|
+
else
|
|
140
|
+
append_tool_call_fragment(stream_key, tool_call)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def start_tool_call(stream_key, tool_call)
|
|
146
|
+
tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
|
|
147
|
+
|
|
148
|
+
@tool_calls[tool_call_id] = ToolCall.new(
|
|
149
|
+
id: tool_call_id,
|
|
150
|
+
name: tool_call.name,
|
|
151
|
+
arguments: initial_tool_call_arguments(tool_call),
|
|
152
|
+
thought_signature: tool_call.thought_signature,
|
|
153
|
+
remote: tool_call.remote?
|
|
154
|
+
)
|
|
155
|
+
@tool_call_ids_by_index[stream_key] = tool_call_id unless stream_key.nil?
|
|
156
|
+
@latest_tool_call_id = tool_call_id
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def initial_tool_call_arguments(tool_call)
|
|
160
|
+
arguments = tool_call.arguments
|
|
161
|
+
return +'' if arguments.nil? || (arguments.respond_to?(:empty?) && arguments.empty?)
|
|
162
|
+
|
|
163
|
+
arguments.dup
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def append_tool_call_fragment(stream_key, tool_call)
|
|
167
|
+
existing = find_tool_call(stream_key)
|
|
168
|
+
return unless existing
|
|
169
|
+
|
|
170
|
+
fragment = tool_call.arguments
|
|
171
|
+
fragment = '' if fragment.nil?
|
|
172
|
+
existing.arguments << fragment
|
|
173
|
+
return unless tool_call.thought_signature && existing.thought_signature.nil?
|
|
174
|
+
|
|
175
|
+
existing.thought_signature = tool_call.thought_signature
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def find_tool_call(stream_key)
|
|
179
|
+
return @tool_calls[@latest_tool_call_id] if stream_key.nil?
|
|
180
|
+
|
|
181
|
+
@tool_calls[@tool_call_ids_by_index[stream_key]] || @tool_calls[stream_key]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def count_tokens(chunk)
|
|
185
|
+
tokens = chunk.tokens
|
|
186
|
+
@input_tokens = tokens.input if tokens.input
|
|
187
|
+
@output_tokens = tokens.output if tokens.output
|
|
188
|
+
@cache_read_tokens = tokens.cache_read if tokens.cache_read
|
|
189
|
+
@cache_write_tokens = tokens.cache_write if tokens.cache_write
|
|
190
|
+
@thinking_tokens = tokens.thinking if tokens.thinking
|
|
191
|
+
@server_tool_use = tokens.server_tool_use if tokens.server_tool_use
|
|
192
|
+
@reported_cost = tokens.reported_cost if tokens.reported_cost
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def handle_chunk_content(chunk)
|
|
196
|
+
accumulate_tool_calls(chunk.tool_calls) if chunk.tool_call?
|
|
197
|
+
|
|
198
|
+
content_text = chunk.content || ''
|
|
199
|
+
@content << (content_text.is_a?(String) ? content_text : content_text.to_s)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def append_thinking_from_chunk(chunk)
|
|
203
|
+
thinking = chunk.thinking
|
|
204
|
+
return unless thinking
|
|
205
|
+
|
|
206
|
+
unless thinking.text.nil?
|
|
207
|
+
@thinking_text ||= +''
|
|
208
|
+
@thinking_text << thinking.text.to_s
|
|
209
|
+
end
|
|
210
|
+
@thinking_signature ||= thinking.signature # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|