actionagent 1.8.0 → 1.9.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/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +76 -73
- data/app/assets/builds/action_agent_recorder.js +78 -0
- data/app/assets/builds/action_agent_replay.js +226 -0
- data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
- data/app/controllers/action_agent/api/agents_controller.rb +1 -0
- data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
- data/app/controllers/action_agent/api/base_controller.rb +63 -2
- data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
- data/app/controllers/action_agent/api/code_sessions_controller.rb +71 -12
- data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
- data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
- data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +58 -7
- data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
- data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
- data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
- data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
- data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
- data/app/controllers/action_agent/api/members_controller.rb +36 -0
- data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
- data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
- data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
- data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
- data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
- data/app/controllers/action_agent/api/projects_controller.rb +512 -0
- data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
- data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
- data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
- data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
- data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
- data/app/controllers/action_agent/api/sandboxes_controller.rb +148 -8
- data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
- data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
- data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
- data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
- data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
- data/app/controllers/action_agent/dashboard_controller.rb +17 -6
- data/app/controllers/action_agent/session_player_controller.rb +58 -0
- data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +627 -20
- data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
- data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
- data/app/jobs/action_agent/agent_execution_job.rb +9 -39
- data/app/jobs/action_agent/agent_resume_job.rb +84 -0
- data/app/jobs/action_agent/code_session_job.rb +39 -10
- data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
- data/app/jobs/action_agent/exploration_job.rb +80 -0
- data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
- data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
- data/app/jobs/action_agent/project_setup_job.rb +21 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
- data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
- data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
- data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
- data/app/models/action_agent/agent.rb +72 -26
- data/app/models/action_agent/agent_run.rb +134 -23
- data/app/models/action_agent/agent_template.rb +35 -1
- data/app/models/action_agent/api_key.rb +14 -0
- data/app/models/action_agent/catalog_scenario.rb +75 -0
- data/app/models/action_agent/code_session.rb +21 -5
- data/app/models/action_agent/draft_pull_request.rb +131 -0
- data/app/models/action_agent/evaluation.rb +148 -0
- data/app/models/action_agent/evaluation_run.rb +143 -53
- data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
- data/app/models/action_agent/exploration.rb +847 -0
- data/app/models/action_agent/github_installation.rb +159 -0
- data/app/models/action_agent/input_request.rb +291 -0
- data/app/models/action_agent/model_pricing.rb +214 -34
- data/app/models/action_agent/project.rb +789 -0
- data/app/models/action_agent/project_secret.rb +339 -0
- data/app/models/action_agent/provider_key.rb +207 -4
- data/app/models/action_agent/recording_event.rb +145 -0
- data/app/models/action_agent/recording_snapshot.rb +7 -7
- data/app/models/action_agent/sandbox_session.rb +300 -16
- data/app/models/action_agent/scenario_catalog.rb +172 -0
- data/app/models/action_agent/scenario_product.rb +57 -0
- data/app/models/action_agent/scenario_set.rb +138 -0
- data/app/models/action_agent/session_recording.rb +121 -14
- data/app/models/action_agent/telemetry_trace.rb +74 -19
- data/app/models/concerns/action_agent/ownable.rb +84 -13
- data/app/serializers/action_agent/evaluation_serializer.rb +41 -10
- data/app/serializers/action_agent/input_request_serializer.rb +47 -0
- data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
- data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
- data/app/services/action_agent/agent_execution_service.rb +450 -72
- data/app/services/action_agent/agent_scorecard.rb +50 -21
- data/app/services/action_agent/agent_sync.rb +5 -2
- data/app/services/action_agent/agent_tool_roster.rb +19 -4
- data/app/services/action_agent/agent_toolbox.rb +215 -9
- data/app/services/action_agent/browser_live_ticket.rb +78 -0
- data/app/services/action_agent/browser_sidecar.rb +213 -0
- data/app/services/action_agent/browser_sign_in.rb +163 -0
- data/app/services/action_agent/browser_tool_redaction.rb +92 -0
- data/app/services/action_agent/claude_code_auth.rb +70 -4
- data/app/services/action_agent/codex_session_events.rb +37 -0
- data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
- data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
- data/app/services/action_agent/evaluation_fix.rb +101 -0
- data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
- data/app/services/action_agent/evaluation_report_import.rb +84 -5
- data/app/services/action_agent/evaluation_run_cost.rb +609 -0
- data/app/services/action_agent/evaluation_runner_service.rb +29 -10
- data/app/services/action_agent/evaluation_standing.rb +152 -0
- data/app/services/action_agent/explorer_execution_service.rb +517 -0
- data/app/services/action_agent/github_client.rb +405 -64
- data/app/services/action_agent/live_updates.rb +31 -0
- data/app/services/action_agent/local_sandbox_backend.rb +1263 -102
- data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
- data/app/services/action_agent/mcp_client.rb +6 -2
- data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
- data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
- data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
- data/app/services/action_agent/playwright_mcp_client.rb +7 -0
- data/app/services/action_agent/project_boot_fix.rb +309 -0
- data/app/services/action_agent/project_capabilities.rb +200 -0
- data/app/services/action_agent/project_install_pull_request.rb +225 -0
- data/app/services/action_agent/project_preflight.rb +178 -0
- data/app/services/action_agent/project_secret_discovery.rb +142 -0
- data/app/services/action_agent/project_setup.rb +429 -0
- data/app/services/action_agent/provider_credentials.rb +102 -0
- data/app/services/action_agent/recording_event_ingest.rb +181 -0
- data/app/services/action_agent/runtime_tool_roster.rb +52 -0
- data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
- data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
- data/app/services/action_agent/sandbox_browser.rb +299 -0
- data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
- data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
- data/app/services/action_agent/sandbox_mail.rb +103 -0
- data/app/services/action_agent/sandbox_manifest.rb +166 -13
- data/app/services/action_agent/sandbox_orchestrator.rb +290 -23
- data/app/services/action_agent/sandbox_patch.rb +253 -0
- data/app/services/action_agent/scenario_catalog_import.rb +255 -0
- data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
- data/app/services/action_agent/scenario_evaluation_runner.rb +121 -17
- data/app/services/action_agent/secret_scrubber.rb +33 -4
- data/app/services/action_agent/session_index.rb +270 -0
- data/app/services/action_agent/session_recording_service.rb +11 -0
- data/app/services/action_agent/session_timeline.rb +505 -0
- data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
- data/app/views/action_agent/session_player/show.html.erb +19 -0
- data/config/routes.rb +153 -2
- data/lib/action_agent/claude_login_process.rb +107 -0
- data/lib/action_agent/engine.rb +55 -5
- data/lib/action_agent/secret_requests.rb +80 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +513 -22
- data/lib/generators/action_agent/install_generator.rb +45 -5
- data/lib/generators/action_agent/templates/action_agent.rb.erb +161 -0
- data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -0
- data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
- data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
- data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
- data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
- data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
- data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
- data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
- data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
- data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
- data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
- data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
- data/lib/tasks/action_agent.rake +33 -0
- data/lib/tasks/browser.rake +22 -0
- data/lib/tasks/sandbox.rake +1 -1
- data/lib/tasks/scenario_catalogs.rake +71 -0
- metadata +100 -2
|
@@ -1,16 +1,40 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
3
5
|
module ActionAgent
|
|
4
6
|
# Estimates LLM spend from token counts. The activeagent gem's telemetry
|
|
5
7
|
# records tokens only; the platform layers pricing on top for the cost
|
|
6
|
-
# figures shown in Traces and
|
|
8
|
+
# figures shown in Traces, Metrics and Evaluations.
|
|
7
9
|
#
|
|
8
10
|
# Rates come from RubyLLM's model registry (USD per million tokens,
|
|
9
|
-
# maintained upstream per model) when the model is known there
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
11
|
+
# maintained upstream per model) when the model is known there — looked
|
|
12
|
+
# up under the provider it ran on when the caller knows it, since
|
|
13
|
+
# `gpt-5.5` through OpenRouter and `gpt-5.5` at OpenAI are two entries.
|
|
14
|
+
# The static tables below are the fallback for aliases, self-hosted
|
|
15
|
+
# models and an install without RubyLLM: exact rows (STATIC_RATES) for
|
|
16
|
+
# the current frontier models, then name patterns (PRICES), then a
|
|
17
|
+
# conservative blended rate so totals stay meaningful. Costs are always
|
|
18
|
+
# presented as estimates, and every rate says where it came from
|
|
19
|
+
# (`source`), so a figure worked out at a fallback rate can say so.
|
|
13
20
|
class ModelPricing
|
|
21
|
+
# Exact rates for models the pattern table would otherwise misprice —
|
|
22
|
+
# claude-sonnet-5 is not a $3/$15 Sonnet, and the gpt-5 family is not
|
|
23
|
+
# one price — keyed by the name with its vendor prefix and date suffix
|
|
24
|
+
# removed and its dots and dashes normalised (see .normalize).
|
|
25
|
+
STATIC_RATES = {
|
|
26
|
+
"claude-sonnet-5" => [ 2.00, 10.00 ],
|
|
27
|
+
"gpt-5-5" => [ 5.00, 30.00 ],
|
|
28
|
+
"gpt-5-5-mini" => [ 0.25, 2.00 ],
|
|
29
|
+
"gpt-5-5-nano" => [ 0.05, 0.40 ],
|
|
30
|
+
"gpt-5-1" => [ 1.25, 10.00 ],
|
|
31
|
+
"gpt-5-1-mini" => [ 0.25, 2.00 ],
|
|
32
|
+
"gpt-5-1-nano" => [ 0.05, 0.40 ],
|
|
33
|
+
"gpt-5" => [ 1.25, 10.00 ],
|
|
34
|
+
"gpt-5-mini" => [ 0.25, 2.00 ],
|
|
35
|
+
"gpt-5-nano" => [ 0.05, 0.40 ]
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
14
38
|
PRICES = [
|
|
15
39
|
# [pattern, input $/1M, output $/1M]
|
|
16
40
|
[ /gpt-4o-mini/i, 0.15, 0.60 ],
|
|
@@ -18,10 +42,15 @@ module ActionAgent
|
|
|
18
42
|
[ /gpt-4\.1-nano/i, 0.10, 0.40 ],
|
|
19
43
|
[ /gpt-4\.1-mini/i, 0.40, 1.60 ],
|
|
20
44
|
[ /gpt-4\.1/i, 2.00, 8.00 ],
|
|
45
|
+
[ /gpt-5\.?5/i, 5.00, 30.00 ],
|
|
46
|
+
[ /gpt-5(\.\d+)?-nano/i, 0.05, 0.40 ],
|
|
47
|
+
[ /gpt-5(\.\d+)?-mini/i, 0.25, 2.00 ],
|
|
48
|
+
[ /gpt-5/i, 1.25, 10.00 ],
|
|
21
49
|
[ /o3-mini|o4-mini/i, 1.10, 4.40 ],
|
|
22
50
|
[ /claude.*(fable|mythos)/i, 10.00, 50.00 ],
|
|
23
51
|
[ /claude.*haiku-?4/i, 1.00, 5.00 ],
|
|
24
52
|
[ /claude.*(haiku)/i, 0.80, 4.00 ],
|
|
53
|
+
[ /claude.*sonnet-?5/i, 2.00, 10.00 ],
|
|
25
54
|
[ /claude.*(sonnet)/i, 3.00, 15.00 ],
|
|
26
55
|
[ /claude.*opus-(5|4-[5-9])/i, 5.00, 25.00 ],
|
|
27
56
|
[ /claude.*(opus)/i, 15.00, 75.00 ],
|
|
@@ -36,45 +65,196 @@ module ActionAgent
|
|
|
36
65
|
# Fallback blended rate for unknown models ($/1M input, $/1M output)
|
|
37
66
|
DEFAULT_RATE = [ 1.00, 4.00 ].freeze
|
|
38
67
|
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
68
|
+
# Where a rate came from: RubyLLM's bundled catalog, a registry the host
|
|
69
|
+
# keeps itself (RubyLLM's model table or a refreshed listing), a row of
|
|
70
|
+
# STATIC_RATES or PRICES, or DEFAULT_RATE. The last two are a guess at
|
|
71
|
+
# the model's price rather than its listing.
|
|
72
|
+
SOURCES = %w[catalog remote pattern default].freeze
|
|
73
|
+
FALLBACK_SOURCES = %w[pattern default].freeze
|
|
44
74
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
# A gateway's prefix on a model name, which names the provider rather
|
|
76
|
+
# than the model.
|
|
77
|
+
GATEWAY_PREFIXES = %w[openrouter requesty].freeze
|
|
78
|
+
# Vendor prefixes a gateway (or a host) puts before a model name, and
|
|
79
|
+
# the provider the bare name is listed under.
|
|
80
|
+
VENDOR_PROVIDERS = {
|
|
81
|
+
"openai" => "openai", "anthropic" => "anthropic", "google" => "gemini", "gemini" => "gemini",
|
|
82
|
+
"meta-llama" => nil, "mistralai" => "mistral", "mistral" => "mistral", "deepseek" => "deepseek",
|
|
83
|
+
"qwen" => nil, "x-ai" => "xai", "xai" => "xai", "cohere" => nil, "perplexity" => "perplexity"
|
|
84
|
+
}.freeze
|
|
48
85
|
|
|
49
|
-
|
|
50
|
-
return
|
|
86
|
+
class << self
|
|
87
|
+
# @return [Float, nil] estimated USD cost, nil when there is nothing to price
|
|
88
|
+
def estimate(model:, input_tokens:, output_tokens:, provider: nil)
|
|
89
|
+
estimate_detailed(model: model, input_tokens: input_tokens, output_tokens: output_tokens, provider: provider)&.fetch(:cost)
|
|
90
|
+
end
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
|
|
92
|
+
# The estimate with its working: `{ cost:, input_rate:, output_rate:,
|
|
93
|
+
# source: }`, rates in $ per million tokens. nil when both token
|
|
94
|
+
# counts are zero — a $0.00 is the caller's to decide on, since
|
|
95
|
+
# nothing generated and nothing recorded look alike here.
|
|
96
|
+
def estimate_detailed(model:, input_tokens:, output_tokens:, provider: nil)
|
|
97
|
+
input = input_tokens.to_i
|
|
98
|
+
output = output_tokens.to_i
|
|
99
|
+
return nil if input.zero? && output.zero?
|
|
100
|
+
|
|
101
|
+
rate = rate_detail(model, provider: provider)
|
|
102
|
+
{
|
|
103
|
+
cost: ((input * rate[:input]) + (output * rate[:output])) / 1_000_000.0,
|
|
104
|
+
input_rate: rate[:input],
|
|
105
|
+
output_rate: rate[:output],
|
|
106
|
+
source: rate[:source]
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @return [Array(Float, Float)] input and output rates in $ per million tokens
|
|
111
|
+
def rate_for(model, provider = nil)
|
|
112
|
+
rate = rate_detail(model, provider: provider)
|
|
113
|
+
[ rate[:input], rate[:output] ]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# `{ input:, output:, source: }` for a model under a provider, memoized
|
|
117
|
+
# per [provider, model]: the registry scan is not free and trace
|
|
118
|
+
# serialization asks per row.
|
|
119
|
+
def rate_detail(model, provider: nil)
|
|
120
|
+
return { input: DEFAULT_RATE[0], output: DEFAULT_RATE[1], source: "default" } if model.blank?
|
|
121
|
+
|
|
122
|
+
key = [ provider.to_s, model.to_s ]
|
|
123
|
+
@rates ||= {}
|
|
124
|
+
return @rates[key] if @rates.key?(key)
|
|
125
|
+
|
|
126
|
+
@rates[key] = registry_rate(model.to_s, provider.to_s.presence) || static_rate(model.to_s)
|
|
127
|
+
end
|
|
54
128
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
129
|
+
# Forgets memoized rates — after RubyLLM is loaded, or in tests.
|
|
130
|
+
def reset!
|
|
131
|
+
@rates = {}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Names the rate tables in force, so a cache of figures worked out at
|
|
135
|
+
# them is invalidated when they change: the static rows, the default,
|
|
136
|
+
# and the version of the registry the install has (or none).
|
|
137
|
+
def fingerprint
|
|
138
|
+
registry = defined?(::RubyLLM) && ::RubyLLM.const_defined?(:VERSION) ? ::RubyLLM::VERSION.to_s : "none"
|
|
139
|
+
tables = [ STATIC_RATES, PRICES.map { |pattern, input, output| [ pattern.source, input, output ] }, DEFAULT_RATE, registry ]
|
|
140
|
+
Digest::SHA256.hexdigest(JSON.generate(tables))[0, 12]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# "openrouter/anthropic/claude-sonnet-4-5-20250929" → "claude-sonnet-4-5":
|
|
144
|
+
# the name without a gateway or vendor prefix, a date suffix or a
|
|
145
|
+
# colon-tagged size, with dots as dashes, so the same model under two
|
|
146
|
+
# spellings keys one row.
|
|
147
|
+
def normalize(model)
|
|
148
|
+
name = model.to_s.strip.downcase
|
|
149
|
+
name = name.split("/").last.to_s
|
|
150
|
+
name = name.sub(/-\d{8}\z/, "").sub(/-\d{4}-\d{2}-\d{2}\z/, "")
|
|
151
|
+
name.tr(".", "-")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
# The registry's rate for the first spelling it knows: the name as
|
|
157
|
+
# given under the provider it ran on, then bare, then with a gateway
|
|
158
|
+
# or vendor prefix stripped (under the vendor's own provider), each
|
|
159
|
+
# with its dots and dashes swapped and its date suffix dropped.
|
|
160
|
+
def registry_rate(model, provider)
|
|
161
|
+
return nil unless registry_available?
|
|
162
|
+
|
|
163
|
+
lookup_candidates(model, provider).each do |id, candidate_provider|
|
|
164
|
+
info = registry_find(id, candidate_provider)
|
|
165
|
+
tokens = info&.pricing&.text_tokens
|
|
166
|
+
next unless tokens.respond_to?(:input) && tokens.input && tokens.output
|
|
167
|
+
|
|
168
|
+
return { input: tokens.input.to_f, output: tokens.output.to_f, source: registry_source }
|
|
67
169
|
end
|
|
170
|
+
nil
|
|
68
171
|
rescue StandardError
|
|
69
172
|
nil
|
|
70
173
|
end
|
|
71
|
-
end
|
|
72
174
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
175
|
+
def registry_available?
|
|
176
|
+
defined?(::RubyLLM) && ::RubyLLM.respond_to?(:models)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A host that keeps its own model registry (RubyLLM's model table or
|
|
180
|
+
# a refreshed listing) prices from it rather than from the gem's
|
|
181
|
+
# bundled catalog.
|
|
182
|
+
def registry_source
|
|
183
|
+
config = ::RubyLLM.respond_to?(:config) ? ::RubyLLM.config : nil
|
|
184
|
+
return "remote" if config.respond_to?(:model_registry_class) && config.model_registry_class.present?
|
|
185
|
+
|
|
186
|
+
"catalog"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# RubyLLM 2 takes the provider as a keyword; 1.x took it positionally.
|
|
190
|
+
def registry_find(id, provider)
|
|
191
|
+
models = ::RubyLLM.models
|
|
192
|
+
return models.find(id) if provider.blank?
|
|
193
|
+
|
|
194
|
+
if registry_find_keyword?(models)
|
|
195
|
+
models.find(id, provider: provider)
|
|
196
|
+
else
|
|
197
|
+
models.find(id, provider)
|
|
198
|
+
end
|
|
199
|
+
rescue StandardError
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def registry_find_keyword?(models)
|
|
204
|
+
models.method(:find).parameters.any? { |type, name| name == :provider && %i[key keyreq].include?(type) }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# [[id, provider], ...] to try in order, without repeats.
|
|
208
|
+
def lookup_candidates(model, provider)
|
|
209
|
+
name = model.strip
|
|
210
|
+
provider = provider&.downcase
|
|
211
|
+
candidates = []
|
|
212
|
+
|
|
213
|
+
head, rest = name.split("/", 2)
|
|
214
|
+
if rest.present? && GATEWAY_PREFIXES.include?(head.downcase)
|
|
215
|
+
provider ||= head.downcase
|
|
216
|
+
name = rest
|
|
217
|
+
head, rest = name.split("/", 2)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
names = [ name ]
|
|
221
|
+
if rest.present? && VENDOR_PROVIDERS.key?(head.downcase)
|
|
222
|
+
names << rest
|
|
223
|
+
vendor_provider = VENDOR_PROVIDERS[head.downcase]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
names.each do |candidate|
|
|
227
|
+
spellings(candidate).each do |spelling|
|
|
228
|
+
candidates << [ spelling, provider ] if provider
|
|
229
|
+
candidates << [ spelling, vendor_provider ] if vendor_provider && vendor_provider != provider && candidate == rest
|
|
230
|
+
candidates << [ spelling, nil ]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
candidates.uniq
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# The name as given, then with its date suffix dropped, each with its
|
|
237
|
+
# dots and dashes swapped: "claude-sonnet-4.5" and "claude-sonnet-4-5"
|
|
238
|
+
# are one model.
|
|
239
|
+
def spellings(name)
|
|
240
|
+
undated = name.sub(/-\d{8}\z/, "").sub(/-\d{4}-\d{2}-\d{2}\z/, "")
|
|
241
|
+
[ name, undated ].uniq.flat_map do |spelling|
|
|
242
|
+
[ spelling, spelling.gsub(/(\d)\.(\d)/, '\1-\2'), spelling.gsub(/(\d)-(\d)/, '\1.\2') ]
|
|
243
|
+
end.uniq
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# An exact row for the normalised name, else the first pattern the
|
|
247
|
+
# name as given matches, else the default.
|
|
248
|
+
def static_rate(model)
|
|
249
|
+
if (exact = STATIC_RATES[normalize(model)])
|
|
250
|
+
return { input: exact[0], output: exact[1], source: "pattern" }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
PRICES.each do |pattern, input_rate, output_rate|
|
|
254
|
+
return { input: input_rate, output: output_rate, source: "pattern" } if model.match?(pattern)
|
|
255
|
+
end
|
|
256
|
+
{ input: DEFAULT_RATE[0], output: DEFAULT_RATE[1], source: "default" }
|
|
76
257
|
end
|
|
77
|
-
DEFAULT_RATE
|
|
78
258
|
end
|
|
79
259
|
end
|
|
80
260
|
end
|