openclacky 1.5.20 → 1.5.21

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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +18 -0
  4. data/lib/clacky/agent/chunk_index.rb +8 -1
  5. data/lib/clacky/agent/feedback.rb +30 -0
  6. data/lib/clacky/agent/history_navigation.rb +17 -3
  7. data/lib/clacky/agent/hook_manager.rb +6 -1
  8. data/lib/clacky/agent/llm_caller.rb +1 -0
  9. data/lib/clacky/agent/message_compressor_helper.rb +28 -2
  10. data/lib/clacky/agent/session_serializer.rb +20 -2
  11. data/lib/clacky/agent.rb +56 -43
  12. data/lib/clacky/agent_config.rb +82 -4
  13. data/lib/clacky/billing/platform_billing.rb +5 -1
  14. data/lib/clacky/brand_config.rb +1 -1
  15. data/lib/clacky/cli.rb +3 -2
  16. data/lib/clacky/client.rb +5 -5
  17. data/lib/clacky/deepseek_web_tools.rb +156 -0
  18. data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
  19. data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +21 -1
  20. data/lib/clacky/extension/loader.rb +5 -3
  21. data/lib/clacky/locales/en.rb +5 -5
  22. data/lib/clacky/locales/zh.rb +5 -5
  23. data/lib/clacky/message_format/anthropic.rb +20 -0
  24. data/lib/clacky/message_format/open_ai.rb +31 -14
  25. data/lib/clacky/message_history.rb +21 -3
  26. data/lib/clacky/platform_http_client.rb +6 -2
  27. data/lib/clacky/providers.rb +23 -14
  28. data/lib/clacky/search_config.rb +47 -13
  29. data/lib/clacky/server/browser_manager.rb +35 -5
  30. data/lib/clacky/server/channel/channel_manager.rb +6 -6
  31. data/lib/clacky/server/discover.rb +5 -4
  32. data/lib/clacky/server/http_server.rb +194 -47
  33. data/lib/clacky/server/model_prices.rb +5 -3
  34. data/lib/clacky/server/pid_files.rb +25 -0
  35. data/lib/clacky/server/server_master.rb +9 -3
  36. data/lib/clacky/server/session_registry.rb +19 -7
  37. data/lib/clacky/server/web_ui_controller.rb +61 -38
  38. data/lib/clacky/session_manager.rb +7 -1
  39. data/lib/clacky/skill.rb +1 -1
  40. data/lib/clacky/tools/ask_user.rb +4 -1
  41. data/lib/clacky/tools/base.rb +3 -2
  42. data/lib/clacky/tools/browser.rb +18 -2
  43. data/lib/clacky/tools/file_reader.rb +1 -1
  44. data/lib/clacky/tools/terminal/session_manager.rb +19 -1
  45. data/lib/clacky/tools/terminal.rb +65 -23
  46. data/lib/clacky/tools/trash_manager.rb +5 -3
  47. data/lib/clacky/tools/web_fetch.rb +81 -22
  48. data/lib/clacky/tools/web_search.rb +45 -4
  49. data/lib/clacky/ui2/ui_controller.rb +1 -1
  50. data/lib/clacky/ui_interface.rb +4 -3
  51. data/lib/clacky/utils/file_processor.rb +51 -0
  52. data/lib/clacky/utils/model_pricing.rb +132 -13
  53. data/lib/clacky/version.rb +1 -1
  54. data/lib/clacky/web/app.css +366 -105
  55. data/lib/clacky/web/components/chat-navigator.js +8 -1
  56. data/lib/clacky/web/components/mentions.js +2 -13
  57. data/lib/clacky/web/components/model-picker.js +3 -1
  58. data/lib/clacky/web/components/onboard.js +13 -10
  59. data/lib/clacky/web/features/archive/store.js +30 -16
  60. data/lib/clacky/web/features/archive/view.js +20 -10
  61. data/lib/clacky/web/features/backup/view.js +5 -5
  62. data/lib/clacky/web/features/new-session/store.js +3 -3
  63. data/lib/clacky/web/features/trash/store.js +10 -7
  64. data/lib/clacky/web/features/trash/view.js +9 -4
  65. data/lib/clacky/web/i18n.js +106 -30
  66. data/lib/clacky/web/index.html +90 -60
  67. data/lib/clacky/web/projects.js +56 -1
  68. data/lib/clacky/web/sessions.js +244 -131
  69. data/lib/clacky/web/settings.js +143 -8
  70. data/lib/clacky/web/ws-dispatcher.js +9 -13
  71. data/lib/clacky.rb +1 -0
  72. metadata +4 -1
