Class: Clacky::AgentConfig
- Inherits:
-
Object
- Object
- Clacky::AgentConfig
- Defined in:
- lib/clacky/agent_config.rb
Constant Summary collapse
- CONFIG_DIR =
File.join(Dir.home, ".clacky")
- CONFIG_FILE =
File.join(CONFIG_DIR, "config.yml")
- CLAUDE_DEFAULT_MODEL =
Default model for ClaudeCode environment
"claude-sonnet-4-5"- PERMISSION_MODES =
[:auto_approve, :confirm_safes, :confirm_all].freeze
- RUNTIME_ONLY_FIELDS =
Convert to YAML format (top-level array) Auto-injected lite models (auto_injected: true) are excluded from persistence —they are regenerated at load time from the provider preset. Runtime-only fields (id, auto_injected) are stripped before writing so config.yml remains backward compatible with users on older versions.
%w[id auto_injected].freeze
- CONFIG_SETTINGS_KEYS =
Settings keys that are persisted to config.yml. These map directly to AgentConfig accessors.
%w[ enable_compression enable_prompt_caching memory_update_enabled skill_evolution max_running_agents max_idle_agents default_working_dir ].freeze
- FALLBACK_COOLING_OFF_SECONDS =
How long to stay on the fallback model before probing the primary again.
30 * 60
Instance Attribute Summary collapse
-
#current_model_id ⇒ Object
Returns the value of attribute current_model_id.
-
#current_model_index ⇒ Object
Returns the value of attribute current_model_index.
-
#default_working_dir ⇒ Object
Returns the value of attribute default_working_dir.
-
#enable_compression ⇒ Object
Returns the value of attribute enable_compression.
-
#enable_prompt_caching ⇒ Object
Returns the value of attribute enable_prompt_caching.
-
#max_idle_agents ⇒ Object
Returns the value of attribute max_idle_agents.
-
#max_running_agents ⇒ Object
Returns the value of attribute max_running_agents.
-
#max_tokens ⇒ Object
Returns the value of attribute max_tokens.
-
#memory_update_enabled ⇒ Object
Returns the value of attribute memory_update_enabled.
-
#models ⇒ Object
Returns the value of attribute models.
-
#permission_mode ⇒ Object
Returns the value of attribute permission_mode.
-
#skill_evolution ⇒ Object
Returns the value of attribute skill_evolution.
-
#verbose ⇒ Object
Returns the value of attribute verbose.
Class Method Summary collapse
-
.load(config_file = CONFIG_FILE) ⇒ Object
Load configuration from file.
Instance Method Summary collapse
-
#activate_fallback!(fallback_model_name) ⇒ Object
Switch to fallback model and start the cooling-off clock.
-
#add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil) ⇒ Object
Add a new model configuration.
-
#anthropic_format? ⇒ Boolean
Check if should use Anthropic format for current model.
-
#api_key ⇒ Object
Get API key for current model.
-
#api_key=(value) ⇒ Object
Set API key for current model.
-
#apply_virtual_model_overlay!(overlay) ⇒ void
Apply a virtual model overlay for this session (and only this session).
-
#base_url ⇒ Object
Get base URL for current model.
-
#base_url=(value) ⇒ Object
Set base URL for current model (overlay-aware; see #api_key=).
-
#bedrock? ⇒ Boolean
Check if current model uses Bedrock Converse API (ABSK key prefix or abs- model prefix).
-
#confirm_fallback_ok! ⇒ Object
Called when a successful API response is received.
-
#current_model ⇒ Object
Get current model configuration.
-
#current_model_supports?(capability) ⇒ Boolean
Query whether the current model supports a given capability.
-
#deep_copy ⇒ Object
Create a per-session copy of this config.
-
#default_model ⇒ Object
Get the default model (type: default) Falls back to current_model for backward compatibility.
-
#derive_media_models! ⇒ Object
Kept as a no-op for backward compatibility.
-
#effective_model_name ⇒ Object
The effective model name to use for API calls.
-
#fallback_active? ⇒ Boolean
Returns true when a fallback model is currently being used (:fallback_active or :probing states).
-
#fallback_model_for(model_name) ⇒ String?
Look up the fallback model name for the given model name.
-
#find_model_by_name_and_url(model_name, base_url = nil) ⇒ Hash?
Find model by composite key (model name + base_url).
-
#find_model_by_type(type) ⇒ Object
Find model by type (default or lite or media kind) Returns the model hash or nil if not found.
-
#get_model(index) ⇒ Object
Get model by index.
-
#initialize(options = {}) ⇒ AgentConfig
constructor
A new instance of AgentConfig.
-
#lite_model ⇒ Object
Explicit lite model entry (type: “lite”) — only present when the user configured ‘CLACKY_LITE_*` environment variables.
-
#lite_model_config_for_current ⇒ Hash?
Return a complete lite model config hash for the currently-active primary model, or nil if none is available.
-
#maybe_start_probing ⇒ Object
Called at the start of every call_llm.
-
#media_state(kind) ⇒ Hash{String=>Object}
Returns the configured/derived media model entry for ‘kind`, plus a hint about its source.
-
#model_name ⇒ Object
Get model name for current model.
-
#model_name=(value) ⇒ Object
Set model name for current model (overlay-aware; see #api_key=).
-
#model_names ⇒ Object
List all model names.
-
#models_configured? ⇒ Boolean
Check if any model is configured.
-
#probing? ⇒ Boolean
Returns true only when we are silently probing the primary model.
-
#remove_model(index) ⇒ Object
Remove a model by index Returns true if removed, false if index out of range or it’s the last model.
- #save(config_file = CONFIG_FILE) ⇒ Object
-
#session_model_overlay ⇒ Hash?
The active session sub-model overlay.
-
#session_model_overlay=(model_name) ⇒ Object
Apply a session-level sub-model override.
-
#session_model_overlay_name ⇒ String?
Convenience accessor: the sub-model name currently pinned on this session, or nil when no override is active.
-
#set_default_model_by_id(id) ⇒ Boolean
Set the global default model marker (‘type: “default”`).
-
#set_model_type(index, type) ⇒ Object
Set a model’s type (default, lite, image, video, or audio).
-
#switch_model_by_id(id) ⇒ Boolean
Switch the current session to a specific model, identified by its stable runtime id.
-
#switch_model_by_name(name) ⇒ Boolean
Switch to a model by its display name (fuzzy match, case-insensitive).
-
#to_yaml ⇒ Object
Serialize the current agent configuration to YAML.
-
#virtual_model_overlay ⇒ Hash?
The active overlay (read-only view; dup before mutating).
Constructor Details
#initialize(options = {}) ⇒ AgentConfig
Returns a new instance of AgentConfig.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/clacky/agent_config.rb', line 161 def initialize( = {}) @permission_mode = ([:permission_mode]) @max_tokens = [:max_tokens] || 16384 @verbose = [:verbose] || false @enable_compression = [:enable_compression].nil? ? true : [:enable_compression] # Enable prompt caching by default for cost savings @enable_prompt_caching = [:enable_prompt_caching].nil? ? true : [:enable_prompt_caching] # Models configuration @models = [:models] || [] # Ensure every model has a stable runtime id — this is the single # invariant the rest of the system relies on. Regardless of how the # config was built (load from yml, direct .new in tests, add_model, # api_save_config), every model in @models will have an id. @models.each { |m| m["id"] ||= SecureRandom.uuid } @current_model_index = [:current_model_index] || 0 # Stable runtime id for the currently-selected model. Preferred over # @current_model_index because ids are immune to list reordering, # additions, and edits to model fields. Ids are injected at load time # and never persisted to config.yml (backward compatible with old files). # If caller didn't specify current_model_id, prefer the model marked # as `type: default` (the documented convention), falling back to # models[current_model_index] only if no default marker exists. @current_model_id = [:current_model_id] || (@models.find { |m| m["type"] == "default" } || @models[@current_model_index])&.dig("id") # Memory and skill evolution configuration @memory_update_enabled = [:memory_update_enabled].nil? ? true : [:memory_update_enabled] @skill_evolution = [:skill_evolution] || { enabled: true, auto_create_threshold: 12, reflection_mode: "llm_analysis" } # Deep-symbolize keys — YAML-loaded hashes come with string keys, # but the rest of the codebase accesses with symbols. @skill_evolution = @skill_evolution.transform_keys(&:to_sym) @skill_evolution.transform_values! { |v| v.is_a?(Hash) ? v.transform_keys(&:to_sym) : v } @max_running_agents = [:max_running_agents] || 10 @max_idle_agents = [:max_idle_agents] || 10 @default_working_dir = [:default_working_dir] || ENV["CLACKY_WORKSPACE_DIR"] # Per-session virtual model overlay. # When set, #current_model returns a *merged* hash (the resolved @models # entry merged with this overlay) without mutating the shared @models # array. Used by fork_subagent's virtual-lite path so a forked subagent # can run on different credentials (e.g. Haiku instead of Opus) without # polluting the parent agent's shared @models hashes. # Keys honored: "api_key", "base_url", "model", "anthropic_format". # @return [Hash, nil] @virtual_model_overlay = [:virtual_model_overlay] # Per-session sub-model override. Persists across restarts via the # session file. Independent of @virtual_model_overlay (which is for # short-lived subagent forks). Used by the WebUI sub-model switcher # to pin a session to e.g. "dsk-deepseek-v4-pro" while the underlying # card still says "abs-claude-sonnet-4-6". Only the "model" key is # honored — sub-model switching never changes credentials. @session_model_overlay = [:session_model_overlay] end |
Instance Attribute Details
#current_model_id ⇒ Object
Returns the value of attribute current_model_id.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def current_model_id @current_model_id end |
#current_model_index ⇒ Object
Returns the value of attribute current_model_index.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def current_model_index @current_model_index end |
#default_working_dir ⇒ Object
Returns the value of attribute default_working_dir.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def default_working_dir @default_working_dir end |
#enable_compression ⇒ Object
Returns the value of attribute enable_compression.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def enable_compression @enable_compression end |
#enable_prompt_caching ⇒ Object
Returns the value of attribute enable_prompt_caching.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def enable_prompt_caching @enable_prompt_caching end |
#max_idle_agents ⇒ Object
Returns the value of attribute max_idle_agents.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def max_idle_agents @max_idle_agents end |
#max_running_agents ⇒ Object
Returns the value of attribute max_running_agents.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def max_running_agents @max_running_agents end |
#max_tokens ⇒ Object
Returns the value of attribute max_tokens.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def max_tokens @max_tokens end |
#memory_update_enabled ⇒ Object
Returns the value of attribute memory_update_enabled.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def memory_update_enabled @memory_update_enabled end |
#models ⇒ Object
Returns the value of attribute models.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def models @models end |
#permission_mode ⇒ Object
Returns the value of attribute permission_mode.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def @permission_mode end |
#skill_evolution ⇒ Object
Returns the value of attribute skill_evolution.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def skill_evolution @skill_evolution end |
#verbose ⇒ Object
Returns the value of attribute verbose.
154 155 156 |
# File 'lib/clacky/agent_config.rb', line 154 def verbose @verbose end |
Class Method Details
.load(config_file = CONFIG_FILE) ⇒ Object
Load configuration from file
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/clacky/agent_config.rb', line 225 def self.load(config_file = CONFIG_FILE) # Load from config file first if File.exist?(config_file) data = YAML.load_file(config_file) else data = nil end # Extract settings from hash-format config (new format). # Old flat-array configs have no settings section — all defaults. loaded_settings = {} if data.is_a?(Hash) && data["settings"].is_a?(Hash) loaded_settings = data["settings"] end # Parse models from config models = parse_models(data) # Priority: config file > CLACKY_XXX env vars > ClaudeCode env vars if models.empty? # Try CLACKY_XXX environment variables first if ClackyEnv.default_configured? models << ClackyEnv.default_model_config # ClaudeCode (Anthropic) environment variable support is disabled # elsif ClaudeCodeEnv.configured? # models << { # "type" => "default", # "api_key" => ClaudeCodeEnv.api_key, # "base_url" => ClaudeCodeEnv.base_url, # "model" => CLAUDE_DEFAULT_MODEL, # "anthropic_format" => true # } end # Add CLACKY_LITE_XXX if configured (only when loading from env) if ClackyEnv.lite_configured? models << ClackyEnv.lite_model_config end else # Config file exists, but check if we need to add env-based models # Only add if no model with that type exists has_default = models.any? { |m| m["type"] == "default" } has_lite = models.any? { |m| m["type"] == "lite" } # Add CLACKY default if not in config and env is set if !has_default && ClackyEnv.default_configured? models << ClackyEnv.default_model_config end # Add CLACKY lite if not in config and env is set if !has_lite && ClackyEnv.lite_configured? models << ClackyEnv.lite_model_config end # Ensure at least one model has type: default # If no model has type: default, assign it to the first model unless models.any? { |m| m["type"] == "default" } models.first["type"] = "default" if models.any? end end # Auto-inject lite model from provider preset is **no longer materialized # into @models**. Lite is now a virtual, on-demand view derived from the # currently-selected primary model — see `#lite_model_config_for_current`. # This keeps @models a clean "list of user-facing models" and lets the # lite companion track the current model at runtime, rather than being # frozen at load time to whichever model happened to be the default. # # Legacy note: prior versions injected an entry here with # `auto_injected: true`. That flag is still honored in to_yaml for # safety (never persisted), but new injections never happen. # Ensure every model has a stable runtime id — covers env-injected # models (CLACKY_XXX, CLAUDE_XXX) that don't go through parse_models. # Ids are NOT persisted to config.yml (see to_yaml). models.each { |m| m["id"] ||= SecureRandom.uuid } # Find the index of the model marked as "default" (type: default) # Fall back to 0 if no model has type: default default_index = models.find_index { |m| m["type"] == "default" } || 0 default_id = models[default_index] && models[default_index]["id"] # Build constructor args from loaded settings (new hash-format config) # plus the parsed models. Only pass settings that have explicit values; # omitted keys get their default from AgentConfig#initialize. constructor_args = { models: models, current_model_index: default_index, current_model_id: default_id } CONFIG_SETTINGS_KEYS.each do |key| if loaded_settings.key?(key) constructor_args[key.to_sym] = loaded_settings[key] end end instance = new(**constructor_args) instance.derive_media_models! instance end |
Instance Method Details
#activate_fallback!(fallback_model_name) ⇒ Object
Switch to fallback model and start the cooling-off clock. Idempotent — calling again while already in :fallback_active renews the timestamp.
795 796 797 798 799 |
# File 'lib/clacky/agent_config.rb', line 795 def activate_fallback!(fallback_model_name) @fallback_state = :fallback_active @fallback_since = Time.now @fallback_model = fallback_model_name end |
#add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil) ⇒ Object
Add a new model configuration
579 580 581 582 583 584 585 586 587 588 |
# File 'lib/clacky/agent_config.rb', line 579 def add_model(model:, api_key:, base_url:, anthropic_format: false, type: nil) @models << { "id" => SecureRandom.uuid, "api_key" => api_key, "base_url" => base_url, "model" => model, "anthropic_format" => anthropic_format, "type" => type }.compact end |
#anthropic_format? ⇒ Boolean
Check if should use Anthropic format for current model
569 570 571 |
# File 'lib/clacky/agent_config.rb', line 569 def anthropic_format? current_model&.dig("anthropic_format") || false end |
#api_key ⇒ Object
Get API key for current model
522 523 524 |
# File 'lib/clacky/agent_config.rb', line 522 def api_key current_model&.dig("api_key") end |
#api_key=(value) ⇒ Object
Set API key for current model. When a virtual overlay is active, writes into the overlay (not the shared @models hash) to keep session-level isolation.
529 530 531 532 533 534 535 536 |
# File 'lib/clacky/agent_config.rb', line 529 def api_key=(value) return unless resolve_current_model_entry if @virtual_model_overlay @virtual_model_overlay["api_key"] = value else resolve_current_model_entry["api_key"] = value end end |
#apply_virtual_model_overlay!(overlay) ⇒ void
This method returns an undefined value.
Apply a virtual model overlay for this session (and only this session). The overlay fields are merged on top of the current model entry when #current_model is called, without ever mutating the shared @models array or its hashes.
Used by Agent#fork_subagent when routing a subagent through a virtual lite model (Haiku for Claude family, Flash for DeepSeek, …). Apply on the forked config only — the parent config is untouched.
913 914 915 916 917 918 919 920 |
# File 'lib/clacky/agent_config.rb', line 913 def () if .nil? || .empty? @virtual_model_overlay = nil else # Dup so later mutations to the passed-in hash don't leak in. @virtual_model_overlay = .dup end end |
#base_url ⇒ Object
Get base URL for current model
539 540 541 |
# File 'lib/clacky/agent_config.rb', line 539 def base_url current_model&.dig("base_url") end |
#base_url=(value) ⇒ Object
Set base URL for current model (overlay-aware; see #api_key=).
544 545 546 547 548 549 550 551 |
# File 'lib/clacky/agent_config.rb', line 544 def base_url=(value) return unless resolve_current_model_entry if @virtual_model_overlay @virtual_model_overlay["base_url"] = value else resolve_current_model_entry["base_url"] = value end end |
#bedrock? ⇒ Boolean
Check if current model uses Bedrock Converse API (ABSK key prefix or abs- model prefix)
574 575 576 |
# File 'lib/clacky/agent_config.rb', line 574 def bedrock? Clacky::MessageFormat::Bedrock.bedrock_api_key?(api_key.to_s, model_name.to_s) end |
#confirm_fallback_ok! ⇒ Object
Called when a successful API response is received. If we were :probing (testing primary after cooling-off), this confirms the primary model is healthy again and resets everything. No-op in :primary_ok or :fallback_active states.
816 817 818 819 820 821 822 |
# File 'lib/clacky/agent_config.rb', line 816 def confirm_fallback_ok! return unless @fallback_state == :probing @fallback_state = nil @fallback_since = nil @fallback_model = nil end |
#current_model ⇒ Object
Get current model configuration.
Resolution order:
1. @current_model_id (primary source of truth — stable across list edits)
2. type: default (for config.yml that sets a default explicitly)
3. @current_model_index (back-compat for very old code paths)
855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 |
# File 'lib/clacky/agent_config.rb', line 855 def current_model return nil if @models.empty? resolved = resolve_current_model_entry return nil unless resolved # Merge order (low → high): base entry, session-level sub-model override, # then short-lived subagent overlay. Both layers are kept separate so # a subagent fork can stack its own credentials on top of an active # sub-model pin without erasing it. merged = resolved if @session_model_overlay && !@session_model_overlay.empty? merged = merged.merge(@session_model_overlay) end if @virtual_model_overlay && !@virtual_model_overlay.empty? merged = merged.merge(@virtual_model_overlay) end merged.equal?(resolved) ? resolved : merged end |
#current_model_supports?(capability) ⇒ Boolean
Query whether the current model supports a given capability.
This is the single entry-point callers (Agent, downgrade pipeline, UI) should use instead of poking Providers directly. Benefits:
- Always reflects the current model — switching with `/model` takes
effect immediately, no caching, no stale warnings.
- Handles the "custom base_url / unknown provider" case with a
conservative default (assume supported), so self-hosted or new
providers don't get accidentally downgraded.
970 971 972 973 974 975 976 977 978 979 980 981 |
# File 'lib/clacky/agent_config.rb', line 970 def current_model_supports?(capability) m = current_model # No model configured yet → nothing to judge; assume supported so we # don't preemptively downgrade before a model is even picked. return true unless m && m["base_url"] provider_id = Clacky::Providers.find_by_base_url(m["base_url"]) # Custom / self-hosted base_url not in our preset list → be conservative. return true unless provider_id Clacky::Providers.supports?(provider_id, capability, model_name: m["model"]) end |
#deep_copy ⇒ Object
Create a per-session copy of this config.
Plan B (shared models): we deliberately share the SAME @models array reference with all sessions (no deep clone). This is the key design decision that keeps session and global views in sync:
- User adds a model in Settings → every live session sees it instantly.
- User edits api_key/base_url → every live session's next API call
picks up the new credentials (via current_model lookup).
- Model ids are stable across edits, so each session's
@current_model_id continues to resolve correctly.
Per-session state that MUST stay isolated (permission_mode, @current_model_id, @current_model_index, fallback state) are scalar copies via ‘dup` and don’t leak between sessions.
Before Plan B, sessions held deep-copied @models — which silently diverged from the global list any time the user added/edited a model in Settings, producing bugs like “Failed to switch model” for newly added models on Windows and Linux. See http_server.rb#api_switch_session_model and http_server.rb#api_save_config for the companion logic.
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 |
# File 'lib/clacky/agent_config.rb', line 356 def deep_copy # dup gives us a new AgentConfig with independent scalar ivars but # the same @models reference — exactly what we want. copy = dup # But @virtual_model_overlay must be independent: a forked subagent # setting/clearing its own overlay must NOT leak into the parent. # (dup copies the ivar reference; an unset overlay is nil which is # already independent, but an active overlay must be cloned.) if @virtual_model_overlay copy.instance_variable_set(:@virtual_model_overlay, @virtual_model_overlay.dup) end if @session_model_overlay copy.instance_variable_set(:@session_model_overlay, @session_model_overlay.dup) end copy end |
#default_model ⇒ Object
Get the default model (type: default) Falls back to current_model for backward compatibility
695 696 697 |
# File 'lib/clacky/agent_config.rb', line 695 def default_model find_model_by_type("default") || current_model end |
#derive_media_models! ⇒ Object
Kept as a no-op for backward compatibility. Media auto entries are now derived virtually on read; nothing is materialized into @models.
630 631 632 |
# File 'lib/clacky/agent_config.rb', line 630 def derive_media_models! @models.reject! { |m| m["auto_injected"] && Clacky::Providers::MEDIA_KINDS.include?(m["type"].to_s) } end |
#effective_model_name ⇒ Object
The effective model name to use for API calls.
-
:primary_ok / nil → configured model_name (primary)
-
:fallback_active → fallback model
-
:probing → configured model_name (trying primary silently)
839 840 841 842 843 844 845 846 847 |
# File 'lib/clacky/agent_config.rb', line 839 def effective_model_name case @fallback_state when :fallback_active @fallback_model || model_name else # :primary_ok (nil) and :probing both use the primary model model_name end end |
#fallback_active? ⇒ Boolean
Returns true when a fallback model is currently being used (:fallback_active or :probing states).
826 827 828 |
# File 'lib/clacky/agent_config.rb', line 826 def fallback_active? @fallback_state == :fallback_active || @fallback_state == :probing end |
#fallback_model_for(model_name) ⇒ String?
Look up the fallback model name for the given model name. Uses the provider preset’s fallback_models table. Returns nil if no fallback is configured for this model.
779 780 781 782 783 784 785 786 787 788 789 790 |
# File 'lib/clacky/agent_config.rb', line 779 def fallback_model_for(model_name) m = current_model return nil unless m provider_id = Clacky::Providers.resolve_provider( base_url: m["base_url"], api_key: m["api_key"] ) return nil unless provider_id Clacky::Providers.fallback_model(provider_id, model_name) end |
#find_model_by_name_and_url(model_name, base_url = nil) ⇒ Hash?
Find model by composite key (model name + base_url). Used when restoring a session to match its original model without relying on the runtime-only id (which changes on every process restart). base_url is optional for backward compatibility with sessions saved before base_url was persisted.
686 687 688 689 690 691 |
# File 'lib/clacky/agent_config.rb', line 686 def find_model_by_name_and_url(model_name, base_url = nil) @models.find do |m| m["model"] == model_name && (base_url.nil? || m["base_url"] == base_url) end end |
#find_model_by_type(type) ⇒ Object
Find model by type (default or lite or media kind) Returns the model hash or nil if not found. For media kinds (image/video/audio): explicit user-configured (custom) entries win; otherwise an auto-derived virtual entry is returned based on the default model’s provider — mirroring how lite is virtually derived via #lite_model_config_for_current.
596 597 598 599 600 601 602 603 604 |
# File 'lib/clacky/agent_config.rb', line 596 def find_model_by_type(type) kind = type.to_s if Clacky::Providers::MEDIA_KINDS.include?(kind) custom = @models.find { |m| m["type"] == kind } return custom if custom return derive_media_model(kind) end @models.find { |m| m["type"] == type } end |
#get_model(index) ⇒ Object
Get model by index
424 425 426 |
# File 'lib/clacky/agent_config.rb', line 424 def get_model(index) @models[index] end |
#lite_model ⇒ Object
Explicit lite model entry (type: “lite”) — only present when the user configured ‘CLACKY_LITE_*` environment variables. Returns nil otherwise.
This is the “user override” path. The preferred way for subagents to obtain a lite model is ‘#lite_model_config_for_current`, which falls back to this method when an explicit lite exists.
705 706 707 |
# File 'lib/clacky/agent_config.rb', line 705 def lite_model find_model_by_type("lite") end |
#lite_model_config_for_current ⇒ Hash?
Return a complete lite model config hash for the currently-active primary model, or nil if none is available.
Resolution order:
1. Explicit user-configured lite (type: "lite", from CLACKY_LITE_*
env vars). Wins over provider presets so power users retain full
control.
2. Provider preset: look up the current model's provider, consult its
per-family `lite_models` table (e.g. openclacky: Claude → Haiku,
DeepSeek V4-pro → DeepSeek V4-flash). If matched, return a virtual
hash that reuses the current model's api_key / base_url — only
the model name (and anthropic_format, if provider-specific) differ.
3. nil — either the provider has no lite mapping for this primary
(e.g. the current model is already lite-class like Haiku), or the
provider is unknown. Callers should treat this as "no lite
available; use the primary as-is".
The returned hash is not added to @models. It’s consumed directly by ‘Agent#fork_subagent(model: “lite”)`, which applies the fields to the forked config. This means:
- Switching the primary model automatically changes which lite is
used, with zero additional bookkeeping.
- @models stays a clean list of user-facing models (no phantom
auto-injected entries cluttering the model picker in the UI).
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 |
# File 'lib/clacky/agent_config.rb', line 737 def lite_model_config_for_current # 1) Explicit user-configured lite wins explicit = find_model_by_type("lite") return explicit if explicit # 2) Provider preset derivation primary = current_model return nil unless primary && primary["base_url"] && primary["model"] # Use resolve_provider (base_url first, then clacky-* api_key fallback # for local-debug / self-hosted proxies). provider_id = Clacky::Providers.resolve_provider( base_url: primary["base_url"], api_key: primary["api_key"] ) return nil unless provider_id lite_name = Clacky::Providers.lite_model(provider_id, primary["model"]) return nil unless lite_name # If the current primary IS already a lite-class model, skip. return nil if lite_name == primary["model"] { "id" => "lite:#{primary["id"]}", "type" => "lite", "api_key" => primary["api_key"], "base_url" => primary["base_url"], "model" => lite_name, "anthropic_format" => primary["anthropic_format"] || false, "virtual" => true # marker: not a real @models entry } end |
#maybe_start_probing ⇒ Object
Called at the start of every call_llm. If cooling-off has expired, transition from :fallback_active → :probing so the next request will silently test the primary model. No-op in any other state.
805 806 807 808 809 810 |
# File 'lib/clacky/agent_config.rb', line 805 def maybe_start_probing return unless @fallback_state == :fallback_active return unless @fallback_since && (Time.now - @fallback_since) >= FALLBACK_COOLING_OFF_SECONDS @fallback_state = :probing end |
#media_state(kind) ⇒ Hash{String=>Object}
Returns the configured/derived media model entry for ‘kind`, plus a hint about its source. UI uses this to render the tri-state control.
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 |
# File 'lib/clacky/agent_config.rb', line 644 def media_state(kind) kind = kind.to_s custom = @models.find { |m| m["type"] == kind } auto = custom ? nil : derive_media_model(kind) entry = custom || auto provider_id = if entry Clacky::Providers.resolve_provider( base_url: entry["base_url"], api_key: entry["api_key"] ) end available_provider_id = if custom provider_id else default = find_model_by_type("default") default && Clacky::Providers.resolve_provider( base_url: default["base_url"], api_key: default["api_key"] ) end available = available_provider_id ? Clacky::Providers.media_models(available_provider_id, kind) : [] { "configured" => !entry.nil?, "source" => custom ? "custom" : (auto ? "auto" : "off"), "model" => entry && entry["model"], "base_url" => entry && entry["base_url"], "provider" => provider_id, "available" => available } end |
#model_name ⇒ Object
Get model name for current model
554 555 556 |
# File 'lib/clacky/agent_config.rb', line 554 def model_name current_model&.dig("model") end |
#model_name=(value) ⇒ Object
Set model name for current model (overlay-aware; see #api_key=).
559 560 561 562 563 564 565 566 |
# File 'lib/clacky/agent_config.rb', line 559 def model_name=(value) return unless resolve_current_model_entry if @virtual_model_overlay @virtual_model_overlay["model"] = value else resolve_current_model_entry["model"] = value end end |
#model_names ⇒ Object
List all model names
517 518 519 |
# File 'lib/clacky/agent_config.rb', line 517 def model_names @models.map { |m| m["model"] } end |
#models_configured? ⇒ Boolean
Check if any model is configured
415 416 417 |
# File 'lib/clacky/agent_config.rb', line 415 def models_configured? !@models.empty? && !current_model.nil? end |
#probing? ⇒ Boolean
Returns true only when we are silently probing the primary model.
831 832 833 |
# File 'lib/clacky/agent_config.rb', line 831 def probing? @fallback_state == :probing end |
#remove_model(index) ⇒ Object
Remove a model by index Returns true if removed, false if index out of range or it’s the last model
1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 |
# File 'lib/clacky/agent_config.rb', line 1010 def remove_model(index) # Don't allow removing the last model return false if @models.length <= 1 return false if index < 0 || index >= @models.length removed = @models.delete_at(index) # Adjust current_model_index if necessary if @current_model_index >= @models.length @current_model_index = @models.length - 1 end # If the removed model was the current one, clear @current_model_id. # current_model will then fall back to type: default / current_model_index. if removed && @current_model_id == removed["id"] @current_model_id = nil end true end |
#save(config_file = CONFIG_FILE) ⇒ Object
373 374 375 376 377 378 |
# File 'lib/clacky/agent_config.rb', line 373 def save(config_file = CONFIG_FILE) config_dir = File.dirname(config_file) FileUtils.mkdir_p(config_dir) File.write(config_file, to_yaml) FileUtils.chmod(0o600, config_file) end |
#session_model_overlay ⇒ Hash?
Returns the active session sub-model overlay.
945 946 947 |
# File 'lib/clacky/agent_config.rb', line 945 def @session_model_overlay end |
#session_model_overlay=(model_name) ⇒ Object
Apply a session-level sub-model override. Lives on this AgentConfig only (each session deep_copy’s its own scalar ivar) and survives a restart through the session file. Pass nil or “” to clear.
The override only rewrites the resolved current_model’s “model” field —api_key / base_url / anthropic_format come from the underlying card, so the user’s credentials and provider identity are untouched.
936 937 938 939 940 941 942 |
# File 'lib/clacky/agent_config.rb', line 936 def (model_name) if model_name.nil? || model_name.to_s.strip.empty? @session_model_overlay = nil else @session_model_overlay = { "model" => model_name.to_s.strip } end end |
#session_model_overlay_name ⇒ String?
Convenience accessor: the sub-model name currently pinned on this session, or nil when no override is active. Used by serializers and the WebUI to surface “card · sub-model” two-line displays.
953 954 955 |
# File 'lib/clacky/agent_config.rb', line 953 def @session_model_overlay && @session_model_overlay["model"] end |
#set_default_model_by_id(id) ⇒ Boolean
Set the global default model marker (‘type: “default”`).
This is separate from ‘switch_model_by_id`:
- `switch_model_by_id` only changes this session's current model.
- `set_default_model_by_id` mutates the shared `@models` array by
moving the `type: "default"` marker to the given model.
Use cases:
- CLI (single-session): when the user picks a model, we both switch
this session AND update the global default so future CLI launches
use the same model. Caller must `save` to persist.
- Web UI Settings save flow: also uses this (via payload).
Do NOT call from per-session model switching in multi-session contexts (Web UI session-level switch), since it would leak into other sessions and change what new sessions start with.
Only one model may carry ‘type: “default”` at a time — this method clears the marker on any other model that had it.
Note: if the target model currently has ‘type: “lite”`, this method will overwrite it with `“default”`. That matches the existing single-slot `type` field semantics in the codebase.
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 |
# File 'lib/clacky/agent_config.rb', line 499 def set_default_model_by_id(id) return false if id.nil? || id.to_s.empty? target = @models.find { |m| m["id"] == id } return false if target.nil? # Clear existing default marker(s) — there should only be one, but # be defensive in case of corrupted config. @models.each do |m| next if m["id"] == id m.delete("type") if m["type"] == "default" end target["type"] = "default" true end |
#set_model_type(index, type) ⇒ Object
Set a model’s type (default, lite, image, video, or audio). At most one model carries each type at a time. Returns true if successful
988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 |
# File 'lib/clacky/agent_config.rb', line 988 def set_model_type(index, type) return false if index < 0 || index >= @models.length return false unless ["default", "lite", "image", "video", "audio", nil].include?(type) if type # Remove type from any other model that has it @models.each do |m| m.delete("type") if m["type"] == type end # Set type on target model @models[index]["type"] = type else # Remove type from target model @models[index].delete("type") end true end |
#switch_model_by_id(id) ⇒ Boolean
Switch the current session to a specific model, identified by its stable runtime id.
This is a per-session operation:
- Updates this AgentConfig's `@current_model_id` (primary truth)
- Updates `@current_model_index` for back-compat observers
- Does NOT mutate the shared `@models` array's `type: "default"`
marker. The "default model" is a global setting (initial model
for new sessions) and is only changed via the Settings UI
"save config" flow (`api_save_config`).
441 442 443 444 445 446 447 448 449 450 451 452 453 454 |
# File 'lib/clacky/agent_config.rb', line 441 def switch_model_by_id(id) return false if id.nil? || id.to_s.empty? index = @models.find_index { |m| m["id"] == id } return false if index.nil? previous_id = @current_model_id @current_model_id = id @current_model_index = index @session_model_overlay = nil if previous_id != id true end |
#switch_model_by_name(name) ⇒ Boolean
Switch to a model by its display name (fuzzy match, case-insensitive).
460 461 462 463 464 465 466 467 468 469 470 471 |
# File 'lib/clacky/agent_config.rb', line 460 def switch_model_by_name(name) return false if name.nil? || name.to_s.strip.empty? name_str = name.to_s.strip.downcase index = @models.find_index { |m| m["model"].to_s.downcase == name_str } return false if index.nil? @current_model_id = @models[index]["id"] @current_model_index = index true end |
#to_yaml ⇒ Object
Serialize the current agent configuration to YAML. Outputs a hash with “settings” and “models” keys (new format). Backward compatibility: old flat-array format is still readable by .load.
398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/clacky/agent_config.rb', line 398 def to_yaml persistable_models = @models.reject { |m| m["auto_injected"] }.map do |m| m.reject { |k, _| RUNTIME_ONLY_FIELDS.include?(k) } end settings = { "enable_compression" => @enable_compression, "enable_prompt_caching" => @enable_prompt_caching, "memory_update_enabled" => @memory_update_enabled, "skill_evolution" => @skill_evolution, "max_running_agents" => @max_running_agents, "max_idle_agents" => @max_idle_agents, "default_working_dir" => @default_working_dir } YAML.dump("settings" => settings, "models" => persistable_models) end |
#virtual_model_overlay ⇒ Hash?
Returns the active overlay (read-only view; dup before mutating).
923 924 925 |
# File 'lib/clacky/agent_config.rb', line 923 def @virtual_model_overlay end |