openclacky 1.5.7 → 1.5.9
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 +71 -0
- data/README.md +3 -3
- data/README_CN.md +2 -2
- data/README_JA.md +2 -2
- data/lib/clacky/agent.rb +2 -3
- data/lib/clacky/agent_config.rb +40 -18
- data/lib/clacky/brand_config.rb +12 -7
- data/lib/clacky/cli.rb +0 -1
- data/lib/clacky/default_extensions/ext-studio/ext.yml +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +27 -14
- data/lib/clacky/default_extensions/ext-studio/skills/ext-develop/SKILL.md +22 -5
- data/lib/clacky/default_extensions/general/ext.yml +1 -1
- data/lib/clacky/extension/api_loader.rb +4 -11
- data/lib/clacky/extension/packager.rb +1 -1
- data/lib/clacky/extension/scaffold/templates/hello/test/handler_test.rb.erb +22 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +3 -3
- data/lib/clacky/message_format/anthropic.rb +19 -2
- data/lib/clacky/message_format/bedrock.rb +8 -3
- data/lib/clacky/message_history.rb +1 -1
- data/lib/clacky/providers.rb +159 -105
- data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +6 -0
- data/lib/clacky/server/http_server.rb +67 -79
- data/lib/clacky/session_manager.rb +7 -0
- data/lib/clacky/skill_loader.rb +4 -0
- data/lib/clacky/tools/base.rb +11 -1
- data/lib/clacky/tools/edit.rb +28 -0
- data/lib/clacky/tools/terminal.rb +5 -0
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/ui2/components/inline_input.rb +1 -0
- data/lib/clacky/ui2/components/input_area.rb +1 -0
- data/lib/clacky/ui2/components/modal_component.rb +0 -1
- data/lib/clacky/ui2/components/todo_area.rb +1 -0
- data/lib/clacky/ui2/line_editor.rb +1 -0
- data/lib/clacky/ui2/markdown_renderer.rb +5 -1
- data/lib/clacky/ui2/ui_controller.rb +1 -0
- data/lib/clacky/utils/model_pricing.rb +62 -27
- data/lib/clacky/utils/string_matcher.rb +29 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +278 -221
- data/lib/clacky/web/components/chat-navigator.js +1 -1
- data/lib/clacky/web/features/billing/store.js +3 -3
- data/lib/clacky/web/features/billing/view.js +5 -61
- data/lib/clacky/web/features/extensions/store.js +65 -41
- data/lib/clacky/web/features/extensions/view.js +74 -10
- data/lib/clacky/web/features/new-session/view.js +24 -0
- data/lib/clacky/web/features/version/view.js +2 -2
- data/lib/clacky/web/i18n.js +36 -22
- data/lib/clacky/web/index.html +17 -17
- data/lib/clacky/web/projects.js +83 -285
- data/lib/clacky/web/sessions.js +151 -17
- data/lib/clacky/web/settings.js +6 -1
- metadata +2 -1
|
@@ -181,10 +181,27 @@ module Clacky
|
|
|
181
181
|
{ id: tc["id"], type: "function", name: tc["name"], arguments: args }
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
+
# Map Anthropic stop_reason → OpenAI-style finish_reason so downstream
|
|
185
|
+
# (llm_caller empty-response detector, agent loop, CostTracker) stays
|
|
186
|
+
# provider-agnostic. Each cluster shares the same semantic class:
|
|
187
|
+
# stop — natural end of turn (end_turn / pause_turn / stop_sequence)
|
|
188
|
+
# length — token or context-window limit hit (max_tokens /
|
|
189
|
+
# model_context_window_exceeded; mapping the latter to
|
|
190
|
+
# "length" also prevents a futile retry in llm_caller's
|
|
191
|
+
# empty-response detector, which exempts "length")
|
|
192
|
+
# content_filter — model declined (refusal)
|
|
193
|
+
# other — context compaction (compaction)
|
|
194
|
+
# Note: `compaction` is a real stop_reason but only emitted under the
|
|
195
|
+
# `compact-2026-01-12` beta header (server-side context compaction),
|
|
196
|
+
# so it is not yet in the stable SDK's StopReason literal list.
|
|
197
|
+
# See: https://platform.claude.com/docs/en/build-with-claude/compaction
|
|
198
|
+
# Unmapped values fall through unchanged.
|
|
184
199
|
finish_reason = case data["stop_reason"]
|
|
185
|
-
when "end_turn"
|
|
200
|
+
when "end_turn", "pause_turn", "stop_sequence" then "stop"
|
|
186
201
|
when "tool_use" then "tool_calls"
|
|
187
|
-
when "max_tokens" then "length"
|
|
202
|
+
when "max_tokens", "model_context_window_exceeded" then "length"
|
|
203
|
+
when "refusal" then "content_filter"
|
|
204
|
+
when "compaction" then "other"
|
|
188
205
|
else data["stop_reason"]
|
|
189
206
|
end
|
|
190
207
|
|
|
@@ -118,11 +118,16 @@ module Clacky
|
|
|
118
118
|
{ id: tc["toolUseId"], type: "function", name: tc["name"], arguments: args }
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
# Map Bedrock stopReason → canonical finish_reason
|
|
121
|
+
# Map Bedrock stopReason → canonical finish_reason (same clusters as
|
|
122
|
+
# the Anthropic direct adapter — see lib/clacky/message_format/anthropic.rb).
|
|
122
123
|
finish_reason = case data["stopReason"]
|
|
123
|
-
when "end_turn"
|
|
124
|
+
when "end_turn", "pause_turn", "stop_sequence" then "stop"
|
|
124
125
|
when "tool_use" then "tool_calls"
|
|
125
|
-
when "max_tokens" then "length"
|
|
126
|
+
when "max_tokens", "model_context_window_exceeded" then "length"
|
|
127
|
+
when "refusal" then "content_filter"
|
|
128
|
+
# `compaction` is a beta stop_reason (compact-2026-01-12);
|
|
129
|
+
# see anthropic.rb for the full note.
|
|
130
|
+
when "compaction" then "other"
|
|
126
131
|
else data["stopReason"]
|
|
127
132
|
end
|
|
128
133
|
|
|
@@ -286,7 +286,7 @@ module Clacky
|
|
|
286
286
|
private def estimate_content_tokens(content)
|
|
287
287
|
case content
|
|
288
288
|
when String
|
|
289
|
-
ascii_chars = content.
|
|
289
|
+
ascii_chars = content.count(" -~")
|
|
290
290
|
multibyte_chars = content.length - ascii_chars
|
|
291
291
|
((ascii_chars / 4.0) + (multibyte_chars / 1.5)).ceil
|
|
292
292
|
when Array
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -253,32 +253,34 @@ module Clacky
|
|
|
253
253
|
"website_url" => "https://platform.deepseek.com/api_keys"
|
|
254
254
|
}.freeze,
|
|
255
255
|
|
|
256
|
-
"
|
|
257
|
-
"name" => "
|
|
258
|
-
"base_url" => "https://
|
|
256
|
+
"glm" => {
|
|
257
|
+
"name" => "GLM (Z.ai / Zhipu)",
|
|
258
|
+
"base_url" => "https://open.bigmodel.cn/api/paas/v4",
|
|
259
259
|
"api" => "openai-completions",
|
|
260
|
-
"default_model" => "
|
|
261
|
-
"models" => ["
|
|
262
|
-
#
|
|
263
|
-
#
|
|
264
|
-
#
|
|
265
|
-
#
|
|
266
|
-
#
|
|
260
|
+
"default_model" => "glm-5.2",
|
|
261
|
+
"models" => ["glm-5.2", "glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
|
|
262
|
+
# Zhipu / Z.ai expose four functionally-equivalent endpoints:
|
|
263
|
+
# two regional sites (mainland open.bigmodel.cn + international api.z.ai)
|
|
264
|
+
# each with a general-billing and a Coding-Plan subpath. They share the
|
|
265
|
+
# same model lineup & identical capability profile, so a single preset
|
|
266
|
+
# with endpoint_variants is the right shape — one source of truth for
|
|
267
|
+
# vision/model_capabilities, four URLs recognised by find_by_base_url.
|
|
268
|
+
# Without this, users pointing at api.z.ai or the /coding/ path fell
|
|
269
|
+
# through to the conservative "assume vision=true" default and got
|
|
270
|
+
# hallucinated image descriptions on text-only GLM models (C-5563).
|
|
267
271
|
"endpoint_variants" => [
|
|
268
|
-
{ "label" => "Mainland
|
|
269
|
-
{ "label" => "
|
|
272
|
+
{ "label" => "Mainland · Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.mainland_cn_payg", "base_url" => "https://open.bigmodel.cn/api/paas/v4", "region" => "cn" }.freeze,
|
|
273
|
+
{ "label" => "Mainland · Coding Plan", "label_key" => "settings.models.baseurl.variant.mainland_cn_coding", "base_url" => "https://open.bigmodel.cn/api/coding/paas/v4", "region" => "cn" }.freeze,
|
|
274
|
+
{ "label" => "International · Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.international_payg", "base_url" => "https://api.z.ai/api/paas/v4", "region" => "intl" }.freeze,
|
|
275
|
+
{ "label" => "International · Coding Plan", "label_key" => "settings.models.baseurl.variant.international_coding","base_url" => "https://api.z.ai/api/coding/paas/v4", "region" => "intl" }.freeze
|
|
270
276
|
].freeze,
|
|
271
|
-
#
|
|
272
|
-
# is natively multimodal — it accepts image input via OpenAI-style
|
|
273
|
-
# image_url content parts — so it overrides the provider-level
|
|
274
|
-
# vision=false below. M3 exposes a 1,000,000-token context window
|
|
275
|
-
# (M2.7: 204,800; M2.5: 40,960).
|
|
277
|
+
# GLM models are text-only except glm-5v-turbo which is vision-capable ("v" = visual).
|
|
276
278
|
"capabilities" => { "vision" => false }.freeze,
|
|
277
279
|
"model_capabilities" => {
|
|
278
|
-
"
|
|
280
|
+
"glm-5v-turbo" => { "vision" => true }.freeze
|
|
279
281
|
}.freeze,
|
|
280
|
-
"default_ocr_model" => "
|
|
281
|
-
"website_url" => "https://
|
|
282
|
+
"default_ocr_model" => "glm-5v-turbo",
|
|
283
|
+
"website_url" => "https://open.bigmodel.cn/console/overview"
|
|
282
284
|
}.freeze,
|
|
283
285
|
|
|
284
286
|
"kimi" => {
|
|
@@ -346,6 +348,34 @@ module Clacky
|
|
|
346
348
|
"website_url" => "https://www.kimi.com/code"
|
|
347
349
|
}.freeze,
|
|
348
350
|
|
|
351
|
+
"minimax" => {
|
|
352
|
+
"name" => "Minimax",
|
|
353
|
+
"base_url" => "https://api.minimaxi.com/v1",
|
|
354
|
+
"api" => "openai-completions",
|
|
355
|
+
"default_model" => "MiniMax-M3",
|
|
356
|
+
"models" => ["MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5"],
|
|
357
|
+
# MiniMax operates two regional endpoints with identical APIs & model
|
|
358
|
+
# lineup — mainland China (.com) and international (.io). Listing both
|
|
359
|
+
# lets find_by_base_url identify either one as provider "minimax",
|
|
360
|
+
# so capability checks (vision=false) fire correctly regardless of
|
|
361
|
+
# which endpoint the user configured.
|
|
362
|
+
"endpoint_variants" => [
|
|
363
|
+
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://api.minimaxi.com/v1", "region" => "cn" }.freeze,
|
|
364
|
+
{ "label" => "International", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://api.minimax.io/v1", "region" => "intl" }.freeze
|
|
365
|
+
].freeze,
|
|
366
|
+
# MiniMax M2.5/M2.7 are text-only on this endpoint. M3 (released 2026-06-01)
|
|
367
|
+
# is natively multimodal — it accepts image input via OpenAI-style
|
|
368
|
+
# image_url content parts — so it overrides the provider-level
|
|
369
|
+
# vision=false below. M3 exposes a 1,000,000-token context window
|
|
370
|
+
# (M2.7: 204,800; M2.5: 40,960).
|
|
371
|
+
"capabilities" => { "vision" => false }.freeze,
|
|
372
|
+
"model_capabilities" => {
|
|
373
|
+
"MiniMax-M3" => { "vision" => true }.freeze
|
|
374
|
+
}.freeze,
|
|
375
|
+
"default_ocr_model" => "MiniMax-M3",
|
|
376
|
+
"website_url" => "https://platform.minimax.io/"
|
|
377
|
+
}.freeze,
|
|
378
|
+
|
|
349
379
|
"anthropic" => {
|
|
350
380
|
"name" => "Anthropic (Claude)",
|
|
351
381
|
"base_url" => "https://api.anthropic.com",
|
|
@@ -356,6 +386,70 @@ module Clacky
|
|
|
356
386
|
"website_url" => "https://console.anthropic.com/settings/keys"
|
|
357
387
|
}.freeze,
|
|
358
388
|
|
|
389
|
+
"openai" => {
|
|
390
|
+
"name" => "OpenAI (GPT)",
|
|
391
|
+
"base_url" => "https://api.openai.com/v1",
|
|
392
|
+
"api" => "openai-completions",
|
|
393
|
+
"default_model" => "gpt-5.5",
|
|
394
|
+
"models" => [
|
|
395
|
+
"gpt-5.5",
|
|
396
|
+
"gpt-5.4",
|
|
397
|
+
"gpt-5.4-mini",
|
|
398
|
+
"gpt-5.4-nano",
|
|
399
|
+
"o4-mini",
|
|
400
|
+
"o3"
|
|
401
|
+
],
|
|
402
|
+
# GPT-5.x and o-series models are multimodal (text + image input).
|
|
403
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
404
|
+
# Per-primary lite pairing: subagents use mini/nano for cheap/fast work.
|
|
405
|
+
# o4-mini and o3 are reasoning models without a lite-tier sibling here.
|
|
406
|
+
"lite_models" => {
|
|
407
|
+
"gpt-5.5" => "gpt-5.4-mini",
|
|
408
|
+
"gpt-5.4" => "gpt-5.4-mini"
|
|
409
|
+
},
|
|
410
|
+
# OpenAI's image generation model — same /v1/images/generations
|
|
411
|
+
# endpoint, so the OpenAICompat image provider handles it.
|
|
412
|
+
"image_models" => [
|
|
413
|
+
"gpt-image-2"
|
|
414
|
+
],
|
|
415
|
+
"default_image_model" => "gpt-image-2",
|
|
416
|
+
"default_ocr_model" => "gpt-5.4-mini",
|
|
417
|
+
"website_url" => "https://platform.openai.com/api-keys"
|
|
418
|
+
}.freeze,
|
|
419
|
+
|
|
420
|
+
"qwen" => {
|
|
421
|
+
"name" => "Qwen (Alibaba)",
|
|
422
|
+
"base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
423
|
+
"api" => "openai-completions",
|
|
424
|
+
"default_model" => "qwen3.7-max",
|
|
425
|
+
"models" => [
|
|
426
|
+
"qwen3.7-max",
|
|
427
|
+
"qwen3.6-plus",
|
|
428
|
+
"qwen3.6-max",
|
|
429
|
+
"qwen3.6-27b",
|
|
430
|
+
"qwen3.6-flash",
|
|
431
|
+
"qwen-plus-latest",
|
|
432
|
+
],
|
|
433
|
+
"endpoint_variants" => [
|
|
434
|
+
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1", "region" => "cn" }.freeze,
|
|
435
|
+
{ "label" => "Singapore", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "region" => "intl" }.freeze,
|
|
436
|
+
{ "label" => "US (Virginia)", "label_key" => "settings.models.baseurl.variant.us", "base_url" => "https://dashscope-us.aliyuncs.com/compatible-mode/v1", "region" => "us" }.freeze
|
|
437
|
+
].freeze,
|
|
438
|
+
"capabilities" => { "vision" => true }.freeze,
|
|
439
|
+
"model_capabilities" => {
|
|
440
|
+
"qwen3.7-max" => { "vision" => false }.freeze
|
|
441
|
+
}.freeze,
|
|
442
|
+
"default_ocr_model" => "qwen3.6-flash",
|
|
443
|
+
"lite_models" => {
|
|
444
|
+
"qwen3.7-max" => "qwen3.6-flash",
|
|
445
|
+
"qwen3.6-plus" => "qwen3.6-flash",
|
|
446
|
+
"qwen3.6-max" => "qwen3.6-flash",
|
|
447
|
+
"qwen3.6-27b" => "qwen3.6-flash",
|
|
448
|
+
"qwen-plus-latest" => "qwen3.6-flash"
|
|
449
|
+
},
|
|
450
|
+
"website_url" => "https://bailian.console.aliyun.com/?apiKey=1"
|
|
451
|
+
}.freeze,
|
|
452
|
+
|
|
359
453
|
"mimo" => {
|
|
360
454
|
"name" => "MiMo (Xiaomi)",
|
|
361
455
|
"base_url" => "https://api.xiaomimimo.com/v1",
|
|
@@ -388,36 +482,6 @@ module Clacky
|
|
|
388
482
|
"website_url" => "https://platform.xiaomimimo.com/"
|
|
389
483
|
}.freeze,
|
|
390
484
|
|
|
391
|
-
"glm" => {
|
|
392
|
-
"name" => "GLM (Z.ai / Zhipu)",
|
|
393
|
-
"base_url" => "https://open.bigmodel.cn/api/paas/v4",
|
|
394
|
-
"api" => "openai-completions",
|
|
395
|
-
"default_model" => "glm-5.2",
|
|
396
|
-
"models" => ["glm-5.2", "glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
|
|
397
|
-
# Zhipu / Z.ai expose four functionally-equivalent endpoints:
|
|
398
|
-
# two regional sites (mainland open.bigmodel.cn + international api.z.ai)
|
|
399
|
-
# each with a general-billing and a Coding-Plan subpath. They share the
|
|
400
|
-
# same model lineup & identical capability profile, so a single preset
|
|
401
|
-
# with endpoint_variants is the right shape — one source of truth for
|
|
402
|
-
# vision/model_capabilities, four URLs recognised by find_by_base_url.
|
|
403
|
-
# Without this, users pointing at api.z.ai or the /coding/ path fell
|
|
404
|
-
# through to the conservative "assume vision=true" default and got
|
|
405
|
-
# hallucinated image descriptions on text-only GLM models (C-5563).
|
|
406
|
-
"endpoint_variants" => [
|
|
407
|
-
{ "label" => "Mainland · Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.mainland_cn_payg", "base_url" => "https://open.bigmodel.cn/api/paas/v4", "region" => "cn" }.freeze,
|
|
408
|
-
{ "label" => "Mainland · Coding Plan", "label_key" => "settings.models.baseurl.variant.mainland_cn_coding", "base_url" => "https://open.bigmodel.cn/api/coding/paas/v4", "region" => "cn" }.freeze,
|
|
409
|
-
{ "label" => "International · Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.international_payg", "base_url" => "https://api.z.ai/api/paas/v4", "region" => "intl" }.freeze,
|
|
410
|
-
{ "label" => "International · Coding Plan", "label_key" => "settings.models.baseurl.variant.international_coding","base_url" => "https://api.z.ai/api/coding/paas/v4", "region" => "intl" }.freeze
|
|
411
|
-
].freeze,
|
|
412
|
-
# GLM models are text-only except glm-5v-turbo which is vision-capable ("v" = visual).
|
|
413
|
-
"capabilities" => { "vision" => false }.freeze,
|
|
414
|
-
"model_capabilities" => {
|
|
415
|
-
"glm-5v-turbo" => { "vision" => true }.freeze
|
|
416
|
-
}.freeze,
|
|
417
|
-
"default_ocr_model" => "glm-5v-turbo",
|
|
418
|
-
"website_url" => "https://open.bigmodel.cn/usercenter/apikeys"
|
|
419
|
-
}.freeze,
|
|
420
|
-
|
|
421
485
|
# Volcengine Ark (Doubao) — ByteDance's model platform, OpenAI-compatible.
|
|
422
486
|
# Exposes three functionally-equivalent endpoints (Pay-as-you-go / Coding
|
|
423
487
|
# Plan / Agent Plan) that share the same model lineup and capability
|
|
@@ -472,7 +536,8 @@ module Clacky
|
|
|
472
536
|
"model_capabilities" => {
|
|
473
537
|
"glm-5.2" => { "vision" => false }.freeze,
|
|
474
538
|
"deepseek-v4-pro" => { "vision" => false }.freeze,
|
|
475
|
-
"deepseek-v4-flash" => { "vision" => false }.freeze
|
|
539
|
+
"deepseek-v4-flash" => { "vision" => false }.freeze,
|
|
540
|
+
"minimax-m2.7" => { "vision" => false }.freeze
|
|
476
541
|
}.freeze,
|
|
477
542
|
"default_ocr_model" => "doubao-seed-2.0-lite",
|
|
478
543
|
"website_url" => "https://console.volcengine.com/ark/region:cn-beijing/overview"
|
|
@@ -539,68 +604,57 @@ module Clacky
|
|
|
539
604
|
"website_url" => "https://ollama.com/settings/keys"
|
|
540
605
|
}.freeze,
|
|
541
606
|
|
|
542
|
-
"
|
|
543
|
-
"name" => "
|
|
544
|
-
"base_url" => "https://api.
|
|
607
|
+
"orcarouter" => {
|
|
608
|
+
"name" => "OrcaRouter",
|
|
609
|
+
"base_url" => "https://api.orcarouter.ai/v1",
|
|
545
610
|
"api" => "openai-completions",
|
|
546
|
-
"default_model" => "gpt-5.5",
|
|
611
|
+
"default_model" => "openai/gpt-5.5",
|
|
612
|
+
# Curated default lineup. OrcaRouter exposes 190+ models from OpenAI,
|
|
613
|
+
# Anthropic, Google, DeepSeek, Qwen, MiniMax, Zhipu and others behind a
|
|
614
|
+
# single OpenAI-compatible endpoint. Shipping a small list of the
|
|
615
|
+
# mainstream Claude + GPT entries gives users a working dropdown out
|
|
616
|
+
# of the box; users can still type any other OrcaRouter model id
|
|
617
|
+
# manually (e.g. "orcarouter/auto" for request-level auto-routing).
|
|
547
618
|
"models" => [
|
|
548
|
-
"
|
|
549
|
-
"
|
|
550
|
-
"
|
|
551
|
-
"gpt-5.
|
|
552
|
-
"
|
|
553
|
-
"
|
|
619
|
+
"anthropic/claude-sonnet-5",
|
|
620
|
+
"anthropic/claude-opus-4.8",
|
|
621
|
+
"anthropic/claude-haiku-4.5",
|
|
622
|
+
"openai/gpt-5.5",
|
|
623
|
+
"openai/gpt-5.4",
|
|
624
|
+
"openai/gpt-5.4-mini",
|
|
625
|
+
"google/gemini-3.5-flash",
|
|
626
|
+
"deepseek/deepseek-v4-flash",
|
|
627
|
+
"z-ai/glm-5.2",
|
|
628
|
+
"orcarouter/auto"
|
|
554
629
|
],
|
|
555
|
-
#
|
|
556
|
-
|
|
557
|
-
#
|
|
558
|
-
# o4-mini and o3 are reasoning models without a lite-tier sibling here.
|
|
630
|
+
# Per-primary lite pairing — Claude family pairs with Haiku, GPT
|
|
631
|
+
# family pairs with the mini variant. Mirrors the openrouter preset
|
|
632
|
+
# so subagents on OrcaRouter get a sensible cheap/fast sidekick.
|
|
559
633
|
"lite_models" => {
|
|
560
|
-
"
|
|
561
|
-
"
|
|
634
|
+
"anthropic/claude-sonnet-5" => "anthropic/claude-haiku-4.5",
|
|
635
|
+
"anthropic/claude-opus-4.8" => "anthropic/claude-haiku-4.5",
|
|
636
|
+
"openai/gpt-5.5" => "openai/gpt-5.4-mini",
|
|
637
|
+
"openai/gpt-5.4" => "openai/gpt-5.4-mini"
|
|
562
638
|
},
|
|
563
|
-
#
|
|
564
|
-
#
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
"
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
"name" => "Qwen (Alibaba)",
|
|
575
|
-
"base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
576
|
-
"api" => "openai-completions",
|
|
577
|
-
"default_model" => "qwen3.7-max",
|
|
578
|
-
"models" => [
|
|
579
|
-
"qwen3.7-max",
|
|
580
|
-
"qwen3.6-plus",
|
|
581
|
-
"qwen3.6-max",
|
|
582
|
-
"qwen3.6-27b",
|
|
583
|
-
"qwen3.6-flash",
|
|
584
|
-
"qwen-plus-latest",
|
|
585
|
-
],
|
|
586
|
-
"endpoint_variants" => [
|
|
587
|
-
{ "label" => "Mainland China", "label_key" => "settings.models.baseurl.variant.mainland_cn", "base_url" => "https://dashscope.aliyuncs.com/compatible-mode/v1", "region" => "cn" }.freeze,
|
|
588
|
-
{ "label" => "Singapore", "label_key" => "settings.models.baseurl.variant.international", "base_url" => "https://dashscope-intl.aliyuncs.com/compatible-mode/v1", "region" => "intl" }.freeze,
|
|
589
|
-
{ "label" => "US (Virginia)", "label_key" => "settings.models.baseurl.variant.us", "base_url" => "https://dashscope-us.aliyuncs.com/compatible-mode/v1", "region" => "us" }.freeze
|
|
590
|
-
].freeze,
|
|
639
|
+
# Per-model API type overrides. OrcaRouter proxies Claude through a
|
|
640
|
+
# native Anthropic /v1/messages endpoint (https://api.orcarouter.ai/v1/messages)
|
|
641
|
+
# in addition to its OpenAI-compatible /chat/completions endpoint.
|
|
642
|
+
# Routing "anthropic/*" via the native endpoint preserves cache_control
|
|
643
|
+
# fidelity, matching what Claude Code CLI does internally (same rationale
|
|
644
|
+
# as the openrouter preset). Non-Claude models keep the OpenAI shim.
|
|
645
|
+
"model_api_overrides" => {
|
|
646
|
+
/\Aanthropic\// => "anthropic-messages"
|
|
647
|
+
}.freeze,
|
|
648
|
+
# Most models on OrcaRouter are vision-capable; DeepSeek / GLM-5.2 and
|
|
649
|
+
# the "orcarouter/auto" router are text-only.
|
|
591
650
|
"capabilities" => { "vision" => true }.freeze,
|
|
592
651
|
"model_capabilities" => {
|
|
593
|
-
"
|
|
652
|
+
"deepseek/deepseek-v4-flash" => { "vision" => false }.freeze,
|
|
653
|
+
"z-ai/glm-5.2" => { "vision" => false }.freeze,
|
|
654
|
+
"orcarouter/auto" => { "vision" => false }.freeze
|
|
594
655
|
}.freeze,
|
|
595
|
-
"default_ocr_model" => "
|
|
596
|
-
"
|
|
597
|
-
"qwen3.7-max" => "qwen3.6-flash",
|
|
598
|
-
"qwen3.6-plus" => "qwen3.6-flash",
|
|
599
|
-
"qwen3.6-max" => "qwen3.6-flash",
|
|
600
|
-
"qwen3.6-27b" => "qwen3.6-flash",
|
|
601
|
-
"qwen-plus-latest" => "qwen3.6-flash"
|
|
602
|
-
},
|
|
603
|
-
"website_url" => "https://bailian.console.aliyun.com/?apiKey=1"
|
|
656
|
+
"default_ocr_model" => "google/gemini-3.5-flash",
|
|
657
|
+
"website_url" => "https://www.orcarouter.ai"
|
|
604
658
|
}.freeze
|
|
605
659
|
|
|
606
660
|
}.freeze
|
|
@@ -71,6 +71,12 @@ module Clacky
|
|
|
71
71
|
ssl_context = OpenSSL::SSL::SSLContext.new
|
|
72
72
|
ssl_context.set_params(verify_mode: OpenSSL::SSL::VERIFY_PEER)
|
|
73
73
|
ssl = OpenSSL::SSL::SSLSocket.new(tcp, ssl_context)
|
|
74
|
+
# Feishu/Lark WebSocket endpoints are served behind CDN edges that
|
|
75
|
+
# require SNI to select the correct certificate/backend. Some
|
|
76
|
+
# Ruby/OpenSSL combinations do not infer the hostname for manually
|
|
77
|
+
# created SSLSocket instances, which can make the peer abort the TLS
|
|
78
|
+
# handshake with `tlsv1 alert internal error`.
|
|
79
|
+
ssl.hostname = uri.host if ssl.respond_to?(:hostname=)
|
|
74
80
|
ssl.sync_close = true
|
|
75
81
|
ssl.connect
|
|
76
82
|
ssl
|
|
@@ -184,7 +184,7 @@ module Clacky
|
|
|
184
184
|
- 将大目标拆解为可执行的小步骤
|
|
185
185
|
MD
|
|
186
186
|
|
|
187
|
-
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false, sessions_dir: nil, socket: nil, master_pid: nil)
|
|
187
|
+
def initialize(host: "127.0.0.1", port: 7070, agent_config:, client_factory:, brand_test: false, sessions_dir: nil, projects_file: nil, socket: nil, master_pid: nil)
|
|
188
188
|
@host = host
|
|
189
189
|
@port = port
|
|
190
190
|
@agent_config = agent_config
|
|
@@ -197,7 +197,7 @@ module Clacky
|
|
|
197
197
|
@restart_script = File.expand_path($0)
|
|
198
198
|
@restart_argv = ARGV.dup
|
|
199
199
|
@session_manager = Clacky::SessionManager.new(sessions_dir: sessions_dir)
|
|
200
|
-
@project_manager = Clacky::Server::ProjectManager.new
|
|
200
|
+
@project_manager = Clacky::Server::ProjectManager.new(projects_file: projects_file)
|
|
201
201
|
@registry = SessionRegistry.new(
|
|
202
202
|
session_manager: @session_manager,
|
|
203
203
|
session_restorer: method(:build_session_from_data),
|
|
@@ -1840,6 +1840,9 @@ module Clacky
|
|
|
1840
1840
|
requested_model: state["requested_model"],
|
|
1841
1841
|
configured: state["configured"]
|
|
1842
1842
|
}
|
|
1843
|
+
if entry && entry["base_url"] && state["source"] != "custom"
|
|
1844
|
+
out[t][:saved_custom] = { model: entry["model"], base_url: entry["base_url"] }
|
|
1845
|
+
end
|
|
1843
1846
|
end
|
|
1844
1847
|
|
|
1845
1848
|
# Surface what the current default model can offer, even when the
|
|
@@ -1949,50 +1952,78 @@ module Clacky
|
|
|
1949
1952
|
end
|
|
1950
1953
|
|
|
1951
1954
|
def api_update_media_config(kind, req, res)
|
|
1952
|
-
|
|
1955
|
+
update_sidecar_config(res, kind, parse_json_body(req) || {})
|
|
1956
|
+
end
|
|
1957
|
+
|
|
1958
|
+
private def update_sidecar_config(res, kind, body)
|
|
1953
1959
|
source = body["source"].to_s
|
|
1954
1960
|
unless %w[off auto custom].include?(source)
|
|
1955
1961
|
return json_response(res, 422, { error: "invalid source" })
|
|
1956
1962
|
end
|
|
1957
1963
|
|
|
1958
|
-
@agent_config.models.
|
|
1964
|
+
existing = @agent_config.models.find { |m| m["type"] == kind }
|
|
1959
1965
|
|
|
1960
1966
|
case source
|
|
1961
1967
|
when "off"
|
|
1962
|
-
|
|
1963
|
-
"
|
|
1964
|
-
|
|
1965
|
-
"disabled" => true
|
|
1966
|
-
}
|
|
1967
|
-
when "auto"
|
|
1968
|
-
override = body["model"].to_s.strip
|
|
1969
|
-
unless override.empty?
|
|
1968
|
+
if existing
|
|
1969
|
+
existing["mode"] = "off"
|
|
1970
|
+
else
|
|
1970
1971
|
@agent_config.models << {
|
|
1971
|
-
"id"
|
|
1972
|
-
"type"
|
|
1973
|
-
"
|
|
1972
|
+
"id" => SecureRandom.uuid,
|
|
1973
|
+
"type" => kind,
|
|
1974
|
+
"mode" => "off"
|
|
1974
1975
|
}
|
|
1975
1976
|
end
|
|
1977
|
+
when "auto"
|
|
1978
|
+
override = body["model"].to_s.strip
|
|
1979
|
+
if existing
|
|
1980
|
+
existing.delete("disabled")
|
|
1981
|
+
existing["mode"] = "auto"
|
|
1982
|
+
existing["model"] = override unless override.empty?
|
|
1983
|
+
else
|
|
1984
|
+
unless override.empty?
|
|
1985
|
+
@agent_config.models << {
|
|
1986
|
+
"id" => SecureRandom.uuid,
|
|
1987
|
+
"type" => kind,
|
|
1988
|
+
"mode" => "auto",
|
|
1989
|
+
"model" => override
|
|
1990
|
+
}
|
|
1991
|
+
end
|
|
1992
|
+
end
|
|
1976
1993
|
when "custom"
|
|
1977
1994
|
model = body["model"].to_s.strip
|
|
1978
1995
|
base_url = body["base_url"].to_s.strip
|
|
1979
1996
|
api_key = body["api_key"].to_s
|
|
1980
|
-
if
|
|
1997
|
+
if api_key.empty? || api_key.include?("****")
|
|
1998
|
+
api_key = existing ? existing["api_key"].to_s : ""
|
|
1999
|
+
end
|
|
2000
|
+
if model.empty? || base_url.empty? || api_key.empty?
|
|
1981
2001
|
return json_response(res, 422, { error: "model, base_url, api_key are required" })
|
|
1982
2002
|
end
|
|
1983
2003
|
|
|
1984
|
-
|
|
1985
|
-
"
|
|
1986
|
-
"
|
|
1987
|
-
"
|
|
1988
|
-
"
|
|
1989
|
-
"
|
|
1990
|
-
"
|
|
1991
|
-
|
|
2004
|
+
if existing
|
|
2005
|
+
existing["model"] = model
|
|
2006
|
+
existing["base_url"] = base_url
|
|
2007
|
+
existing["api_key"] = api_key
|
|
2008
|
+
existing["anthropic_format"] = body["anthropic_format"] || false
|
|
2009
|
+
existing["mode"] = "custom"
|
|
2010
|
+
existing.delete("disabled")
|
|
2011
|
+
else
|
|
2012
|
+
@agent_config.models << {
|
|
2013
|
+
"id" => SecureRandom.uuid,
|
|
2014
|
+
"model" => model,
|
|
2015
|
+
"base_url" => base_url,
|
|
2016
|
+
"api_key" => api_key,
|
|
2017
|
+
"anthropic_format" => body["anthropic_format"] || false,
|
|
2018
|
+
"type" => kind,
|
|
2019
|
+
"mode" => "custom"
|
|
2020
|
+
}
|
|
2021
|
+
end
|
|
1992
2022
|
end
|
|
1993
2023
|
|
|
1994
2024
|
@agent_config.save
|
|
1995
|
-
|
|
2025
|
+
state = kind == "ocr" ? @agent_config.ocr_state : @agent_config.media_state(kind)
|
|
2026
|
+
json_response(res, 200, { ok: true, state: state })
|
|
1996
2027
|
rescue => e
|
|
1997
2028
|
json_response(res, 422, { error: e.message })
|
|
1998
2029
|
end
|
|
@@ -2016,6 +2047,9 @@ module Clacky
|
|
|
2016
2047
|
configured: state["configured"],
|
|
2017
2048
|
primary: state["primary"] || false
|
|
2018
2049
|
}
|
|
2050
|
+
if entry && entry["base_url"] && state["source"] != "custom"
|
|
2051
|
+
out[:saved_custom] = { model: entry["model"], base_url: entry["base_url"] }
|
|
2052
|
+
end
|
|
2019
2053
|
|
|
2020
2054
|
# Auto-mode preview: surface what the OCR sidecar *would* be if the
|
|
2021
2055
|
# user flipped to "auto" — derived from the same provider as the
|
|
@@ -2037,58 +2071,8 @@ module Clacky
|
|
|
2037
2071
|
# PATCH /api/config/ocr
|
|
2038
2072
|
# Body: { source: "off"|"auto"|"custom", model?, base_url?, api_key?,
|
|
2039
2073
|
# anthropic_format? }
|
|
2040
|
-
# Mirrors api_update_media_config but for the single "ocr" type.
|
|
2041
2074
|
def api_update_ocr_config(req, res)
|
|
2042
|
-
|
|
2043
|
-
source = body["source"].to_s
|
|
2044
|
-
unless %w[off auto custom].include?(source)
|
|
2045
|
-
return json_response(res, 422, { error: "invalid source" })
|
|
2046
|
-
end
|
|
2047
|
-
|
|
2048
|
-
@agent_config.models.reject! { |m| m["type"] == "ocr" }
|
|
2049
|
-
|
|
2050
|
-
case source
|
|
2051
|
-
when "off"
|
|
2052
|
-
@agent_config.models << {
|
|
2053
|
-
"id" => SecureRandom.uuid,
|
|
2054
|
-
"type" => "ocr",
|
|
2055
|
-
"disabled" => true
|
|
2056
|
-
}
|
|
2057
|
-
when "auto"
|
|
2058
|
-
override = body["model"].to_s.strip
|
|
2059
|
-
unless override.empty?
|
|
2060
|
-
@agent_config.models << {
|
|
2061
|
-
"id" => SecureRandom.uuid,
|
|
2062
|
-
"type" => "ocr",
|
|
2063
|
-
"model" => override
|
|
2064
|
-
}
|
|
2065
|
-
end
|
|
2066
|
-
when "custom"
|
|
2067
|
-
model = body["model"].to_s.strip
|
|
2068
|
-
base_url = body["base_url"].to_s.strip
|
|
2069
|
-
api_key = body["api_key"].to_s
|
|
2070
|
-
if api_key.include?("****")
|
|
2071
|
-
existing = @agent_config.models.find { |m| m["type"] == "ocr" && m["api_key"] }
|
|
2072
|
-
api_key = existing ? existing["api_key"].to_s : ""
|
|
2073
|
-
end
|
|
2074
|
-
if model.empty? || base_url.empty? || api_key.empty?
|
|
2075
|
-
return json_response(res, 422, { error: "model, base_url, api_key are required" })
|
|
2076
|
-
end
|
|
2077
|
-
|
|
2078
|
-
@agent_config.models << {
|
|
2079
|
-
"id" => SecureRandom.uuid,
|
|
2080
|
-
"model" => model,
|
|
2081
|
-
"base_url" => base_url,
|
|
2082
|
-
"api_key" => api_key,
|
|
2083
|
-
"anthropic_format" => body["anthropic_format"] || false,
|
|
2084
|
-
"type" => "ocr"
|
|
2085
|
-
}
|
|
2086
|
-
end
|
|
2087
|
-
|
|
2088
|
-
@agent_config.save
|
|
2089
|
-
json_response(res, 200, { ok: true, state: @agent_config.ocr_state })
|
|
2090
|
-
rescue => e
|
|
2091
|
-
json_response(res, 422, { error: e.message })
|
|
2075
|
+
update_sidecar_config(res, "ocr", parse_json_body(req) || {})
|
|
2092
2076
|
end
|
|
2093
2077
|
|
|
2094
2078
|
# POST /api/config/ocr/test
|
|
@@ -2562,7 +2546,7 @@ module Clacky
|
|
|
2562
2546
|
# the same public marketplace here.
|
|
2563
2547
|
def api_store_extensions(req, res)
|
|
2564
2548
|
brand = Clacky::BrandConfig.load
|
|
2565
|
-
result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"])
|
|
2549
|
+
result = brand.search_extensions!(query: req.query["q"], sort: req.query["sort"], page: req.query["page"], per_page: req.query["per_page"])
|
|
2566
2550
|
|
|
2567
2551
|
if result[:success]
|
|
2568
2552
|
installed = installed_extension_containers
|
|
@@ -2574,7 +2558,7 @@ module Clacky
|
|
|
2574
2558
|
"installed_version" => container&.dig(:version)
|
|
2575
2559
|
)
|
|
2576
2560
|
end
|
|
2577
|
-
json_response(res, 200, { ok: true, extensions: extensions })
|
|
2561
|
+
json_response(res, 200, { ok: true, extensions: extensions, meta: result[:meta] })
|
|
2578
2562
|
else
|
|
2579
2563
|
json_response(res, 200, {
|
|
2580
2564
|
ok: true,
|
|
@@ -4521,7 +4505,11 @@ module Clacky
|
|
|
4521
4505
|
upload_meta = Clacky::BrandConfig.load_upload_meta
|
|
4522
4506
|
shadowed = @skill_loader.shadowed_by_local
|
|
4523
4507
|
|
|
4524
|
-
skills = @skill_loader.all_skills.reject(&:brand_skill).
|
|
4508
|
+
skills = @skill_loader.all_skills.reject(&:brand_skill).reject do |skill|
|
|
4509
|
+
# Third-party extension skills (installed/local) are managed from the
|
|
4510
|
+
# extension panel, not the skills panel.
|
|
4511
|
+
@skill_loader.loaded_from[skill.identifier] == :extension
|
|
4512
|
+
end.map do |skill|
|
|
4525
4513
|
source = @skill_loader.loaded_from[skill.identifier]
|
|
4526
4514
|
meta = upload_meta[skill.identifier] || {}
|
|
4527
4515
|
|