@@ -6,6 +6,7 @@ require "uri"
6
6
  require "cgi"
7
7
  require "base64"
8
8
  require_relative "../utils/encoding"
9
+ require_relative "../deepseek_web_tools"
9
10
 
10
11
  module Clacky
11
12
  module Tools
@@ -37,7 +38,9 @@ module Clacky
37
38
  SEARCHER_TIMEOUT = 30
38
39
  PARALLEL_ENDPOINT = "https://search.parallel.ai/mcp"
39
40
  PARALLEL_SNIPPET_MAX_CHARS = 400
40
- PROVIDER_COOLDOWN_SECONDS = 600
41
+ # Shared by every provider, hosted or free: one failure keeps that
42
+ # provider out of the retry path for the whole window.
43
+ PROVIDER_COOLDOWN_SECONDS = 3600
41
44
 
42
45
  def execute(query:, max_results: 10, working_dir: nil)
43
46
  if query.nil? || query.strip.empty?
@@ -62,10 +65,13 @@ module Clacky
62
65
  }
63
66
  rescue StandardError => e
64
67
  # Avoid paying the same network/handshake failure on every search.
65
- if provider == :parallel
68
+ case provider
69
+ when :deepseek
70
+ Clacky::DeepSeekWebTools.mark_unavailable!
71
+ when :parallel
66
72
  reset_parallel_client
67
73
  @parallel_unavailable_until = Time.now + PROVIDER_COOLDOWN_SECONDS
68
- elsif provider == :duckduckgo
74
+ when :duckduckgo
69
75
  @ddg_unavailable_until = Time.now + PROVIDER_COOLDOWN_SECONDS
70
76
  end
71
77
  last_error = e
@@ -90,6 +96,10 @@ module Clacky
90
96
  "Configured search provider '#{name}' failed. #{reason} " \
91
97
  "Check the key and the script at #{Clacky::SearchConfig.script_path}, " \
92
98
  "or reconfigure it in Settings → Search."
99
+ elsif providers.include?(:deepseek)
100
+ "DeepSeek web search failed and no free engine returned results. #{reason} " \
101
+ "Check the API key and balance in Settings → Web Search; DeepSeek will be " \
102
+ "retried in #{PROVIDER_COOLDOWN_SECONDS / 60} minutes."
93
103
  else
94
104
  "All search providers failed. #{reason}"
95
105
  end
@@ -102,13 +112,23 @@ module Clacky
102
112
  # broken key or script look like a low-quality result set.
103
113
  return [:custom] if Clacky::SearchConfig.script_path
104
114
 
105
- PROVIDERS.reject do |provider|
115
+ free_chain = PROVIDERS.reject do |provider|
106
116
  unavailable_until = case provider
107
117
  when :parallel then @parallel_unavailable_until
108
118
  when :duckduckgo then @ddg_unavailable_until
109
119
  end
110
120
  unavailable_until && Time.now < unavailable_until
111
121
  end
122
+
123
+ return free_chain unless Clacky::SearchConfig.deepseek_web?
124
+
125
+ # DeepSeek leads the chain but does not replace the free engines: a
126
+ # missing key, an exhausted balance or an outage should degrade to a
127
+ # working search rather than return nothing.
128
+ return free_chain if Clacky::DeepSeekWebTools.cooling_down?
129
+ return free_chain unless deepseek_web_available?
130
+
131
+ [:deepseek] + free_chain
112
132
  end
