ruby_llm 2.0.0 → 2.1.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 +65 -8
- data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
- data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
- data/lib/ruby_llm/accounting/usage.rb +86 -49
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
- data/lib/ruby_llm/active_record/batch.rb +1 -1
- data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
- data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
- data/lib/ruby_llm/active_record/message_methods.rb +32 -7
- data/lib/ruby_llm/active_record/model.rb +1 -1
- data/lib/ruby_llm/active_record/provider_file.rb +64 -0
- data/lib/ruby_llm/active_record/record.rb +19 -0
- data/lib/ruby_llm/active_record/tool_call.rb +1 -1
- data/lib/ruby_llm/active_record/usage.rb +53 -3
- data/lib/ruby_llm/agent.rb +198 -33
- data/lib/ruby_llm/aliases.json +66 -8
- data/lib/ruby_llm/attachment.rb +54 -10
- data/lib/ruby_llm/batch.rb +26 -3
- data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
- data/lib/ruby_llm/chat/tool_search.rb +26 -0
- data/lib/ruby_llm/chat.rb +488 -82
- data/lib/ruby_llm/choice.rb +38 -0
- data/lib/ruby_llm/configuration.rb +36 -11
- data/lib/ruby_llm/context.rb +45 -32
- data/lib/ruby_llm/cost.rb +42 -1
- data/lib/ruby_llm/embedding.rb +9 -2
- data/lib/ruby_llm/error.rb +34 -2
- data/lib/ruby_llm/evaluation/assertions.rb +40 -0
- data/lib/ruby_llm/evaluation/case.rb +52 -0
- data/lib/ruby_llm/evaluation/dataset.rb +65 -0
- data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
- data/lib/ruby_llm/evaluation/evidence.rb +125 -0
- data/lib/ruby_llm/evaluation/minitest.rb +26 -0
- data/lib/ruby_llm/evaluation/report.rb +103 -0
- data/lib/ruby_llm/evaluation/result.rb +75 -0
- data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
- data/lib/ruby_llm/evaluation/rspec.rb +23 -0
- data/lib/ruby_llm/evaluation/runner.rb +93 -0
- data/lib/ruby_llm/evaluation/trial.rb +101 -0
- data/lib/ruby_llm/evaluation.rb +289 -0
- data/lib/ruby_llm/image.rb +12 -4
- data/lib/ruby_llm/judge/builder.rb +51 -0
- data/lib/ruby_llm/judge/data.rb +43 -0
- data/lib/ruby_llm/judge/question.rb +101 -0
- data/lib/ruby_llm/judge.rb +179 -0
- data/lib/ruby_llm/judgment.rb +125 -0
- data/lib/ruby_llm/mcp/apps.rb +25 -0
- data/lib/ruby_llm/mcp/client.rb +213 -0
- data/lib/ruby_llm/mcp/collection.rb +56 -0
- data/lib/ruby_llm/mcp/content.rb +44 -0
- data/lib/ruby_llm/mcp/error.rb +27 -0
- data/lib/ruby_llm/mcp/http.rb +379 -0
- data/lib/ruby_llm/mcp/input_request.rb +118 -0
- data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
- data/lib/ruby_llm/mcp/listener.rb +168 -0
- data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
- data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
- data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
- data/lib/ruby_llm/mcp/oauth.rb +653 -0
- data/lib/ruby_llm/mcp/param_headers.rb +83 -0
- data/lib/ruby_llm/mcp/prompt.rb +71 -0
- data/lib/ruby_llm/mcp/resource.rb +84 -0
- data/lib/ruby_llm/mcp/resource_template.rb +99 -0
- data/lib/ruby_llm/mcp/result.rb +76 -0
- data/lib/ruby_llm/mcp/session_expired.rb +10 -0
- data/lib/ruby_llm/mcp/stdio.rb +250 -0
- data/lib/ruby_llm/mcp/task.rb +151 -0
- data/lib/ruby_llm/mcp/tool.rb +130 -0
- data/lib/ruby_llm/mcp.rb +1101 -0
- data/lib/ruby_llm/message.rb +41 -6
- data/lib/ruby_llm/model.rb +2 -1
- data/lib/ruby_llm/models/aliases.rb +5 -2
- data/lib/ruby_llm/models/lookup.rb +80 -0
- data/lib/ruby_llm/models/registry.rb +2 -2
- data/lib/ruby_llm/models/schema.rb +1 -1
- data/lib/ruby_llm/models.json +23082 -10158
- data/lib/ruby_llm/models.rb +20 -69
- data/lib/ruby_llm/moderation.rb +7 -2
- data/lib/ruby_llm/ocr.rb +7 -2
- data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
- data/lib/ruby_llm/open_telemetry.rb +82 -0
- data/lib/ruby_llm/probability.rb +30 -0
- data/lib/ruby_llm/progress.rb +44 -0
- data/lib/ruby_llm/prompt/context.rb +55 -0
- data/lib/ruby_llm/prompt.rb +1 -1
- data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
- data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
- data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
- data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
- data/lib/ruby_llm/protocol/streaming.rb +38 -31
- data/lib/ruby_llm/protocol.rb +192 -40
- data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
- data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
- data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
- data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
- data/lib/ruby_llm/protocols/anthropic.rb +20 -1
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
- data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
- data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
- data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
- data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
- data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
- data/lib/ruby_llm/protocols/cohere.rb +1 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
- data/lib/ruby_llm/protocols/converse/media.rb +16 -2
- data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
- data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
- data/lib/ruby_llm/protocols/converse.rb +3 -5
- data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
- data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
- data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
- data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
- data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
- data/lib/ruby_llm/protocols/gemini.rb +1 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
- data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
- data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
- data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
- data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
- data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
- data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
- data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
- data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
- data/lib/ruby_llm/protocols/openai/files.rb +0 -7
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
- data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
- data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
- data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
- data/lib/ruby_llm/protocols/responses/media.rb +11 -7
- data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
- data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
- data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
- data/lib/ruby_llm/protocols/responses.rb +1 -0
- data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
- data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
- data/lib/ruby_llm/protocols/system_one.rb +27 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
- data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
- data/lib/ruby_llm/provider.rb +87 -15
- data/lib/ruby_llm/providers/anthropic.rb +23 -0
- data/lib/ruby_llm/providers/azure/chat.rb +4 -0
- data/lib/ruby_llm/providers/azure/images.rb +2 -2
- data/lib/ruby_llm/providers/azure/media.rb +8 -3
- data/lib/ruby_llm/providers/azure/videos.rb +7 -2
- data/lib/ruby_llm/providers/azure.rb +24 -4
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
- data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
- data/lib/ruby_llm/providers/bedrock.rb +38 -6
- data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
- data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
- data/lib/ruby_llm/providers/hetzner.rb +41 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
- data/lib/ruby_llm/providers/mistral/media.rb +8 -4
- data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
- data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -2
- data/lib/ruby_llm/providers/ollama/models.rb +2 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
- data/lib/ruby_llm/providers/openai.rb +14 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
- data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
- data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
- data/lib/ruby_llm/providers/perplexity.rb +11 -0
- data/lib/ruby_llm/providers/typesafe.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/vertexai.rb +27 -28
- data/lib/ruby_llm/providers/xai/chat.rb +6 -0
- data/lib/ruby_llm/providers/xai/images.rb +9 -10
- data/lib/ruby_llm/providers/xai/responses.rb +10 -8
- data/lib/ruby_llm/providers/xai/videos.rb +11 -9
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/railtie.rb +11 -0
- data/lib/ruby_llm/request_shape/part.rb +117 -0
- data/lib/ruby_llm/request_shape/problem.rb +63 -0
- data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
- data/lib/ruby_llm/request_shape/turn.rb +54 -0
- data/lib/ruby_llm/request_shape.rb +160 -0
- data/lib/ruby_llm/rerank.rb +9 -2
- data/lib/ruby_llm/research_job.rb +21 -5
- data/lib/ruby_llm/score.rb +42 -0
- data/lib/ruby_llm/search_results.rb +2 -2
- data/lib/ruby_llm/server_tool_call.rb +10 -2
- data/lib/ruby_llm/speech.rb +8 -1
- data/lib/ruby_llm/support/cancellation.rb +39 -0
- data/lib/ruby_llm/support/inspectable.rb +3 -2
- data/lib/ruby_llm/support/instrumentation.rb +38 -1
- data/lib/ruby_llm/support/process_cache.rb +60 -0
- data/lib/ruby_llm/support/progress_reporter.rb +33 -0
- data/lib/ruby_llm/thinking.rb +2 -1
- data/lib/ruby_llm/tokens.rb +57 -14
- data/lib/ruby_llm/tool.rb +50 -3
- data/lib/ruby_llm/tool_call.rb +17 -2
- data/lib/ruby_llm/transcription.rb +10 -3
- data/lib/ruby_llm/transport/connection.rb +127 -44
- data/lib/ruby_llm/transport/error_middleware.rb +90 -24
- data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
- data/lib/ruby_llm/transport/json_response.rb +33 -0
- data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +5 -3
- data/lib/ruby_llm/video_job.rb +48 -5
- data/lib/ruby_llm.rb +106 -5
- data/lib/tasks/ruby_llm/evaluations.rb +43 -0
- data/lib/tasks/ruby_llm.rake +6 -9
- data/skills/rubyllm/SKILL.md +33 -12
- metadata +95 -37
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Anthropic
|
|
6
|
+
# Describes Messages API requests, and the token counting requests
|
|
7
|
+
# that share their shape, as RequestShape objects. Claude pairs tool
|
|
8
|
+
# results with calls by id and takes a thinking block back only with
|
|
9
|
+
# its signature.
|
|
10
|
+
module RequestShapes
|
|
11
|
+
MEDIA_KINDS = { 'image' => :image, 'document' => :document }.freeze
|
|
12
|
+
THINKING_SETTINGS = %w[type budget_tokens display].freeze
|
|
13
|
+
|
|
14
|
+
def parse_request_shape(payload)
|
|
15
|
+
messages = payload['messages']
|
|
16
|
+
return unless messages.is_a?(Array)
|
|
17
|
+
|
|
18
|
+
turns = messages.each_with_index.map { |message, index| shape_anthropic_turn(shape_hash(message), index) }
|
|
19
|
+
shape_request(turns, payload:, rules: %i[signed_thinking],
|
|
20
|
+
instructions: shape_anthropic_content(payload['system']),
|
|
21
|
+
tool_names: shape_list(payload['tools']).map { |tool| shape_tool_name(tool) },
|
|
22
|
+
thinking_settings: shape_anthropic_thinking(payload))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def shape_anthropic_thinking(payload)
|
|
28
|
+
effort = shape_settings(payload['output_config'], 'effort')
|
|
29
|
+
shape_settings(payload['thinking'], *THINKING_SETTINGS).merge(effort)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def shape_anthropic_turn(message, index)
|
|
33
|
+
parts = shape_anthropic_content(message['content'])
|
|
34
|
+
shape_turn(index, message['role'], message['role'] == 'assistant' ? :model : shape_answer(parts), parts)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def shape_anthropic_content(content)
|
|
38
|
+
return [shape_text(content)] if content.is_a?(String)
|
|
39
|
+
|
|
40
|
+
shape_list(content).flat_map { |block| shape_anthropic_block(shape_hash(block)) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def shape_anthropic_block(block)
|
|
44
|
+
case block['type']
|
|
45
|
+
when 'text' then shape_text(block['text'])
|
|
46
|
+
when 'thinking' then shape_text(block['thinking'], kind: :thinking,
|
|
47
|
+
signed: shape_signature?(block['signature']))
|
|
48
|
+
when 'redacted_thinking' then shape_text(nil, kind: :thinking, signed: shape_signature?(block['data']))
|
|
49
|
+
when 'tool_use' then shape_tool_call(block['name'], block['input'], id: block['id'])
|
|
50
|
+
when 'tool_result'
|
|
51
|
+
shape_tool_result(block['content'], id: block['tool_use_id']) { |inner| shape_anthropic_block(inner) }
|
|
52
|
+
when *MEDIA_KINDS.keys then shape_anthropic_source(MEDIA_KINDS[block['type']], shape_hash(block['source']))
|
|
53
|
+
else shape_other(block['type'])
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def shape_anthropic_source(kind, source)
|
|
58
|
+
case source['type']
|
|
59
|
+
when 'base64' then shape_inline(source['data'], kind:, mime_type: source['media_type'])
|
|
60
|
+
when 'text' then shape_media(kind, source: :inline, size: shape_length(source['data']),
|
|
61
|
+
mime_type: source['media_type'])
|
|
62
|
+
when 'url' then shape_media(kind, source: :url)
|
|
63
|
+
when 'file' then shape_file(kind:)
|
|
64
|
+
else shape_media(kind, source: :inline, size: shape_length(source['content']))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -7,6 +7,19 @@ module RubyLLM
|
|
|
7
7
|
class Anthropic
|
|
8
8
|
# Streaming methods of the Anthropic API integration
|
|
9
9
|
module Streaming
|
|
10
|
+
ERROR_STATUSES = {
|
|
11
|
+
'invalid_request_error' => 400,
|
|
12
|
+
'authentication_error' => 401,
|
|
13
|
+
'billing_error' => 402,
|
|
14
|
+
'permission_error' => 403,
|
|
15
|
+
'not_found_error' => 404,
|
|
16
|
+
'request_too_large' => 413,
|
|
17
|
+
'rate_limit_error' => 429,
|
|
18
|
+
'api_error' => 500,
|
|
19
|
+
'timeout_error' => 504,
|
|
20
|
+
'overloaded_error' => 529
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
10
23
|
private
|
|
11
24
|
|
|
12
25
|
def stream_response(payload, additional_headers = {}, &)
|
|
@@ -37,6 +50,7 @@ module RubyLLM
|
|
|
37
50
|
thinking_tokens: extract_thinking_tokens(data),
|
|
38
51
|
cache_read_tokens: extract_cache_read_tokens(data),
|
|
39
52
|
cache_write_tokens: extract_cache_write_tokens(data),
|
|
53
|
+
cache_write_tokens_by_ttl: extract_cache_write_by_ttl(data),
|
|
40
54
|
server_tool_use: extract_server_tool_use(data),
|
|
41
55
|
tool_calls: extract_tool_calls(data),
|
|
42
56
|
finish_reason: normalize_finish_reason(data.dig('delta', 'stop_reason')),
|
|
@@ -84,15 +98,20 @@ module RubyLLM
|
|
|
84
98
|
return unless block
|
|
85
99
|
|
|
86
100
|
case delta_type
|
|
87
|
-
when 'text_delta' then block
|
|
88
|
-
when 'thinking_delta' then block
|
|
101
|
+
when 'text_delta' then append_stream_text(block, 'text', data.dig('delta', 'text'))
|
|
102
|
+
when 'thinking_delta' then append_stream_text(block, 'thinking', data.dig('delta', 'thinking'))
|
|
89
103
|
when 'signature_delta' then block['signature'] = data.dig('delta', 'signature')
|
|
90
104
|
when 'input_json_delta' then @stream_block_json&.dig(data['index'])&.<< data.dig('delta', 'partial_json').to_s
|
|
91
105
|
when 'citations_delta' then (block['citations'] ||= []) << data.dig('delta', 'citation')
|
|
92
|
-
when 'compaction_delta' then block
|
|
106
|
+
when 'compaction_delta' then append_stream_text(block, 'content', data.dig('delta', 'content'))
|
|
93
107
|
end
|
|
94
108
|
end
|
|
95
109
|
|
|
110
|
+
def append_stream_text(block, key, text)
|
|
111
|
+
block[key] = +block[key].to_s
|
|
112
|
+
block[key] << text.to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
96
115
|
def finalize_stream_block(index)
|
|
97
116
|
json = @stream_block_json&.dig(index)
|
|
98
117
|
block = @stream_blocks&.dig(index)
|
|
@@ -154,12 +173,7 @@ module RubyLLM
|
|
|
154
173
|
error = error_data['error']
|
|
155
174
|
return [500, error.to_s] unless error.is_a?(Hash)
|
|
156
175
|
|
|
157
|
-
|
|
158
|
-
when 'overloaded_error'
|
|
159
|
-
[529, error['message']]
|
|
160
|
-
else
|
|
161
|
-
[500, error['message']]
|
|
162
|
-
end
|
|
176
|
+
[ERROR_STATUSES.fetch(error['type'], 500), error['message']]
|
|
163
177
|
end
|
|
164
178
|
end
|
|
165
179
|
end
|
|
@@ -7,10 +7,21 @@ module RubyLLM
|
|
|
7
7
|
module Tools
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
NATIVE_TOOL_SEARCH = {
|
|
11
|
+
type: 'tool_search_tool_bm25_20251119',
|
|
12
|
+
name: 'tool_search_tool_bm25'
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
10
15
|
def find_tool_uses(blocks)
|
|
11
16
|
blocks.select { |c| c['type'] == 'tool_use' }
|
|
12
17
|
end
|
|
13
18
|
|
|
19
|
+
def tool_search_block?(block)
|
|
20
|
+
return true if block['type'] == 'tool_search_tool_result'
|
|
21
|
+
|
|
22
|
+
block['type'] == 'server_tool_use' && block['name'].to_s.start_with?('tool_search_tool_')
|
|
23
|
+
end
|
|
24
|
+
|
|
14
25
|
def format_tool_result(msg)
|
|
15
26
|
{
|
|
16
27
|
role: 'user',
|
|
@@ -56,12 +67,44 @@ module RubyLLM
|
|
|
56
67
|
description: tool.description,
|
|
57
68
|
input_schema: input_schema || default_input_schema
|
|
58
69
|
}
|
|
70
|
+
declaration[:defer_loading] = true if deferred?(tool)
|
|
59
71
|
|
|
60
72
|
return declaration if tool.provider_options.empty?
|
|
61
73
|
|
|
62
74
|
RubyLLM::Support::Utils.deep_merge(declaration, tool.provider_options)
|
|
63
75
|
end
|
|
64
76
|
|
|
77
|
+
def deferred?(tool)
|
|
78
|
+
tool.is_a?(RubyLLM::Tool::Deferred)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The API requires a search tool next to deferred tools, and rejects
|
|
82
|
+
# search blocks in the history of a request that declares none.
|
|
83
|
+
def apply_tool_search(payload)
|
|
84
|
+
tools = Array(payload[:tools])
|
|
85
|
+
return payload if tools.any? { |tool| search_tool?(tool) }
|
|
86
|
+
return payload.merge(tools: tools + [NATIVE_TOOL_SEARCH.dup]) if defers_loading?(tools)
|
|
87
|
+
|
|
88
|
+
payload.merge(messages: without_tool_search_blocks(payload[:messages]))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def defers_loading?(tools)
|
|
92
|
+
tools.any? { |tool| tool[:defer_loading] || tool['defer_loading'] }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def search_tool?(tool)
|
|
96
|
+
(tool[:type] || tool['type']).to_s.start_with?('tool_search_tool_')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def without_tool_search_blocks(messages)
|
|
100
|
+
messages.filter_map do |message|
|
|
101
|
+
next message unless message[:content].is_a?(Array)
|
|
102
|
+
|
|
103
|
+
content = message[:content].reject { |block| tool_search_block?(block) }
|
|
104
|
+
message.merge(content:) unless content.empty?
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
65
108
|
def extract_tool_calls(data)
|
|
66
109
|
if json_delta?(data)
|
|
67
110
|
extract_tool_call_delta(data)
|
|
@@ -8,6 +8,7 @@ module RubyLLM
|
|
|
8
8
|
include Anthropic::Embeddings
|
|
9
9
|
include Anthropic::Media
|
|
10
10
|
include Anthropic::Models
|
|
11
|
+
include Anthropic::RequestShapes
|
|
11
12
|
include Anthropic::Streaming
|
|
12
13
|
include Anthropic::Tools
|
|
13
14
|
|
|
@@ -15,6 +16,8 @@ module RubyLLM
|
|
|
15
16
|
# continued before RubyLLM returns what it has.
|
|
16
17
|
MAX_PAUSE_TURN_CONTINUATIONS = 8
|
|
17
18
|
|
|
19
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 4096
|
|
20
|
+
|
|
18
21
|
# web_search/web_fetch pin allowed_callers to direct invocation: their
|
|
19
22
|
# 2026 tool versions otherwise default to requiring programmatic tool
|
|
20
23
|
# calling, which not every model supports.
|
|
@@ -28,13 +31,29 @@ module RubyLLM
|
|
|
28
31
|
name = options[:name] || 'mcp'
|
|
29
32
|
server = { type: 'url', name: name }.merge(options.slice(:url, :authorization_token))
|
|
30
33
|
{
|
|
31
|
-
tool: { type: 'mcp_toolset', mcp_server_name: name }
|
|
34
|
+
tool: { type: 'mcp_toolset', mcp_server_name: name }
|
|
35
|
+
.merge(Anthropic.mcp_tool_filter(options))
|
|
36
|
+
.merge(options.slice(:default_config, :configs)),
|
|
32
37
|
payload: { mcp_servers: [server] },
|
|
33
38
|
headers: { 'anthropic-beta' => 'mcp-client-2025-11-20' }
|
|
34
39
|
}
|
|
35
40
|
end
|
|
36
41
|
}.freeze
|
|
37
42
|
|
|
43
|
+
def self.mcp_tool_filter(options) # :nodoc:
|
|
44
|
+
approval = options[:require_approval]
|
|
45
|
+
unless approval.nil? || approval.to_s == 'never'
|
|
46
|
+
raise ArgumentError, "Anthropic runs MCP tools without approval; use require_approval: 'never'"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
tools = options[:allowed_tools]
|
|
50
|
+
tools = tools[:tool_names] if tools.is_a?(Hash) && tools.keys == [:tool_names]
|
|
51
|
+
return {} if tools.nil?
|
|
52
|
+
raise ArgumentError, 'Anthropic filters MCP tools by name; use allowed_tools: [name]' unless tools.is_a?(Array)
|
|
53
|
+
|
|
54
|
+
{ default_config: { enabled: false }, configs: tools.to_h { |tool| [tool.to_sym, { enabled: true }] } }
|
|
55
|
+
end
|
|
56
|
+
|
|
38
57
|
def server_tool_aliases
|
|
39
58
|
SERVER_TOOL_ALIASES
|
|
40
59
|
end
|
|
@@ -5,18 +5,12 @@ module RubyLLM
|
|
|
5
5
|
module Bedrock
|
|
6
6
|
# Luma Ray 2 video generation through Bedrock asynchronous invocation.
|
|
7
7
|
class AsyncVideos < Protocol
|
|
8
|
-
IMAGE_TYPES = %w[image/png image/jpeg].freeze
|
|
9
|
-
|
|
10
8
|
def video_url
|
|
11
9
|
'/async-invoke'
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
def render_video_payload(prompt, model:, with: [], provider_options: {})
|
|
15
13
|
output_uri = video_output_uri
|
|
16
|
-
unless prompt.is_a?(String) && (1..5000).cover?(prompt.length)
|
|
17
|
-
raise ArgumentError, 'Luma Ray 2 requires a prompt between 1 and 5000 characters'
|
|
18
|
-
end
|
|
19
|
-
|
|
20
14
|
options = Support::Utils.deep_symbolize_keys(provider_options)
|
|
21
15
|
outer = options.slice(:clientRequestToken, :tags)
|
|
22
16
|
input = { prompt: prompt }.merge(render_keyframes(with)).merge(options.except(*outer.keys))
|
|
@@ -25,16 +19,18 @@ module RubyLLM
|
|
|
25
19
|
end
|
|
26
20
|
|
|
27
21
|
def post_video(url, payload)
|
|
28
|
-
@connection.post(url, payload, idempotent: false)
|
|
29
|
-
request.headers.merge!(@provider.sign_headers('POST', url, JSON.generate(payload)))
|
|
30
|
-
end
|
|
22
|
+
@connection.post(url, JSON.generate(payload), idempotent: false)
|
|
31
23
|
end
|
|
32
24
|
|
|
33
|
-
def parse_video_job(response, model
|
|
25
|
+
def parse_video_job(response, model:, **request)
|
|
34
26
|
id = response.body['invocationArn']
|
|
35
27
|
raise Error.new('Bedrock did not return a video invocation ARN', response:) unless id
|
|
36
28
|
|
|
37
|
-
VideoJob.new(id: id, protocol: self, model: model, raw: response.body)
|
|
29
|
+
VideoJob.new(id: id, protocol: self, model: model, raw: response.body, **request)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def video_request_settings(request)
|
|
33
|
+
super(request[:modelInput])
|
|
38
34
|
end
|
|
39
35
|
|
|
40
36
|
def video_job_url(job)
|
|
@@ -42,11 +38,7 @@ module RubyLLM
|
|
|
42
38
|
end
|
|
43
39
|
|
|
44
40
|
def refresh_video_job(job)
|
|
45
|
-
|
|
46
|
-
response = @connection.get(url) do |request|
|
|
47
|
-
request.headers.merge!(@provider.sign_headers('GET', url, ''))
|
|
48
|
-
end
|
|
49
|
-
parse_video_job_status(response, job:)
|
|
41
|
+
parse_video_job_status(@connection.get(video_job_url(job)), job:)
|
|
50
42
|
end
|
|
51
43
|
|
|
52
44
|
def parse_video_job_status(response, **)
|
|
@@ -91,7 +83,7 @@ module RubyLLM
|
|
|
91
83
|
raise ArgumentError, 'Luma Ray 2 requires image bytes or URLs, not uploaded file ids'
|
|
92
84
|
end
|
|
93
85
|
|
|
94
|
-
raise UnsupportedAttachmentError, attachment.mime_type unless
|
|
86
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
|
|
95
87
|
end
|
|
96
88
|
end
|
|
97
89
|
|
|
@@ -14,9 +14,8 @@ module RubyLLM
|
|
|
14
14
|
}.freeze
|
|
15
15
|
private_constant :POLICY_FILTERS
|
|
16
16
|
|
|
17
|
-
IMAGE_TYPES = %w[image/png image/jpeg].freeze
|
|
18
17
|
INTERVENTIONS = %w[BLOCKED ANONYMIZED].freeze
|
|
19
|
-
private_constant :
|
|
18
|
+
private_constant :INTERVENTIONS
|
|
20
19
|
|
|
21
20
|
def moderate(input, model:, with: [], provider_options: {})
|
|
22
21
|
raise ArgumentError, 'Bedrock guardrails do not accept a model' unless model.nil?
|
|
@@ -27,7 +26,7 @@ module RubyLLM
|
|
|
27
26
|
payloads = (input.is_a?(Array) ? input : [input]).map do |text|
|
|
28
27
|
render_moderation_payload(text, attachments, provider_options)
|
|
29
28
|
end
|
|
30
|
-
track_usage(:moderation) do
|
|
29
|
+
track_usage(:moderation, model_id: guardrail_model_id) do
|
|
31
30
|
responses = payloads.map do |payload|
|
|
32
31
|
response = post_moderation(url, payload)
|
|
33
32
|
result = parse_moderation_result(response)
|
|
@@ -40,6 +39,10 @@ module RubyLLM
|
|
|
40
39
|
|
|
41
40
|
private
|
|
42
41
|
|
|
42
|
+
def guardrail_model_id
|
|
43
|
+
"#{@config.bedrock_guardrail_id}:#{@config.bedrock_guardrail_version}"
|
|
44
|
+
end
|
|
45
|
+
|
|
43
46
|
def parse_moderation_responses(responses)
|
|
44
47
|
RubyLLM::Moderation.new(
|
|
45
48
|
id: responses.one? ? responses.first.first.headers['x-amzn-requestid'] : nil,
|
|
@@ -59,9 +62,7 @@ module RubyLLM
|
|
|
59
62
|
raise ArgumentError, 'Bedrock moderation requires text or an image' if input.nil? && attachments.empty?
|
|
60
63
|
|
|
61
64
|
attachments.each do |attachment|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
65
|
+
raise UnsupportedAttachmentError, attachment.mime_type unless attachment.image?
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
|
|
@@ -88,9 +89,7 @@ module RubyLLM
|
|
|
88
89
|
end
|
|
89
90
|
|
|
90
91
|
def post_moderation(url, payload)
|
|
91
|
-
@connection.post(url, payload, usage: @usage_tracker)
|
|
92
|
-
request.headers.merge!(@provider.sign_headers('POST', url, JSON.generate(payload)))
|
|
93
|
-
end
|
|
92
|
+
@connection.post(url, JSON.generate(payload), usage: @usage_tracker)
|
|
94
93
|
end
|
|
95
94
|
|
|
96
95
|
def parse_moderation_result(response)
|
|
@@ -16,7 +16,6 @@ module RubyLLM
|
|
|
16
16
|
private
|
|
17
17
|
|
|
18
18
|
def render_rerank_payload(query, documents, model:, top_n:, provider_options:)
|
|
19
|
-
validate_rerank_input(query, documents, top_n)
|
|
20
19
|
payload = {
|
|
21
20
|
queries: [{ type: 'TEXT', textQuery: { text: query } }],
|
|
22
21
|
sources: documents.map { |document| render_rerank_source(document) },
|
|
@@ -31,18 +30,6 @@ module RubyLLM
|
|
|
31
30
|
Support::Utils.deep_merge(payload, provider_options)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
def validate_rerank_input(query, documents, top_n)
|
|
35
|
-
unless query.is_a?(String) && !query.empty?
|
|
36
|
-
raise ArgumentError, 'Bedrock reranking requires one nonempty text query'
|
|
37
|
-
end
|
|
38
|
-
unless documents.is_a?(Array) && (1..1000).cover?(documents.length)
|
|
39
|
-
raise ArgumentError, 'Bedrock reranking accepts between 1 and 1000 documents'
|
|
40
|
-
end
|
|
41
|
-
return if top_n.nil? || (top_n.is_a?(Integer) && (1..1000).cover?(top_n))
|
|
42
|
-
|
|
43
|
-
raise ArgumentError, 'Bedrock reranking top_n must be between 1 and 1000'
|
|
44
|
-
end
|
|
45
|
-
|
|
46
33
|
def render_rerank_source(document)
|
|
47
34
|
content = case document
|
|
48
35
|
when String then { type: 'TEXT', textDocument: { text: document } }
|
|
@@ -70,10 +57,7 @@ module RubyLLM
|
|
|
70
57
|
end
|
|
71
58
|
|
|
72
59
|
def post_rerank(payload)
|
|
73
|
-
@provider.agent_connection.post('/rerank', payload, usage: @usage_tracker)
|
|
74
|
-
request.headers.merge!(@provider.sign_headers('POST', '/rerank', JSON.generate(payload),
|
|
75
|
-
base_url: @provider.agent_api_base))
|
|
76
|
-
end
|
|
60
|
+
@provider.agent_connection.post('/rerank', JSON.generate(payload), usage: @usage_tracker)
|
|
77
61
|
end
|
|
78
62
|
|
|
79
63
|
def parse_rerank_pages(pages, model:, documents:)
|
|
@@ -11,7 +11,6 @@ module RubyLLM
|
|
|
11
11
|
}.freeze
|
|
12
12
|
|
|
13
13
|
OPENAI_INLINE_FILE_LIMIT = 50 * 1024 * 1024
|
|
14
|
-
OPENAI_FILE_UPLOAD_LIMIT = 512 * 1024 * 1024
|
|
15
14
|
PROMPT_CACHE_OPTIONS = %i[key ttl mode retention].freeze
|
|
16
15
|
|
|
17
16
|
def completion_url
|
|
@@ -106,7 +105,7 @@ module RubyLLM
|
|
|
106
105
|
end
|
|
107
106
|
|
|
108
107
|
def parse_completion_body(data, raw:)
|
|
109
|
-
|
|
108
|
+
raise_stream_error(JSON.generate(data), data, nil) if data.dig('error', 'message')
|
|
110
109
|
|
|
111
110
|
message_data = data.dig('choices', 0, 'message')
|
|
112
111
|
raise no_completion_message_error(data, raw) unless message_data
|
|
@@ -143,8 +142,11 @@ module RubyLLM
|
|
|
143
142
|
nil
|
|
144
143
|
end
|
|
145
144
|
|
|
145
|
+
# Per-tool counters end in _requests; OpenRouter's tool_calls_requested
|
|
146
|
+
# and tool_calls_executed count every tool at once.
|
|
146
147
|
def server_tool_use(usage)
|
|
147
|
-
usage['server_tool_use'] || usage['server_tool_use_details']
|
|
148
|
+
counters = usage['server_tool_use'] || usage['server_tool_use_details']
|
|
149
|
+
counters.to_h.select { |counter, _| counter.end_with?('_requests') }
|
|
148
150
|
end
|
|
149
151
|
|
|
150
152
|
def no_completion_message_error(data, raw)
|
|
@@ -404,10 +406,6 @@ module RubyLLM
|
|
|
404
406
|
OPENAI_INLINE_FILE_LIMIT
|
|
405
407
|
end
|
|
406
408
|
|
|
407
|
-
def provider_file_upload_limit
|
|
408
|
-
OPENAI_FILE_UPLOAD_LIMIT
|
|
409
|
-
end
|
|
410
|
-
|
|
411
409
|
def provider_file_attachable?(attachment)
|
|
412
410
|
attachment.pdf?
|
|
413
411
|
end
|
|
@@ -7,6 +7,20 @@ module RubyLLM
|
|
|
7
7
|
module Media
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
|
+
def supported_attachment?(attachment, document_attachments: :pdf, image_attachments: true,
|
|
11
|
+
audio_attachments: true)
|
|
12
|
+
return document_attachments != :none if attachment.provider_file?
|
|
13
|
+
|
|
14
|
+
case attachment.type
|
|
15
|
+
when :image then image_attachments
|
|
16
|
+
when :audio then audio_attachments
|
|
17
|
+
when :text then true
|
|
18
|
+
when :pdf, :document
|
|
19
|
+
document_attachments == :all || (document_attachments == :pdf && attachment.pdf?)
|
|
20
|
+
else false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
10
24
|
def format_content(content, attachments = [], document_attachments: :pdf, image_attachments: true,
|
|
11
25
|
audio_attachments: true)
|
|
12
26
|
format_parts(content, attachments) do |attachment|
|
|
@@ -35,23 +49,21 @@ module RubyLLM
|
|
|
35
49
|
end
|
|
36
50
|
|
|
37
51
|
def format_attachment(attachment, document_attachments:, image_attachments:, audio_attachments:)
|
|
52
|
+
unless supported_attachment?(attachment, document_attachments:, image_attachments:, audio_attachments:)
|
|
53
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
54
|
+
end
|
|
55
|
+
|
|
38
56
|
return format_provider_file(attachment, document_attachments:) if attachment.provider_file?
|
|
39
57
|
|
|
40
58
|
case attachment.type
|
|
41
59
|
when :image
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
format_image(attachment)
|
|
60
|
+
with_image_detail(format_image(attachment), attachment)
|
|
45
61
|
when :audio
|
|
46
|
-
raise UnsupportedAttachmentError, attachment.mime_type unless audio_attachments
|
|
47
|
-
|
|
48
62
|
format_audio(attachment)
|
|
49
63
|
when :pdf, :document
|
|
50
64
|
format_document_attachment(attachment, document_attachments)
|
|
51
65
|
when :text
|
|
52
66
|
format_text_file(attachment)
|
|
53
|
-
else
|
|
54
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
55
67
|
end
|
|
56
68
|
end
|
|
57
69
|
|
|
@@ -59,11 +71,25 @@ module RubyLLM
|
|
|
59
71
|
{
|
|
60
72
|
type: 'image_url',
|
|
61
73
|
image_url: {
|
|
62
|
-
url: image.
|
|
74
|
+
url: image.url_or_data_uri
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
end
|
|
66
78
|
|
|
79
|
+
def with_image_detail(part, image, original_detail: @provider&.original_image_detail?)
|
|
80
|
+
return part unless image.resolution
|
|
81
|
+
|
|
82
|
+
part[:image_url][:detail] = image_detail(image.resolution, original_detail:)
|
|
83
|
+
part
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def image_detail(resolution, original_detail: false)
|
|
87
|
+
return 'low' if resolution == :low
|
|
88
|
+
return 'original' if resolution == :original && original_detail
|
|
89
|
+
|
|
90
|
+
'high'
|
|
91
|
+
end
|
|
92
|
+
|
|
67
93
|
def format_document(document)
|
|
68
94
|
{
|
|
69
95
|
type: 'file',
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class ChatCompletions
|
|
6
|
+
# Describes Chat Completions requests as RequestShape objects. Cohere
|
|
7
|
+
# v2 sends messages in the same shape. Tool messages answer calls by id.
|
|
8
|
+
module RequestShapes
|
|
9
|
+
SPEAKERS = {
|
|
10
|
+
'system' => :system, 'developer' => :system, 'assistant' => :model, 'tool' => :tool, 'function' => :tool
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def parse_request_shape(payload)
|
|
14
|
+
messages = payload['messages']
|
|
15
|
+
return unless messages.is_a?(Array)
|
|
16
|
+
|
|
17
|
+
turns = messages.each_with_index.map { |message, index| shape_message(shape_hash(message), index) }
|
|
18
|
+
tool_names = shape_list(payload['tools']).map { |tool| shape_function_name(tool) }
|
|
19
|
+
shape_request(turns, payload:, tool_names:, thinking_settings: shape_reasoning_settings(payload))
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def shape_message(message, index)
|
|
25
|
+
speaker = SPEAKERS.fetch(message['role'], :user)
|
|
26
|
+
content = message['content']
|
|
27
|
+
parts = shape_message_thinking(message)
|
|
28
|
+
if speaker == :tool
|
|
29
|
+
parts.concat(shape_tool_result(content, name: message['name'], id: message['tool_call_id']) do |block|
|
|
30
|
+
shape_content_part(block)
|
|
31
|
+
end)
|
|
32
|
+
else
|
|
33
|
+
parts.concat(shape_content(content))
|
|
34
|
+
end
|
|
35
|
+
parts.concat(shape_list(message['tool_calls']).map { |call| shape_function_call(shape_hash(call)) })
|
|
36
|
+
shape_turn(index, message['role'], speaker, parts)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def shape_reasoning_settings(payload)
|
|
40
|
+
shape_settings(payload, 'reasoning_effort')
|
|
41
|
+
.merge(shape_settings(payload['reasoning'], 'effort', 'max_tokens', 'enabled', 'exclude'))
|
|
42
|
+
.merge(shape_settings(payload['thinking'], 'type', 'token_budget', 'budget_tokens'))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def shape_message_thinking(message)
|
|
46
|
+
details = message['reasoning_details']
|
|
47
|
+
return details.map { |detail| shape_reasoning_detail(shape_hash(detail)) } if details.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
text = message['reasoning_content'] || message['reasoning']
|
|
50
|
+
signed = shape_signature?(message['reasoning_signature'])
|
|
51
|
+
text || signed ? [shape_text(text, kind: :thinking, signed:)] : []
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def shape_reasoning_detail(detail)
|
|
55
|
+
case detail['type']
|
|
56
|
+
when 'reasoning.encrypted' then shape_text(nil, kind: :thinking, signed: shape_signature?(detail['data']))
|
|
57
|
+
when 'reasoning.summary' then shape_text(detail['summary'], kind: :thinking)
|
|
58
|
+
else shape_text(detail['text'], kind: :thinking, signed: shape_signature?(detail['signature']))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def shape_content(content)
|
|
63
|
+
return [shape_text(content)] if content.is_a?(String)
|
|
64
|
+
|
|
65
|
+
shape_list(content).map { |part| shape_content_part(shape_hash(part)) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shape_content_part(part)
|
|
69
|
+
case part['type']
|
|
70
|
+
when 'text' then shape_text(part['text'])
|
|
71
|
+
when 'image_url' then shape_url(shape_url_field(part['image_url']), kind: :image)
|
|
72
|
+
when 'video_url' then shape_url(shape_url_field(part['video_url']), kind: :video)
|
|
73
|
+
when 'input_audio' then shape_inline(shape_hash(part['input_audio'])['data'], kind: :audio)
|
|
74
|
+
when 'file' then shape_content_file(shape_hash(part['file']))
|
|
75
|
+
when 'document_url' then shape_url(shape_url_field(part['document_url']), kind: :document)
|
|
76
|
+
when 'file_url' then shape_file_url(shape_url_field(part['file_url']))
|
|
77
|
+
when 'thinking' then shape_content_thinking(part)
|
|
78
|
+
else shape_other(part['type'])
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def shape_url_field(value)
|
|
83
|
+
value.is_a?(Hash) ? value['url'] : value
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def shape_content_file(file)
|
|
87
|
+
return shape_file(kind: :document) if file.key?('file_id')
|
|
88
|
+
|
|
89
|
+
shape_url(file['file_data'], kind: :document)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Perplexity sends a document's bytes as bare base64 where its link
|
|
93
|
+
# would go, and base64 never holds the colon that starts a scheme.
|
|
94
|
+
def shape_file_url(url)
|
|
95
|
+
return shape_inline(url, kind: :document) if url.is_a?(String) && !url.include?(':')
|
|
96
|
+
|
|
97
|
+
shape_url(url, kind: :document)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def shape_content_thinking(part)
|
|
101
|
+
thinking = part['thinking']
|
|
102
|
+
signed = shape_signature?(part['signature'])
|
|
103
|
+
return shape_summary(thinking, signed:) if thinking.is_a?(Array)
|
|
104
|
+
|
|
105
|
+
shape_text(thinking, kind: :thinking, signed:)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def shape_function_call(call)
|
|
109
|
+
function = shape_hash(call['function'])
|
|
110
|
+
signature = shape_hash(shape_hash(call['extra_content'])['google'])['thought_signature']
|
|
111
|
+
shape_tool_call(function['name'], function['arguments'], id: call['id'], signed: shape_signature?(signature))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def shape_function_name(tool)
|
|
115
|
+
tool = shape_hash(tool)
|
|
116
|
+
shape_hash(tool['function'])['name'] || tool['type']
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -54,6 +54,9 @@ module RubyLLM
|
|
|
54
54
|
error = error_data['error']
|
|
55
55
|
return [nil, error.to_s] unless error.is_a?(Hash)
|
|
56
56
|
|
|
57
|
+
code = error['code']
|
|
58
|
+
return [code, error['message']] if code.is_a?(Integer) && code.between?(400, 599)
|
|
59
|
+
|
|
57
60
|
case error['type']
|
|
58
61
|
when 'server_error'
|
|
59
62
|
[500, error['message']]
|
|
@@ -8,6 +8,7 @@ module RubyLLM
|
|
|
8
8
|
include ChatCompletions::Embeddings
|
|
9
9
|
include ChatCompletions::Models
|
|
10
10
|
include ChatCompletions::Moderation
|
|
11
|
+
include ChatCompletions::RequestShapes
|
|
11
12
|
include ChatCompletions::Streaming
|
|
12
13
|
include ChatCompletions::Tools
|
|
13
14
|
include ChatCompletions::Images
|