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
data/lib/ruby_llm/message.rb
CHANGED
|
@@ -34,11 +34,37 @@ module RubyLLM
|
|
|
34
34
|
|
|
35
35
|
# The ID of the model that produced the message, +nil+ on user messages.
|
|
36
36
|
attr_reader :model
|
|
37
|
+
alias model_id model
|
|
37
38
|
|
|
38
39
|
attr_writer :model_info # :nodoc:
|
|
39
40
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
41
|
+
# Returns the slug of the provider that produced the message, from its
|
|
42
|
+
# last successful attempt, or +nil+ when none is recorded, as on user
|
|
43
|
+
# messages. A reply from a model given to Chat#with_fallbacks names that
|
|
44
|
+
# model's provider.
|
|
45
|
+
def provider
|
|
46
|
+
@provider || ruby_llm_usage_entries.reverse.find(&:succeeded?)&.provider
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Marks the message as the work of a fallback model.
|
|
50
|
+
def stood_in(model:, provider:) # :nodoc:
|
|
51
|
+
@model ||= model
|
|
52
|
+
@provider = provider
|
|
53
|
+
@stood_in = true
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def stood_in? # :nodoc:
|
|
58
|
+
@stood_in || false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# The tool calls the assistant requested, as an Enumerable of ToolCall
|
|
62
|
+
# objects in provider order, also readable by call ID. Empty when there
|
|
63
|
+
# are none.
|
|
64
|
+
#
|
|
65
|
+
# response.tool_calls.map(&:name) # => ["weather"]
|
|
66
|
+
# response.tool_calls["call_1"] # => #<RubyLLM::ToolCall ...>
|
|
67
|
+
#
|
|
42
68
|
attr_reader :tool_calls
|
|
43
69
|
|
|
44
70
|
# The ID of the tool call this message answers. Set only on tool result
|
|
@@ -59,9 +85,16 @@ module RubyLLM
|
|
|
59
85
|
|
|
60
86
|
# Why the model stopped: +:stop+, +:max_tokens+, +:tool_calls+, or
|
|
61
87
|
# +:content_filter+. Any other reason comes through as the provider
|
|
62
|
-
#
|
|
88
|
+
# named it, as a lowercase Symbol, such as Anthropic's +:pause_turn+ or
|
|
89
|
+
# Gemini's +:malformed_function_call+.
|
|
63
90
|
attr_reader :finish_reason
|
|
64
91
|
|
|
92
|
+
# The ContentFilter verdicts the provider reported on the request and
|
|
93
|
+
# this response, or an empty Array. Read them to see which filter ended
|
|
94
|
+
# a response that is #content_filtered?. Only the live response carries
|
|
95
|
+
# them: #to_h leaves them out, so a persisted chat never stores them.
|
|
96
|
+
attr_reader :content_filters
|
|
97
|
+
|
|
65
98
|
# The provider-executed tool steps in this response, as an array of
|
|
66
99
|
# ServerToolCall objects. Empty unless the chat enabled tools with
|
|
67
100
|
# Chat#with_provider_tools and the model used one.
|
|
@@ -82,6 +115,16 @@ module RubyLLM
|
|
|
82
115
|
# +nil+ when the provider returned none.
|
|
83
116
|
attr_reader :raw_reasoning # :nodoc:
|
|
84
117
|
|
|
118
|
+
# The provider's label for this assistant text within its turn, such as
|
|
119
|
+
# OpenAI's +commentary+ or +final_answer+, kept so later requests can
|
|
120
|
+
# send it back. +nil+ when the provider gave none.
|
|
121
|
+
attr_reader :phase # :nodoc:
|
|
122
|
+
|
|
123
|
+
# The provider's opaque state for an assistant message it has not
|
|
124
|
+
# finished, which a later request sends back to continue it. +nil+ once
|
|
125
|
+
# the message is complete.
|
|
126
|
+
attr_reader :continuation # :nodoc:
|
|
127
|
+
|
|
85
128
|
# The Chat this message belongs to, set when it is added to a
|
|
86
129
|
# conversation. Backs #tool_results.
|
|
87
130
|
attr_accessor :conversation # :nodoc:
|
|
@@ -93,8 +136,10 @@ module RubyLLM
|
|
|
93
136
|
@config = options[:config]
|
|
94
137
|
@attachments = Attachment.wrap(options[:attachments], config: @config)
|
|
95
138
|
@model = options[:model]
|
|
139
|
+
@provider = options[:provider]
|
|
96
140
|
@supplied_cost = coerce_value(options[:cost], Cost)
|
|
97
141
|
@tool_call_id = options[:tool_call_id]
|
|
142
|
+
@provider_call_id = options[:provider_call_id] || options[:provider_tool_call_id]
|
|
98
143
|
@tokens = options[:tokens] || Tokens.new(
|
|
99
144
|
input: options[:input_tokens],
|
|
100
145
|
output: options[:output_tokens],
|
|
@@ -111,8 +156,11 @@ module RubyLLM
|
|
|
111
156
|
@server_tool_calls = Array(options[:server_tool_calls]).map { |call| coerce_value(call, ServerToolCall) }
|
|
112
157
|
@raw_content = options[:raw_content]
|
|
113
158
|
@raw_reasoning = options[:raw_reasoning]
|
|
159
|
+
@phase = options[:phase]
|
|
160
|
+
@continuation = options[:continuation]
|
|
114
161
|
@mcp_result = coerce_mcp_result(options[:mcp_result])
|
|
115
162
|
@finish_reason = options[:finish_reason]&.to_sym
|
|
163
|
+
@content_filters = coerce_list(options[:content_filters], ContentFilter)
|
|
116
164
|
self.ruby_llm_usage_entries = options[:usage_entries] if options[:usage_entries]
|
|
117
165
|
@cache_until_here = options.fetch(:cache_until_here, false)
|
|
118
166
|
@cache_ttl = options[:cache_ttl] if @cache_until_here
|
|
@@ -121,15 +169,99 @@ module RubyLLM
|
|
|
121
169
|
end
|
|
122
170
|
|
|
123
171
|
# Returns #content parsed as JSON, memoized after the first call.
|
|
124
|
-
# Useful for reading structured output responses.
|
|
172
|
+
# Useful for reading structured output responses. When the provider
|
|
173
|
+
# marks part of the turn as commentary before the final answer, only
|
|
174
|
+
# the final answer is parsed.
|
|
125
175
|
#
|
|
126
176
|
# response = chat.with_schema(PersonSchema).ask "Generate a person"
|
|
127
177
|
# response.parsed # => {"name" => "Alice", "age" => 30}
|
|
128
178
|
#
|
|
129
179
|
def parsed
|
|
130
|
-
|
|
180
|
+
text = answer_text
|
|
181
|
+
return if text.nil? || text.empty?
|
|
182
|
+
|
|
183
|
+
@parsed ||= JSON.parse(text)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Returns the pages a hosted web search returned for this message, as an
|
|
187
|
+
# array of WebSource objects with each address once. Reads what the message
|
|
188
|
+
# keeps, so a restored message answers the same. Empty when the message
|
|
189
|
+
# ran no search or its provider reports no sources.
|
|
190
|
+
#
|
|
191
|
+
# response = chat.with_provider_tools(:web_search).ask "What changed in Ruby 4.0?"
|
|
192
|
+
# response.web_sources.map(&:url)
|
|
193
|
+
#
|
|
194
|
+
# Sources come from the outside world and are not a safe allowlist on their
|
|
195
|
+
# own: a result can be attacker-controlled, and OpenAI can list a page the
|
|
196
|
+
# model chose to open beside what its search found.
|
|
197
|
+
#
|
|
198
|
+
def web_sources
|
|
199
|
+
return [] if server_tool_calls.empty? || provider.nil?
|
|
200
|
+
|
|
201
|
+
Array(Provider.resolve(provider)&.web_sources(self)).uniq(&:url)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Returns a copy of the message with the given fields changed, like
|
|
205
|
+
# Data#with. The message itself is left alone. Change +content+ or
|
|
206
|
+
# +attachments+:
|
|
207
|
+
#
|
|
208
|
+
# message.with(content: "[left out]")
|
|
209
|
+
#
|
|
210
|
+
# New content also drops what holds the old text: on an assistant
|
|
211
|
+
# message its thinking, citations, server tool calls, raw response, and
|
|
212
|
+
# native response blocks, and on a tool result its MCP result. Raises
|
|
213
|
+
# ArgumentError for another field.
|
|
214
|
+
def with(**changes)
|
|
215
|
+
unknown = changes.keys - %i[content attachments]
|
|
216
|
+
raise ArgumentError, "Message#with cannot change #{unknown.join(', ')}" if unknown.any?
|
|
217
|
+
|
|
218
|
+
changes.reduce(self) do |message, (field, value)|
|
|
219
|
+
field == :content ? message.with_content(value) : message.with_attachments(value)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def with_content(content) # :nodoc:
|
|
224
|
+
(role == :assistant ? without_native_content : dup).tap do |message|
|
|
225
|
+
message.instance_variable_set(:@content, normalize_content(content))
|
|
226
|
+
message.instance_variable_set(:@mcp_result, nil)
|
|
227
|
+
if role == :assistant
|
|
228
|
+
message.instance_variable_set(:@citations, [])
|
|
229
|
+
message.instance_variable_set(:@server_tool_calls, [])
|
|
230
|
+
message.instance_variable_set(:@raw, nil)
|
|
231
|
+
end
|
|
232
|
+
message.remove_instance_variable(:@parsed) if message.instance_variable_defined?(:@parsed)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Returns whether a tool ended the turn with this result, with
|
|
237
|
+
# Tool#halt. Its finish reason is +:halted+.
|
|
238
|
+
def halted?
|
|
239
|
+
finish_reason == :halted
|
|
240
|
+
end
|
|
131
241
|
|
|
132
|
-
|
|
242
|
+
# Returns whether this is the part of a streamed reply the chat kept
|
|
243
|
+
# when it was cancelled (see Chat#cancel), or the result that closed a
|
|
244
|
+
# tool call it left unfinished. Its finish reason is +:cancelled+.
|
|
245
|
+
def cancelled?
|
|
246
|
+
finish_reason == :cancelled
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Returns the path, in the chat's files, of the whole of a tool result
|
|
250
|
+
# too long to send whole, or +nil+ when the result went whole. A chat
|
|
251
|
+
# with a file store keeps such a result in a file and sends the model
|
|
252
|
+
# its start and this path (see Chat#with_file_store).
|
|
253
|
+
#
|
|
254
|
+
# if (path = message.spilled_path)
|
|
255
|
+
# chat.file_store.read(path)
|
|
256
|
+
# end
|
|
257
|
+
#
|
|
258
|
+
def spilled_path
|
|
259
|
+
Chat::FileReferences.spilled_path(content) if tool_result?
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def mark_cancelled # :nodoc:
|
|
263
|
+
@finish_reason = :cancelled
|
|
264
|
+
self
|
|
133
265
|
end
|
|
134
266
|
|
|
135
267
|
def with_attachments(attachments) # :nodoc:
|
|
@@ -137,19 +269,29 @@ module RubyLLM
|
|
|
137
269
|
dup.tap { |message| message.instance_variable_set(:@attachments, wrapped) }
|
|
138
270
|
end
|
|
139
271
|
|
|
272
|
+
def with_tool_calls(tool_calls) # :nodoc:
|
|
273
|
+
dup.tap { |message| message.instance_variable_set(:@tool_calls, coerce_tool_calls(tool_calls)) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def with_tool_call_id(tool_call_id) # :nodoc:
|
|
277
|
+
dup.tap { |message| message.instance_variable_set(:@tool_call_id, tool_call_id) }
|
|
278
|
+
end
|
|
279
|
+
|
|
140
280
|
def without_native_content(raw_content: nil) # :nodoc:
|
|
141
281
|
dup.tap do |message|
|
|
142
282
|
message.instance_variable_set(:@thinking, nil)
|
|
143
283
|
message.instance_variable_set(:@raw_reasoning, nil)
|
|
284
|
+
message.instance_variable_set(:@phase, nil)
|
|
285
|
+
message.instance_variable_set(:@continuation, nil)
|
|
144
286
|
message.instance_variable_set(:@raw_content, raw_content)
|
|
145
|
-
message.instance_variable_set(:@tool_calls, tool_calls_without_thought_signatures)
|
|
287
|
+
message.instance_variable_set(:@tool_calls, coerce_tool_calls(tool_calls_without_thought_signatures))
|
|
146
288
|
end
|
|
147
289
|
end
|
|
148
290
|
|
|
149
291
|
# Returns +true+ if the assistant requested one or more tool calls,
|
|
150
292
|
# +false+ otherwise.
|
|
151
293
|
def tool_call?
|
|
152
|
-
!tool_calls.
|
|
294
|
+
!tool_calls.empty?
|
|
153
295
|
end
|
|
154
296
|
|
|
155
297
|
# Returns +true+ if the message carries the result of a tool call,
|
|
@@ -158,6 +300,17 @@ module RubyLLM
|
|
|
158
300
|
!tool_call_id.nil? && !tool_call_id.empty?
|
|
159
301
|
end
|
|
160
302
|
|
|
303
|
+
def provider_call_id # :nodoc:
|
|
304
|
+
return @provider_call_id if @provider_call_id
|
|
305
|
+
return tool_call_id unless tool_result? && conversation
|
|
306
|
+
|
|
307
|
+
conversation.messages.reverse_each do |message|
|
|
308
|
+
call = message.tool_calls[tool_call_id]
|
|
309
|
+
return call.provider_call_id if call
|
|
310
|
+
end
|
|
311
|
+
tool_call_id
|
|
312
|
+
end
|
|
313
|
+
|
|
161
314
|
# Returns the tool result messages answering this message's tool calls,
|
|
162
315
|
# or an empty array when it made none. Mirrors the +tool_results+
|
|
163
316
|
# association on acts_as_message records.
|
|
@@ -176,11 +329,17 @@ module RubyLLM
|
|
|
176
329
|
finish_reason == :stop && !tool_call?
|
|
177
330
|
end
|
|
178
331
|
|
|
332
|
+
# Returns the error text of a failed tool result, or +nil+.
|
|
333
|
+
def tool_error_message
|
|
334
|
+
tool_result_payload&.[]('error')
|
|
335
|
+
end
|
|
336
|
+
|
|
179
337
|
# Returns +true+ if the response was cut off by a token limit,
|
|
180
338
|
# +false+ otherwise.
|
|
181
339
|
def max_tokens?
|
|
182
340
|
finish_reason == :max_tokens
|
|
183
341
|
end
|
|
342
|
+
alias truncated? max_tokens?
|
|
184
343
|
|
|
185
344
|
# Returns +true+ if the model stopped to request tool calls,
|
|
186
345
|
# +false+ otherwise.
|
|
@@ -213,18 +372,21 @@ module RubyLLM
|
|
|
213
372
|
return ruby_llm_usage_cost if model.nil? && ruby_llm_usage_entries.any?
|
|
214
373
|
return @supplied_cost if model.nil? && @supplied_cost
|
|
215
374
|
|
|
216
|
-
Cost.new(tokens
|
|
375
|
+
Cost.new(tokens: model ? tokens.without_reported_cost : tokens, model: model || model_info)
|
|
217
376
|
end
|
|
218
377
|
|
|
219
378
|
# Marks this message as an explicit prompt cache boundary. Providers
|
|
220
379
|
# with boundary controls use the conversation up to and including this
|
|
221
|
-
# message as the cacheable prefix. Pass +ttl
|
|
222
|
-
#
|
|
380
|
+
# message as the cacheable prefix. Pass +ttl:+, in any form
|
|
381
|
+
# Chat#with_caching takes, to give this boundary its own cache lifetime
|
|
382
|
+
# instead of the +ttl:+ from Chat#with_caching, on
|
|
223
383
|
# providers that set a lifetime per boundary. Returns +self+.
|
|
224
384
|
#
|
|
225
385
|
# chat.add_message(role: :user, content: long_context).cache_until_here
|
|
226
386
|
# chat.add_message(role: :user, content: handbook).cache_until_here(ttl: "1h")
|
|
227
387
|
#
|
|
388
|
+
# To mark system instructions, pass +cache_until_here:+ to
|
|
389
|
+
# Chat#with_instructions.
|
|
228
390
|
def cache_until_here(ttl: nil)
|
|
229
391
|
@cache_until_here = true
|
|
230
392
|
@cache_ttl = ttl
|
|
@@ -261,16 +423,20 @@ module RubyLLM
|
|
|
261
423
|
content: content,
|
|
262
424
|
attachments: list_to_h(attachments),
|
|
263
425
|
model: model,
|
|
426
|
+
provider: provider,
|
|
264
427
|
cost: @supplied_cost && cost.to_h,
|
|
265
428
|
reported_cost: tokens.reported_cost,
|
|
266
|
-
tool_calls: tool_calls
|
|
429
|
+
tool_calls: tool_calls.empty? ? nil : tool_calls.transform_values(&:to_h),
|
|
267
430
|
tool_call_id: tool_call_id,
|
|
431
|
+
provider_call_id: @provider_call_id,
|
|
268
432
|
thinking: thinking&.text,
|
|
269
433
|
thinking_signature: thinking&.signature,
|
|
270
434
|
citations: list_to_h(citations),
|
|
271
435
|
server_tool_calls: list_to_h(server_tool_calls),
|
|
272
436
|
raw_content: raw_content,
|
|
273
437
|
raw_reasoning: raw_reasoning,
|
|
438
|
+
phase: phase,
|
|
439
|
+
continuation: continuation,
|
|
274
440
|
mcp_result: mcp_result&.dump,
|
|
275
441
|
finish_reason: finish_reason,
|
|
276
442
|
cache_until_here: cache_until_here? || nil,
|
|
@@ -297,25 +463,49 @@ module RubyLLM
|
|
|
297
463
|
|
|
298
464
|
private
|
|
299
465
|
|
|
466
|
+
def answer_text
|
|
467
|
+
slug = ruby_llm_usage_entries.reverse.find(&:succeeded?)&.provider if raw_content || phase
|
|
468
|
+
(Provider.resolve(slug)&.answer_text(self) if slug) || content
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def tool_result_payload
|
|
472
|
+
return unless tool_result? && content
|
|
473
|
+
|
|
474
|
+
payload = JSON.parse(content)
|
|
475
|
+
payload if payload.is_a?(Hash)
|
|
476
|
+
rescue JSON::ParserError
|
|
477
|
+
nil
|
|
478
|
+
end
|
|
479
|
+
|
|
300
480
|
def list_to_h(list)
|
|
301
481
|
list.empty? ? nil : list.map(&:to_h)
|
|
302
482
|
end
|
|
303
483
|
|
|
304
484
|
def coerce_tool_calls(tool_calls)
|
|
305
|
-
return tool_calls
|
|
485
|
+
return tool_calls if tool_calls.is_a?(ToolCall::Collection)
|
|
486
|
+
if tool_calls.is_a?(Array) && tool_calls.all?(ToolCall)
|
|
487
|
+
return ToolCall::Collection.new(ToolCall.index(tool_calls))
|
|
488
|
+
end
|
|
306
489
|
|
|
307
|
-
|
|
308
|
-
|
|
490
|
+
ToolCall::Collection.new(
|
|
491
|
+
tool_calls.to_h do |id, call|
|
|
492
|
+
[id, call.is_a?(Hash) ? tool_call_from_h(id, call) : call]
|
|
493
|
+
end
|
|
494
|
+
)
|
|
495
|
+
end
|
|
309
496
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
497
|
+
def tool_call_from_h(id, call)
|
|
498
|
+
attributes = call.transform_keys(&:to_sym)
|
|
499
|
+
ToolCall.new(id: attributes[:id] || id, name: attributes[:name],
|
|
500
|
+
provider_call_id: attributes[:provider_call_id] || attributes[:provider_id],
|
|
501
|
+
arguments: attributes[:arguments] || {},
|
|
502
|
+
malformed_arguments: attributes[:malformed_arguments],
|
|
503
|
+
namespace: attributes[:namespace],
|
|
504
|
+
thought_signature: attributes[:thought_signature], remote: attributes.fetch(:remote, false))
|
|
315
505
|
end
|
|
316
506
|
|
|
317
507
|
def tool_calls_without_thought_signatures
|
|
318
|
-
tool_calls
|
|
508
|
+
tool_calls.transform_values do |call|
|
|
319
509
|
call.dup.tap { |copy| copy.thought_signature = nil }
|
|
320
510
|
end
|
|
321
511
|
end
|
|
@@ -333,12 +523,16 @@ module RubyLLM
|
|
|
333
523
|
value.is_a?(Hash) ? klass.from_h(value) : value
|
|
334
524
|
end
|
|
335
525
|
|
|
526
|
+
def coerce_list(values, klass)
|
|
527
|
+
Array(values).map { |value| coerce_value(value, klass) }
|
|
528
|
+
end
|
|
529
|
+
|
|
336
530
|
def coerce_mcp_result(value)
|
|
337
531
|
value.is_a?(Hash) ? MCP::Result.load(value.transform_keys(&:to_s)) : value
|
|
338
532
|
end
|
|
339
533
|
|
|
340
534
|
def normalize_content(content)
|
|
341
|
-
return '' if role == :assistant && content.nil? &&
|
|
535
|
+
return '' if role == :assistant && content.nil? && tool_call?
|
|
342
536
|
return content if content.nil? || content.is_a?(String)
|
|
343
537
|
|
|
344
538
|
raise ArgumentError,
|
|
@@ -354,7 +548,7 @@ module RubyLLM
|
|
|
354
548
|
{
|
|
355
549
|
role: role,
|
|
356
550
|
content: content,
|
|
357
|
-
tool_calls: tool_calls
|
|
551
|
+
tool_calls: tool_calls.map(&:name),
|
|
358
552
|
tool_call_id: tool_call_id,
|
|
359
553
|
model: model,
|
|
360
554
|
finish_reason: finish_reason
|
|
@@ -5,7 +5,7 @@ module RubyLLM
|
|
|
5
5
|
# A Model::Modalities lists the kinds of content a model accepts and
|
|
6
6
|
# produces, as arrays of Strings. Instances come from Model#modalities.
|
|
7
7
|
#
|
|
8
|
-
# model = RubyLLM.models.find('gpt-
|
|
8
|
+
# model = RubyLLM.models.find('gpt-6.1-sol')
|
|
9
9
|
# model.modalities.input # => ["text", "image", "pdf"]
|
|
10
10
|
# model.modalities.output # => ["text"]
|
|
11
11
|
#
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
class Model
|
|
5
5
|
# A Pricing groups a model's prices by usage category: text tokens,
|
|
6
|
-
# images, audio tokens, and embeddings
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# images, audio tokens, and embeddings, plus the characters, audio
|
|
7
|
+
# seconds, and search units some operations bill instead of tokens.
|
|
8
|
+
# Each category is a PricingCategory. Prices are in USD per million
|
|
9
|
+
# units of the category. Instances come from Model#pricing.
|
|
9
10
|
#
|
|
10
11
|
# model = RubyLLM.models.find "claude-sonnet-5"
|
|
11
12
|
# model.pricing.text_tokens.input # => 3
|
|
@@ -13,7 +14,7 @@ module RubyLLM
|
|
|
13
14
|
#
|
|
14
15
|
class Pricing
|
|
15
16
|
# The pricing categories a model may define.
|
|
16
|
-
CATEGORIES = %i[text_tokens images audio_tokens embeddings].freeze
|
|
17
|
+
CATEGORIES = %i[text_tokens images audio_tokens embeddings characters audio_seconds search_units].freeze
|
|
17
18
|
|
|
18
19
|
def initialize(data) # :nodoc:
|
|
19
20
|
@data = {}
|
|
@@ -47,6 +48,27 @@ module RubyLLM
|
|
|
47
48
|
category(:embeddings)
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
# Returns the PricingCategory for prices per million input
|
|
52
|
+
# characters, such as a text-to-speech model's, or an empty category
|
|
53
|
+
# if the model has none.
|
|
54
|
+
def characters
|
|
55
|
+
category(:characters)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns the PricingCategory for prices per million seconds of
|
|
59
|
+
# input audio, such as a transcription model's, or an empty category
|
|
60
|
+
# if the model has none.
|
|
61
|
+
def audio_seconds
|
|
62
|
+
category(:audio_seconds)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns the PricingCategory for prices per million search units,
|
|
66
|
+
# such as a rerank model's, or an empty category if the model has
|
|
67
|
+
# none.
|
|
68
|
+
def search_units
|
|
69
|
+
category(:search_units)
|
|
70
|
+
end
|
|
71
|
+
|
|
50
72
|
# Returns the pricing data as a nested Hash keyed by category.
|
|
51
73
|
# Categories without prices are omitted.
|
|
52
74
|
def to_h
|
|
@@ -5,7 +5,9 @@ module RubyLLM
|
|
|
5
5
|
# A PricingCategory holds the standard, batch, and long-context pricing
|
|
6
6
|
# tiers for one kind of model usage, such as text tokens or images.
|
|
7
7
|
# Model#pricing returns a Pricing collection whose categories are
|
|
8
|
-
# PricingCategory instances. Prices are in USD per million tokens
|
|
8
|
+
# PricingCategory instances. Prices are in USD per million tokens, or
|
|
9
|
+
# per million units for categories billed in characters, audio seconds,
|
|
10
|
+
# or search units.
|
|
9
11
|
#
|
|
10
12
|
# model = RubyLLM.models.find "claude-sonnet-5"
|
|
11
13
|
# category = model.pricing.text_tokens
|
|
@@ -39,6 +41,7 @@ module RubyLLM
|
|
|
39
41
|
@batch = tier_from(data[:batch])
|
|
40
42
|
@long_context = tier_from(data[:long_context])
|
|
41
43
|
@long_context_threshold = Integer(data[:long_context_threshold], exception: false)
|
|
44
|
+
@context_tiers = context_tiers_from(data[:context_tiers])
|
|
42
45
|
end
|
|
43
46
|
|
|
44
47
|
# Returns the standard-tier input price in USD per million tokens,
|
|
@@ -72,15 +75,11 @@ module RubyLLM
|
|
|
72
75
|
end
|
|
73
76
|
|
|
74
77
|
# Returns the PricingTier that applies for a prompt of the given size.
|
|
75
|
-
# Uses
|
|
76
|
-
#
|
|
78
|
+
# Uses the largest context tier +prompt_tokens+ is greater than, such
|
|
79
|
+
# as #long_context above #long_context_threshold; otherwise returns
|
|
80
|
+
# #standard.
|
|
77
81
|
def tier_for(prompt_tokens)
|
|
78
|
-
|
|
79
|
-
prompt_tokens.to_i > long_context_threshold
|
|
80
|
-
long_context
|
|
81
|
-
else
|
|
82
|
-
standard
|
|
83
|
-
end
|
|
82
|
+
@context_tiers.find { |size, _| prompt_tokens.to_i > size }&.last || standard
|
|
84
83
|
end
|
|
85
84
|
|
|
86
85
|
# Returns a Hash with present tier hashes and optional
|
|
@@ -105,20 +104,38 @@ module RubyLLM
|
|
|
105
104
|
entry, threshold = context_cost(cost)
|
|
106
105
|
return [nil, nil] unless entry
|
|
107
106
|
|
|
108
|
-
rates =
|
|
107
|
+
rates = context_rates(entry)
|
|
108
|
+
rates.empty? ? [nil, nil] : [rates, threshold]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns every context tier in +cost+ as <tt>[size, rates]</tt>. The
|
|
112
|
+
# registry schema holds one long-context tier, so a model with more
|
|
113
|
+
# keeps the rest in its metadata.
|
|
114
|
+
def self.context_tiers_from_cost(cost) # :nodoc:
|
|
115
|
+
cost = RubyLLM::Support::Utils.deep_symbolize_keys(cost || {})
|
|
116
|
+
context_entries(cost).filter_map do |entry|
|
|
117
|
+
size = Integer(entry.dig(:tier, :size), exception: false)
|
|
118
|
+
rates = context_rates(entry)
|
|
119
|
+
[size, rates] if size && rates.any?
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.context_rates(entry) # :nodoc:
|
|
124
|
+
{
|
|
109
125
|
input_per_million: entry[:input],
|
|
110
126
|
output_per_million: entry[:output],
|
|
111
127
|
cache_read_input_per_million: entry[:cache_read],
|
|
112
128
|
cache_write_input_per_million: entry[:cache_write],
|
|
113
129
|
reasoning_output_per_million: entry[:reasoning]
|
|
114
130
|
}.compact
|
|
115
|
-
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def self.context_entries(cost) # :nodoc:
|
|
134
|
+
Array(cost[:tiers]).select { |entry| entry.is_a?(Hash) && entry.dig(:tier, :type).to_s == 'context' }
|
|
116
135
|
end
|
|
117
136
|
|
|
118
137
|
def self.context_cost(cost) # :nodoc:
|
|
119
|
-
context_tier =
|
|
120
|
-
entry.is_a?(Hash) && entry.dig(:tier, :type).to_s == 'context'
|
|
121
|
-
end
|
|
138
|
+
context_tier = context_entries(cost).first
|
|
122
139
|
if context_tier
|
|
123
140
|
[context_tier, Integer(context_tier.dig(:tier, :size), exception: false)]
|
|
124
141
|
elsif cost[:context_over_200k].is_a?(Hash)
|
|
@@ -126,10 +143,16 @@ module RubyLLM
|
|
|
126
143
|
end
|
|
127
144
|
end
|
|
128
145
|
|
|
129
|
-
private_class_method :context_cost
|
|
146
|
+
private_class_method :context_cost, :context_rates, :context_entries
|
|
130
147
|
|
|
131
148
|
private
|
|
132
149
|
|
|
150
|
+
def context_tiers_from(tiers)
|
|
151
|
+
tiers = Array(tiers).to_h { |size, rates| [size, tier_from(rates)] }
|
|
152
|
+
tiers[long_context_threshold] = long_context if long_context && long_context_threshold
|
|
153
|
+
tiers.sort_by { |size, _| -size }
|
|
154
|
+
end
|
|
155
|
+
|
|
133
156
|
def tier_from(tier_data)
|
|
134
157
|
return nil if empty_tier?(tier_data)
|
|
135
158
|
|
|
@@ -4,8 +4,9 @@ module RubyLLM
|
|
|
4
4
|
class Model
|
|
5
5
|
# A PricingTier holds the prices a model charges within one billing tier,
|
|
6
6
|
# such as standard, batch, or long_context. Each price is in USD per one
|
|
7
|
-
# million tokens
|
|
8
|
-
#
|
|
7
|
+
# million tokens, or per million units of a category billed in other
|
|
8
|
+
# units, such as Pricing#characters. Prices missing from the registry
|
|
9
|
+
# read as +nil+. A zero price means the usage is free.
|
|
9
10
|
#
|
|
10
11
|
# Instances come from PricingCategory#standard, #batch, and #long_context:
|
|
11
12
|
#
|
data/lib/ruby_llm/model.rb
CHANGED
|
@@ -5,7 +5,7 @@ module RubyLLM
|
|
|
5
5
|
# capabilities, modalities, pricing, and provider metadata. Instances come
|
|
6
6
|
# from the registry through RubyLLM.models and from Chat#model.
|
|
7
7
|
#
|
|
8
|
-
# model = RubyLLM.models.find('gpt-
|
|
8
|
+
# model = RubyLLM.models.find('gpt-6.1-sol')
|
|
9
9
|
# model.name # => "GPT-5.6"
|
|
10
10
|
# model.provider # => "openai"
|
|
11
11
|
# model.context_window # => 1050000
|
|
@@ -14,7 +14,7 @@ module RubyLLM
|
|
|
14
14
|
class Model
|
|
15
15
|
include Support::Inspectable
|
|
16
16
|
|
|
17
|
-
# The provider's identifier for the model, e.g. <tt>"gpt-
|
|
17
|
+
# The provider's identifier for the model, e.g. <tt>"gpt-6.1-sol"</tt>.
|
|
18
18
|
attr_reader :id
|
|
19
19
|
|
|
20
20
|
# The human-readable model name, e.g. <tt>"GPT-5.6"</tt>.
|
|
@@ -150,16 +150,24 @@ module RubyLLM
|
|
|
150
150
|
end
|
|
151
151
|
|
|
152
152
|
# Builds a Cost for +tokens+ (a Tokens object, or anything responding
|
|
153
|
-
# to +tokens+) using this model's pricing.
|
|
153
|
+
# to +tokens+) using this model's pricing. A Chat or Message is priced
|
|
154
|
+
# one provider request at a time, so each request pays the context
|
|
155
|
+
# tier its own prompt reached.
|
|
154
156
|
#
|
|
155
157
|
# cost = model.cost_for(response.tokens)
|
|
156
158
|
# puts cost.total
|
|
157
159
|
#
|
|
158
160
|
# Pass <tt>tier: :batch</tt> to use the model's explicit batch rates.
|
|
161
|
+
# Amounts the model has no batch price for are +nil+.
|
|
159
162
|
#
|
|
160
163
|
def cost_for(tokens, tier: :standard)
|
|
161
|
-
|
|
164
|
+
requests = usage_entries_of(tokens)
|
|
165
|
+
if requests.any?
|
|
166
|
+
complete = requests.all? { |entry| entry.usage_available? || entry.tokens.reported_cost }
|
|
167
|
+
return Cost.aggregate(requests.map { |entry| cost_for(entry.tokens, tier:) }, complete:)
|
|
168
|
+
end
|
|
162
169
|
|
|
170
|
+
tokens = tokens.tokens if tokens.respond_to?(:tokens)
|
|
163
171
|
Cost.new(tokens:, model: self, tier:)
|
|
164
172
|
end
|
|
165
173
|
|
|
@@ -207,15 +215,25 @@ module RubyLLM
|
|
|
207
215
|
|
|
208
216
|
private
|
|
209
217
|
|
|
218
|
+
def usage_entries_of(source)
|
|
219
|
+
return source.usage_entries if source.respond_to?(:usage_entries)
|
|
220
|
+
return source.ruby_llm_usage_entries if source.respond_to?(:ruby_llm_usage_entries)
|
|
221
|
+
|
|
222
|
+
[]
|
|
223
|
+
end
|
|
224
|
+
|
|
210
225
|
def pricing_data_with_long_context(pricing)
|
|
211
226
|
pricing = RubyLLM::Support::Utils.deep_symbolize_keys(RubyLLM::Support::Utils.deep_dup(pricing))
|
|
227
|
+
cost = metadata[:cost] || metadata['cost']
|
|
212
228
|
text = pricing[:text_tokens] || {}
|
|
229
|
+
tiers = PricingCategory.context_tiers_from_cost(cost)
|
|
230
|
+
pricing[:text_tokens] = text.merge(context_tiers: tiers) if tiers.size > 1
|
|
213
231
|
return pricing if text[:long_context]
|
|
214
232
|
|
|
215
|
-
rates, threshold = PricingCategory.long_context_from_cost(
|
|
233
|
+
rates, threshold = PricingCategory.long_context_from_cost(cost)
|
|
216
234
|
return pricing unless rates
|
|
217
235
|
|
|
218
|
-
pricing[:text_tokens] =
|
|
236
|
+
pricing[:text_tokens] = (pricing[:text_tokens] || {}).merge(long_context: rates)
|
|
219
237
|
pricing[:text_tokens][:long_context_threshold] = threshold if threshold
|
|
220
238
|
pricing
|
|
221
239
|
end
|