113
133
 
114
134
  # ── Parallel Free MCP ──────────────────────────────────────────────────
@@ -171,6 +191,27 @@ module Clacky
171
191
  end.join("\n")
172
192
  end
173
193
 
194
+ # ── DeepSeek hosted search (paid, opt-in) ──────────────────────────────
195
+
196
+ private def deepseek_web_available?
197
+ return false unless agent
198
+
199
+ agent.config.deepseek_web_ready?
200
+ rescue StandardError
201
+ false
202
+ end
203
+
204
+ private def search_deepseek(query, max_results)
205
+ config = agent.config
206
+ key = config.deepseek_web_key
207
+ Clacky::DeepSeekWebTools.search(
208
+ query: query,
209
+ count: max_results,
210
+ api_key: key,
211
+ base_url: config.deepseek_web_base_url(key)
212
+ )
213
+ end
214
+
174
215
  # ── Configured searcher (~/.clacky/searchers/<provider>.rb) ────────────
175
216
 
176
217
  private def search_custom(query, max_results)
@@ -1396,7 +1396,7 @@ module Clacky
1396
1396
  # * the countdown reaches zero with no interaction -> returns :timeout
1397
1397
  # @param seconds [Integer] Countdown duration
1398
1398
  # @return [String, Symbol] feedback text, or :timeout
1399
- def request_feedback_with_countdown(seconds: 10)
1399
+ def request_feedback_with_countdown(seconds: 20)
1400
1400
  theme = ThemeManager.current_theme
1401
1401
 
1402
1402
  queue = Queue.new
@@ -145,9 +145,10 @@ module Clacky
145
145
  # Auto-approve countdown for ask_user. Shows a live countdown
146
146
  # and lets the user press a key to take over and answer. Returns :timeout
147
147
  # when no one intervenes (agent should auto-decide and continue), or a
148
- # feedback string / "" when the user steps in. Non-interactive UIs (web,
149
- # json, channel) have no human watching a TTY, so they default to :timeout.
150
- def request_feedback_with_countdown(seconds: 10)
148
+ # feedback string / "" when the user steps in, or :answered when Web has
149
+ # queued a full answer for steering. Non-interactive UIs (json, channel)
150
+ # default to :timeout.
151
+ def request_feedback_with_countdown(seconds: 20)
151
152
  :timeout
152
153
  end
153
154
 
@@ -226,6 +226,57 @@ module Clacky
226
226
  MIME_TYPES[File.extname(path).downcase] || "application/octet-stream"
227
227
  end
228
228
 
229
+ # Quantize with a frequency-weighted median-cut palette using only chunky_png.
230
+ # Keep the input on failure so the caller retains its existing size handling.
231
+ def self.quantize_png_base64(b64)
232
+ require "base64"
233
+ require "chunky_png"
234
+
235
+ image = ChunkyPNG::Image.from_blob(Base64.strict_decode64(b64))
236
+ counts = Hash.new(0)
237
+ image.pixels.each { |color| counts[color] += 1 }
238
+ if counts.size > 256
239
+ # Cache each box's widest RGBA channel; split until at most 256 remain.
240
+ make_box = lambda do |colors|
241
+ ranges = [24, 16, 8, 0].map do |shift|
242
+ values = colors.map { |color| (color >> shift) & 255 }
243
+ values.max - values.min
244
+ end
245
+ channel = ranges.each_index.max_by { |i| ranges[i] }
246
+ [colors, [24, 16, 8, 0][channel], ranges[channel]]
247
+ end
248
+ boxes = [make_box.call(counts.keys)]
249
+ while boxes.size < 256
250
+ index = boxes.each_index.max_by { |i| boxes[i][2] }
251
+ colors, shift, range = boxes.delete_at(index)
252
+ break if range.zero?
253
+
254
+ colors.sort_by! { |color| (color >> shift) & 255 }
255
+ halfway = colors.sum { |color| counts[color] } / 2.0
256
+ total = 0
257
+ split = colors.index { |color| (total += counts[color]) >= halfway } + 1
258
+ split = [split, colors.size - 1].min
259
+ boxes << make_box.call(colors.take(split))
260
+ boxes << make_box.call(colors.drop(split))
261
+ end
262
+
263
+ mapping = {}
264
+ boxes.each do |colors, _shift, _range|
265
+ weight = colors.sum { |color| counts[color] }
266
+ channels = [24, 16, 8, 0].map do |shift|
267
+ (colors.sum { |color| ((color >> shift) & 255) * counts[color] }.to_f / weight).round
268
+ end
269
+ representative = ChunkyPNG::Color.rgba(*channels)
270
+ colors.each { |color| mapping[color] = representative }
271
+ end
272
+ image.pixels.map! { |color| mapping.fetch(color) }
273
+ end
274
+ Base64.strict_encode64(image.to_blob(color_mode: ChunkyPNG::COLOR_INDEXED))
275
+ rescue StandardError => e
276
+ Clacky::Logger.debug("image_quantization_skipped", format: "png", reason: e.message)
277
+ b64
278
+ end
279
+
229
280
  # Downscale a base64-encoded image so its width is at most max_width pixels.
