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 Responses
|
|
6
|
+
# Describes Responses API requests, including token counting and
|
|
7
|
+
# compaction, as RequestShape objects. Each input item is a turn, and a
|
|
8
|
+
# call's output answers the call by id.
|
|
9
|
+
module RequestShapes
|
|
10
|
+
def parse_request_shape(payload)
|
|
11
|
+
input = payload['input']
|
|
12
|
+
return unless input.is_a?(Array)
|
|
13
|
+
|
|
14
|
+
turns = input.each_with_index.map do |item, index|
|
|
15
|
+
item = shape_hash(item)
|
|
16
|
+
shape_turn(index, item['role'] || item['type'], shape_input_speaker(item), shape_input_item(item))
|
|
17
|
+
end
|
|
18
|
+
shape_request(turns, payload:, instructions: shape_instructions(payload['instructions']),
|
|
19
|
+
tool_names: shape_list(payload['tools']).map { |tool| shape_tool_name(tool) },
|
|
20
|
+
thinking_settings: shape_settings(payload['reasoning'], 'effort', 'summary'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def shape_input_speaker(item)
|
|
26
|
+
case item['type']
|
|
27
|
+
when nil, 'message' then ChatCompletions::RequestShapes::SPEAKERS.fetch(item['role'], :user)
|
|
28
|
+
when /_output\z|_response\z/ then :tool
|
|
29
|
+
else :model
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def shape_input_item(item)
|
|
34
|
+
case item['type']
|
|
35
|
+
when nil, 'message' then shape_input_content(item['content'])
|
|
36
|
+
when 'function_call' then [shape_tool_call(item['name'], item['arguments'], id: item['call_id'])]
|
|
37
|
+
when 'function_call_output'
|
|
38
|
+
shape_tool_result(item['output'], id: item['call_id']) { |part| shape_input_part(part) }
|
|
39
|
+
when 'reasoning' then [shape_summary(item['summary'], signed: shape_signature?(item['encrypted_content']))]
|
|
40
|
+
else [shape_other(item['type'])]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def shape_input_content(content)
|
|
45
|
+
return [shape_text(content)] if content.is_a?(String)
|
|
46
|
+
|
|
47
|
+
shape_list(content).map { |part| shape_input_part(shape_hash(part)) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def shape_input_part(part)
|
|
51
|
+
case part['type']
|
|
52
|
+
when 'input_text', 'output_text' then shape_text(part['text'])
|
|
53
|
+
when 'input_image'
|
|
54
|
+
part.key?('file_id') ? shape_file(kind: :image) : shape_url(part['image_url'], kind: :image)
|
|
55
|
+
when 'input_file' then shape_input_file(part)
|
|
56
|
+
when 'input_audio' then shape_inline(shape_hash(part['input_audio'])['data'], kind: :audio)
|
|
57
|
+
else shape_other(part['type'])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def shape_input_file(part)
|
|
62
|
+
return shape_file(kind: :document) if part.key?('file_id')
|
|
63
|
+
return shape_url(part['file_url'], kind: :document) if part.key?('file_url')
|
|
64
|
+
|
|
65
|
+
shape_url(part['file_data'], kind: :document)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -9,6 +9,7 @@ module RubyLLM
|
|
|
9
9
|
ERROR_STATUSES = {
|
|
10
10
|
'server_error' => 500,
|
|
11
11
|
'rate_limit_exceeded' => 429,
|
|
12
|
+
'too_many_requests' => 429,
|
|
12
13
|
'insufficient_quota' => 429
|
|
13
14
|
}.freeze
|
|
14
15
|
|
|
@@ -38,7 +39,7 @@ module RubyLLM
|
|
|
38
39
|
when 'response.completed', 'response.incomplete'
|
|
39
40
|
build_final_chunk(data)
|
|
40
41
|
when 'response.failed'
|
|
41
|
-
|
|
42
|
+
raise_failed_response(data['response'] || {})
|
|
42
43
|
else
|
|
43
44
|
chunk
|
|
44
45
|
end
|
|
@@ -95,17 +96,32 @@ module RubyLLM
|
|
|
95
96
|
finish_reason: parse_finish_reason(response),
|
|
96
97
|
citations: parse_citations(response, output, nil),
|
|
97
98
|
server_tool_calls: server_tool_calls,
|
|
98
|
-
raw_content:
|
|
99
|
-
**parse_usage(response['usage'] || {})
|
|
99
|
+
raw_content: keep_raw_output?(output, server_tool_calls) ? output : nil,
|
|
100
|
+
**parse_usage(response['usage'] || {}),
|
|
101
|
+
server_tool_use: parse_server_tool_use(response)
|
|
100
102
|
end
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
def raise_failed_response(response)
|
|
105
|
+
body = { 'error' => response['error'] }
|
|
106
|
+
raise_stream_error(JSON.generate(body), body, nil)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# OpenAI reports a stream error as a flat event carrying a code. Azure
|
|
110
|
+
# nests the code and type under an error object, as a failed response
|
|
111
|
+
# and Chat Completions do.
|
|
104
112
|
def parse_streaming_error(data)
|
|
105
113
|
event = JSON.parse(data)
|
|
106
|
-
|
|
114
|
+
error = stream_error(event)
|
|
115
|
+
return super unless error
|
|
116
|
+
|
|
117
|
+
[ERROR_STATUSES[error['code']] || ERROR_STATUSES.fetch(error['type'], 400), error['message']]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def stream_error(event)
|
|
121
|
+
return unless event.is_a?(Hash)
|
|
122
|
+
return event['error'] if event['error'].is_a?(Hash)
|
|
107
123
|
|
|
108
|
-
|
|
124
|
+
event if event['type'] == 'error'
|
|
109
125
|
end
|
|
110
126
|
|
|
111
127
|
def chunk(content: nil, **attributes)
|
|
@@ -13,8 +13,8 @@ module RubyLLM
|
|
|
13
13
|
"#{completion_url}/input_tokens"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def render_count_tokens_payload(messages, model:, **
|
|
17
|
-
render_payload(messages, model: model, temperature: nil, **
|
|
16
|
+
def render_count_tokens_payload(messages, model:, **)
|
|
17
|
+
render_payload(messages, model: model, temperature: nil, **).slice(*COUNT_TOKENS_KEYS)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def parse_count_tokens_response(response)
|
|
@@ -8,6 +8,8 @@ module RubyLLM
|
|
|
8
8
|
module Tools
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
|
+
NATIVE_TOOL_SEARCH = { type: 'tool_search' }.freeze
|
|
12
|
+
|
|
11
13
|
def tool_for(tool)
|
|
12
14
|
definition = {
|
|
13
15
|
type: 'function',
|
|
@@ -16,12 +18,37 @@ module RubyLLM
|
|
|
16
18
|
parameters: ChatCompletions::Tools.parameters_schema_for(tool),
|
|
17
19
|
strict: false
|
|
18
20
|
}
|
|
21
|
+
definition[:defer_loading] = true if deferred?(tool)
|
|
19
22
|
|
|
20
23
|
return definition if tool.provider_options.empty?
|
|
21
24
|
|
|
22
25
|
RubyLLM::Support::Utils.deep_merge(definition, tool.provider_options)
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
def deferred?(tool)
|
|
29
|
+
tool.is_a?(RubyLLM::Tool::Deferred)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The API requires a search tool next to deferred tools, and rejects
|
|
33
|
+
# search items in the input of a request that declares none.
|
|
34
|
+
def apply_tool_search(payload)
|
|
35
|
+
tools = Array(payload[:tools])
|
|
36
|
+
return payload if tools.any? { |tool| search_tool?(tool) }
|
|
37
|
+
return payload.merge(tools: tools + [NATIVE_TOOL_SEARCH.dup]) if defers_loading?(tools)
|
|
38
|
+
|
|
39
|
+
payload.merge(input: payload[:input].reject do |item|
|
|
40
|
+
Chat::TOOL_SEARCH_ITEM_TYPES.include?(item[:type] || item['type'])
|
|
41
|
+
end)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def defers_loading?(tools)
|
|
45
|
+
tools.any? { |tool| tool[:defer_loading] || tool['defer_loading'] }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def search_tool?(tool)
|
|
49
|
+
(tool[:type] || tool['type']) == NATIVE_TOOL_SEARCH[:type]
|
|
50
|
+
end
|
|
51
|
+
|
|
25
52
|
def build_tool_choice(tool_choice)
|
|
26
53
|
case tool_choice
|
|
27
54
|
when :auto, :none, :required
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class SystemOne
|
|
6
|
+
module Judgments # :nodoc:
|
|
7
|
+
TYPES = { probability: 'noul', choice: 'choice', score: 'score' }.freeze
|
|
8
|
+
BOOLEAN_KEYS = { 'yes' => 'true', 'true' => 'true', 'no' => 'false', 'false' => 'false' }.freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def judgment_url
|
|
13
|
+
'v1/systemone'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render_judgment_payload(input, questions:, model:, with: [], provider_options: {})
|
|
17
|
+
raise UnsupportedAttachmentError, with.first.mime_type unless with.empty?
|
|
18
|
+
|
|
19
|
+
reserved = provider_options.keys.map(&:to_s) & %w[model state questions]
|
|
20
|
+
unless reserved.empty?
|
|
21
|
+
raise ArgumentError, "Use the judgment arguments instead of provider_options for #{reserved.join(', ')}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
model:,
|
|
26
|
+
state: input,
|
|
27
|
+
questions: questions.transform_values { |question| render_question(question) }
|
|
28
|
+
}.merge(provider_options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render_question(question)
|
|
32
|
+
criteria = question.criteria
|
|
33
|
+
if question.type == :probability && criteria
|
|
34
|
+
criteria = criteria.transform_keys { |key| BOOLEAN_KEYS.fetch(key.to_s) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
{ type: TYPES.fetch(question.type), instructions: question.instructions, criteria: }.compact
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class SystemOne
|
|
6
|
+
module Models # :nodoc:
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def models_url
|
|
10
|
+
'v1/models'
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse_list_models_response(response, slug)
|
|
14
|
+
Array(response.body['models']).map do |entry|
|
|
15
|
+
Model.new(
|
|
16
|
+
id: entry['name'], name: entry['name'], provider: slug,
|
|
17
|
+
created_at: entry['release_date'],
|
|
18
|
+
modalities: { input: ['text'], output: ['judgment'] },
|
|
19
|
+
capabilities: ['judgment'],
|
|
20
|
+
metadata: { description: entry['description'] }.compact
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class SystemOne
|
|
6
|
+
module Responses # :nodoc:
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def parse_judgment_response(response, questions:)
|
|
10
|
+
body = response.body
|
|
11
|
+
answers = questions.values.to_h do |question|
|
|
12
|
+
[question.name, parse_answer(body.fetch('answers').fetch(question.name.to_s), question)]
|
|
13
|
+
end
|
|
14
|
+
RubyLLM::Judgment.new(
|
|
15
|
+
answers:, model: body['model'], raw: response, model_info: @model,
|
|
16
|
+
tokens: Tokens.new(input: body.dig('usage', 'input_tokens'), output: body.dig('usage', 'output_tokens'))
|
|
17
|
+
)
|
|
18
|
+
rescue KeyError, NoMethodError, TypeError => e
|
|
19
|
+
raise Error.new("System One returned an invalid judgment: #{e.message}", response:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def parse_answer(answer, question)
|
|
23
|
+
case question.type
|
|
24
|
+
when :probability then RubyLLM::Probability.new(probability: answer.fetch('noul'))
|
|
25
|
+
when :choice then parse_choice(answer, question)
|
|
26
|
+
when :score then parse_score(answer, question)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_choice(answer, question)
|
|
31
|
+
options = question.criteria.keys.to_h { |key| [key.to_s, key] }
|
|
32
|
+
RubyLLM::Choice.new(
|
|
33
|
+
choice: options.fetch(answer.fetch('choice')),
|
|
34
|
+
probabilities: parse_probabilities(answer.fetch('probabilities'), options),
|
|
35
|
+
confidence: answer.fetch('confidence')
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def parse_score(answer, question)
|
|
40
|
+
indexes = question.criteria.each_index.to_h { |index| [index.to_s, index] }
|
|
41
|
+
legend = answer.fetch('legend')
|
|
42
|
+
RubyLLM::Score.new(
|
|
43
|
+
score: answer.fetch('score'), levels: indexes.keys.map { |key| legend.fetch(key) },
|
|
44
|
+
probabilities: parse_probabilities(answer.fetch('probabilities'), indexes),
|
|
45
|
+
confidence: answer.fetch('confidence')
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def parse_probabilities(values, keys)
|
|
50
|
+
keys.to_h { |wire, key| [key, values.fetch(wire)] }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The System One protocol for typed questions and probabilistic answers.
|
|
6
|
+
class SystemOne < Protocol
|
|
7
|
+
include SystemOne::Models
|
|
8
|
+
include SystemOne::Judgments
|
|
9
|
+
include SystemOne::Responses
|
|
10
|
+
|
|
11
|
+
def parse_error_response(response)
|
|
12
|
+
body = response.body.is_a?(String) ? JSON.parse(response.body) : response.body
|
|
13
|
+
return unless body.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
detail = body['detail']
|
|
16
|
+
case detail
|
|
17
|
+
when String
|
|
18
|
+
detail
|
|
19
|
+
when Hash
|
|
20
|
+
detail['message']
|
|
21
|
+
end
|
|
22
|
+
rescue JSON::ParserError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,8 +11,8 @@ module RubyLLM
|
|
|
11
11
|
def validate_embedding_request!(request)
|
|
12
12
|
text = request.fetch(:text)
|
|
13
13
|
values = text.is_a?(Array) ? text : [text]
|
|
14
|
-
unless values.any? && values.all?
|
|
15
|
-
raise ArgumentError, 'Vertex AI embedding batches require nonempty text strings'
|
|
14
|
+
unless values.any? && values.all?(String)
|
|
15
|
+
raise ArgumentError, 'Vertex AI embedding batches require a nonempty list of text strings'
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
payload = request.fetch(:payload)
|
|
@@ -8,6 +8,12 @@ module RubyLLM
|
|
|
8
8
|
# GCS object names allow spaces and other characters URI() rejects.
|
|
9
9
|
GCS_URI = %r{\Ags://([^/]+)/?(.*)\z}m
|
|
10
10
|
|
|
11
|
+
STORAGE_CLIENTS = Support::ProcessCache.new
|
|
12
|
+
|
|
13
|
+
def self.storage_clients
|
|
14
|
+
STORAGE_CLIENTS
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
12
18
|
def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
|
|
13
19
|
provider_options: {})
|
|
@@ -74,17 +80,39 @@ module RubyLLM
|
|
|
74
80
|
def storage
|
|
75
81
|
require 'google/cloud/storage'
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
if @config.vertexai_service_account_key
|
|
79
|
-
options[:credentials] =
|
|
80
|
-
JSON.parse(@config.vertexai_service_account_key)
|
|
81
|
-
end
|
|
82
|
-
::Google::Cloud::Storage.new(**options)
|
|
83
|
+
STORAGE_CLIENTS.fetch(storage_key) { ::Google::Cloud::Storage.new(**storage_options) }
|
|
83
84
|
rescue LoadError
|
|
84
85
|
raise Error, 'The google-cloud-storage gem is required for Vertex AI file uploads. ' \
|
|
85
86
|
'Please add it to your Gemfile: gem "google-cloud-storage"'
|
|
86
87
|
end
|
|
87
88
|
|
|
89
|
+
# Without a key, Cloud Storage resolves its own credentials, including
|
|
90
|
+
# settings of its own such as STORAGE_KEYFILE.
|
|
91
|
+
def storage_options
|
|
92
|
+
options = { project_id: @config.vertexai_project_id }
|
|
93
|
+
options[:credentials] = @provider.google_credentials if @config.vertexai_service_account_key
|
|
94
|
+
options
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# A client keeps the credentials, scope, endpoint, and settings it was
|
|
98
|
+
# built with, so only callers that would build the same one share it.
|
|
99
|
+
def storage_key
|
|
100
|
+
settings = ::Google::Cloud::Storage.configure
|
|
101
|
+
[
|
|
102
|
+
storage_options,
|
|
103
|
+
settings.fields!.to_h { |field| [field, settings[field]] },
|
|
104
|
+
::Google::Cloud.configure.credentials,
|
|
105
|
+
ENV.values_at(*credential_variables)
|
|
106
|
+
]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def credential_variables
|
|
110
|
+
storage = ::Google::Cloud::Storage::Credentials
|
|
111
|
+
loader = ::Google::Auth::CredentialsLoader
|
|
112
|
+
storage::PATH_ENV_VARS + storage::JSON_ENV_VARS +
|
|
113
|
+
loader.constants.grep(/_VAR\z/).map { |name| loader.const_get(name) }
|
|
114
|
+
end
|
|
115
|
+
|
|
88
116
|
def bucket(name)
|
|
89
117
|
storage.bucket(name) || raise(Error, "GCS bucket not found: #{name}")
|
|
90
118
|
end
|
|
@@ -15,50 +15,29 @@ module RubyLLM
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def render_rerank_payload(query, documents, model:, top_n: nil, provider_options: {})
|
|
18
|
-
|
|
18
|
+
raise ArgumentError, 'Vertex AI Search reranking documents must be text' unless documents.all?(String)
|
|
19
|
+
|
|
19
20
|
records = documents.each_with_index.map { |document, index| { id: index.to_s, content: document } }
|
|
20
21
|
{ model: model, query: query, records: records, topN: top_n }.compact.merge(provider_options)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def parse_rerank_response(response, model:, documents: [])
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
seen = []
|
|
28
|
-
results = records.map do |record|
|
|
25
|
+
seen = Set.new
|
|
26
|
+
results = Array(response.body['records']).map do |record|
|
|
29
27
|
index = ranking_index(record, documents)
|
|
30
|
-
raise Error.new('Vertex AI Search returned a duplicate document id', response:)
|
|
28
|
+
raise Error.new('Vertex AI Search returned a duplicate document id', response:) unless seen.add?(index)
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
Rerank::Result.new(index: index, document: documents[index], score: record.fetch('score'))
|
|
30
|
+
Rerank::Result.new(index: index, document: documents[index], score: record['score'])
|
|
34
31
|
end
|
|
35
32
|
Rerank.new(results: results, model: model, raw: response.body)
|
|
36
33
|
end
|
|
37
34
|
|
|
38
35
|
private
|
|
39
36
|
|
|
40
|
-
def validate_ranking_input(query, documents, top_n)
|
|
41
|
-
unless query.is_a?(String) && !query.empty?
|
|
42
|
-
raise ArgumentError, 'Vertex AI Search reranking requires a nonempty query'
|
|
43
|
-
end
|
|
44
|
-
unless valid_ranking_documents?(documents)
|
|
45
|
-
raise ArgumentError, 'Vertex AI Search reranking accepts between 1 and 1000 nonempty text documents'
|
|
46
|
-
end
|
|
47
|
-
return if top_n.nil? || (top_n.is_a?(Integer) && top_n.positive?)
|
|
48
|
-
|
|
49
|
-
raise ArgumentError, 'top_n must be a positive integer'
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def valid_ranking_documents?(documents)
|
|
53
|
-
documents.is_a?(Array) && (1..1000).cover?(documents.length) &&
|
|
54
|
-
documents.all? { |document| document.is_a?(String) && !document.empty? }
|
|
55
|
-
end
|
|
56
|
-
|
|
57
37
|
def ranking_index(record, documents)
|
|
58
|
-
id = record['id']
|
|
59
|
-
unless id.is_a?(String) && id.match?(/\A(?:0|[1-9]\d*)\z/) && id.to_i < documents.length
|
|
60
|
-
|
|
61
|
-
raise Error, 'Vertex AI Search returned an invalid document id or score'
|
|
38
|
+
id = record['id']
|
|
39
|
+
unless id.is_a?(String) && id.match?(/\A(?:0|[1-9]\d*)\z/) && id.to_i < documents.length
|
|
40
|
+
raise Error, 'Vertex AI Search returned an invalid document id'
|
|
62
41
|
end
|
|
63
42
|
|
|
64
43
|
id.to_i
|
|
@@ -22,7 +22,6 @@ module RubyLLM
|
|
|
22
22
|
def server_tool_aliases = SERVER_TOOL_ALIASES
|
|
23
23
|
|
|
24
24
|
def create_research_job(prompt, agent:, with: nil, provider_tools: nil, provider_options: {})
|
|
25
|
-
validate_research_agent(agent)
|
|
26
25
|
payload = render_research_payload(prompt, agent:, with:, provider_tools:, provider_options:)
|
|
27
26
|
response = @connection.post(research_url, payload, idempotent: false)
|
|
28
27
|
parse_research_job(response, agent:)
|
|
@@ -37,14 +36,14 @@ module RubyLLM
|
|
|
37
36
|
response = @connection.get(research_url(job.id)) do |request|
|
|
38
37
|
request.options.timeout = request_timeout(timeout)
|
|
39
38
|
end
|
|
40
|
-
parse_research_state(response
|
|
39
|
+
parse_research_state(response)
|
|
41
40
|
end
|
|
42
41
|
|
|
43
42
|
def cancel_research_job(job, timeout: nil)
|
|
44
43
|
response = @connection.post("#{research_url(job.id)}/cancel", {}, idempotent: false) do |request|
|
|
45
44
|
request.options.timeout = request_timeout(timeout)
|
|
46
45
|
end
|
|
47
|
-
parse_research_state(response
|
|
46
|
+
parse_research_state(response)
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
private
|
|
@@ -65,17 +64,10 @@ module RubyLLM
|
|
|
65
64
|
[timeout, @config.request_timeout].compact.min
|
|
66
65
|
end
|
|
67
66
|
|
|
68
|
-
def validate_research_agent(agent)
|
|
69
|
-
return if agent == 'deep-research-preview-04-2026'
|
|
70
|
-
|
|
71
|
-
raise ArgumentError, 'Vertex AI research requires the supported Deep Research agent ID'
|
|
72
|
-
end
|
|
73
|
-
|
|
74
67
|
def render_research_payload(prompt, agent:, with:, provider_tools:, provider_options:)
|
|
75
68
|
raise ArgumentError, 'Research requires a nonempty prompt' unless prompt.is_a?(String) && !prompt.empty?
|
|
76
69
|
|
|
77
70
|
attachments = Attachment.wrap(with, config: @config)
|
|
78
|
-
validate_research_attachments(attachments)
|
|
79
71
|
payload = { agent:, input: render_interaction_content(prompt, attachments), background: true, stream: false }
|
|
80
72
|
payload[:tools] = [] unless provider_tools.nil?
|
|
81
73
|
options = render_research_options(provider_options)
|
|
@@ -87,14 +79,6 @@ module RubyLLM
|
|
|
87
79
|
apply_provider_tools(payload, entries).merge(options)
|
|
88
80
|
end
|
|
89
81
|
|
|
90
|
-
def validate_research_attachments(attachments)
|
|
91
|
-
attachments.each do |attachment|
|
|
92
|
-
next if attachment.image? || attachment.pdf? || attachment.text?
|
|
93
|
-
|
|
94
|
-
raise UnsupportedAttachmentError, attachment.mime_type
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
82
|
def render_research_options(provider_options)
|
|
99
83
|
options = Support::Utils.deep_symbolize_keys(provider_options)
|
|
100
84
|
unsupported = options.keys - %i[agent_config service_tier]
|
|
@@ -121,7 +105,7 @@ module RubyLLM
|
|
|
121
105
|
|
|
122
106
|
def parse_research_job(response, agent: nil, id: nil)
|
|
123
107
|
data = response.body
|
|
124
|
-
state = parse_research_state(response
|
|
108
|
+
state = parse_research_state(response)
|
|
125
109
|
ResearchJob.new(id: data['id'], provider: @provider.slug, agent: data['agent'] || agent, protocol: self,
|
|
126
110
|
**state)
|
|
127
111
|
rescue RubyLLM::Error, ArgumentError => e
|
|
@@ -135,9 +119,9 @@ module RubyLLM
|
|
|
135
119
|
job:, response:), cause: e
|
|
136
120
|
end
|
|
137
121
|
|
|
138
|
-
def parse_research_state(response
|
|
122
|
+
def parse_research_state(response)
|
|
139
123
|
data = response.body
|
|
140
|
-
validate_research_response(data, response
|
|
124
|
+
validate_research_response(data, response)
|
|
141
125
|
status = STATUSES.fetch(data['status'])
|
|
142
126
|
message = parse_research_message(data, response) if %i[completed incomplete].include?(status)
|
|
143
127
|
{
|
|
@@ -146,21 +130,10 @@ module RubyLLM
|
|
|
146
130
|
}
|
|
147
131
|
end
|
|
148
132
|
|
|
149
|
-
def validate_research_response(data, response
|
|
150
|
-
|
|
151
|
-
raise RubyLLM::Error.new('Vertex AI research returned no recognized job state', response:)
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
validate_research_identity(data, response, id:, agent:)
|
|
155
|
-
validate_research_agent(data['agent'] || agent)
|
|
156
|
-
rescue ArgumentError => e
|
|
157
|
-
raise RubyLLM::Error.new(e.message, response:), cause: e
|
|
158
|
-
end
|
|
133
|
+
def validate_research_response(data, response)
|
|
134
|
+
return if data.is_a?(Hash) && !data['id'].to_s.empty? && STATUSES.key?(data['status'])
|
|
159
135
|
|
|
160
|
-
|
|
161
|
-
return unless (id && id != data['id']) || (agent && data['agent'] && agent != data['agent'])
|
|
162
|
-
|
|
163
|
-
raise RubyLLM::Error.new('Vertex AI research returned a different job identity', response:)
|
|
136
|
+
raise RubyLLM::Error.new('Vertex AI research returned no recognized job state', response:)
|
|
164
137
|
end
|
|
165
138
|
|
|
166
139
|
def parse_research_tokens(data)
|
|
@@ -174,12 +147,7 @@ module RubyLLM
|
|
|
174
147
|
state = data.merge('status' => data['status'] == 'completed' ? 'completed' : 'incomplete')
|
|
175
148
|
tokens = parse_research_tokens(data)
|
|
176
149
|
cost = Cost.from_h({ total: tokens.reported_cost }.compact, tokens:)
|
|
177
|
-
|
|
178
|
-
if message.content.to_s.empty? && message.attachments.empty?
|
|
179
|
-
raise RubyLLM::Error.new('Vertex AI research finished without a report', response:)
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
message
|
|
150
|
+
parse_completion_body(state, raw: response, model: nil, cost:)
|
|
183
151
|
end
|
|
184
152
|
|
|
185
153
|
def parse_research_error(data)
|
|
@@ -4,7 +4,6 @@ module RubyLLM
|
|
|
4
4
|
module Protocols
|
|
5
5
|
module XAI
|
|
6
6
|
module StreamingTranscription # :nodoc: all
|
|
7
|
-
SAMPLE_RATES = [8000, 16_000, 22_050, 24_000, 44_100, 48_000].freeze
|
|
8
7
|
ENCODINGS = { [1, 16] => 'pcm', [6, 8] => 'alaw', [7, 8] => 'mulaw' }.freeze
|
|
9
8
|
|
|
10
9
|
def stream_transcription(payload, model:, &block)
|
|
@@ -53,9 +52,7 @@ module RubyLLM
|
|
|
53
52
|
|
|
54
53
|
def streaming_transcription_url(payload, audio:)
|
|
55
54
|
encoding = ENCODINGS[[audio.encoding, audio.bits_per_sample]]
|
|
56
|
-
|
|
57
|
-
raise ArgumentError, 'xAI streaming requires 16-bit PCM or 8-bit G.711 WAV audio at a supported sample rate'
|
|
58
|
-
end
|
|
55
|
+
raise ArgumentError, 'xAI streaming requires 16-bit PCM or 8-bit G.711 WAV audio' unless encoding
|
|
59
56
|
|
|
60
57
|
params = payload.except(:file).merge(encoding:, sample_rate: audio.sample_rate, interim_results: true)
|
|
61
58
|
params[:channels] = audio.channels
|
|
@@ -68,7 +65,7 @@ module RubyLLM
|
|
|
68
65
|
|
|
69
66
|
def send_transcription_audio(socket, audio)
|
|
70
67
|
offset = 0
|
|
71
|
-
bytes = audio.sample_rate * audio.channels * audio.bits_per_sample / 8 / 10
|
|
68
|
+
bytes = [audio.sample_rate * audio.channels * audio.bits_per_sample / 8 / 10, 1].max
|
|
72
69
|
while offset < audio.data.bytesize
|
|
73
70
|
socket.send_binary(audio.data.byteslice(offset, bytes))
|
|
74
71
|
offset += bytes
|