openclacky 1.5.19 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +95 -0
- data/README.md +19 -1
- data/README_CN.md +1 -1
- data/README_JA.md +1 -1
- data/lib/clacky/agent/chunk_index.rb +8 -1
- data/lib/clacky/agent/feedback.rb +30 -0
- data/lib/clacky/agent/history_navigation.rb +17 -3
- data/lib/clacky/agent/hook_manager.rb +6 -1
- data/lib/clacky/agent/llm_caller.rb +17 -3
- data/lib/clacky/agent/message_compressor_helper.rb +36 -2
- data/lib/clacky/agent/session_serializer.rb +45 -4
- data/lib/clacky/agent.rb +135 -55
- data/lib/clacky/agent_config.rb +82 -4
- data/lib/clacky/billing/platform_billing.rb +10 -5
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/cli.rb +5 -4
- data/lib/clacky/client.rb +12 -5
- data/lib/clacky/deepseek_web_tools.rb +156 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +8 -0
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +97 -20
- data/lib/clacky/default_extensions/time_machine/panels/time_machine/view.js +1 -0
- data/lib/clacky/default_skills/onboard/SKILL.md +1 -1
- data/lib/clacky/extension/loader.rb +5 -3
- data/lib/clacky/locales/en.rb +5 -5
- data/lib/clacky/locales/zh.rb +5 -5
- data/lib/clacky/message_format/anthropic.rb +20 -0
- data/lib/clacky/message_format/open_ai.rb +31 -14
- data/lib/clacky/message_history.rb +45 -4
- data/lib/clacky/platform_http_client.rb +6 -2
- data/lib/clacky/providers.rb +38 -138
- data/lib/clacky/search_config.rb +47 -13
- data/lib/clacky/server/browser_manager.rb +35 -5
- data/lib/clacky/server/channel/channel_manager.rb +6 -6
- data/lib/clacky/server/discover.rb +5 -4
- data/lib/clacky/server/http_server.rb +285 -53
- data/lib/clacky/server/model_prices.rb +10 -5
- data/lib/clacky/server/pid_files.rb +25 -0
- data/lib/clacky/server/server_master.rb +9 -3
- data/lib/clacky/server/session_registry.rb +37 -7
- data/lib/clacky/server/web_ui_controller.rb +72 -0
- data/lib/clacky/session_manager.rb +28 -6
- data/lib/clacky/skill.rb +1 -1
- data/lib/clacky/tools/ask_user.rb +4 -1
- data/lib/clacky/tools/base.rb +3 -2
- data/lib/clacky/tools/browser.rb +18 -2
- data/lib/clacky/tools/file_reader.rb +1 -1
- data/lib/clacky/tools/terminal/session_manager.rb +19 -1
- data/lib/clacky/tools/terminal.rb +65 -23
- data/lib/clacky/tools/trash_manager.rb +5 -3
- data/lib/clacky/tools/web_fetch.rb +81 -22
- data/lib/clacky/tools/web_search.rb +45 -4
- data/lib/clacky/ui2/ui_controller.rb +1 -1
- data/lib/clacky/ui_interface.rb +4 -3
- data/lib/clacky/utils/file_processor.rb +51 -0
- data/lib/clacky/utils/model_pricing.rb +242 -39
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +1158 -249
- data/lib/clacky/web/app.js +82 -13
- data/lib/clacky/web/components/chat-navigator.js +8 -1
- data/lib/clacky/web/components/code-editor.js +6 -2
- data/lib/clacky/web/components/mentions.js +2 -13
- data/lib/clacky/web/components/model-picker.js +92 -97
- data/lib/clacky/web/components/onboard.js +13 -10
- data/lib/clacky/web/components/sidebar.js +6 -2
- data/lib/clacky/web/features/archive/store.js +196 -0
- data/lib/clacky/web/features/archive/view.js +409 -0
- data/lib/clacky/web/features/backup/view.js +17 -7
- data/lib/clacky/web/features/billing/view.js +84 -10
- data/lib/clacky/web/features/channels/view.js +75 -29
- data/lib/clacky/web/features/new-session/store.js +3 -3
- data/lib/clacky/web/features/new-session/view.js +1 -0
- data/lib/clacky/web/features/trash/store.js +14 -11
- data/lib/clacky/web/features/trash/view.js +66 -13
- data/lib/clacky/web/features/workspace/view.js +421 -16
- data/lib/clacky/web/i18n.js +242 -70
- data/lib/clacky/web/index.html +270 -117
- data/lib/clacky/web/projects.js +56 -1
- data/lib/clacky/web/sessions.js +609 -267
- data/lib/clacky/web/settings.js +186 -32
- data/lib/clacky/web/ws-dispatcher.js +33 -21
- data/lib/clacky.rb +4 -2
- metadata +6 -1
|
@@ -11,11 +11,11 @@ module Clacky
|
|
|
11
11
|
class MessageHistory
|
|
12
12
|
# Fields that are internal to the agent and must not be sent to the API.
|
|
13
13
|
INTERNAL_FIELDS = %i[
|
|
14
|
-
task_id created_at system_injected session_context memory_update
|
|
14
|
+
message_id task_id created_at system_injected session_context memory_update
|
|
15
15
|
subagent_instructions subagent_result subagent_transcript token_usage
|
|
16
16
|
compressed_summary chunk_path truncated transient
|
|
17
17
|
chunk_index chunk_count ext_events skill_command skill_command_display
|
|
18
|
-
display_files display_references
|
|
18
|
+
display_files display_references feedback_requests
|
|
19
19
|
].freeze
|
|
20
20
|
|
|
21
21
|
INTERNAL_CONTENT_BLOCK_FIELDS = %i[image_path image_name].freeze
|
|
@@ -37,7 +37,8 @@ module Clacky
|
|
|
37
37
|
# Write operations
|
|
38
38
|
# ─────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
# Append a single message hash to the history.
|
|
40
|
+
# Append a single message hash to the history. An optional block receives
|
|
41
|
+
# the stored, sanitized message so callers can retain its identity.
|
|
41
42
|
#
|
|
42
43
|
# When appending a user message, automatically drop any trailing assistant
|
|
43
44
|
# message that has unanswered tool_calls (no tool_result follows it).
|
|
@@ -48,7 +49,9 @@ module Clacky
|
|
|
48
49
|
if message[:role] == "user"
|
|
49
50
|
drop_dangling_tool_calls!
|
|
50
51
|
end
|
|
51
|
-
|
|
52
|
+
stored_message = deep_sanitize_utf8(message)
|
|
53
|
+
@messages << stored_message
|
|
54
|
+
yield stored_message if block_given?
|
|
52
55
|
self
|
|
53
56
|
end
|
|
54
57
|
|
|
@@ -65,6 +68,18 @@ module Clacky
|
|
|
65
68
|
self
|
|
66
69
|
end
|
|
67
70
|
|
|
71
|
+
# Replace one real user turn after its attachments have been resolved.
|
|
72
|
+
# message_id is the stable identity; created_at remains presentation data.
|
|
73
|
+
def replace_user_message(message_id:, message:)
|
|
74
|
+
idx = @messages.index do |m|
|
|
75
|
+
m[:role] == "user" && !m[:system_injected] && m[:message_id] == message_id
|
|
76
|
+
end
|
|
77
|
+
return false unless idx
|
|
78
|
+
|
|
79
|
+
@messages[idx] = deep_sanitize_utf8(message)
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
68
83
|
# Replace the entire message list (used by compression rebuild).
|
|
69
84
|
def replace_all(new_messages)
|
|
70
85
|
@messages = new_messages.map { |m| deep_sanitize_utf8(m) }
|
|
@@ -99,6 +114,16 @@ module Clacky
|
|
|
99
114
|
self
|
|
100
115
|
end
|
|
101
116
|
|
|
117
|
+
# Save the effective request before waiting, including while no tool result
|
|
118
|
+
# exists yet. The original model-authored call remains unchanged.
|
|
119
|
+
def attach_feedback_request(message:, tool_call_id:, request:)
|
|
120
|
+
return false unless message
|
|
121
|
+
|
|
122
|
+
message[:feedback_requests] ||= {}
|
|
123
|
+
message[:feedback_requests][tool_call_id.to_s] = deep_sanitize_utf8(request)
|
|
124
|
+
true
|
|
125
|
+
end
|
|
126
|
+
|
|
102
127
|
# Settle an interrupted fan-out whose assistant.tool_calls turn is dangling
|
|
103
128
|
# because the batch was cancelled before any tool result was written. Mirrors
|
|
104
129
|
# repair_tool_call_pairing (same helpers, same scan) but PERSISTS the result:
|
|
@@ -179,6 +204,17 @@ module Clacky
|
|
|
179
204
|
truncate_from(idx)
|
|
180
205
|
end
|
|
181
206
|
|
|
207
|
+
# Truncate history starting from the message with the given stable ID.
|
|
208
|
+
# Used to roll back an attachment turn that failed before resolution.
|
|
209
|
+
def truncate_from_message(message_id)
|
|
210
|
+
idx = @messages.index do |m|
|
|
211
|
+
m[:role] == "user" && !m[:system_injected] && m[:message_id] == message_id
|
|
212
|
+
end
|
|
213
|
+
return self unless idx
|
|
214
|
+
|
|
215
|
+
truncate_from(idx)
|
|
216
|
+
end
|
|
217
|
+
|
|
182
218
|
# Roll back the history to just before the given message object.
|
|
183
219
|
# Removes the message and anything appended after it.
|
|
184
220
|
# Used to undo a failed speculative append (e.g. compression message that errored).
|
|
@@ -292,6 +328,11 @@ module Clacky
|
|
|
292
328
|
@messages
|
|
293
329
|
end
|
|
294
330
|
msgs = source.map { |m| strip_for_api(m) }
|
|
331
|
+
# A tool message with a blank id answers no announced call, and one of
|
|
332
|
+
# them makes every later request fail (422 "invalid type: null"). Drop
|
|
333
|
+
# these before pairing: a blank id is not recognized as a tool result,
|
|
334
|
+
# so pairing would otherwise insert a placeholder and orphan the real one.
|
|
335
|
+
msgs.reject! { |m| m[:role] == "tool" && m[:tool_call_id].to_s.strip.empty? }
|
|
295
336
|
msgs = repair_tool_call_pairing(msgs)
|
|
296
337
|
ensure_reasoning_content_consistency(msgs, force: force_reasoning_content_pad)
|
|
297
338
|
end
|
|
@@ -50,9 +50,13 @@ module Clacky
|
|
|
50
50
|
# Auto-detects the platform endpoint:
|
|
51
51
|
# - When CLACKY_LICENSE_SERVER is set → that URL (dev override)
|
|
52
52
|
# - Otherwise → PRIMARY_HOST
|
|
53
|
+
def self.base_url
|
|
54
|
+
override = ENV["CLACKY_LICENSE_SERVER"]
|
|
55
|
+
override && !override.empty? ? override.chomp("/") : PRIMARY_HOST
|
|
56
|
+
end
|
|
57
|
+
|
|
53
58
|
def initialize
|
|
54
|
-
|
|
55
|
-
@base_url = override && !override.empty? ? override : PRIMARY_HOST
|
|
59
|
+
@base_url = self.class.base_url
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
# Send a POST request with a JSON body and return a normalised result hash.
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -40,7 +40,6 @@ module Clacky
|
|
|
40
40
|
"dsk-deepseek-v4-pro",
|
|
41
41
|
"oc-glm-5.3",
|
|
42
42
|
"oc-kimi-k3",
|
|
43
|
-
"oc-minimax-m2.7",
|
|
44
43
|
"abs-claude-fable-5-1",
|
|
45
44
|
"abs-claude-fable-5",
|
|
46
45
|
"abs-claude-opus-5-5",
|
|
@@ -50,17 +49,18 @@ module Clacky
|
|
|
50
49
|
"abs-claude-sonnet-5-5",
|
|
51
50
|
"abs-claude-sonnet-5",
|
|
52
51
|
"abs-claude-sonnet-4-6",
|
|
52
|
+
"abs-claude-haiku-5-5",
|
|
53
53
|
"abs-claude-haiku-4-5",
|
|
54
|
-
"abs-gpt-6-astra",
|
|
55
54
|
"abs-gpt-6.1-sol",
|
|
55
|
+
"abs-gpt-6-astra",
|
|
56
56
|
"abs-gpt-6-sol",
|
|
57
57
|
"abs-gpt-6-luna",
|
|
58
58
|
"abs-gpt-5.6-sol",
|
|
59
59
|
"abs-gpt-5.6-terra",
|
|
60
60
|
"abs-gpt-5.6-luna",
|
|
61
|
+
"or-gemini-4-argon",
|
|
61
62
|
"or-gemini-3-1-pro",
|
|
62
|
-
"or-gemini-3-8-flash"
|
|
63
|
-
"or-gemini-3-7-flash"
|
|
63
|
+
"or-gemini-3-8-flash"
|
|
64
64
|
],
|
|
65
65
|
# Image generation models served by the openclacky platform
|
|
66
66
|
# gateway. The gateway exposes a standard OpenAI-compatible
|
|
@@ -112,12 +112,10 @@ module Clacky
|
|
|
112
112
|
# audio parts). The gateway returns transcription text.
|
|
113
113
|
"stt_models" => [
|
|
114
114
|
"or-stt-gemini-3-8-flash",
|
|
115
|
-
"or-stt-gemini-3-7-flash",
|
|
116
115
|
"or-stt-gemini-1-5-pro"
|
|
117
116
|
],
|
|
118
117
|
"stt_model_aliases" => {
|
|
119
118
|
"or-stt-gemini-3-8-flash" => "Gemini 3.8 Flash STT",
|
|
120
|
-
"or-stt-gemini-3-7-flash" => "Gemini 3.7 Flash STT",
|
|
121
119
|
"or-stt-gemini-1-5-pro" => "Gemini 1.5 Pro STT"
|
|
122
120
|
},
|
|
123
121
|
"default_stt_model" => "or-stt-gemini-3-8-flash",
|
|
@@ -126,12 +124,10 @@ module Clacky
|
|
|
126
124
|
# parts). The gateway returns analysis text.
|
|
127
125
|
"video_understanding_models" => [
|
|
128
126
|
"or-gemini-3-8-flash",
|
|
129
|
-
"or-gemini-3-7-flash",
|
|
130
127
|
"or-gemini-3-1-pro"
|
|
131
128
|
],
|
|
132
129
|
"video_understanding_model_aliases" => {
|
|
133
130
|
"or-gemini-3-8-flash" => "Gemini 3.8 Flash",
|
|
134
|
-
"or-gemini-3-7-flash" => "Gemini 3.7 Flash",
|
|
135
131
|
"or-gemini-3-1-pro" => "Gemini 3.1 Pro"
|
|
136
132
|
},
|
|
137
133
|
"default_video_understanding_model" => "or-gemini-3-8-flash",
|
|
@@ -148,15 +144,13 @@ module Clacky
|
|
|
148
144
|
# (it accepts image/audio/video input natively via OpenRouter).
|
|
149
145
|
# oc-glm-5.3 is likewise text-only: its upstream rejects any content
|
|
150
146
|
# block other than `type: "text"`, so images must never be sent
|
|
151
|
-
# inline. oc-
|
|
152
|
-
#
|
|
153
|
-
#
|
|
154
|
-
# oc-kimi-k3 is natively multimodal and keeps vision=true.
|
|
147
|
+
# inline. oc-kimi-k3 and oc-grok-4.7 are natively multimodal and keep
|
|
148
|
+
# vision=true; oc-gpt-6.1-sol, oc-gpt-6-astra, oc-gpt-6-sol and
|
|
149
|
+
# oc-gpt-6-luna do too.
|
|
155
150
|
"model_capabilities" => {
|
|
156
151
|
"dsk-deepseek-flash" => { "vision" => true }.freeze,
|
|
157
152
|
"dsk-deepseek-v4-pro" => { "vision" => false }.freeze,
|
|
158
|
-
"oc-glm-5.3" => { "vision" => false }.freeze
|
|
159
|
-
"oc-minimax-m2.7" => { "vision" => false }.freeze
|
|
153
|
+
"oc-glm-5.3" => { "vision" => false }.freeze
|
|
160
154
|
}.freeze,
|
|
161
155
|
# Bedrock GPT models (abs-gpt-*) are served through the OpenAI
|
|
162
156
|
# Responses API — their Chat Completions endpoint rejects function
|
|
@@ -173,15 +167,15 @@ module Clacky
|
|
|
173
167
|
# no injection happens when the default model is already lite-class.
|
|
174
168
|
"lite_models" => {
|
|
175
169
|
"auto" => "dsk-deepseek-flash",
|
|
176
|
-
"abs-claude-fable-5-1" => "abs-claude-haiku-
|
|
177
|
-
"abs-claude-fable-5" => "abs-claude-haiku-
|
|
178
|
-
"abs-claude-opus-5-5" => "abs-claude-haiku-
|
|
179
|
-
"abs-claude-opus-5" => "abs-claude-haiku-
|
|
180
|
-
"abs-claude-opus-4-8" => "abs-claude-haiku-
|
|
181
|
-
"abs-claude-opus-4-7" => "abs-claude-haiku-
|
|
182
|
-
"abs-claude-sonnet-5-5" => "abs-claude-haiku-
|
|
183
|
-
"abs-claude-sonnet-5" => "abs-claude-haiku-
|
|
184
|
-
"abs-claude-sonnet-4-6" => "abs-claude-haiku-
|
|
170
|
+
"abs-claude-fable-5-1" => "abs-claude-haiku-5-5",
|
|
171
|
+
"abs-claude-fable-5" => "abs-claude-haiku-5-5",
|
|
172
|
+
"abs-claude-opus-5-5" => "abs-claude-haiku-5-5",
|
|
173
|
+
"abs-claude-opus-5" => "abs-claude-haiku-5-5",
|
|
174
|
+
"abs-claude-opus-4-8" => "abs-claude-haiku-5-5",
|
|
175
|
+
"abs-claude-opus-4-7" => "abs-claude-haiku-5-5",
|
|
176
|
+
"abs-claude-sonnet-5-5" => "abs-claude-haiku-5-5",
|
|
177
|
+
"abs-claude-sonnet-5" => "abs-claude-haiku-5-5",
|
|
178
|
+
"abs-claude-sonnet-4-6" => "abs-claude-haiku-5-5",
|
|
185
179
|
"abs-gpt-6-astra" => "abs-gpt-6-luna",
|
|
186
180
|
"abs-gpt-6-sol" => "abs-gpt-6-luna",
|
|
187
181
|
"abs-gpt-5.6-sol" => "abs-gpt-5.6-luna",
|
|
@@ -205,7 +199,8 @@ module Clacky
|
|
|
205
199
|
"endpoint_variants" => [
|
|
206
200
|
{ "label" => "Primary (Global)", "label_key" => "settings.models.baseurl.variant.openclacky_primary", "base_url" => "https://api.openclacky.com" }.freeze
|
|
207
201
|
].freeze,
|
|
208
|
-
"
|
|
202
|
+
"deepseek_web_tools" => true,
|
|
203
|
+
"website_url" => "https://www.openclacky.com/llm_keys"
|
|
209
204
|
}.freeze,
|
|
210
205
|
|
|
211
206
|
|
|
@@ -235,6 +230,7 @@ module Clacky
|
|
|
235
230
|
"model_capabilities" => {
|
|
236
231
|
"deepseek-flash" => { "vision" => true }.freeze
|
|
237
232
|
}.freeze,
|
|
233
|
+
"deepseek_web_tools" => true,
|
|
238
234
|
"website_url" => "https://platform.deepseek.com/api_keys"
|
|
239
235
|
}.freeze,
|
|
240
236
|
|
|
@@ -340,8 +336,8 @@ module Clacky
|
|
|
340
336
|
"name" => "Minimax",
|
|
341
337
|
"base_url" => "https://api.minimaxi.com/v1",
|
|
342
338
|
"api" => "openai-completions",
|
|
343
|
-
"default_model" => "MiniMax-M3",
|
|
344
|
-
"models" => ["MiniMax-M3
|
|
339
|
+
"default_model" => "MiniMax-M3.1-Flash",
|
|
340
|
+
"models" => ["MiniMax-M3.1-Flash", "MiniMax-M3"],
|
|
345
341
|
# MiniMax operates two regional endpoints with identical APIs & model
|
|
346
342
|
# lineup — mainland China (.com) and international (.io). Listing both
|
|
347
343
|
# lets find_by_base_url identify either one as provider "minimax",
|
|
@@ -351,16 +347,15 @@ module Clacky
|
|
|
351
347
|
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
|
|
352
348
|
{ "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
|
|
353
349
|
].freeze,
|
|
354
|
-
#
|
|
355
|
-
#
|
|
356
|
-
#
|
|
357
|
-
# vision=false below. M3 exposes a 1,000,000-token context window
|
|
358
|
-
# (M2.7: 204,800; M2.5: 40,960).
|
|
350
|
+
# M3.1-Flash and M3 are natively multimodal (image_url content parts,
|
|
351
|
+
# 1M context) and override the provider-level vision=false, which
|
|
352
|
+
# stays as the safe default for retired text-only ids (M2.x).
|
|
359
353
|
"capabilities" => { "vision" => false }.freeze,
|
|
360
354
|
"model_capabilities" => {
|
|
361
|
-
"MiniMax-M3" => { "vision" => true }.freeze
|
|
355
|
+
"MiniMax-M3.1-Flash" => { "vision" => true }.freeze,
|
|
356
|
+
"MiniMax-M3" => { "vision" => true }.freeze
|
|
362
357
|
}.freeze,
|
|
363
|
-
"default_ocr_model" => "MiniMax-M3",
|
|
358
|
+
"default_ocr_model" => "MiniMax-M3.1-Flash",
|
|
364
359
|
"website_url" => "https://platform.minimax.io/"
|
|
365
360
|
}.freeze,
|
|
366
361
|
|
|
@@ -648,7 +643,6 @@ module Clacky
|
|
|
648
643
|
"glm-5.2",
|
|
649
644
|
"kimi-k3",
|
|
650
645
|
"glm-5.1",
|
|
651
|
-
"minimax-m2.7",
|
|
652
646
|
"minimax-m3",
|
|
653
647
|
"kimi-k2.7-code",
|
|
654
648
|
"kimi-k2.6",
|
|
@@ -670,114 +664,12 @@ module Clacky
|
|
|
670
664
|
"lite_models" => {
|
|
671
665
|
"glm-5.2" => "glm-5.1",
|
|
672
666
|
"kimi-k3" => "kimi-k2.6",
|
|
673
|
-
"minimax-m3" => "minimax-m2.7",
|
|
674
667
|
"kimi-k2.7-code" => "kimi-k2.6",
|
|
675
668
|
"deepseek-v4-pro" => "deepseek-v4-flash",
|
|
676
669
|
"gemini-3-flash-preview" => "deepseek-v4-flash"
|
|
677
670
|
},
|
|
678
671
|
"default_ocr_model" => "kimi-k2.7-code",
|
|
679
672
|
"website_url" => "https://ollama.com/settings/keys"
|
|
680
|
-
}.freeze,
|
|
681
|
-
|
|
682
|
-
"orcarouter" => {
|
|
683
|
-
"name" => "OrcaRouter",
|
|
684
|
-
"base_url" => "https://api.orcarouter.ai/v1",
|
|
685
|
-
"api" => "openai-completions",
|
|
686
|
-
"default_model" => "openai/gpt-5.5",
|
|
687
|
-
# Curated default lineup. OrcaRouter exposes 190+ models from OpenAI,
|
|
688
|
-
# Anthropic, Google, DeepSeek, Qwen, MiniMax, Zhipu and others behind a
|
|
689
|
-
# single OpenAI-compatible endpoint. Shipping a small list of the
|
|
690
|
-
# mainstream Claude + GPT entries gives users a working dropdown out
|
|
691
|
-
# of the box; users can still type any other OrcaRouter model id manually.
|
|
692
|
-
"models" => [
|
|
693
|
-
"anthropic/claude-sonnet-5",
|
|
694
|
-
"anthropic/claude-opus-4.8",
|
|
695
|
-
"anthropic/claude-haiku-4.5",
|
|
696
|
-
"openai/gpt-5.5",
|
|
697
|
-
"openai/gpt-5.4",
|
|
698
|
-
"openai/gpt-5.4-mini",
|
|
699
|
-
"google/gemini-3.5-flash",
|
|
700
|
-
"deepseek/deepseek-v4-flash",
|
|
701
|
-
"z-ai/glm-5.2"
|
|
702
|
-
],
|
|
703
|
-
# Per-primary lite pairing — Claude family pairs with Haiku, GPT
|
|
704
|
-
# family pairs with the mini variant. Mirrors the openrouter preset
|
|
705
|
-
# so subagents on OrcaRouter get a sensible cheap/fast sidekick.
|
|
706
|
-
"lite_models" => {
|
|
707
|
-
"anthropic/claude-sonnet-5" => "anthropic/claude-haiku-4.5",
|
|
708
|
-
"anthropic/claude-opus-4.8" => "anthropic/claude-haiku-4.5",
|
|
709
|
-
"openai/gpt-5.5" => "openai/gpt-5.4-mini",
|
|
710
|
-
"openai/gpt-5.4" => "openai/gpt-5.4-mini"
|
|
711
|
-
},
|
|
712
|
-
# Per-model API type overrides. OrcaRouter proxies Claude through a
|
|
713
|
-
# native Anthropic /v1/messages endpoint (https://api.orcarouter.ai/v1/messages)
|
|
714
|
-
# in addition to its OpenAI-compatible /chat/completions endpoint.
|
|
715
|
-
# Routing "anthropic/*" via the native endpoint preserves cache_control
|
|
716
|
-
# fidelity, matching what Claude Code CLI does internally (same rationale
|
|
717
|
-
# as the openrouter preset). Non-Claude models keep the OpenAI shim.
|
|
718
|
-
"model_api_overrides" => {
|
|
719
|
-
/\Aanthropic\// => "anthropic-messages"
|
|
720
|
-
}.freeze,
|
|
721
|
-
# Most models on OrcaRouter are vision-capable; DeepSeek and GLM-5.2
|
|
722
|
-
# are text-only.
|
|
723
|
-
"capabilities" => { "vision" => true }.freeze,
|
|
724
|
-
"model_capabilities" => {
|
|
725
|
-
"deepseek/deepseek-v4-flash" => { "vision" => false }.freeze,
|
|
726
|
-
"z-ai/glm-5.2" => { "vision" => false }.freeze
|
|
727
|
-
}.freeze,
|
|
728
|
-
"default_ocr_model" => "google/gemini-3.5-flash",
|
|
729
|
-
"website_url" => "https://www.orcarouter.ai"
|
|
730
|
-
}.freeze,
|
|
731
|
-
|
|
732
|
-
"requesty" => {
|
|
733
|
-
"name" => "Requesty",
|
|
734
|
-
"base_url" => "https://router.requesty.ai/v1",
|
|
735
|
-
"api" => "openai-completions",
|
|
736
|
-
"default_model" => "claude-sonnet-4-6",
|
|
737
|
-
# Curated default lineup of Requesty managed policies (GET /v1/models/managed).
|
|
738
|
-
# A managed policy is a short model id that Requesty routes across several
|
|
739
|
-
# upstream providers. Users can still type any other id manually, including
|
|
740
|
-
# the full "vendor/model" catalogue ids (e.g. "openai/gpt-4o-mini").
|
|
741
|
-
"models" => [
|
|
742
|
-
"claude-sonnet-4-6",
|
|
743
|
-
"claude-opus-4-8",
|
|
744
|
-
"claude-opus-4-7",
|
|
745
|
-
"claude-opus-4-6",
|
|
746
|
-
"claude-haiku-4-5",
|
|
747
|
-
"gpt-5.6-sol",
|
|
748
|
-
"gpt-5.6-terra",
|
|
749
|
-
"gpt-5.6-luna",
|
|
750
|
-
"gpt-5.5",
|
|
751
|
-
"gpt-5.4",
|
|
752
|
-
"gpt-5.4-mini",
|
|
753
|
-
"gemini-3.8-flash"
|
|
754
|
-
],
|
|
755
|
-
# Per-primary lite pairing, same shape as the openrouter preset.
|
|
756
|
-
"lite_models" => {
|
|
757
|
-
"claude-sonnet-4-6" => "claude-haiku-4-5",
|
|
758
|
-
"claude-opus-4-8" => "claude-haiku-4-5",
|
|
759
|
-
"claude-opus-4-7" => "claude-haiku-4-5",
|
|
760
|
-
"claude-opus-4-6" => "claude-haiku-4-5",
|
|
761
|
-
"gpt-5.6-sol" => "gpt-5.6-luna",
|
|
762
|
-
"gpt-5.6-terra" => "gpt-5.6-luna",
|
|
763
|
-
"gpt-5.5" => "gpt-5.4-mini",
|
|
764
|
-
"gpt-5.4" => "gpt-5.4-mini"
|
|
765
|
-
},
|
|
766
|
-
# Requesty also serves the native Anthropic /v1/messages endpoint, so
|
|
767
|
-
# Claude models route there to preserve cache_control fidelity (same
|
|
768
|
-
# rationale as the openrouter preset). Other models keep the OpenAI shim.
|
|
769
|
-
"model_api_overrides" => {
|
|
770
|
-
/\Aanthropic\// => "anthropic-messages",
|
|
771
|
-
/\Aclaude[-.]/ => "anthropic-messages"
|
|
772
|
-
}.freeze,
|
|
773
|
-
# Same API key works on every regional endpoint; the EU one keeps
|
|
774
|
-
# traffic inside the EU (Frankfurt).
|
|
775
|
-
"endpoint_variants" => [
|
|
776
|
-
{ "label" => "Global", "base_url" => "https://router.requesty.ai/v1" }.freeze,
|
|
777
|
-
{ "label" => "EU", "base_url" => "https://router.eu.requesty.ai/v1", "region" => "eu" }.freeze
|
|
778
|
-
].freeze,
|
|
779
|
-
"default_ocr_model" => "gemini-3.8-flash",
|
|
780
|
-
"website_url" => "https://app.requesty.ai/api-keys"
|
|
781
673
|
}.freeze
|
|
782
674
|
|
|
783
675
|
}.freeze
|
|
@@ -797,8 +689,6 @@ module Clacky
|
|
|
797
689
|
MIMO_ID = "mimo"
|
|
798
690
|
VOLCENGINE_ARK_ID = "volcengine-ark"
|
|
799
691
|
OLLAMA_ID = "ollama"
|
|
800
|
-
ORCAROUTER_ID = "orcarouter"
|
|
801
|
-
REQUESTY_ID = "requesty"
|
|
802
692
|
|
|
803
693
|
MEDIA_KINDS = %w[image video audio stt video_understanding].freeze
|
|
804
694
|
|
|
@@ -1282,6 +1172,16 @@ module Clacky
|
|
|
1282
1172
|
return true unless caps.key?(key)
|
|
1283
1173
|
caps[key] != false
|
|
1284
1174
|
end
|
|
1175
|
+
|
|
1176
|
+
# True when the preset declares native DeepSeek web tools (search + fetch).
|
|
1177
|
+
# Currently DeepSeek direct ("deepseekv4") and the OpenClacky platform
|
|
1178
|
+
# gateway ("openclacky").
|
|
1179
|
+
def deepseek_web_tools?(provider_id)
|
|
1180
|
+
preset = PRESETS[provider_id]
|
|
1181
|
+
return false unless preset
|
|
1182
|
+
|
|
1183
|
+
preset["deepseek_web_tools"] == true
|
|
1184
|
+
end
|
|
1285
1185
|
end
|
|
1286
1186
|
end
|
|
1287
1187
|
end
|
data/lib/clacky/search_config.rb
CHANGED
|
@@ -2,19 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require_relative "deepseek_web_tools"
|
|
5
6
|
|
|
6
7
|
module Clacky
|
|
7
8
|
# Search provider selection, backed by ~/.clacky/search.yml.
|
|
8
9
|
#
|
|
9
|
-
# The built-in Parallel/DuckDuckGo/Bing
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# The built-in Parallel/DuckDuckGo/Bing chain is the default: it needs no key
|
|
11
|
+
# and no configuration, so a fresh install searches the web out of the box.
|
|
12
|
+
# DeepSeek's hosted web tools are opt-in — they render JS-heavy pages to
|
|
13
|
+
# Markdown but bill per call, so they only run once the user picks them.
|
|
13
14
|
#
|
|
14
15
|
# search.yml schema:
|
|
15
|
-
# provider: tavily —
|
|
16
|
-
# blank
|
|
17
|
-
#
|
|
16
|
+
# provider: tavily — "deepseek_web" for DeepSeek's hosted
|
|
17
|
+
# search/fetch, blank for built-in engines only,
|
|
18
|
+
# otherwise the basename of a script in ~/.clacky/searchers/
|
|
19
|
+
# key: tvly-xxx — credential for the selected provider (optional for
|
|
20
|
+
# deepseek_web, which can reuse a configured model key)
|
|
21
|
+
#
|
|
22
|
+
# A missing file, or one without a `provider` key, means "never chosen" and
|
|
23
|
+
# resolves to the free chain.
|
|
18
24
|
#
|
|
19
25
|
# The directory is the registry: dropping my_engine.rb into
|
|
20
26
|
# ~/.clacky/searchers/ and setting `provider: my_engine` is all it takes,
|
|
@@ -22,21 +28,49 @@ module Clacky
|
|
|
22
28
|
module SearchConfig
|
|
23
29
|
CONFIG_PATH = File.expand_path("~/.clacky/search.yml").freeze
|
|
24
30
|
|
|
31
|
+
# Hosted DeepSeek search + page fetch, billed per successful call upstream.
|
|
32
|
+
# It behaves like a provider but ships with the gem, so it has no script.
|
|
33
|
+
DEEPSEEK_WEB = "deepseek_web"
|
|
34
|
+
|
|
25
35
|
class << self
|
|
26
36
|
# @return [Hash] { "provider" => String, "key" => String }
|
|
27
37
|
def load
|
|
28
38
|
raw = File.exist?(CONFIG_PATH) ? YAML.load_file(CONFIG_PATH) : nil
|
|
29
|
-
return
|
|
39
|
+
return default_config unless raw.is_a?(Hash)
|
|
30
40
|
|
|
31
|
-
{
|
|
41
|
+
{
|
|
42
|
+
"provider" => raw["provider"].to_s.strip,
|
|
43
|
+
"key" => raw["key"].to_s.strip
|
|
44
|
+
}
|
|
32
45
|
rescue StandardError
|
|
33
|
-
|
|
46
|
+
default_config
|
|
34
47
|
end
|
|
35
48
|
|
|
36
|
-
def save(provider
|
|
49
|
+
def save(provider: nil, key: nil)
|
|
50
|
+
current = load
|
|
51
|
+
new_provider = provider.nil? ? current["provider"] : provider.to_s.strip
|
|
52
|
+
new_key = key.nil? ? current["key"] : key.to_s.strip
|
|
53
|
+
|
|
54
|
+
payload = {
|
|
55
|
+
"provider" => new_provider,
|
|
56
|
+
"key" => new_key
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
FileUtils.mkdir_p(File.dirname(CONFIG_PATH))
|
|
38
|
-
File.write(CONFIG_PATH,
|
|
60
|
+
File.write(CONFIG_PATH, payload.to_yaml)
|
|
39
61
|
File.chmod(0o600, CONFIG_PATH)
|
|
62
|
+
|
|
63
|
+
# A new provider or key invalidates an earlier failure, so the next
|
|
64
|
+
# search retries DeepSeek instead of sitting out the cooldown.
|
|
65
|
+
Clacky::DeepSeekWebTools.reset_cooldown!
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def deepseek_web?
|
|
69
|
+
load["provider"] == DEEPSEEK_WEB
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private def default_config
|
|
73
|
+
{ "provider" => "", "key" => "" }
|
|
40
74
|
end
|
|
41
75
|
|
|
42
76
|
# Absolute path of the configured searcher, or nil when unset/missing.
|
|
@@ -44,7 +78,7 @@ module Clacky
|
|
|
44
78
|
# web_search silently falls back instead of erroring on every search.
|
|
45
79
|
def script_path
|
|
46
80
|
name = load["provider"]
|
|
47
|
-
return nil if name.empty?
|
|
81
|
+
return nil if name.empty? || name == DEEPSEEK_WEB
|
|
48
82
|
|
|
49
83
|
Clacky::Utils::SearcherManager.path_for(name)
|
|
50
84
|
end
|
|
@@ -19,6 +19,7 @@ module Clacky
|
|
|
19
19
|
# enabled: true/false — whether the browser tool is active
|
|
20
20
|
# chrome_version: "146" — detected Chrome version (set by browser-setup skill)
|
|
21
21
|
# configured_at: date — when setup was last run
|
|
22
|
+
# screenshot_max_base64_bytes: Integer — screenshot size ceiling (settings UI)
|
|
22
23
|
#
|
|
23
24
|
# Liveness check strategy:
|
|
24
25
|
# process_alive? sends an MCP `ping` (standard in MCP spec 2024-11-05) and
|
|
@@ -116,27 +117,49 @@ module Clacky
|
|
|
116
117
|
enabled = cfg["enabled"] == true
|
|
117
118
|
running = @process && @process[:wait_thr]&.alive?
|
|
118
119
|
{
|
|
119
|
-
enabled:
|
|
120
|
-
daemon_running:
|
|
121
|
-
chrome_version:
|
|
120
|
+
enabled: enabled,
|
|
121
|
+
daemon_running: !!running,
|
|
122
|
+
chrome_version: cfg["chrome_version"],
|
|
123
|
+
screenshot_max_base64_bytes: effective_screenshot_limit(cfg)
|
|
122
124
|
}
|
|
123
125
|
end
|
|
124
126
|
|
|
125
127
|
# Write browser.yml with the given config and reload the daemon.
|
|
126
128
|
# Called by HttpServer POST /api/browser/configure.
|
|
129
|
+
# Existing keys are preserved so re-running browser-setup does not wipe
|
|
130
|
+
# user preferences such as the screenshot limits.
|
|
127
131
|
# @param chrome_version [String] detected Chrome major version
|
|
128
132
|
def configure(chrome_version:)
|
|
129
|
-
cfg =
|
|
133
|
+
cfg = load_config.merge(
|
|
130
134
|
"enabled" => true,
|
|
131
135
|
"browser" => "chrome",
|
|
132
136
|
"chrome_version" => chrome_version.to_s,
|
|
133
137
|
"configured_at" => Date.today.to_s
|
|
134
|
-
|
|
138
|
+
)
|
|
135
139
|
FileUtils.mkdir_p(File.dirname(BROWSER_CONFIG_PATH))
|
|
136
140
|
File.write(BROWSER_CONFIG_PATH, cfg.to_yaml)
|
|
137
141
|
reload
|
|
138
142
|
end
|
|
139
143
|
|
|
144
|
+
# Persist the screenshot preferences written by the settings UI.
|
|
145
|
+
# No daemon reload: the browser tool re-reads browser.yml on every screenshot.
|
|
146
|
+
# @param max_base64_bytes [Integer, nil] screenshot size ceiling in bytes
|
|
147
|
+
# @return [Hash] the effective settings after the update
|
|
148
|
+
def update_screenshot_settings(max_base64_bytes: nil)
|
|
149
|
+
raise "Browser not configured. Run /browser-setup first." unless File.exist?(BROWSER_CONFIG_PATH)
|
|
150
|
+
|
|
151
|
+
if !max_base64_bytes.nil? && !(max_base64_bytes.is_a?(Integer) && max_base64_bytes.positive?)
|
|
152
|
+
raise ArgumentError, "screenshot_max_base64_bytes must be a positive integer"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
cfg = load_config
|
|
156
|
+
cfg["screenshot_max_base64_bytes"] = max_base64_bytes unless max_base64_bytes.nil?
|
|
157
|
+
|
|
158
|
+
File.write(BROWSER_CONFIG_PATH, cfg.to_yaml)
|
|
159
|
+
@config = cfg
|
|
160
|
+
{ screenshot_max_base64_bytes: effective_screenshot_limit(cfg) }
|
|
161
|
+
end
|
|
162
|
+
|
|
140
163
|
# Toggle the browser tool on/off by flipping `enabled` in browser.yml.
|
|
141
164
|
# Raises if browser.yml doesn't exist (not yet set up).
|
|
142
165
|
# @return [Boolean] new enabled state
|
|
@@ -224,6 +247,13 @@ module Clacky
|
|
|
224
247
|
{}
|
|
225
248
|
end
|
|
226
249
|
|
|
250
|
+
private def effective_screenshot_limit(cfg)
|
|
251
|
+
limit = cfg["screenshot_max_base64_bytes"]
|
|
252
|
+
return limit if limit.is_a?(Integer) && limit.positive?
|
|
253
|
+
|
|
254
|
+
Clacky::Tools::Browser::SCREENSHOT_MAX_BASE64_BYTES
|
|
255
|
+
end
|
|
256
|
+
|
|
227
257
|
# Must be called inside @mutex
|
|
228
258
|
def ensure_process!
|
|
229
259
|
return if process_alive?
|
|
@@ -152,7 +152,7 @@ module Clacky
|
|
|
152
152
|
# receive_id for outbound messages), so we extract the chat portion.
|
|
153
153
|
prefix = "#{platform}:"
|
|
154
154
|
ids = []
|
|
155
|
-
@registry.list.each do |summary|
|
|
155
|
+
@registry.list(archived: nil).each do |summary|
|
|
156
156
|
@registry.with_session(summary[:id]) do |s|
|
|
157
157
|
(s[:channel_keys] || []).each do |key|
|
|
158
158
|
next unless key.start_with?(prefix)
|
|
@@ -463,7 +463,7 @@ module Clacky
|
|
|
463
463
|
when "/unbind"
|
|
464
464
|
unbound = false
|
|
465
465
|
cleared_agents = []
|
|
466
|
-
@registry.list.each do |summary|
|
|
466
|
+
@registry.list(archived: nil).each do |summary|
|
|
467
467
|
@registry.with_session(summary[:id]) do |s|
|
|
468
468
|
# Set#delete always returns self — use delete? so an unknown key
|
|
469
469
|
# reports "No binding found." instead of a false "Unbound.".
|
|
@@ -726,7 +726,7 @@ module Clacky
|
|
|
726
726
|
# /bind and /unbind keep agent.channel_info strictly in sync with channel_keys
|
|
727
727
|
# (see handle_bind / handle_unbind), so the two sources never disagree on the
|
|
728
728
|
# same key for two different sessions — a single pass is sufficient.
|
|
729
|
-
@registry.list.each do |summary|
|
|
729
|
+
@registry.list(archived: nil).each do |summary|
|
|
730
730
|
found = nil
|
|
731
731
|
@registry.with_session(summary[:id]) { |s| found = s[:channel_keys]&.include?(key) }
|
|
732
732
|
return summary[:id] if found
|
|
@@ -814,7 +814,7 @@ module Clacky
|
|
|
814
814
|
# The on-disk summary is authoritative here, so sessions evicted from
|
|
815
815
|
# memory are cleaned too — which is exactly the case /bind used to miss.
|
|
816
816
|
def clear_channel_info_for_key(key, keep_session_id: nil)
|
|
817
|
-
@registry.list(limit: nil).each do |summary|
|
|
817
|
+
@registry.list(limit: nil, archived: nil).each do |summary|
|
|
818
818
|
next if summary[:id] == keep_session_id
|
|
819
819
|
|
|
820
820
|
info = summary[:channel_info]
|
|
@@ -839,7 +839,7 @@ module Clacky
|
|
|
839
839
|
end
|
|
840
840
|
|
|
841
841
|
def bind_key_to_session(key, session_id)
|
|
842
|
-
@registry.list.each do |summary|
|
|
842
|
+
@registry.list(archived: nil).each do |summary|
|
|
843
843
|
@registry.with_session(summary[:id]) { |s| s[:channel_keys]&.delete(key) }
|
|
844
844
|
end
|
|
845
845
|
@registry.with_session(session_id) do |s|
|
|
@@ -932,7 +932,7 @@ module Clacky
|
|
|
932
932
|
def restore_channel_bindings
|
|
933
933
|
bound_keys = Set.new
|
|
934
934
|
restored_count = 0
|
|
935
|
-
@registry.list(limit: nil).each do |summary|
|
|
935
|
+
@registry.list(limit: nil, archived: nil).each do |summary|
|
|
936
936
|
info = summary[:channel_info]
|
|
937
937
|
next unless info.is_a?(Hash) && info[:platform] && info[:user_id] && info[:chat_id]
|
|
938
938
|
|