230
281
  #
231
282
  # Strategy:
@@ -441,6 +441,91 @@ module Clacky
441
441
  }
442
442
  },
443
443
 
444
+ # GPT-6.1 Sol served through the openclacky gateway (oc- prefix, TokHub
445
+ # gpt group) — the same model as abs-gpt-6.1-sol, which is served by
446
+ # Bedrock; the two aliases exist because the lanes are priced differently.
447
+ # OpenAI's own card, tiered at 272K prompt tokens (the model's breakpoint,
448
+ # not the global 200K): above it every bucket doubles.
449
+ "oc-gpt-6.1-sol" => {
450
+ input: {
451
+ default: 2.00, # $2.00/MTok
452
+ over_272k: 4.00 # $4.00/MTok
453
+ },
454
+ output: {
455
+ default: 10.00, # $10.00/MTok
456
+ over_272k: 20.00 # $20.00/MTok
457
+ },
458
+ cache: {
459
+ write_default: 2.50, # $2.50/MTok cache write
460
+ write_over_272k: 5.00,
461
+ read_default: 0.10, # $0.10/MTok cache hit
462
+ read_over_272k: 0.20
463
+ },
464
+ tier_threshold: 272_000
465
+ },
466
+
467
+ # GPT-6 Astra served through the openclacky gateway (oc- prefix, same
468
+ # ciyuan gpt group as oc-gpt-6.1-sol, hence the same 272K breakpoint and
469
+ # the same series discount). Its card is much higher than Sol's.
470
+ "oc-gpt-6-astra" => {
471
+ input: {
472
+ default: 10.00, # $10.00/MTok
473
+ over_272k: 20.00 # $20.00/MTok
474
+ },
475
+ output: {
476
+ default: 50.00, # $50.00/MTok
477
+ over_272k: 75.00 # $75.00/MTok
478
+ },
479
+ cache: {
480
+ write_default: 12.50, # $12.50/MTok cache write
481
+ write_over_272k: 25.00,
482
+ read_default: 1.00, # $1.00/MTok cache hit
483
+ read_over_272k: 2.00
484
+ },
485
+ tier_threshold: 272_000
486
+ },
487
+
488
+ # GPT-6 Sol rides the same ciyuan gpt group and 272K breakpoint, and opens
489
+ # at the same $2 / $10 as 6.1 Sol, but its long-context card is its own:
490
+ # output goes to $15 (1.5x, not 2x) while cache reads jump 4x to $0.40.
491
+ "oc-gpt-6-sol" => {
492
+ input: {
493
+ default: 2.00, # $2.00/MTok
494
+ over_272k: 4.00 # $4.00/MTok
495
+ },
496
+ output: {
497
+ default: 10.00, # $10.00/MTok
498
+ over_272k: 15.00 # $15.00/MTok
499
+ },
500
+ cache: {
501
+ write_default: 2.50, # $2.50/MTok cache write
502
+ write_over_272k: 5.00,
503
+ read_default: 0.10, # $0.10/MTok cache hit
504
+ read_over_272k: 0.40
505
+ },
506
+ tier_threshold: 272_000
507
+ },
508
+
509
+ # GPT-6 Luna rides the same ciyuan gpt group and 272K breakpoint as
510
+ # Sol/Astra, but sits at the cheap end: $0.10 in / $0.50 out.
511
+ "oc-gpt-6-luna" => {
512
+ input: {
513
+ default: 0.10, # $0.10/MTok
514
+ over_272k: 0.20 # $0.20/MTok
515
+ },
516
+ output: {
517
+ default: 0.50, # $0.50/MTok
518
+ over_272k: 0.75 # $0.75/MTok
519
+ },
520
+ cache: {
521
+ write_default: 0.125, # $0.125/MTok cache write
522
+ write_over_272k: 0.25,
523
+ read_default: 0.01, # $0.01/MTok cache hit
524
+ read_over_272k: 0.02
525
+ },
526
+ tier_threshold: 272_000
527
+ },
528
+
444
529
  # Grok 4.7 served through the openclacky gateway (oc- prefix, TokHub
