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
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLMUpgrade # :nodoc: all
|
|
4
|
-
TABLE = 'ruby_llm_v2_upgrades'
|
|
5
|
-
VERSION_COLUMN = 'ruby_llm_version'
|
|
6
|
-
OPERATIONS = %i[ask say ask_later add_message generate compact step complete run_tools cancel resume
|
|
7
|
-
approve deny with_model with_instructions cache_until_here reset_messages!].freeze
|
|
8
|
-
READ_OPERATIONS = %i[reload to_llm count_tokens].freeze
|
|
9
|
-
|
|
10
|
-
module Records
|
|
11
|
-
extend ActiveSupport::Concern
|
|
12
|
-
|
|
13
|
-
included do
|
|
14
|
-
class_attribute :ruby_llm_upgrade_kind, instance_writer: false
|
|
15
|
-
self.enumerate_columns_in_select_statements = true
|
|
16
|
-
default_scope { RubyLLMUpgrade.visible(self) }
|
|
17
|
-
after_initialize { @ruby_llm_upgrade_epoch = RubyLLMUpgrade.epoch }
|
|
18
|
-
around_save :guard_ruby_llm_upgrade
|
|
19
|
-
around_destroy :guard_ruby_llm_upgrade, prepend: true
|
|
20
|
-
before_save { RubyLLMUpgrade.prepare_model(self) if ruby_llm_upgrade_kind == :chat }
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
def guard_ruby_llm_upgrade(&)
|
|
26
|
-
RubyLLMUpgrade.guard(self, &)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
class << self
|
|
31
|
-
def install(chat:, message:, tool_call:)
|
|
32
|
-
if version == 2 && message.table_exists? && message.column_names.include?('ruby_llm_content')
|
|
33
|
-
message.alias_attribute :content, :ruby_llm_content
|
|
34
|
-
end
|
|
35
|
-
{ chat => :chat, message => :message, tool_call => :tool_call }.each do |model, kind|
|
|
36
|
-
next if model < Records
|
|
37
|
-
|
|
38
|
-
model.include Records
|
|
39
|
-
model.ruby_llm_upgrade_kind = kind
|
|
40
|
-
model.prepend guarded_operations(model, READ_OPERATIONS, claim: false)
|
|
41
|
-
end
|
|
42
|
-
return if chat.instance_variable_defined?(:@ruby_llm_upgrade_operations)
|
|
43
|
-
|
|
44
|
-
operations = guarded_operations(chat, OPERATIONS, claim: true)
|
|
45
|
-
%i[persist_usage_entry persist_new_message persist_message_completion].each do |name|
|
|
46
|
-
next unless chat.private_method_defined?(name)
|
|
47
|
-
|
|
48
|
-
operations.define_method(name) do |*args, **options, &block|
|
|
49
|
-
RubyLLMUpgrade.guard(self) { super(*args, **options, &block) }
|
|
50
|
-
end
|
|
51
|
-
operations.send(:private, name)
|
|
52
|
-
end
|
|
53
|
-
chat.prepend operations
|
|
54
|
-
chat.instance_variable_set(:@ruby_llm_upgrade_operations, operations)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def visible(model)
|
|
58
|
-
return model.where(nil) unless available?
|
|
59
|
-
|
|
60
|
-
state = current_state
|
|
61
|
-
check_version(state)
|
|
62
|
-
return model.where(nil) if version == 2
|
|
63
|
-
|
|
64
|
-
settings = state.settings
|
|
65
|
-
return model.where(nil) unless ownership_column?(settings)
|
|
66
|
-
|
|
67
|
-
chats = records(settings.fetch('chat_table')).where(VERSION_COLUMN => 1)
|
|
68
|
-
case model.ruby_llm_upgrade_kind
|
|
69
|
-
when :chat
|
|
70
|
-
model.where(VERSION_COLUMN => 1)
|
|
71
|
-
when :message
|
|
72
|
-
model.where(settings.fetch('chat_foreign_key') => chats.select(chats.primary_key))
|
|
73
|
-
when :tool_call
|
|
74
|
-
messages = records(settings.fetch('message_table'))
|
|
75
|
-
.where(settings.fetch('chat_foreign_key') => chats.select(chats.primary_key))
|
|
76
|
-
model.where(settings.fetch('message_foreign_key') => messages.select(messages.primary_key))
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def prepare_model(chat)
|
|
81
|
-
return unless available?
|
|
82
|
-
|
|
83
|
-
state = current_state
|
|
84
|
-
settings = state.settings
|
|
85
|
-
return if version == 1 && settings['online'] && state.status == 'preparing'
|
|
86
|
-
if version == 1
|
|
87
|
-
return unless ActiveRecord::Base.connection.column_exists?(settings.fetch('chat_table'), :ruby_llm_model_id)
|
|
88
|
-
return unless ActiveRecord::Base.connection.table_exists?('ruby_llm_models')
|
|
89
|
-
end
|
|
90
|
-
ensure_model_reference(chat, settings)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def guard(record, claim: true)
|
|
94
|
-
return yield unless available?
|
|
95
|
-
|
|
96
|
-
states = records(TABLE)
|
|
97
|
-
states.uncached do
|
|
98
|
-
states.transaction do
|
|
99
|
-
if ActiveRecord::Base.connection.adapter_name == 'SQLite' && claim
|
|
100
|
-
states.update_all('epoch = epoch')
|
|
101
|
-
end
|
|
102
|
-
state = states.lock(lock_clause).first!
|
|
103
|
-
check_version(state)
|
|
104
|
-
protect(record, state, claim: claim)
|
|
105
|
-
if record.instance_variable_get(:@ruby_llm_upgrade_epoch) != state[:epoch].to_i
|
|
106
|
-
raise ActiveRecord::ReadOnlyRecord, 'Reload the conversation after switching RubyLLM versions'
|
|
107
|
-
end
|
|
108
|
-
yield
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def epoch
|
|
114
|
-
available? ? current_state[:epoch].to_i : 0
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
private
|
|
118
|
-
|
|
119
|
-
def lock_clause
|
|
120
|
-
ActiveRecord::Base.connection.adapter_name == 'Mysql2' ? 'LOCK IN SHARE MODE' : 'FOR SHARE'
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def guarded_operations(model, names, claim:)
|
|
124
|
-
Module.new do
|
|
125
|
-
names.each do |name|
|
|
126
|
-
next unless model.method_defined?(name)
|
|
127
|
-
|
|
128
|
-
define_method(name) do |*args, **options, &block|
|
|
129
|
-
RubyLLMUpgrade.guard(self, claim: claim) { nil }
|
|
130
|
-
super(*args, **options, &block)
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def protect(record, state, claim:)
|
|
137
|
-
return if state.status == 'finalized'
|
|
138
|
-
|
|
139
|
-
settings = state.settings
|
|
140
|
-
return if version == 1 && !ownership_column?(settings)
|
|
141
|
-
|
|
142
|
-
owners(record, settings).each { |chat| protect_owner(chat, claim: claim) }
|
|
143
|
-
return unless claim && version == 2 && record.class.ruby_llm_upgrade_kind == :chat
|
|
144
|
-
|
|
145
|
-
record[VERSION_COLUMN] = 2
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def protect_owner(chat, claim:)
|
|
149
|
-
chat.lock!
|
|
150
|
-
if version == 1 && chat[VERSION_COLUMN] != 1
|
|
151
|
-
raise ActiveRecord::ReadOnlyRecord, 'This conversation contains RubyLLM 2.0 changes'
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
chat.update!(VERSION_COLUMN => 2) if claim && version == 2 && chat[VERSION_COLUMN] != 2
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def version
|
|
158
|
-
Gem::Version.new(RubyLLM::VERSION).segments.first >= 2 ? 2 : 1
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def available?
|
|
162
|
-
ActiveRecord::Base.connection.table_exists?(TABLE)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def records(table)
|
|
166
|
-
@records ||= {}
|
|
167
|
-
model = @records[table] ||= Class.new(ActiveRecord::Base) do
|
|
168
|
-
self.table_name = table
|
|
169
|
-
self.inheritance_column = :_type_disabled
|
|
170
|
-
end
|
|
171
|
-
if !model.column_names.include?(VERSION_COLUMN) &&
|
|
172
|
-
ActiveRecord::Base.connection.column_exists?(table, VERSION_COLUMN)
|
|
173
|
-
model.reset_column_information
|
|
174
|
-
end
|
|
175
|
-
model
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def current_state
|
|
179
|
-
records(TABLE).uncached { records(TABLE).first! }
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def ownership_column?(settings)
|
|
183
|
-
ActiveRecord::Base.connection.column_exists?(settings.fetch('chat_table'), VERSION_COLUMN)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def check_version(state)
|
|
187
|
-
return if version == 1 && state.active_version == 1 && state.settings['online'] && state.status == 'preparing'
|
|
188
|
-
return if state.active_version == version && %w[active finalized].include?(state.status)
|
|
189
|
-
|
|
190
|
-
raise ActiveRecord::ReadOnlyRecord,
|
|
191
|
-
"RubyLLM #{version} cannot access conversations while the copy upgrade is " \
|
|
192
|
-
"#{state.status} on #{state.active_version}"
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def ensure_model_reference(chat, settings)
|
|
196
|
-
if version == 1
|
|
197
|
-
legacy_id = chat[settings.fetch('model_foreign_key')]
|
|
198
|
-
legacy_models = records(settings.fetch('model_table'))
|
|
199
|
-
legacy = legacy_models.find_by(legacy_models.primary_key => legacy_id)
|
|
200
|
-
unless legacy
|
|
201
|
-
chat.errors.add(:model, 'must identify an existing legacy model')
|
|
202
|
-
raise ActiveRecord::RecordInvalid, chat
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
target = records('ruby_llm_models')
|
|
206
|
-
model = target.find_by(provider: legacy.provider, model_id: legacy.model_id) ||
|
|
207
|
-
target.create!(legacy.attributes.slice(*target.column_names).except(target.primary_key))
|
|
208
|
-
chat[:ruby_llm_model_id] = model.id
|
|
209
|
-
end
|
|
210
|
-
return if chat[:ruby_llm_model_id]
|
|
211
|
-
|
|
212
|
-
chat.errors.add(:model, 'must exist before saving the conversation')
|
|
213
|
-
raise ActiveRecord::RecordInvalid, chat
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def owners(record, settings)
|
|
217
|
-
chats = records(settings.fetch('chat_table'))
|
|
218
|
-
return chat_owner(record, chats) if record.class.ruby_llm_upgrade_kind == :chat
|
|
219
|
-
|
|
220
|
-
values = owner_attributes(record, settings)
|
|
221
|
-
ids = values.filter_map { |value| value[settings.fetch('chat_foreign_key')] }
|
|
222
|
-
raise ActiveRecord::ReadOnlyRecord, 'The conversation no longer exists' if ids.empty?
|
|
223
|
-
|
|
224
|
-
found = chats.where(chats.primary_key => ids).to_a
|
|
225
|
-
raise ActiveRecord::ReadOnlyRecord, 'The conversation no longer exists' unless found.length == ids.uniq.length
|
|
226
|
-
|
|
227
|
-
found
|
|
228
|
-
end
|
|
229
|
-
|
|
230
|
-
def chat_owner(record, chats)
|
|
231
|
-
found = chats.where(chats.primary_key => record.id).to_a
|
|
232
|
-
return found unless record.persisted? && found.empty?
|
|
233
|
-
|
|
234
|
-
raise ActiveRecord::ReadOnlyRecord, 'The conversation no longer exists'
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def owner_attributes(record, settings)
|
|
238
|
-
values = [record.attributes]
|
|
239
|
-
if record.persisted?
|
|
240
|
-
stored = records(record.class.table_name).find(record.id)
|
|
241
|
-
values << stored.attributes
|
|
242
|
-
end
|
|
243
|
-
if record.class.ruby_llm_upgrade_kind == :tool_call
|
|
244
|
-
message_key = version == 1 ? settings.fetch('message_foreign_key') : 'message_id'
|
|
245
|
-
messages = records(settings.fetch('message_table'))
|
|
246
|
-
values = messages.where(messages.primary_key => values.map { |value| value[message_key] }).map(&:attributes)
|
|
247
|
-
end
|
|
248
|
-
values
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
end
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require Rails.root.join('app/models/concerns/ruby_llm_upgrade')
|
|
4
|
-
|
|
5
|
-
Rails.application.config.to_prepare do
|
|
6
|
-
require 'active_record/base'
|
|
7
|
-
tool_call = if Gem::Version.new(RubyLLM::VERSION).segments.first >= 2
|
|
8
|
-
RubyLLM::ActiveRecord::ToolCall
|
|
9
|
-
else
|
|
10
|
-
<%= v1_model_name(:tool_call) %>
|
|
11
|
-
end
|
|
12
|
-
RubyLLMUpgrade.install(chat: <%= chat_model_name %>, message: <%= message_model_name %>, tool_call: tool_call)
|
|
13
|
-
end
|
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module RubyLLM
|
|
4
|
-
module Generators
|
|
5
|
-
class UpgradeMigration # :nodoc: all
|
|
6
|
-
TABLE = :ruby_llm_v2_upgrades
|
|
7
|
-
VERSION_COLUMN = :ruby_llm_version
|
|
8
|
-
|
|
9
|
-
def self.for(connection: ::ActiveRecord::Base.connection)
|
|
10
|
-
require_relative 'online_copy_migration'
|
|
11
|
-
OnlineCopyMigration.new(connection:)
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def initialize(connection: ::ActiveRecord::Base.connection)
|
|
15
|
-
@connection = connection
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def prepare(settings)
|
|
19
|
-
create_state_table unless @connection.table_exists?(TABLE)
|
|
20
|
-
prepare_state(settings)
|
|
21
|
-
yield if block_given?
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def prepare_state(settings)
|
|
25
|
-
existing = states.first
|
|
26
|
-
if existing
|
|
27
|
-
raise 'This database has a different RubyLLM copy upgrade' unless existing.settings == settings.stringify_keys
|
|
28
|
-
unless existing.status == 'preparing' || (existing.active_version == 1 && existing.needs_reconcile)
|
|
29
|
-
raise 'The copy upgrade is already active'
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
existing.update!(status: 'preparing')
|
|
33
|
-
else
|
|
34
|
-
states.create!(settings: settings.stringify_keys)
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
private :prepare_state
|
|
38
|
-
|
|
39
|
-
def online? = false
|
|
40
|
-
|
|
41
|
-
def copy_table(source, target)
|
|
42
|
-
return if @connection.table_exists?(target)
|
|
43
|
-
|
|
44
|
-
source_records = records(source)
|
|
45
|
-
create_copy_table(source, target)
|
|
46
|
-
columns = source_records.column_names & records(target).column_names
|
|
47
|
-
names = columns.map { |column| @connection.quote_column_name(column) }.join(', ')
|
|
48
|
-
key = @connection.quote_column_name(source_records.primary_key)
|
|
49
|
-
@connection.execute(<<~SQL)
|
|
50
|
-
INSERT INTO #{@connection.quote_table_name(target)} (#{names})
|
|
51
|
-
SELECT #{names} FROM #{@connection.quote_table_name(source)}
|
|
52
|
-
WHERE #{key} NOT IN (SELECT #{key} FROM #{@connection.quote_table_name(target)})
|
|
53
|
-
SQL
|
|
54
|
-
@connection.reset_pk_sequence!(target) if @connection.respond_to?(:reset_pk_sequence!)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def copy_model_references
|
|
58
|
-
records(configuration.fetch('chat_table')).find_each do |chat|
|
|
59
|
-
model = resolve_model(chat[configuration.fetch('model_foreign_key')])
|
|
60
|
-
chat.update!(ruby_llm_model_id: model&.id)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def backfill(force: true)
|
|
65
|
-
state = states.first!
|
|
66
|
-
state.with_lock do
|
|
67
|
-
unless %w[preparing reconciling].include?(state.status) ||
|
|
68
|
-
(state.active_version == 1 && state.needs_reconcile)
|
|
69
|
-
raise 'Expected an unfinished copy upgrade'
|
|
70
|
-
end
|
|
71
|
-
return if !force && !state.needs_reconcile
|
|
72
|
-
|
|
73
|
-
state.update!(status: 'preparing', needs_reconcile: true)
|
|
74
|
-
end
|
|
75
|
-
reconcile
|
|
76
|
-
state.update!(needs_reconcile: false)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def activate
|
|
80
|
-
state = states.first!
|
|
81
|
-
return if state.active_version == 2 && state.status == 'active'
|
|
82
|
-
|
|
83
|
-
unless %w[preparing reconciling].include?(state.status) || (state.active_version == 1 && state.needs_reconcile)
|
|
84
|
-
raise 'Expected an unfinished copy upgrade'
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
raise 'Run the copy backfill before activating 2.0' if state.needs_reconcile
|
|
88
|
-
|
|
89
|
-
state.update!(active_version: 2, status: 'active', needs_reconcile: false)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def verify_settings(settings)
|
|
93
|
-
return if configuration == settings.stringify_keys
|
|
94
|
-
|
|
95
|
-
raise 'Use the same model mappings for every copy-upgrade phase'
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def rollback
|
|
99
|
-
state = states.first!
|
|
100
|
-
if state.active_version == 1 && %w[preparing reconciling].include?(state.status)
|
|
101
|
-
state.with_lock do
|
|
102
|
-
state.update!(status: 'active', needs_reconcile: state.needs_reconcile || state.status == 'preparing')
|
|
103
|
-
end
|
|
104
|
-
return
|
|
105
|
-
end
|
|
106
|
-
transition(2, 1) { verify_completed_work }
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def resume
|
|
110
|
-
state = states.first!
|
|
111
|
-
state.with_lock do
|
|
112
|
-
unless state.active_version == 1 && %w[active reconciling].include?(state.status)
|
|
113
|
-
raise 'Resume requires a copy upgrade running on 1.16'
|
|
114
|
-
end
|
|
115
|
-
raise 'Finish the initial copy migrations before resuming 2.0' if state.needs_reconcile
|
|
116
|
-
|
|
117
|
-
state.update!(status: 'reconciling')
|
|
118
|
-
end
|
|
119
|
-
reconcile
|
|
120
|
-
state.update!(active_version: 2, status: 'active')
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def finalize
|
|
124
|
-
state = states.first!
|
|
125
|
-
state.with_lock do
|
|
126
|
-
require_active_version(state, 2)
|
|
127
|
-
verify_completed_work
|
|
128
|
-
state.update!(status: 'finalized')
|
|
129
|
-
end
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def verify_cleanup
|
|
133
|
-
state = states.first!
|
|
134
|
-
return if state.active_version == 2 && state.status == 'finalized'
|
|
135
|
-
|
|
136
|
-
raise 'Run ruby_llm:upgrade:finalize on 2.0 before copy-mode cleanup'
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def cleanup
|
|
140
|
-
verify_cleanup
|
|
141
|
-
settings = configuration
|
|
142
|
-
legacy_tables = settings.values_at('model_table', 'tool_call_table')
|
|
143
|
-
remove_legacy_foreign_keys(legacy_tables)
|
|
144
|
-
chats = settings.fetch('chat_table')
|
|
145
|
-
column = settings.fetch('model_foreign_key')
|
|
146
|
-
@connection.remove_column(chats, column) if @connection.column_exists?(chats, column)
|
|
147
|
-
@connection.remove_column(chats, VERSION_COLUMN) if @connection.column_exists?(chats, VERSION_COLUMN)
|
|
148
|
-
legacy_tables.reverse_each { |table| @connection.drop_table(table) if @connection.table_exists?(table) }
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
def create_state_table
|
|
154
|
-
if %i[ruby_llm_models ruby_llm_tool_calls].any? { |table| @connection.table_exists?(table) }
|
|
155
|
-
raise 'Copy mode requires new RubyLLM supporting tables; do not reuse an earlier upgrade'
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
@connection.create_table(TABLE) do |table|
|
|
159
|
-
table.integer :active_version, null: false, default: 1
|
|
160
|
-
table.string :status, null: false, default: 'preparing'
|
|
161
|
-
table.boolean :needs_reconcile, null: false, default: false
|
|
162
|
-
table.bigint :epoch, null: false, default: 0
|
|
163
|
-
table.json :settings, null: false
|
|
164
|
-
end
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def create_copy_table(source, target)
|
|
168
|
-
primary = @connection.columns(source).find { |column| column.name == records(source).primary_key }
|
|
169
|
-
@connection.create_table(target, primary_key: primary.name, id: copy_primary_options(primary)) do |table|
|
|
170
|
-
@connection.columns(source).each do |column|
|
|
171
|
-
next if column.name == primary.name
|
|
172
|
-
|
|
173
|
-
table.column(column.name, column.type, **copy_column_options(source, column))
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
def copy_primary_options(primary)
|
|
179
|
-
type = primary.type == :integer && primary.limit == 8 ? :bigint : primary.type
|
|
180
|
-
unless %i[integer bigint uuid].include?(type)
|
|
181
|
-
raise 'Copy mode requires integer or UUID model/tool-call primary keys'
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
options = { type: type }
|
|
185
|
-
options[:default] = -> { primary.default_function } if type == :uuid && primary.default_function
|
|
186
|
-
options
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def copy_column_options(source, column)
|
|
190
|
-
options = { null: column.null, limit: column.limit, precision: column.precision, scale: column.scale }.compact
|
|
191
|
-
options[:default] = if column.default_function
|
|
192
|
-
-> { column.default_function }
|
|
193
|
-
else
|
|
194
|
-
records(source).type_for_attribute(column.name).deserialize(column.default)
|
|
195
|
-
end
|
|
196
|
-
options
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
def remove_legacy_foreign_keys(legacy_tables)
|
|
200
|
-
(@connection.tables - legacy_tables).each do |table|
|
|
201
|
-
@connection.foreign_keys(table).each do |key|
|
|
202
|
-
next unless legacy_tables.include?(key.to_table)
|
|
203
|
-
|
|
204
|
-
allowed = table == configuration['chat_table'] && key.column == configuration['model_foreign_key']
|
|
205
|
-
raise "Move the application foreign key #{table}.#{key.column} before cleanup" unless allowed
|
|
206
|
-
|
|
207
|
-
@connection.remove_foreign_key(table, column: key.column)
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def states
|
|
213
|
-
records(TABLE)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def configuration
|
|
217
|
-
@configuration ||= states.first!.settings
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def records(table)
|
|
221
|
-
@records ||= {}
|
|
222
|
-
@records[table.to_s] ||= Class.new(::ActiveRecord::Base) do
|
|
223
|
-
self.table_name = table.to_s
|
|
224
|
-
self.inheritance_column = :_type_disabled
|
|
225
|
-
end.tap(&:reset_column_information)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def transition(from, to)
|
|
229
|
-
state = states.first!
|
|
230
|
-
state.with_lock do
|
|
231
|
-
require_active_version(state, from)
|
|
232
|
-
yield
|
|
233
|
-
state.update!(active_version: to)
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
def require_active_version(state, version)
|
|
238
|
-
return if state.active_version == version && state.status == 'active'
|
|
239
|
-
|
|
240
|
-
raise "The copy upgrade must be active on version #{version}"
|
|
241
|
-
end
|
|
242
|
-
|
|
243
|
-
def verify_completed_work
|
|
244
|
-
if @connection.table_exists?(:ruby_llm_batches) && records(:ruby_llm_batches).where(completed: false).exists?
|
|
245
|
-
raise 'Finish or cancel pending RubyLLM batches before switching versions'
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
calls = records(:ruby_llm_tool_calls).where(message_type: configuration.fetch('message_class'), result_id: nil)
|
|
249
|
-
return unless calls.exists?
|
|
250
|
-
|
|
251
|
-
raise 'Finish pending tool calls and approvals before switching versions. ' \
|
|
252
|
-
'For abandoned legacy calls, see https://rubyllm.com/upgrading/#incomplete-tool-calls'
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
def reconcile
|
|
256
|
-
settings = configuration
|
|
257
|
-
chats = records(settings.fetch('chat_table'))
|
|
258
|
-
messages = records(settings.fetch('message_table'))
|
|
259
|
-
protected_chats = chats.where(VERSION_COLUMN => 2).select(chats.primary_key)
|
|
260
|
-
protected_messages = messages.where(settings.fetch('chat_foreign_key') => protected_chats)
|
|
261
|
-
.select(messages.primary_key)
|
|
262
|
-
records(:ruby_llm_tool_calls).where(message_type: settings.fetch('message_class'))
|
|
263
|
-
.where.not(message_id: protected_messages).delete_all
|
|
264
|
-
records(:ruby_llm_usages).where(chat_type: settings.fetch('chat_class'))
|
|
265
|
-
.where.not(chat_id: protected_chats).delete_all
|
|
266
|
-
chats.where(VERSION_COLUMN => 1).find_each do |chat|
|
|
267
|
-
chats.transaction { reconcile_chat(chat) }
|
|
268
|
-
end
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def reconcile_chat(chat)
|
|
272
|
-
settings = configuration
|
|
273
|
-
model = resolve_model(chat[settings.fetch('model_foreign_key')])
|
|
274
|
-
raise "Chat #{chat.id} has no identifiable legacy model" unless model
|
|
275
|
-
|
|
276
|
-
chat.update!(ruby_llm_model_id: model.id)
|
|
277
|
-
messages = records(settings.fetch('message_table')).where(settings.fetch('chat_foreign_key') => chat.id)
|
|
278
|
-
messages.find_each do |message|
|
|
279
|
-
source = message.attributes
|
|
280
|
-
raw = source['content_raw']
|
|
281
|
-
message.update!(raw_content: raw, content: raw.present? ? JSON.generate(raw) : source['content'])
|
|
282
|
-
reconcile_usage(chat, message, source, model)
|
|
283
|
-
end
|
|
284
|
-
reconcile_tools(messages)
|
|
285
|
-
end
|
|
286
|
-
|
|
287
|
-
def resolve_model(id)
|
|
288
|
-
return unless id
|
|
289
|
-
|
|
290
|
-
legacy = records(configuration.fetch('model_table')).find(id)
|
|
291
|
-
target = records(:ruby_llm_models)
|
|
292
|
-
target.find_by(provider: legacy.provider, model_id: legacy.model_id) ||
|
|
293
|
-
target.create!(legacy.attributes.slice(*target.column_names).except(target.primary_key))
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def reconcile_tools(messages)
|
|
297
|
-
settings = configuration
|
|
298
|
-
source = records(settings.fetch('tool_call_table'))
|
|
299
|
-
target = records(:ruby_llm_tool_calls)
|
|
300
|
-
ids = messages.select(messages.klass.primary_key)
|
|
301
|
-
source.where(settings.fetch('message_foreign_key') => ids).find_each do |call|
|
|
302
|
-
result = messages.find_by(settings.fetch('tool_call_foreign_key') => call.id)
|
|
303
|
-
raise "Finish legacy tool call #{call.id} before resuming 2.0" unless result
|
|
304
|
-
|
|
305
|
-
attributes = call.attributes.slice(*target.column_names).except(target.primary_key)
|
|
306
|
-
attributes.merge!(
|
|
307
|
-
message_type: settings.fetch('message_class'), message_id: call[settings.fetch('message_foreign_key')],
|
|
308
|
-
result_type: settings.fetch('message_class'), result_id: result.id,
|
|
309
|
-
tool_call_id: verified_tool_id(target, call)
|
|
310
|
-
)
|
|
311
|
-
target.create!(attributes)
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def verified_tool_id(target, call)
|
|
316
|
-
if target.exists?(tool_call_id: call.tool_call_id)
|
|
317
|
-
raise "Tool call #{call.id} repeats provider ID #{call.tool_call_id.inspect}; " \
|
|
318
|
-
'reconcile its history before retrying'
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
call.tool_call_id
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
def reconcile_usage(chat, message, source, chat_model)
|
|
325
|
-
tokens = legacy_tokens(source)
|
|
326
|
-
costs = legacy_costs(source)
|
|
327
|
-
return unless source['role'] == 'assistant' || (tokens.values + costs.values).any?
|
|
328
|
-
|
|
329
|
-
model = resolve_model(source[configuration.fetch('model_foreign_key')]) || chat_model
|
|
330
|
-
raise "Message #{message.id} has no identifiable model" unless model
|
|
331
|
-
|
|
332
|
-
attributes = tokens.merge(costs).merge(
|
|
333
|
-
chat_type: configuration.fetch('chat_class'), chat_id: chat.id,
|
|
334
|
-
message_type: configuration.fetch('message_class'), message_id: message.id,
|
|
335
|
-
operation: 'chat', provider: model.provider, model: model.model_id, status: 'succeeded',
|
|
336
|
-
created_at: source['created_at'], updated_at: source['updated_at']
|
|
337
|
-
)
|
|
338
|
-
records(:ruby_llm_usages).create!(attributes)
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
def legacy_tokens(source)
|
|
342
|
-
{
|
|
343
|
-
input_tokens: source['input_tokens'], output_tokens: source['output_tokens'],
|
|
344
|
-
cache_read_tokens: source['cache_read_tokens'] || source['cached_tokens'],
|
|
345
|
-
cache_write_tokens: source['cache_write_tokens'] || source['cache_creation_tokens'],
|
|
346
|
-
thinking_tokens: source['thinking_tokens']
|
|
347
|
-
}
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
def legacy_costs(source)
|
|
351
|
-
details = source['cost_details'] || {}
|
|
352
|
-
details = JSON.parse(details) if details.is_a?(String)
|
|
353
|
-
costs = %w[input output cache_read cache_write thinking].to_h { |name| ["#{name}_cost", details[name]] }
|
|
354
|
-
costs['total_cost'] = source['total_cost'] || details['total']
|
|
355
|
-
costs
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
end
|
|
359
|
-
end
|