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
|
@@ -6,6 +6,9 @@ require 'ruby_llm/error'
|
|
|
6
6
|
module RubyLLM
|
|
7
7
|
module Transport # :nodoc:
|
|
8
8
|
class ErrorMiddleware < Faraday::Middleware # :nodoc: all
|
|
9
|
+
PROVIDER_KEY = :ruby_llm_provider
|
|
10
|
+
STREAM_RESET_KEY = :ruby_llm_stream_reset
|
|
11
|
+
|
|
9
12
|
def initialize(app, options = {})
|
|
10
13
|
super(app)
|
|
11
14
|
@provider = options[:provider]
|
|
@@ -17,25 +20,37 @@ module RubyLLM
|
|
|
17
20
|
def call(env)
|
|
18
21
|
env[:streaming_error_response] = nil
|
|
19
22
|
env[:streaming_state] = nil
|
|
23
|
+
context = env[:request]&.context
|
|
24
|
+
context&.[](STREAM_RESET_KEY)&.call
|
|
25
|
+
provider = context&.[](PROVIDER_KEY) || @provider
|
|
20
26
|
@app.call(env).on_complete do |response|
|
|
21
|
-
apply_retry_delay(response)
|
|
22
|
-
self.class.parse_error(provider
|
|
27
|
+
apply_retry_delay(response, provider)
|
|
28
|
+
self.class.parse_error(provider:, response: streaming_error_response(response))
|
|
23
29
|
end
|
|
24
30
|
end
|
|
25
31
|
|
|
26
32
|
private
|
|
27
33
|
|
|
28
34
|
# The retry middleware only reads the standard Retry-After header, so
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
def apply_retry_delay(response)
|
|
35
|
+
# other retry hints are normalized into seconds here.
|
|
36
|
+
def apply_retry_delay(response, provider)
|
|
32
37
|
status = response.respond_to?(:status) ? response.status : response[:status]
|
|
33
|
-
return unless status
|
|
38
|
+
return unless status && status >= 400
|
|
34
39
|
|
|
35
40
|
headers = response[:response_headers]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
return unless headers && !headers['Retry-After']
|
|
42
|
+
|
|
43
|
+
delay = millisecond_retry_delay(headers) || provider_retry_delay(provider, response, status)
|
|
44
|
+
headers['Retry-After'] = delay.to_s if delay
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def provider_retry_delay(provider, response, status)
|
|
48
|
+
provider&.retry_delay(response) if status == 429
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def millisecond_retry_delay(headers)
|
|
52
|
+
value = Float(headers['retry-after-ms'], exception: false)
|
|
53
|
+
value / 1000 if value&.finite? && value >= 0
|
|
39
54
|
end
|
|
40
55
|
|
|
41
56
|
def streaming_error_response(response)
|
|
@@ -56,7 +71,7 @@ module RubyLLM
|
|
|
56
71
|
/context window/i,
|
|
57
72
|
/exceeds?.*context size/i,
|
|
58
73
|
/maximum context/i,
|
|
59
|
-
/request too large/i,
|
|
74
|
+
/request (?:was )?too large/i,
|
|
60
75
|
/too many tokens/i,
|
|
61
76
|
/token count exceeds/i,
|
|
62
77
|
/input[_\s-]?token/i,
|
|
@@ -70,25 +85,41 @@ module RubyLLM
|
|
|
70
85
|
/rate limit/i,
|
|
71
86
|
/per minute/i,
|
|
72
87
|
/per hour/i,
|
|
73
|
-
/per day/i
|
|
88
|
+
/per day/i,
|
|
89
|
+
/quota exceeded/i,
|
|
90
|
+
/wait before trying again/i
|
|
91
|
+
].freeze
|
|
92
|
+
|
|
93
|
+
OVERLOAD_PATTERNS = [
|
|
94
|
+
/currently overloaded/i
|
|
95
|
+
].freeze
|
|
96
|
+
|
|
97
|
+
PAYMENT_REQUIRED_PATTERNS = [
|
|
98
|
+
/credit balance is too low/i
|
|
99
|
+
].freeze
|
|
100
|
+
|
|
101
|
+
AUTHENTICATION_PATTERNS = [
|
|
102
|
+
/api key not valid/i,
|
|
103
|
+
/incorrect api key/i,
|
|
104
|
+
/security token included in the request is (?:invalid|expired)/i
|
|
74
105
|
].freeze
|
|
75
106
|
|
|
76
107
|
def parse_error(provider:, response:)
|
|
108
|
+
return if (200..399).cover?(response.status)
|
|
109
|
+
|
|
77
110
|
message = provider&.parse_error(response)
|
|
78
111
|
|
|
79
112
|
case response.status
|
|
80
|
-
when 200..399
|
|
81
|
-
message
|
|
82
113
|
when 400
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
raise BadRequestError.new(message, response:)
|
|
114
|
+
raise_bad_request(message, response)
|
|
86
115
|
when 401
|
|
87
116
|
raise UnauthorizedError.new(message, response:)
|
|
88
117
|
when 402
|
|
89
118
|
raise PaymentRequiredError.new(message, response:)
|
|
90
119
|
when 403
|
|
91
|
-
|
|
120
|
+
raise_forbidden(message, response)
|
|
121
|
+
when 413
|
|
122
|
+
raise_too_large(message, response)
|
|
92
123
|
when 429
|
|
93
124
|
raise RateLimitError.new(message, response:) if rate_limited?(message)
|
|
94
125
|
raise ContextLengthExceededError.new(message, response:) if context_length_exceeded?(message)
|
|
@@ -107,21 +138,56 @@ module RubyLLM
|
|
|
107
138
|
|
|
108
139
|
private
|
|
109
140
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
def raise_bad_request(message, response)
|
|
142
|
+
raise UnauthorizedError.new(message, response:) if authentication_failed?(message)
|
|
143
|
+
raise ContextLengthExceededError.new(message, response:) if context_length_exceeded?(message)
|
|
144
|
+
raise RateLimitError.new(message, response:) if rate_limited?(message)
|
|
145
|
+
raise OverloadedError.new(message, response:) if overloaded?(message)
|
|
146
|
+
raise PaymentRequiredError.new(message, response:) if payment_required?(message)
|
|
147
|
+
|
|
148
|
+
raise BadRequestError.new(message, response:)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def raise_forbidden(message, response)
|
|
152
|
+
raise UnauthorizedError.new(message, response:) if authentication_failed?(message)
|
|
153
|
+
|
|
154
|
+
raise ForbiddenError.new(message, response:)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def raise_too_large(message, response)
|
|
158
|
+
raise ContextLengthExceededError.new(message, response:) if context_length_exceeded?(message)
|
|
159
|
+
|
|
160
|
+
raise Error.new(message, response:)
|
|
161
|
+
end
|
|
162
|
+
|
|
113
163
|
def context_length_exceeded?(message)
|
|
114
|
-
|
|
115
|
-
|
|
164
|
+
matches?(message, CONTEXT_LENGTH_PATTERNS)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def overloaded?(message)
|
|
168
|
+
matches?(message, OVERLOAD_PATTERNS)
|
|
169
|
+
end
|
|
116
170
|
|
|
117
|
-
|
|
171
|
+
def payment_required?(message)
|
|
172
|
+
matches?(message, PAYMENT_REQUIRED_PATTERNS)
|
|
118
173
|
end
|
|
119
174
|
|
|
120
175
|
def rate_limited?(message)
|
|
176
|
+
matches?(message, RATE_LIMIT_PATTERNS)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def authentication_failed?(message)
|
|
180
|
+
matches?(message, AUTHENTICATION_PATTERNS)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Providers hand back whatever their error body holds, which is not
|
|
184
|
+
# always a String: bedrock-mantle nests code, message, and type in a
|
|
185
|
+
# Hash. Match on the rendered text so any shape classifies.
|
|
186
|
+
def matches?(message, patterns)
|
|
121
187
|
text = message.to_s
|
|
122
188
|
return false if text.empty?
|
|
123
189
|
|
|
124
|
-
|
|
190
|
+
patterns.any? { |pattern| text.match?(pattern) }
|
|
125
191
|
end
|
|
126
192
|
end
|
|
127
193
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Transport
|
|
5
|
+
# Interprets a text/event-stream as the HTML standard describes, fed in
|
|
6
|
+
# whatever pieces the network delivers. Each dispatched event is yielded
|
|
7
|
+
# as its type, data, and last event ID. The last event ID and the
|
|
8
|
+
# reconnection time the server asked for stay readable between events.
|
|
9
|
+
class EventStreamParser # :nodoc:
|
|
10
|
+
BOM = "\xEF\xBB\xBF".b.freeze
|
|
11
|
+
LINE_BREAK = /[\r\n]/
|
|
12
|
+
DIGITS = /\A[0-9]+\z/
|
|
13
|
+
CR = 13
|
|
14
|
+
LF = 10
|
|
15
|
+
SPACE = 32
|
|
16
|
+
|
|
17
|
+
attr_reader :last_event_id, :reconnection_time
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@buffer = String.new(encoding: Encoding::BINARY)
|
|
21
|
+
@position = 0
|
|
22
|
+
@searched = 0
|
|
23
|
+
@data = +''
|
|
24
|
+
@event_type = +''
|
|
25
|
+
@id_buffer = +''
|
|
26
|
+
@last_event_id = +''
|
|
27
|
+
@reconnection_time = nil
|
|
28
|
+
@started = false
|
|
29
|
+
@after_cr = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def feed(chunk, &)
|
|
33
|
+
compact
|
|
34
|
+
@buffer << (chunk.encoding == Encoding::BINARY ? chunk : chunk.b)
|
|
35
|
+
return unless start
|
|
36
|
+
|
|
37
|
+
skip_line_feed_after_carriage_return
|
|
38
|
+
while (index = @buffer.index(LINE_BREAK, @searched))
|
|
39
|
+
line = @buffer.byteslice(@position, index - @position)
|
|
40
|
+
@position = @searched = line_end(index)
|
|
41
|
+
process_line(line, &)
|
|
42
|
+
end
|
|
43
|
+
@searched = @buffer.bytesize
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def compact
|
|
49
|
+
return if @position.zero?
|
|
50
|
+
|
|
51
|
+
@buffer = @buffer.byteslice(@position, @buffer.bytesize - @position)
|
|
52
|
+
@searched -= @position
|
|
53
|
+
@position = 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The stream decodes as UTF-8, which drops one byte order mark at its
|
|
57
|
+
# very start, so the first bytes wait until they can be told apart.
|
|
58
|
+
def start
|
|
59
|
+
return true if @started
|
|
60
|
+
return false if @buffer.bytesize < BOM.bytesize && BOM.start_with?(@buffer)
|
|
61
|
+
|
|
62
|
+
@position = @searched = BOM.bytesize if @buffer.start_with?(BOM)
|
|
63
|
+
@started = true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# A carriage return that ended the previous piece may be the first
|
|
67
|
+
# half of a CRLF pair.
|
|
68
|
+
def skip_line_feed_after_carriage_return
|
|
69
|
+
return unless @after_cr && @buffer.bytesize > @position
|
|
70
|
+
|
|
71
|
+
@after_cr = false
|
|
72
|
+
@position = @searched = @position + 1 if @buffer.getbyte(@position) == LF
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def line_end(index)
|
|
76
|
+
return index + 1 unless @buffer.getbyte(index) == CR
|
|
77
|
+
return index + 2 if @buffer.getbyte(index + 1) == LF
|
|
78
|
+
|
|
79
|
+
@after_cr = index + 1 == @buffer.bytesize
|
|
80
|
+
index + 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def process_line(line, &)
|
|
84
|
+
return dispatch(&) if line.empty?
|
|
85
|
+
return if line.start_with?(':')
|
|
86
|
+
|
|
87
|
+
colon = line.index(':')
|
|
88
|
+
return process_field(line, +'') unless colon
|
|
89
|
+
|
|
90
|
+
offset = line.getbyte(colon + 1) == SPACE ? colon + 2 : colon + 1
|
|
91
|
+
process_field(line.byteslice(0, colon), line.byteslice(offset, line.bytesize - offset))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def process_field(field, value)
|
|
95
|
+
case field
|
|
96
|
+
when 'event' then @event_type = decode(value)
|
|
97
|
+
when 'data' then @data << decode(value) << "\n"
|
|
98
|
+
when 'id' then @id_buffer = decode(value) unless value.include?("\0")
|
|
99
|
+
when 'retry' then @reconnection_time = value.to_i if value.match?(DIGITS)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def dispatch
|
|
104
|
+
@last_event_id = @id_buffer
|
|
105
|
+
data = @data
|
|
106
|
+
type = @event_type
|
|
107
|
+
@data = +''
|
|
108
|
+
@event_type = +''
|
|
109
|
+
return if data.empty?
|
|
110
|
+
|
|
111
|
+
data.delete_suffix!("\n")
|
|
112
|
+
yield type.empty? ? 'message' : type, data, @last_event_id
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def decode(bytes)
|
|
116
|
+
text = bytes.force_encoding(Encoding::UTF_8)
|
|
117
|
+
text.valid_encoding? ? text : text.scrub
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module Transport
|
|
8
|
+
class JsonResponse < Faraday::Middleware # :nodoc:
|
|
9
|
+
def initialize(app, parser_options: {}, preserve_raw: false)
|
|
10
|
+
super(app)
|
|
11
|
+
@parser_options = parser_options || {}
|
|
12
|
+
@preserve_raw = preserve_raw
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def on_complete(env)
|
|
16
|
+
body = env[:body]
|
|
17
|
+
return unless body.respond_to?(:to_str) && json_response?(env)
|
|
18
|
+
|
|
19
|
+
env[:raw_body] = body if @preserve_raw
|
|
20
|
+
env[:body] = body.strip.empty? ? nil : JSON.parse(body, **@parser_options)
|
|
21
|
+
rescue StandardError, SyntaxError => e
|
|
22
|
+
raise Faraday::ParsingError.new(e, env[:response])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def json_response?(env)
|
|
28
|
+
content_type = env[:response_headers]['content-type'].to_s.split(';', 2).first
|
|
29
|
+
content_type&.match?(/\bjson$/)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module Transport # :nodoc:
|
|
7
|
+
# Sits inside Faraday retry middleware so every transport attempt is
|
|
8
|
+
# signed when it is sent, over the body it sends. A signature computed
|
|
9
|
+
# once before the first attempt can expire before a retry goes out.
|
|
10
|
+
class SigningMiddleware < Faraday::Middleware # :nodoc: all
|
|
11
|
+
CONTEXT_KEY = :ruby_llm_signer
|
|
12
|
+
|
|
13
|
+
def call(env)
|
|
14
|
+
signer = env.request.context&.[](CONTEXT_KEY)
|
|
15
|
+
sign(env.request_headers, signer.call(env)) if signer
|
|
16
|
+
@app.call(env)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# A nil value removes a header an earlier attempt was signed with.
|
|
22
|
+
def sign(headers, signed)
|
|
23
|
+
signed.each { |name, value| value.nil? ? headers.delete(name) : headers[name] = value }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Faraday::Middleware.register_middleware(llm_signing: RubyLLM::Transport::SigningMiddleware)
|
data/lib/ruby_llm/version.rb
CHANGED
data/lib/ruby_llm/video.rb
CHANGED
|
@@ -49,7 +49,8 @@ module RubyLLM
|
|
|
49
49
|
# vocabulary, such as durations and resolutions, and merges them into
|
|
50
50
|
# the request as-is. +context:+ supplies a Context whose configuration
|
|
51
51
|
# replaces the global one. +metadata:+ is included in the
|
|
52
|
-
# instrumentation payload.
|
|
52
|
+
# instrumentation payload. +owner:+ attributes the usage to a record,
|
|
53
|
+
# such as a user, and wins over RubyLLM.with_usage_owner.
|
|
53
54
|
#
|
|
54
55
|
# video = RubyLLM.animate("a hummingbird in slow motion", model: "veo-3.1-fast-generate-preview")
|
|
55
56
|
#
|
|
@@ -68,13 +69,14 @@ module RubyLLM
|
|
|
68
69
|
with: nil,
|
|
69
70
|
extend: nil,
|
|
70
71
|
provider_options: {},
|
|
71
|
-
metadata: nil
|
|
72
|
+
metadata: nil,
|
|
73
|
+
owner: nil)
|
|
72
74
|
config = context&.config || RubyLLM.config
|
|
73
75
|
payload = { model:, prompt:, provider_options:, metadata: }
|
|
74
76
|
|
|
75
77
|
RubyLLM.instrument('video.ruby_llm', payload, config: config) do |event|
|
|
76
78
|
job = VideoJob.animate_later(prompt, model:, provider:, assume_model_exists:,
|
|
77
|
-
context:, with:, extend:, provider_options:, metadata:)
|
|
79
|
+
context:, with:, extend:, provider_options:, metadata:, owner:)
|
|
78
80
|
event[:model] = job.model
|
|
79
81
|
event[:job_id] = job.id
|
|
80
82
|
job.wait
|
data/lib/ruby_llm/video_job.rb
CHANGED
|
@@ -27,9 +27,20 @@ module RubyLLM
|
|
|
27
27
|
# The provider's raw response from the last submit or poll.
|
|
28
28
|
attr_reader :raw
|
|
29
29
|
|
|
30
|
+
# The clip length in seconds the request asked for, or +nil+ when it
|
|
31
|
+
# left the length to the provider's default.
|
|
32
|
+
attr_reader :duration
|
|
33
|
+
|
|
34
|
+
# The resolution the request asked for, as the provider spells it,
|
|
35
|
+
# such as <tt>"720p"</tt> or <tt>"1280x720"</tt>, or +nil+ when it left
|
|
36
|
+
# the resolution to the provider's default.
|
|
37
|
+
attr_reader :resolution
|
|
38
|
+
|
|
30
39
|
# Submits a video generation job and returns a VideoJob without
|
|
31
40
|
# waiting for the result. Most code calls this through
|
|
32
41
|
# RubyLLM.animate_later. Takes the same arguments as Video.animate.
|
|
42
|
+
# The job records its usage, attributed to +owner:+ or the owner of
|
|
43
|
+
# RubyLLM.with_usage_owner, once it finishes.
|
|
33
44
|
#
|
|
34
45
|
# job = RubyLLM.animate_later("a hummingbird in slow motion")
|
|
35
46
|
# job.id # => "0eb6910f-a353-4699-9d1e-6a4f7a5b39e2"
|
|
@@ -43,7 +54,8 @@ module RubyLLM
|
|
|
43
54
|
with: nil,
|
|
44
55
|
extend: nil,
|
|
45
56
|
provider_options: {},
|
|
46
|
-
metadata: nil
|
|
57
|
+
metadata: nil,
|
|
58
|
+
owner: nil)
|
|
47
59
|
config = context&.config || RubyLLM.config
|
|
48
60
|
model ||= config.default_video_model
|
|
49
61
|
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
@@ -59,19 +71,27 @@ module RubyLLM
|
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
RubyLLM.instrument('video_job.ruby_llm', payload, config: config) do |event|
|
|
62
|
-
job =
|
|
74
|
+
job = Accounting::Usage.owned_by(owner) do
|
|
75
|
+
provider_instance.animate_later(prompt, model:, with:, extend:, provider_options:)
|
|
76
|
+
end
|
|
63
77
|
event[:job_id] = job.id
|
|
64
78
|
job
|
|
65
79
|
end
|
|
66
80
|
end
|
|
67
81
|
|
|
68
|
-
def initialize(id:, protocol:, model: nil, status: :pending, raw: nil, error: nil
|
|
82
|
+
def initialize(id:, protocol:, model: nil, status: :pending, raw: nil, error: nil, # :nodoc:
|
|
83
|
+
reported_cost: nil, duration: nil, resolution: nil)
|
|
69
84
|
@id = id
|
|
70
85
|
@protocol = protocol
|
|
71
86
|
@model = model
|
|
72
87
|
@status = status
|
|
73
88
|
@raw = raw
|
|
74
89
|
@error = error
|
|
90
|
+
@reported_cost = reported_cost
|
|
91
|
+
@duration = duration
|
|
92
|
+
@resolution = resolution
|
|
93
|
+
@usage_owner = Accounting::Usage.owner
|
|
94
|
+
record_usage
|
|
75
95
|
end
|
|
76
96
|
|
|
77
97
|
# Returns +true+ while the provider is still rendering the video.
|
|
@@ -104,9 +124,22 @@ module RubyLLM
|
|
|
104
124
|
@status = state.fetch(:status)
|
|
105
125
|
@raw = state[:raw]
|
|
106
126
|
@error = state[:error]
|
|
127
|
+
@reported_cost = state[:reported_cost]
|
|
128
|
+
record_usage
|
|
107
129
|
self
|
|
108
130
|
end
|
|
109
131
|
|
|
132
|
+
# Returns the Cost the provider reported for the job. Its total is
|
|
133
|
+
# +nil+ while the job is pending or when the provider reports no
|
|
134
|
+
# price.
|
|
135
|
+
#
|
|
136
|
+
# job.wait
|
|
137
|
+
# job.cost.total # => 0.05
|
|
138
|
+
#
|
|
139
|
+
def cost
|
|
140
|
+
Cost.new(tokens: Tokens.new(reported_cost: @reported_cost))
|
|
141
|
+
end
|
|
142
|
+
|
|
110
143
|
# Polls the job until it finishes, then returns self. Raises Error
|
|
111
144
|
# when the job fails or +timeout+ elapses first. +timeout+ and
|
|
112
145
|
# +interval+ are seconds and default to the configured
|
|
@@ -117,9 +150,10 @@ module RubyLLM
|
|
|
117
150
|
deadline = monotonic_time + timeout
|
|
118
151
|
|
|
119
152
|
until done?
|
|
120
|
-
|
|
153
|
+
remaining = deadline - monotonic_time
|
|
154
|
+
raise Error, "Video generation timed out after #{timeout} seconds" unless remaining.positive?
|
|
121
155
|
|
|
122
|
-
sleep interval
|
|
156
|
+
sleep [interval, remaining].min
|
|
123
157
|
refresh
|
|
124
158
|
end
|
|
125
159
|
raise Error, "Video generation failed: #{error}" if failed?
|
|
@@ -139,6 +173,15 @@ module RubyLLM
|
|
|
139
173
|
|
|
140
174
|
private
|
|
141
175
|
|
|
176
|
+
def record_usage
|
|
177
|
+
return if pending? || @usage_recorded
|
|
178
|
+
|
|
179
|
+
@usage_recorded = true
|
|
180
|
+
entry = Accounting::Usage::Entry.new(operation: :video, provider: @protocol.provider.slug, model:,
|
|
181
|
+
status: completed? ? :succeeded : :failed, cost:, owner: @usage_owner)
|
|
182
|
+
Accounting::Usage.report(entry, config: @protocol.config)
|
|
183
|
+
end
|
|
184
|
+
|
|
142
185
|
def monotonic_time
|
|
143
186
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
144
187
|
end
|