445
530
  # upstream). Official xAI list price; the long-context surcharge xAI
446
531
  # applies above 200K prompt tokens is not passed through, so both tiers
@@ -970,6 +1055,7 @@ module Clacky
970
1055
  # rest of this file; normalize_model_name() lowercases incoming model
971
1056
  # names before lookup.
972
1057
 
1058
+ # M2.x is retired on MiniMax's own API but still served via OpenRouter/Ollama — keep these rows.
973
1059
  # M2.5 — high-throughput text model, optimised for coding and agent tasks
974
1060
  # (40,960-token context window). Listed at Pay-as-You-Go prices.
975
1061
  # Source: https://www.minimax.io/models/text (MiniMax-M2.5 product page).
@@ -979,19 +1065,25 @@ module Clacky
979
1065
  cache: { write: 0.30, read: 0.03 }
980
1066
  },
981
1067
 
982
- # M3 (released 2026-06-01) is MiniMax's multimodal flagship (image +
983
- # video input, 1,000,000-token context window). Official pricing is
984
- # tiered by context length (≤512K vs 512K–1M); per the project's
985
- # "displayed ≤ actual" convention we record only the lowest (≤512K)
986
- # tier as a flat rate — the global TIERED_PRICING_THRESHOLD is 200K,
987
- # so applying the 512K–1M rate to the 200K–512K band would over-charge.
988
- # Listed at original (non-promotional) prices: input $0.60, output
989
- # $2.40, cache read $0.12 per 1M tokens; cache write is billed at the
990
- # input miss rate.
1068
+ # M3.1-Flash (released 2026-10-19) — flat pricing, 1M context. Source:
1069
+ # https://www.minimax.io/blog/minimax-m31f. No cache-write price is
1070
+ # published; billed at the input rate per this table's convention.
1071
+ "minimax-m3.1-flash" => {
1072
+ input: { default: 0.15, over_200k: 0.15 },
1073
+ output: { default: 0.60, over_200k: 0.60 },
1074
+ cache: { write: 0.15, read: 0.015 }
1075
+ },
1076
+
1077
+ # M3 — tiered at 512K input tokens; cache write billed at the input rate.
1078
+ # Source: https://platform.minimax.io/docs/guides/pricing-paygo (Standard).
991
1079
  "minimax-m3" => {
992
- input: { default: 0.60, over_200k: 0.60 },
993
- output: { default: 2.40, over_200k: 2.40 },
994
- cache: { write: 0.60, read: 0.12 }
1080
+ input: { default: 0.30, over_512k: 0.60 },
1081
+ output: { default: 1.20, over_512k: 2.40 },
1082
+ cache: {
1083
+ write_default: 0.30, write_over_512k: 0.60,
1084
+ read_default: 0.06, read_over_512k: 0.12
1085
+ },
1086
+ tier_threshold: 512_000
995
1087
  },
