openclacky 1.2.6 → 1.2.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a71c34a2e50f2202679cafd9c31fe7807ea5e9a6a6fad42ba352e19f9ea01282
4
- data.tar.gz: 3baa2b1aed288586bec516576bf8729b630392e4e4b9aeb3b65bca704060b3d1
3
+ metadata.gz: 8057fdabcb077c8ca378fcbbc0efa442abc6b9664464d2d8d1b737e0206d2ed9
4
+ data.tar.gz: 657f282a20664ef793d7ff663e963739f5fbdb478b8e174df9c5e459ec09231b
5
5
  SHA512:
6
- metadata.gz: e334e6f5e8726d02933c60d69aed8da4b98f417b1f7d0e0676b3d58bc8a15f9214317a5cde6f3ef8094f69e1fdff8cee6fb822beb076c0eef9e5544770256029
7
- data.tar.gz: 76c8e019789e513382079f942dd7d4036dd68f7b6dac26822e6ec4e0dda410e8d2ad55b1ebaabd58ad388fb2cafa4fa1cb3350fc9a5673fbbb9120893c98ffd6
6
+ metadata.gz: fad3e045271032a1150745f1d8531aeed24ea376efdcd99346aeaeec4eb5f1edec187e9dbe38ee8d19e5a9cf599bfb7e5431ad55e5993f1dd243bb4ebf5faa2d
7
+ data.tar.gz: 95b1a7ec783b459f5c70b99d3535f5a0054d4a8c72d855d9c98b9771cde9d59cb33dcf609be844fade932e1487c82c525cf4354438c5ad4b271494a4166dc729
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.2.7] - 2026-06-01
9
+
10
+ ### Added
11
+ - Session workspace file explorer — browse and download files in Web UI sidebar
12
+ - Channel manager: new, clear, and skill commands
13
+ - Top-up link in Web UI under billing section
14
+ - Sub-model switching within active sessions
15
+ - Claude 4.8 model support
16
+ - Connection test improvements for custom Claude models
17
+
18
+ ### Fixed
19
+ - Nil error in compress top match parsing
20
+ - Empty 0-token responses causing agent stalls (close #218)
21
+ - Brand config built-in skills loaded without MANIFEST.enc.json (C-5627)
22
+ - Terminal .exe stdin isolation at command-wrap on WSL (close #221)
23
+ - Overly verbose builtin skills installed message during onboarding
24
+
25
+ ### More
26
+ - Add Docker installation section to README
27
+
8
28
  ## [1.2.6] - 2026-05-29
9
29
 
10
30
  ### Added
data/README.md CHANGED
@@ -108,6 +108,40 @@ gem install openclacky
108
108
 
109
109
  see more: https://www.openclacky.com/docs/installation
110
110
 
111
+ ### Docker
112
+
113
+ Build:
114
+
115
+ ```bash
116
+ git clone https://github.com/clacky-ai/openclacky.git
117
+ cd openclacky
118
+ docker build -t openclacky .
119
+ ```
120
+
121
+ **Linux:**
122
+
123
+ ```bash
124
+ docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
125
+ ```
126
+
127
+ `--network=host` is required so the agent inside the container can reach Chrome's remote debugging port running on the host.
128
+
129
+ **macOS / Windows:**
130
+
131
+ ```bash
132
+ docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
133
+ ```
134
+
135
+ > **Note:** On macOS/Windows, `--network=host` is not supported — browser automation may be limited.
136
+
137
+ Open **http://localhost:7070** after starting.
138
+
139
+ Environment variables:
140
+
141
+ | Variable | Description |
142
+ |---|---|
143
+ | `CLACKY_ACCESS_KEY` | Protect the Web UI with an access key (empty = public mode) |
144
+
111
145
 
112
146
  ## Quick Start
113
147
 
data/README_CN.md CHANGED
@@ -104,6 +104,40 @@ gem install openclacky
104
104
 
105
105
  详见:https://www.openclacky.com/docs/installation
106
106
 
107
+ ### Docker
108
+
109
+ 构建:
110
+
111
+ ```bash
112
+ git clone https://github.com/clacky-ai/openclacky.git
113
+ cd openclacky
114
+ docker build -t openclacky .
115
+ ```
116
+
117
+ **Linux:**
118
+
119
+ ```bash
120
+ docker run -d --network=host -e CLACKY_ACCESS_KEY="" openclacky
121
+ ```
122
+
123
+ `--network=host` 使容器与宿主机共享网络栈,Agent 可直接访问宿主机上运行的 Chrome 远程调试端口。
124
+
125
+ **macOS / Windows:**
126
+
127
+ ```bash
128
+ docker run -d -p 7070:7070 -e CLACKY_ACCESS_KEY="" openclacky
129
+ ```
130
+
131
+ > **注意:** macOS/Windows 不支持 `--network=host`,浏览器自动化功能可能受限。
132
+
133
+ 启动后访问 **http://localhost:7070**。
134
+
135
+ 环境变量:
136
+
137
+ | 变量 | 说明 |
138
+ |---|---|
139
+ | `CLACKY_ACCESS_KEY` | 设置访问密钥保护 Web UI(留空 = 公开模式) |
140
+
107
141
  ## 快速开始
108
142
 
109
143
  ### 终端(CLI)
@@ -177,7 +177,13 @@ module Clacky
177
177
  else
178
178
  total_tokens - @previous_total_tokens
179
179
  end
180
- @previous_total_tokens = total_tokens # Update for next iteration
180
+
181
+ # Guard: do NOT overwrite @previous_total_tokens with 0 when the upstream
182
+ # returns missing/zero usage (observed when history overflows the model
183
+ # context: response comes back as content="" + finish_reason="stop" +
184
+ # zero usage). Resetting to 0 would disable the compression trigger on
185
+ # subsequent turns and poison the session permanently.
186
+ @previous_total_tokens = total_tokens if total_tokens > 0
181
187
 
182
188
  {
183
189
  delta_tokens: delta_tokens,
@@ -137,7 +137,8 @@ module Clacky
137
137
  # Returns the topics string if found, nil otherwise.
138
138
  # e.g. "<topics>Rails setup, database config</topics>" → "Rails setup, database config"
139
139
  def parse_topics(content)
140
- m = content.match(/<topics>(.*?)<\/topics>/m)
140
+ return nil if content.nil? || content.to_s.empty?
141
+ m = content.to_s.match(/<topics>(.*?)<\/topics>/m)
141
142
  m ? m[1].strip : nil
142
143
  end
143
144
 
@@ -124,8 +124,12 @@ module Clacky
124
124
  # Check if compression is enabled
125
125
  return nil unless @config.enable_compression
126
126
 
127
- # Use actual API-reported tokens from last request
128
- total_tokens = @previous_total_tokens
127
+ # Use the larger of: API-reported tokens from last response, or current
128
+ # estimated history size. The estimate guards the case where a single
129
+ # huge tool result was just appended after the last API call — without
130
+ # this, the next request can ship the bloated history before any
131
+ # compression triggers (issue #218).
132
+ total_tokens = [@previous_total_tokens, @history.estimate_tokens].max
129
133
  message_count = @history.size
130
134
 
131
135
  # Force compression (for idle compression) - use lower threshold
@@ -80,12 +80,28 @@ module Clacky
80
80
  end
81
81
  end
82
82
 
83
+ # Re-apply the per-session sub-model pin (if any). Done AFTER
84
+ # switch_model_by_id so the overlay isn't cleared by the card switch
85
+ # invariant. Validation happens at write-time (the WebUI/API enforces
86
+ # the name belongs to the card's provider) — at restore-time we trust
87
+ # what we previously wrote.
88
+ saved_sub_model = session_data.dig(:config, :sub_model)
89
+ if saved_sub_model && !saved_sub_model.to_s.empty?
90
+ set_session_sub_model(saved_sub_model)
91
+ end
92
+
83
93
  # Rebuild and refresh the system prompt so any newly installed skills
84
94
  # (or other configuration changes since the session was saved) are
85
95
  # reflected immediately — without requiring the user to create a new session.
86
96
  refresh_system_prompt
87
97
  end
88
98
 
99
+ private def persisted_card_field(key)
100
+ card_id = @config.current_model_id
101
+ return nil unless card_id
102
+ @config.models.find { |m| m["id"] == card_id }&.dig(key)
103
+ end
104
+
89
105
  # Generate session data for saving
90
106
  # @param status [Symbol] Status of the last task: :success, :error, or :interrupted
91
107
  # @param error_message [String] Error message if status is :error
@@ -134,10 +150,13 @@ module Clacky
134
150
  reasoning_effort: @reasoning_effort,
135
151
  # Persist the current model identity so the session can restore its
136
152
  # original model on restart. model_name + model_base_url form a
137
- # composite key to avoid matching a different provider's model of
138
- # the same name. Falls back to default if the model no longer exists.
139
- model_name: @config.current_model&.dig("model"),
140
- model_base_url: @config.current_model&.dig("base_url")
153
+ # composite key that points at the underlying card (NOT the
154
+ # sub-model overlay) overlays are layered on top via :sub_model
155
+ # below so card lookup stays stable when the user toggles
156
+ # sub-models.
157
+ model_name: persisted_card_field("model"),
158
+ model_base_url: persisted_card_field("base_url"),
159
+ sub_model: @config.session_model_overlay_name
141
160
  },
142
161
  stats: stats_data,
143
162
  messages: @history.to_a
data/lib/clacky/agent.rb CHANGED
@@ -172,6 +172,18 @@ module Clacky
172
172
  true
173
173
  end
174
174
 
175
+ # Pin this session to a sub-model name without changing its underlying
176
+ # card (credentials / base_url stay put). Pass nil or "" to clear and
177
+ # fall back to the card's default model. Validation that the name is
178
+ # listed under the current provider is the caller's job.
179
+ # @param model_name [String, nil]
180
+ # @return [Boolean]
181
+ def set_session_sub_model(model_name)
182
+ @config.session_model_overlay = model_name
183
+ rebuild_client_for_current_model!
184
+ true
185
+ end
186
+
175
187
  # Rebuild the underlying Client (and dependent components) to pick up
176
188
  # credentials/model name from the currently-selected model in @config.
177
189
  private def rebuild_client_for_current_model!
@@ -206,10 +218,16 @@ module Clacky
206
218
  model = @config.current_model
207
219
  return nil unless model
208
220
 
221
+ card_id = @config.current_model_id
222
+ base_entry = card_id ? @config.models.find { |m| m["id"] == card_id } : nil
223
+ sub_model = @config.session_model_overlay_name
224
+
209
225
  {
210
226
  id: model["id"],
211
227
  model: model["model"],
212
- base_url: model["base_url"]
228
+ base_url: model["base_url"],
229
+ card_model: base_entry&.dig("model"),
230
+ sub_model: sub_model
213
231
  }
214
232
  end
215
233
 
@@ -1019,7 +1037,10 @@ module Clacky
1019
1037
  check_stale!
1020
1038
 
1021
1039
  formatted_messages = @client.format_tool_results(response, tool_results, model: current_model)
1022
- formatted_messages.each { |msg| @history.append(msg.merge(task_id: @current_task_id)) }
1040
+ formatted_messages.each do |msg|
1041
+ truncated = truncate_oversized_tool_content(msg)
1042
+ @history.append(truncated.merge(task_id: @current_task_id))
1043
+ end
1023
1044
 
1024
1045
  # Append a follow-up `role:"user"` message for any image payloads that
1025
1046
  # could not be delivered inside the tool message.
@@ -1055,6 +1076,26 @@ module Clacky
1055
1076
  end
1056
1077
  end
1057
1078
 
1079
+ # Cap oversized tool result content to keep a single tool message from
1080
+ # blowing up the prompt budget (issue #218: a 7350-path glob produced a
1081
+ # ~890k-char result that pushed history past the model context window
1082
+ # and poisoned the session). Only string content is truncated — Array
1083
+ # content (multipart/image blocks) is left alone since image payloads
1084
+ # are handled by the image_inject path above.
1085
+ MAX_TOOL_RESULT_CHARS = 80_000
1086
+
1087
+ private def truncate_oversized_tool_content(msg)
1088
+ content = msg[:content]
1089
+ return msg unless content.is_a?(String) && content.length > MAX_TOOL_RESULT_CHARS
1090
+
1091
+ original_len = content.length
1092
+ head = content[0, MAX_TOOL_RESULT_CHARS]
1093
+ truncated = head + "\n\n[Tool result truncated: #{original_len} chars total, " \
1094
+ "showing first #{MAX_TOOL_RESULT_CHARS}. Use a more specific query/limit, " \
1095
+ "or read the raw output via file_reader/grep on the underlying source.]"
1096
+ msg.merge(content: truncated)
1097
+ end
1098
+
1058
1099
  # Enqueue an inline skill injection to be flushed after observe().
1059
1100
  # Called by InvokeSkill#execute to avoid injecting during tool execution,
1060
1101
  # which would break Bedrock's toolUse/toolResult pairing requirement.
@@ -211,6 +211,14 @@ module Clacky
211
211
  # Keys honored: "api_key", "base_url", "model", "anthropic_format".
212
212
  # @return [Hash, nil]
213
213
  @virtual_model_overlay = options[:virtual_model_overlay]
214
+
215
+ # Per-session sub-model override. Persists across restarts via the
216
+ # session file. Independent of @virtual_model_overlay (which is for
217
+ # short-lived subagent forks). Used by the WebUI sub-model switcher
218
+ # to pin a session to e.g. "dsk-deepseek-v4-pro" while the underlying
219
+ # card still says "abs-claude-sonnet-4-6". Only the "model" key is
220
+ # honored — sub-model switching never changes credentials.
221
+ @session_model_overlay = options[:session_model_overlay]
214
222
  end
215
223
 
216
224
  # Load configuration from file
@@ -354,6 +362,9 @@ module Clacky
354
362
  if @virtual_model_overlay
355
363
  copy.instance_variable_set(:@virtual_model_overlay, @virtual_model_overlay.dup)
356
364
  end
365
+ if @session_model_overlay
366
+ copy.instance_variable_set(:@session_model_overlay, @session_model_overlay.dup)
367
+ end
357
368
  copy
358
369
  end
359
370
 
@@ -431,9 +442,12 @@ module Clacky
431
442
  index = @models.find_index { |m| m["id"] == id }
432
443
  return false if index.nil?
433
444
 
445
+ previous_id = @current_model_id
434
446
  @current_model_id = id
435
447
  @current_model_index = index
436
448
 
449
+ @session_model_overlay = nil if previous_id != id
450
+
437
451
  true
438
452
  end
439
453
 
@@ -760,14 +774,18 @@ module Clacky
760
774
  resolved = resolve_current_model_entry
761
775
  return nil unless resolved
762
776
 
763
- # If a virtual overlay is active (e.g. subagent running on lite-model
764
- # credentials), return a *merged copy* so callers see the overlay fields
765
- # but the shared @models hash is never mutated.
777
+ # Merge order (low high): base entry, session-level sub-model override,
778
+ # then short-lived subagent overlay. Both layers are kept separate so
779
+ # a subagent fork can stack its own credentials on top of an active
780
+ # sub-model pin without erasing it.
781
+ merged = resolved
782
+ if @session_model_overlay && !@session_model_overlay.empty?
783
+ merged = merged.merge(@session_model_overlay)
784
+ end
766
785
  if @virtual_model_overlay && !@virtual_model_overlay.empty?
767
- resolved.merge(@virtual_model_overlay)
768
- else
769
- resolved
786
+ merged = merged.merge(@virtual_model_overlay)
770
787
  end
788
+ merged.equal?(resolved) ? resolved : merged
771
789
  end
772
790
 
773
791
  # Internal: resolve the current model entry from @models (no overlay).
@@ -822,6 +840,36 @@ module Clacky
822
840
  @virtual_model_overlay
823
841
  end
824
842
 
843
+ # Apply a session-level sub-model override. Lives on this AgentConfig
844
+ # only (each session deep_copy's its own scalar ivar) and survives a
845
+ # restart through the session file. Pass nil or "" to clear.
846
+ #
847
+ # The override only rewrites the resolved current_model's "model" field —
848
+ # api_key / base_url / anthropic_format come from the underlying card,
849
+ # so the user's credentials and provider identity are untouched.
850
+ #
851
+ # @param model_name [String, nil] sub-model name, e.g. "dsk-deepseek-v4-pro"
852
+ def session_model_overlay=(model_name)
853
+ if model_name.nil? || model_name.to_s.strip.empty?
854
+ @session_model_overlay = nil
855
+ else
856
+ @session_model_overlay = { "model" => model_name.to_s.strip }
857
+ end
858
+ end
859
+
860
+ # @return [Hash, nil] the active session sub-model overlay
861
+ def session_model_overlay
862
+ @session_model_overlay
863
+ end
864
+
865
+ # Convenience accessor: the sub-model name currently pinned on this
866
+ # session, or nil when no override is active. Used by serializers and
867
+ # the WebUI to surface "card · sub-model" two-line displays.
868
+ # @return [String, nil]
869
+ def session_model_overlay_name
870
+ @session_model_overlay && @session_model_overlay["model"]
871
+ end
872
+
825
873
  # Query whether the *current* model supports a given capability.
826
874
  #
827
875
  # This is the single entry-point callers (Agent, downgrade pipeline, UI)
@@ -783,12 +783,6 @@ module Clacky
783
783
 
784
784
  FileUtils.rm_f(tmp_zip)
785
785
 
786
- if encrypted
787
- manifest_path = File.join(dest_dir, "MANIFEST.enc.json")
788
- raise "MANIFEST.enc.json missing after extraction" unless File.exist?(manifest_path)
789
- JSON.parse(File.read(manifest_path))
790
- end
791
-
792
786
  record_installed_skill(slug, version, skill_info["description"],
793
787
  encrypted: encrypted,
794
788
  description_zh: skill_info["description_zh"],
data/lib/clacky/cli.rb CHANGED
@@ -559,7 +559,8 @@ module Clacky
559
559
  else
560
560
  error_message = format_error(exception)
561
561
  session_manager&.save(agent.to_session_data(status: :error, error_message: error_message))
562
- ui_controller.show_error("Error: #{exception.message}")
562
+ code = exception.is_a?(Clacky::InsufficientCreditError) ? exception.error_code : nil
563
+ ui_controller.show_error("Error: #{exception.message}", code: code)
563
564
  end
564
565
  end
565
566
 
data/lib/clacky/client.rb CHANGED
@@ -301,7 +301,12 @@ module Clacky
301
301
  end
302
302
  end
303
303
 
304
- raise_error(response) unless response.status == 200
304
+ unless response.status == 200
305
+ recovered_body = response.body.to_s
306
+ recovered_body = sse_buf.to_s if recovered_body.empty?
307
+ recovered = Struct.new(:status, :body).new(response.status, recovered_body)
308
+ raise_error(recovered)
309
+ end
305
310
  MessageFormat::Anthropic.parse_response(aggregator.to_h)
306
311
  end
307
312
 
@@ -536,13 +541,23 @@ module Clacky
536
541
  def raise_error(response)
537
542
  error_body = JSON.parse(response.body) rescue nil
538
543
  error_message = extract_error_message(error_body, response.body)
544
+ error_code = extract_error_code(error_body)
539
545
 
540
546
  Clacky::Logger.warn("client.raise_error",
541
547
  status: response.status,
542
548
  body: response.body.to_s[0, 2000],
543
- error_message: error_message.to_s[0, 500]
549
+ error_message: error_message.to_s[0, 500],
550
+ error_code: error_code
544
551
  )
545
552
 
553
+ if error_code == "insufficient_credit" || response.status == 402
554
+ raise InsufficientCreditError.new(
555
+ "[LLM] Insufficient credit: #{error_message}",
556
+ error_code: "insufficient_credit",
557
+ provider_id: @provider_id
558
+ )
559
+ end
560
+
546
561
  case response.status
547
562
  when 400
548
563
  # Well-behaved APIs (Anthropic, OpenAI) never put quota/availability issues in 400.
@@ -557,7 +572,6 @@ module Clacky
557
572
  # broken message is not replayed on the next user turn.
558
573
  raise BadRequestError, "[LLM] Client request error: #{error_message}"
559
574
  when 401 then raise AgentError, "[LLM] Invalid API key"
560
- when 402 then raise AgentError, "[LLM] Billing or payment issue (possibly out of credits): #{error_message}"
561
575
  when 403 then raise AgentError, "[LLM] Access denied: #{error_message}"
562
576
  when 404 then raise AgentError, "[LLM] API endpoint not found: #{error_message}"
563
577
  when 429 then raise RetryableError, "[LLM] Rate limit exceeded, please wait a moment"
@@ -574,6 +588,13 @@ module Clacky
574
588
  end
575
589
  end
576
590
 
591
+ private def extract_error_code(error_body)
592
+ return nil unless error_body.is_a?(Hash)
593
+ err = error_body["error"]
594
+ return err["code"] if err.is_a?(Hash) && err["code"].is_a?(String)
595
+ nil
596
+ end
597
+
577
598
  def extract_error_message(error_body, raw_body)
578
599
  if raw_body.is_a?(String) && raw_body.strip.start_with?("<!DOCTYPE", "<html")
579
600
  return "Invalid API endpoint or server error (received HTML instead of JSON)"
@@ -216,8 +216,8 @@ Silently run `ruby "SKILL_DIR/scripts/install_builtin_skills.rb"`,
216
216
  then parse the last stdout line as JSON and read `installed` as N.
217
217
 
218
218
  - If N > 0, show one line:
219
- - zh: `✅ 已为你内置 N 个技能,输入 /skills 随时查看。`
220
- - en: `✅ Installed N builtin skills. Type /skills anytime to view them.`
219
+ - zh: `✅ 已为你内置 N 个技能。`
220
+ - en: `✅ Installed N builtin skills.`
221
221
 
222
222
  ### A.10. Import external skills (optional)
223
223
 
@@ -101,8 +101,11 @@ module Clacky
101
101
  emit("warning", message: message)
102
102
  end
103
103
 
104
- def show_error(message)
105
- emit("error", message: message)
104
+ def show_error(message, code: nil, top_up_url: nil)
105
+ payload = { message: message }
106
+ payload[:code] = code if code
107
+ payload[:top_up_url] = top_up_url if top_up_url
108
+ emit("error", **payload)
106
109
  end
107
110
 
108
111
  def show_success(message)
@@ -117,7 +117,7 @@ module Clacky
117
117
  puts_line("[warn] #{message}")
118
118
  end
119
119
 
120
- def show_error(message)
120
+ def show_error(message, code: nil, top_up_url: nil)
121
121
  puts_line("[error] #{message}")
122
122
  end
123
123
 
@@ -31,6 +31,7 @@ module Clacky
31
31
  "api" => "bedrock",
32
32
  "default_model" => "abs-claude-sonnet-4-6",
33
33
  "models" => [
34
+ "abs-claude-opus-4-8",
34
35
  "abs-claude-opus-4-7",
35
36
  "abs-claude-opus-4-6",
36
37
  "abs-claude-sonnet-4-6",
@@ -61,6 +62,7 @@ module Clacky
61
62
  # sibling wired up (yet) on this provider; subagents using the
62
63
  # Gemini default will just reuse it for lite work until we add one.
63
64
  "lite_models" => {
65
+ "abs-claude-opus-4-8" => "abs-claude-haiku-4-5",
64
66
  "abs-claude-opus-4-7" => "abs-claude-haiku-4-5",
65
67
  "abs-claude-opus-4-6" => "abs-claude-haiku-4-5",
66
68
  "abs-claude-sonnet-4-6" => "abs-claude-haiku-4-5",
@@ -88,6 +90,7 @@ module Clacky
88
90
  # ID manually; this list only seeds the picker.
89
91
  "models" => [
90
92
  "anthropic/claude-sonnet-4-6",
93
+ "anthropic/claude-opus-4-8",
91
94
  "anthropic/claude-opus-4-7",
92
95
  "anthropic/claude-opus-4-6",
93
96
  "anthropic/claude-haiku-4-5",
@@ -101,6 +104,7 @@ module Clacky
101
104
  # cheap/fast sidekick automatically.
102
105
  "lite_models" => {
103
106
  "anthropic/claude-sonnet-4-6" => "anthropic/claude-haiku-4-5",
107
+ "anthropic/claude-opus-4-8" => "anthropic/claude-haiku-4-5",
104
108
  "anthropic/claude-opus-4-7" => "anthropic/claude-haiku-4-5",
105
109
  "anthropic/claude-opus-4-6" => "anthropic/claude-haiku-4-5",
106
110
  "openai/gpt-5.5" => "openai/gpt-5.4-mini",
@@ -232,8 +236,8 @@ module Clacky
232
236
  "name" => "Anthropic (Claude)",
233
237
  "base_url" => "https://api.anthropic.com",
234
238
  "api" => "anthropic-messages",
235
- "default_model" => "claude-sonnet-4.6",
236
- "models" => ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4.6", "claude-haiku-4.5"],
239
+ "default_model" => "claude-sonnet-4-6",
240
+ "models" => ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"],
237
241
  "website_url" => "https://console.anthropic.com/settings/keys"
238
242
  }.freeze,
239
243
 
@@ -545,6 +549,11 @@ module Clacky
545
549
  return "openclacky"
546
550
  end
547
551
 
552
+ if base_url.is_a?(String) &&
553
+ base_url.match?(%r{\Ahttps?://(localhost|127\.0\.0\.1|0\.0\.0\.0)(:|/|\z)}i)
554
+ return "openclacky"
555
+ end
556
+
548
557
  nil
549
558
  end
550
559