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
- 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.
-
#enable_compression ⇒ Object
Returns the value of attribute enable_compression.
-
#enable_prompt_caching ⇒ Object
Returns the value of attribute enable_prompt_caching.
-
#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.
-
#base_url ⇒ Object
Get base URL for current model.
-
#base_url=(value) ⇒ Object
Set base URL for current model.
-
#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.
-
#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.
-
#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_type(type) ⇒ Object
Find model by type (default or lite) 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
Get the lite model (type: lite) Returns nil if no lite model configured.
-
#maybe_start_probing ⇒ Object
Called at the start of every call_llm.
-
#model_name ⇒ Object
Get model name for current model.
-
#model_name=(value) ⇒ Object
Set model name for current model.
-
#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
-
#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 or lite) Ensures only one model has each type Returns true if successful.
-
#switch_model_by_id(id) ⇒ Boolean
Switch the current session to a specific model, identified by its stable runtime id.
- #to_yaml ⇒ Object
Constructor Details
#initialize(options = {}) ⇒ AgentConfig
Returns a new instance of AgentConfig.
159 160 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 |
# File 'lib/clacky/agent_config.rb', line 159 def initialize( = {}) @permission_mode = ([:permission_mode]) @max_tokens = [:max_tokens] || 8192 @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" } 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 |
#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_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
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 223 224 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 |
# File 'lib/clacky/agent_config.rb', line 196 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 # 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 when: # 1. A default model exists # 2. No lite model is configured yet (neither in file nor env) # 3. The default model's provider has a known lite_model # The injected lite model is runtime-only (not persisted to config.yml) inject_provider_lite_model(models) # 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"] new(models: models, current_model_index: default_index, current_model_id: default_id) 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.
528 529 530 531 532 |
# File 'lib/clacky/agent_config.rb', line 528 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
478 479 480 481 482 483 484 485 486 487 |
# File 'lib/clacky/agent_config.rb', line 478 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
468 469 470 |
# File 'lib/clacky/agent_config.rb', line 468 def anthropic_format? current_model&.dig("anthropic_format") || false end |
#api_key ⇒ Object
Get API key for current model
435 436 437 |
# File 'lib/clacky/agent_config.rb', line 435 def api_key current_model&.dig("api_key") end |
#api_key=(value) ⇒ Object
Set API key for current model
440 441 442 443 |
# File 'lib/clacky/agent_config.rb', line 440 def api_key=(value) return unless current_model current_model["api_key"] = value end |
#base_url ⇒ Object
Get base URL for current model
446 447 448 |
# File 'lib/clacky/agent_config.rb', line 446 def base_url current_model&.dig("base_url") end |
#base_url=(value) ⇒ Object
Set base URL for current model
451 452 453 454 |
# File 'lib/clacky/agent_config.rb', line 451 def base_url=(value) return unless current_model current_model["base_url"] = value end |
#bedrock? ⇒ Boolean
Check if current model uses Bedrock Converse API (ABSK key prefix or abs- model prefix)
473 474 475 |
# File 'lib/clacky/agent_config.rb', line 473 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.
549 550 551 552 553 554 555 |
# File 'lib/clacky/agent_config.rb', line 549 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)
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 |
# File 'lib/clacky/agent_config.rb', line 588 def current_model return nil if @models.empty? if @current_model_id m = @models.find { |mm| mm["id"] == @current_model_id } return m if m # id no longer exists (model was deleted). Fall through to other # resolution strategies below, and clear the stale id. @current_model_id = nil end default_model = find_model_by_type("default") if default_model # Opportunistically re-anchor to this default's id so subsequent # lookups are O(1) and survive list reordering. @current_model_id = default_model["id"] return default_model end # Fallback to index-based for backward compatibility m = @models[@current_model_index] @current_model_id = m["id"] if m m 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.
320 321 322 323 324 |
# File 'lib/clacky/agent_config.rb', line 320 def deep_copy # dup gives us a new AgentConfig with independent scalar ivars but # the same @models reference — exactly what we want. dup end |
#default_model ⇒ Object
Get the default model (type: default) Falls back to current_model for backward compatibility
497 498 499 |
# File 'lib/clacky/agent_config.rb', line 497 def default_model find_model_by_type("default") || current_model 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)
572 573 574 575 576 577 578 579 580 |
# File 'lib/clacky/agent_config.rb', line 572 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).
559 560 561 |
# File 'lib/clacky/agent_config.rb', line 559 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.
515 516 517 518 519 520 521 522 523 |
# File 'lib/clacky/agent_config.rb', line 515 def fallback_model_for(model_name) m = current_model return nil unless m provider_id = Clacky::Providers.find_by_base_url(m["base_url"]) return nil unless provider_id Clacky::Providers.fallback_model(provider_id, model_name) end |
#find_model_by_type(type) ⇒ Object
Find model by type (default or lite) Returns the model hash or nil if not found
491 492 493 |
# File 'lib/clacky/agent_config.rb', line 491 def find_model_by_type(type) @models.find { |m| m["type"] == type } end |
#get_model(index) ⇒ Object
Get model by index
357 358 359 |
# File 'lib/clacky/agent_config.rb', line 357 def get_model(index) @models[index] end |
#lite_model ⇒ Object
Get the lite model (type: lite) Returns nil if no lite model configured
503 504 505 |
# File 'lib/clacky/agent_config.rb', line 503 def lite_model find_model_by_type("lite") 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.
538 539 540 541 542 543 |
# File 'lib/clacky/agent_config.rb', line 538 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 |
#model_name ⇒ Object
Get model name for current model
457 458 459 |
# File 'lib/clacky/agent_config.rb', line 457 def model_name current_model&.dig("model") end |
#model_name=(value) ⇒ Object
Set model name for current model
462 463 464 465 |
# File 'lib/clacky/agent_config.rb', line 462 def model_name=(value) return unless current_model current_model["model"] = value end |
#model_names ⇒ Object
List all model names
430 431 432 |
# File 'lib/clacky/agent_config.rb', line 430 def model_names @models.map { |m| m["model"] } end |
#models_configured? ⇒ Boolean
Check if any model is configured
348 349 350 |
# File 'lib/clacky/agent_config.rb', line 348 def models_configured? !@models.empty? && !current_model.nil? end |
#probing? ⇒ Boolean
Returns true only when we are silently probing the primary model.
564 565 566 |
# File 'lib/clacky/agent_config.rb', line 564 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
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 |
# File 'lib/clacky/agent_config.rb', line 640 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
326 327 328 329 330 331 |
# File 'lib/clacky/agent_config.rb', line 326 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 |
#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.
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/clacky/agent_config.rb', line 412 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 or lite) Ensures only one model has each type Returns true if successful
618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 |
# File 'lib/clacky/agent_config.rb', line 618 def set_model_type(index, type) return false if index < 0 || index >= @models.length return false unless ["default", "lite", 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`).
374 375 376 377 378 379 380 381 382 383 384 |
# File 'lib/clacky/agent_config.rb', line 374 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? @current_model_id = id @current_model_index = index true end |
#to_yaml ⇒ Object
340 341 342 343 344 345 |
# File 'lib/clacky/agent_config.rb', line 340 def to_yaml persistable = @models.reject { |m| m["auto_injected"] }.map do |m| m.reject { |k, _| RUNTIME_ONLY_FIELDS.include?(k) } end YAML.dump(persistable) end |