996
1088
 
997
1089
  # M2.7 — text-only model (204,800-token context window). Listed at
@@ -1126,9 +1218,20 @@ module Clacky
1126
1218
  { match: /\Aoc-glm-/, rate: 0.95, reason: "channel" }, # GLM via ciyuan glm - 5% off
1127
1219
  { match: /\Aoc-kimi-/, rate: 0.9, reason: "channel" }, # Kimi K3 via ciyuan - 10% off
1128
1220
  { match: /\Aoc-grok-/, rate: 0.3, reason: "channel" }, # Grok 4.7 via ciyuan grok - 70% off
1221
+ { match: /\Aoc-gpt-/, rate: 0.3, reason: "channel" }, # GPT-6.1 Sol via ciyuan gpt - 70% off
1129
1222
  { match: /\Aor-(gemini|veo)-/, rate: 0.8, reason: "promo" } # Vertex Gemini/Veo launch promo - 20% off
1130
1223
  ].freeze
1131
1224
 
1225
+ # Lanes sold above the rates in PRICING_TABLE. Bedrock GPT is the only
1226
+ # one: it bills at OpenAI list (Global CRIS carries no premium), so its
1227
+ # 20% is a pricing decision on top of the standard 5% processing fee -
1228
+ # mirrored by bedrockOpenAIMarkup in llm_proxy. BYOK callers hit Bedrock
1229
+ # under their own account and keep the list price, so only the "abs-"
1230
+ # alias matches.
1231
+ MARKUP_RULES = [
1232
+ { match: /\Aabs-gpt-/, rate: 1.2 }
1233
+ ].freeze
1234
+
1132
1235
  class << self
1133
1236
  # Matching promotion for a model alias, or nil when none applies.
1134
1237
  # rate - multiplier applied to the list price
@@ -1149,6 +1252,20 @@ module Clacky
1149
1252
  info ? info[:rate] : 1.0
1150
1253
  end
1151
1254
 
1255
+ # Multiplier applied to the list price by the lane serving this alias.
1256
+ def markup_rate(model)
1257
+ name = model.to_s.downcase
1258
+ return 1.0 if name.empty?
1259
+
1260
+ rule = MARKUP_RULES.find { |r| r[:match].match?(name) }
1261
+ rule ? rule[:rate] : 1.0
1262
+ end
1263
+
1264
+ # What a token actually costs us: promotion and lane markup combined.
1265
+ def effective_rate(model)
1266
+ discount_rate(model) * markup_rate(model)
1267
+ end
1268
+
1152
1269
  # Calculate cost for the given model and usage
1153
1270
  #
1154
1271
  # @param model [String] Model identifier
@@ -1213,7 +1330,7 @@ module Clacky
1213
1330
  # Promotions are keyed on the requested alias, so apply them last: the
1214
1331
  # table above holds list prices shared with BYOK callers.
1215
1332
  {
1216
- cost: (input_cost + output_cost + cache_cost) * discount_rate(model),
1333
+ cost: (input_cost + output_cost + cache_cost) * effective_rate(model),
1217
1334
  source: source
1218
1335
  }
1219
1336
  end
@@ -1378,6 +1495,8 @@ module Clacky
1378
1495
  "glm-4.7"
1379
1496
  # MiniMax — model ids in providers.rb use capitalised "MiniMax-M*"
1380
1497
  # but we match case-insensitively and map to the lowercased table key.
1498
+ when /^minimax-m3[.]1-flash$/i
1499
+ "minimax-m3.1-flash"
1381
1500
  when /^minimax-m3$/i
1382
1501
  "minimax-m3"
1383
1502
  when /^minimax-m2\.7$/i
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "1.5.20"
4
+ VERSION = "1.5.21"
5
5
  end