openclacky 1.5.1 → 1.5.3
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 +48 -0
- data/lib/clacky/agent/llm_caller.rb +48 -0
- data/lib/clacky/agent/message_compressor_helper.rb +7 -0
- data/lib/clacky/agent/session_serializer.rb +42 -0
- data/lib/clacky/agent/skill_manager.rb +7 -0
- data/lib/clacky/agent.rb +58 -1
- data/lib/clacky/agent_config.rb +71 -1
- data/lib/clacky/brand_config.rb +88 -59
- data/lib/clacky/client.rb +13 -2
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +1 -1
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +15 -3
- data/lib/clacky/extension/packager.rb +17 -0
- data/lib/clacky/locales/en.rb +4 -2
- data/lib/clacky/locales/zh.rb +5 -3
- data/lib/clacky/message_format/anthropic.rb +56 -1
- data/lib/clacky/message_format/open_ai.rb +69 -4
- data/lib/clacky/message_history.rb +15 -1
- data/lib/clacky/providers.rb +73 -6
- data/lib/clacky/server/http_server.rb +61 -29
- data/lib/clacky/server/session_registry.rb +4 -2
- data/lib/clacky/skill.rb +4 -0
- data/lib/clacky/tools/invoke_skill.rb +21 -0
- data/lib/clacky/tools/security.rb +11 -13
- data/lib/clacky/ui_interface.rb +2 -0
- data/lib/clacky/utils/model_pricing.rb +8 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +210 -21
- data/lib/clacky/web/app.js +8 -3
- data/lib/clacky/web/components/code-editor.js +50 -12
- data/lib/clacky/web/features/billing/view.js +7 -1
- data/lib/clacky/web/features/extensions/store.js +23 -7
- data/lib/clacky/web/features/extensions/view.js +8 -4
- data/lib/clacky/web/features/new-session/view.js +61 -0
- data/lib/clacky/web/features/workspace/store.js +9 -0
- data/lib/clacky/web/features/workspace/view.js +8 -1
- data/lib/clacky/web/i18n.js +14 -0
- data/lib/clacky/web/index.html +4 -2
- data/lib/clacky/web/settings.js +49 -14
- metadata +2 -2
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"err.version_conflict": "Version {{ver}} has already been published. Please enter a higher version number above and try again.",
|
|
52
52
|
"err.invalid_version": "Invalid version. Use format like 1.0.0.",
|
|
53
53
|
"err.name_taken": "The name \"{{id}}\" is already taken by another user.",
|
|
54
|
+
"err.id_folder_mismatch": "Extension ID mismatch: the folder is named \"{{folder}}\" but ext.yml declares id \"{{id}}\". Rename the folder or the id so they match before publishing.",
|
|
54
55
|
"rename.label": "Choose a new ID for your extension:",
|
|
55
56
|
"rename.placeholder": "e.g. my-ext-yourname",
|
|
56
57
|
"rename.hint": "Only lowercase letters, digits and hyphens. This will rename the local folder and update ext.yml.",
|
|
@@ -235,6 +236,7 @@
|
|
|
235
236
|
"err.version_conflict": "版本 {{ver}} 已发布过,请在上方输入更高的版本号后重试。",
|
|
236
237
|
"err.invalid_version": "版本号格式不对,请使用如 1.0.0 的格式。",
|
|
237
238
|
"err.name_taken": "扩展名「{{id}}」已被其他用户占用。",
|
|
239
|
+
"err.id_folder_mismatch": "扩展 ID 不一致:文件夹名为「{{folder}}」,但 ext.yml 声明的 id 是「{{id}}」。请先把文件夹名或 id 改成一致再发布。",
|
|
238
240
|
"rename.label": "请为你的扩展选择一个新 ID:",
|
|
239
241
|
"rename.placeholder": "例如 my-ext-yourname",
|
|
240
242
|
"rename.hint": "只能使用小写字母、数字和连字符。",
|
|
@@ -726,6 +728,7 @@
|
|
|
726
728
|
} catch (e) {
|
|
727
729
|
const msg = e.message || "";
|
|
728
730
|
const isConflict = /must be greater than/i.test(msg);
|
|
731
|
+
const mismatch = msg.match(/folder is '([^']+)' but ext\.yml declares id '([^']+)'/);
|
|
729
732
|
if (isConflict) {
|
|
730
733
|
setProgress(t("err.version_conflict", { ver }), true);
|
|
731
734
|
verInput.classList.add("studio-input-error");
|
|
@@ -733,6 +736,8 @@
|
|
|
733
736
|
verInput.focus();
|
|
734
737
|
verInput.select();
|
|
735
738
|
verInput.addEventListener("input", () => verInput.classList.remove("studio-input-error"), { once: true });
|
|
739
|
+
} else if (mismatch) {
|
|
740
|
+
setProgress(t("err.id_folder_mismatch", { folder: mismatch[1], id: mismatch[2] }), true);
|
|
736
741
|
} else {
|
|
737
742
|
setProgress(t("err.generic", { msg }), true);
|
|
738
743
|
}
|
|
@@ -1952,7 +1957,7 @@
|
|
|
1952
1957
|
.studio-form-status { font-size: 12px; color: var(--color-text-tertiary); }
|
|
1953
1958
|
.studio-btn-sm { padding: 4px 10px; font-size: 12px; }
|
|
1954
1959
|
.studio-readme-overlay { align-items: center; justify-content: center; }
|
|
1955
|
-
.studio-readme-modal { width:
|
|
1960
|
+
.studio-readme-modal { max-width: 1280px; width: 98%; height: 90vh; max-height: 960px; border-radius: var(--radius-md, 8px); margin: 0; padding: 24px 28px 20px; display: flex; flex-direction: column; }
|
|
1956
1961
|
.studio-readme-modal .studio-modal-title { margin-bottom: 16px; flex-shrink: 0; }
|
|
1957
1962
|
.studio-readme-split { display: flex; gap: 14px; flex: 1; min-height: 0; margin-bottom: 18px; }
|
|
1958
1963
|
.studio-readme-pane { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
@@ -1960,13 +1965,20 @@
|
|
|
1960
1965
|
.studio-readme-textarea { flex: 1; resize: none; font-family: monospace; font-size: 12.5px; line-height: 1.65; border-radius: var(--radius-sm); }
|
|
1961
1966
|
.studio-readme-preview { flex: 1; overflow-y: auto; overflow-x: hidden; border: 1px solid var(--color-border-primary); border-radius: var(--radius-sm); padding: 12px 16px; background: var(--color-bg-secondary); font-size: 13px; line-height: 1.75; }
|
|
1962
1967
|
.studio-readme-preview > *:first-child { margin-top: 0; }
|
|
1963
|
-
.studio-readme-preview img { max-width: 100%; height: auto; display: block; border-radius: var(--radius-sm); }
|
|
1964
1968
|
.studio-readme-screenshots { border-top: 1px solid var(--color-border-primary); padding-top: 14px; margin-bottom: 6px; }
|
|
1965
1969
|
.studio-readme-screenshots-row { display: flex; align-items: center; gap: 10px; }
|
|
1966
1970
|
.studio-readme-screenshots-hint { font-size: 11px; color: var(--color-text-muted); }
|
|
1967
1971
|
.studio-readme-card { margin-top: 8px; }
|
|
1968
1972
|
.studio-readme-card-preview { max-height: 200px; overflow-y: auto; font-size: 13px; }
|
|
1969
|
-
.extension-readme-body img { max-width: min(800px, 100%); height: auto; }
|
|
1973
|
+
.extension-readme-body img { max-width: min(800px, 100%); height: auto; display: inline; vertical-align: middle; }
|
|
1974
|
+
.studio-readme-preview img { max-width: 100%; height: auto; display: inline; vertical-align: middle; }
|
|
1975
|
+
.studio-readme-preview hr, .extension-readme-body hr { border: none; border-top: 1px solid var(--color-border-primary); margin: 1.25em 0; }
|
|
1976
|
+
.studio-readme-preview table, .extension-readme-body table { border-collapse: collapse; width: 100%; margin: 0.75em 0; font-size: 0.8125rem; }
|
|
1977
|
+
.studio-readme-preview th, .studio-readme-preview td, .extension-readme-body th, .extension-readme-body td { border: 1px solid var(--color-border-primary); padding: 0.375em 0.75em; text-align: left; }
|
|
1978
|
+
.studio-readme-preview th, .extension-readme-body th { background: rgba(128,128,128,0.15); font-weight: 600; }
|
|
1979
|
+
.studio-readme-preview pre, .extension-readme-body pre { background: rgba(128,128,128,0.15); border-radius: 6px; padding: 0.75em 1em; overflow-x: auto; margin: 0.625em 0; }
|
|
1980
|
+
.studio-readme-preview code, .extension-readme-body code { font-family: monospace; font-size: 0.8125em; background: rgba(128,128,128,0.15); border-radius: 4px; padding: 0.15em 0.4em; }
|
|
1981
|
+
.studio-readme-preview pre code, .extension-readme-body pre code { background: none; padding: 0; }
|
|
1970
1982
|
.studio-link-btn { background: none; border: none; padding: 0; font-size: 12px; color: var(--color-accent-primary); cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
|
1971
1983
|
.studio-link-btn:hover { color: var(--color-accent-hover); }
|
|
1972
1984
|
`;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "tmpdir"
|
|
5
5
|
require "open-uri"
|
|
6
|
+
require "psych"
|
|
6
7
|
require "zip"
|
|
7
8
|
|
|
8
9
|
module Clacky
|
|
@@ -38,6 +39,7 @@ module Clacky
|
|
|
38
39
|
raise Error, "no container found at #{container_dir} (expected #{MANIFEST})"
|
|
39
40
|
end
|
|
40
41
|
|
|
42
|
+
refuse_id_folder_mismatch!(slug, container_dir)
|
|
41
43
|
verify_container!(slug, container_dir)
|
|
42
44
|
refuse_protected!(slug, container_dir)
|
|
43
45
|
|
|
@@ -96,6 +98,21 @@ module Clacky
|
|
|
96
98
|
Clacky::ExtensionLoader.invalidate_cache!
|
|
97
99
|
end
|
|
98
100
|
|
|
101
|
+
# The container's identity must be the same on both sides of the publish
|
|
102
|
+
# boundary: the client keys extensions by folder name, the marketplace
|
|
103
|
+
# keys them by ext.yml `id`. If they diverge, a rename on one side alone
|
|
104
|
+
# lets a container masquerade under a name the uniqueness check never saw.
|
|
105
|
+
private def refuse_id_folder_mismatch!(slug, container_dir)
|
|
106
|
+
manifest = File.join(container_dir, MANIFEST)
|
|
107
|
+
data = Psych.safe_load(File.read(manifest), permitted_classes: [], aliases: true) || {}
|
|
108
|
+
declared = data["id"].to_s.strip
|
|
109
|
+
return if declared == slug
|
|
110
|
+
|
|
111
|
+
raise Error,
|
|
112
|
+
"extension id mismatch: folder is '#{slug}' but #{MANIFEST} declares id '#{declared}'. " \
|
|
113
|
+
"Rename the folder or the id so they match before packing."
|
|
114
|
+
end
|
|
115
|
+
|
|
99
116
|
# Packing is for authored (open) containers. Anything already protected or
|
|
100
117
|
# carrying encrypted skills is produced by the platform pipeline; refuse
|
|
101
118
|
# to re-pack it so we never smuggle marketplace artifacts out of band.
|
data/lib/clacky/locales/en.rb
CHANGED
|
@@ -9,11 +9,12 @@ module Clacky
|
|
|
9
9
|
"llm.error.403.model_not_allowed" => "This model is not available on your current plan",
|
|
10
10
|
"llm.error.403.api_key_revoked" => "API key has been revoked, please generate a new one",
|
|
11
11
|
"llm.error.403.api_key_expired" => "API key has expired, please generate a new one",
|
|
12
|
-
"llm.error.403.quota_exceeded" => "
|
|
12
|
+
"llm.error.403.quota_exceeded" => "API key quota exceeded. Please update the quota or disable the limit in the admin console",
|
|
13
13
|
"llm.error.403.access_denied" => "Access denied, please check your API key permissions",
|
|
14
14
|
"llm.error.403.default" => "Access denied",
|
|
15
15
|
"llm.error.endpoint_not_found" => "API endpoint not found, please check your service URL",
|
|
16
16
|
"llm.error.rate_limit_429" => "Rate limit exceeded, please wait a moment",
|
|
17
|
+
"llm.error.quota_exhausted" => "API quota exhausted (key usage limit reached). Please raise the quota or upgrade your plan in the console",
|
|
17
18
|
"llm.error.server_error" => "Service temporarily unavailable (%<status>d), retrying...",
|
|
18
19
|
"llm.error.unexpected" => "Unexpected error (%<status>d)",
|
|
19
20
|
"llm.error.html_response" => "Service temporarily unavailable (received HTML error page), retrying...",
|
|
@@ -21,6 +22,7 @@ module Clacky
|
|
|
21
22
|
"llm.error.request_timeout" => "Request timed out after %<retries>d retries",
|
|
22
23
|
"llm.error.network_failed" => "Network connection failed after %<retries>d retries",
|
|
23
24
|
"llm.error.service_unavailable" => "Service unavailable after %<retries>d retries",
|
|
25
|
+
"llm.warn.switching_to_fallback_url" => "Primary endpoint unreachable. Switching to fallback gateway: %<url>s",
|
|
24
26
|
"platform.error.invalid_proof" => "Invalid license key — please check and try again.",
|
|
25
27
|
"platform.error.invalid_signature" => "Invalid request signature.",
|
|
26
28
|
"platform.error.nonce_replayed" => "Duplicate request detected. Please try again.",
|
|
@@ -28,7 +30,7 @@ module Clacky
|
|
|
28
30
|
"platform.error.license_revoked" => "This license has been revoked. Please contact support.",
|
|
29
31
|
"platform.error.license_expired" => "This license has expired. Please renew to continue.",
|
|
30
32
|
"platform.error.device_limit_reached" => "Device limit reached for this license.",
|
|
31
|
-
"platform.error.device_revoked" => "This device has been revoked from the license.",
|
|
33
|
+
"platform.error.device_revoked" => "This device has been revoked from the license. To re-activate, please wait 15 minutes and try again.",
|
|
32
34
|
"platform.error.invalid_license" => "License key not found. Please verify the key.",
|
|
33
35
|
"platform.error.device_not_found" => "Device not registered. Please re-activate.",
|
|
34
36
|
"platform.error.contributor_required" => "Publishing extensions requires becoming a contributor. Sign in, open \"My Extensions\", and click \"Become a contributor\" (no review needed).",
|
data/lib/clacky/locales/zh.rb
CHANGED
|
@@ -9,11 +9,12 @@ module Clacky
|
|
|
9
9
|
"llm.error.403.model_not_allowed" => "当前模型不支持免费试用,请升级套餐或切换其他模型",
|
|
10
10
|
"llm.error.403.api_key_revoked" => "API 密钥已被撤销,请前往控制台重新生成",
|
|
11
11
|
"llm.error.403.api_key_expired" => "API 密钥已过期,请前往控制台重新生成",
|
|
12
|
-
"llm.error.403.quota_exceeded" => "
|
|
12
|
+
"llm.error.403.quota_exceeded" => "API Key 配额已超限,请在管理后台调整配额或关闭限制",
|
|
13
13
|
"llm.error.403.access_denied" => "访问被拒绝,请检查 API 密钥权限",
|
|
14
14
|
"llm.error.403.default" => "访问被拒绝",
|
|
15
15
|
"llm.error.endpoint_not_found" => "API 端点不存在,请检查服务地址配置",
|
|
16
|
-
"llm.error.rate_limit_429" => "
|
|
16
|
+
"llm.error.rate_limit_429" => "请求过于频繁,请稍后重试",
|
|
17
|
+
"llm.error.quota_exhausted" => "API 配额已用尽(已达 Key 的用量上限),请前往控制台提升配额或升级套餐后继续",
|
|
17
18
|
"llm.error.server_error" => "服务暂时不可用(%<status>d),正在重试...",
|
|
18
19
|
"llm.error.unexpected" => "请求失败(%<status>d)",
|
|
19
20
|
"llm.error.html_response" => "服务暂时不可用(收到 HTML 错误页),正在重试...",
|
|
@@ -21,6 +22,7 @@ module Clacky
|
|
|
21
22
|
"llm.error.request_timeout" => "请求超时(已重试 %<retries>d 次)",
|
|
22
23
|
"llm.error.network_failed" => "网络连接失败(已重试 %<retries>d 次)",
|
|
23
24
|
"llm.error.service_unavailable" => "服务暂时不可用(已重试 %<retries>d 次)",
|
|
25
|
+
"llm.warn.switching_to_fallback_url" => "主节点无法连接,正在切换到备用节点:%<url>s",
|
|
24
26
|
"platform.error.invalid_proof" => "许可证密钥无效,请检查后重试。",
|
|
25
27
|
"platform.error.invalid_signature" => "请求签名无效。",
|
|
26
28
|
"platform.error.nonce_replayed" => "检测到重复请求,请重试。",
|
|
@@ -28,7 +30,7 @@ module Clacky
|
|
|
28
30
|
"platform.error.license_revoked" => "该许可证已被吊销,请联系客服。",
|
|
29
31
|
"platform.error.license_expired" => "该许可证已过期,请续订后继续。",
|
|
30
32
|
"platform.error.device_limit_reached" => "该许可证的设备数量已达上限。",
|
|
31
|
-
"platform.error.device_revoked" => "
|
|
33
|
+
"platform.error.device_revoked" => "该设备已从许可证中移除。如需重新绑定,请等待 15 分钟后再试。",
|
|
32
34
|
"platform.error.invalid_license" => "未找到许可证密钥,请核对后重试。",
|
|
33
35
|
"platform.error.device_not_found" => "设备未注册,请重新激活。",
|
|
34
36
|
"platform.error.contributor_required" => "发布扩展需要先成为扩展贡献者。请登录平台,打开「我的扩展」页面,点击「成为扩展贡献者」即可开通(无需审核)。",
|
|
@@ -66,6 +66,7 @@ module Clacky
|
|
|
66
66
|
system_text = system_messages.map { |m| extract_text(m[:content]) }.join("\n\n")
|
|
67
67
|
|
|
68
68
|
api_messages = regular_messages.map { |msg| to_api_message(msg, caching_enabled) }
|
|
69
|
+
api_messages = merge_consecutive_tool_results(api_messages)
|
|
69
70
|
api_tools = tools&.map { |t| to_api_tool(t) }
|
|
70
71
|
|
|
71
72
|
if caching_enabled && api_tools&.any?
|
|
@@ -76,7 +77,33 @@ module Clacky
|
|
|
76
77
|
body[:system] = system_text unless system_text.empty?
|
|
77
78
|
body[:tools] = api_tools if api_tools&.any?
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
# Kimi (Moonshot Coding Plan) models routed through the Anthropic
|
|
81
|
+
# /v1/messages format expect thinking:{type:"enabled"} — the native
|
|
82
|
+
# Anthropic "adaptive" type is Claude-specific and is silently ignored
|
|
83
|
+
# by the Kimi backend, so thinking never actually activates on K3.
|
|
84
|
+
#
|
|
85
|
+
# K3 additionally supports a "max" effort level (strongest reasoning)
|
|
86
|
+
# beyond the standard low/medium/high triple. The generic
|
|
87
|
+
# normalized_effort() helper drops "max" because it only whitelists
|
|
88
|
+
# the standard three levels, making K3's signature strongest-reasoning
|
|
89
|
+
# mode unreachable through this format adapter.
|
|
90
|
+
#
|
|
91
|
+
# This branch only changes K3's wire format; every other model keeps
|
|
92
|
+
# using the adaptive path below unchanged. When reasoning_effort is
|
|
93
|
+
# nil/empty we leave the body untouched (provider default), and we
|
|
94
|
+
# never emit output_config for "on" so the backend can pick its own
|
|
95
|
+
# default — both behaviours match the official kimi CLI.
|
|
96
|
+
if model.to_s.match?(/\A(kimi-)?k3/i)
|
|
97
|
+
effort_str = reasoning_effort.to_s
|
|
98
|
+
k3_effort =
|
|
99
|
+
case effort_str
|
|
100
|
+
when "max", "xhigh" then "max"
|
|
101
|
+
when "high", "medium", "low" then effort_str
|
|
102
|
+
else nil
|
|
103
|
+
end
|
|
104
|
+
body[:thinking] = { type: "enabled" }
|
|
105
|
+
body[:output_config] = { effort: k3_effort } if k3_effort
|
|
106
|
+
elsif (effort = normalized_effort(reasoning_effort))
|
|
80
107
|
body[:thinking] = { type: "adaptive" }
|
|
81
108
|
body[:output_config] = { effort: effort }
|
|
82
109
|
end
|
|
@@ -90,6 +117,34 @@ module Clacky
|
|
|
90
117
|
%w[low medium high].include?(s) ? s : nil
|
|
91
118
|
end
|
|
92
119
|
|
|
120
|
+
# Merge consecutive tool_result user messages into a single user message.
|
|
121
|
+
# Anthropic requires all tool_results for an assistant turn to appear in
|
|
122
|
+
# a SINGLE user message immediately following the assistant. When an
|
|
123
|
+
# assistant calls multiple tools, the canonical format produces one
|
|
124
|
+
# role:"tool" message per result, which to_api_message converts into
|
|
125
|
+
# separate role:"user" messages — breaking the Anthropic contract.
|
|
126
|
+
private_class_method def self.merge_consecutive_tool_results(api_messages)
|
|
127
|
+
result = []
|
|
128
|
+
pending = nil
|
|
129
|
+
|
|
130
|
+
api_messages.each do |msg|
|
|
131
|
+
if msg[:role] == "user" && msg[:content].is_a?(Array) &&
|
|
132
|
+
msg[:content].any? { |b| b.is_a?(Hash) && b[:type] == "tool_result" }
|
|
133
|
+
if pending
|
|
134
|
+
pending[:content] += msg[:content]
|
|
135
|
+
else
|
|
136
|
+
pending = msg
|
|
137
|
+
result << pending
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
pending = nil
|
|
141
|
+
result << msg
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
result
|
|
146
|
+
end
|
|
147
|
+
|
|
93
148
|
# ── Response parsing ──────────────────────────────────────────────────────
|
|
94
149
|
|
|
95
150
|
# Parse Anthropic API response into canonical internal format.
|
|
@@ -47,7 +47,7 @@ module Clacky
|
|
|
47
47
|
def build_request_body(messages, model, tools, max_tokens, caching_enabled, vision_supported: true, reasoning_effort: nil)
|
|
48
48
|
api_messages = messages.map { |msg| normalize_message_content(msg, vision_supported: vision_supported) }
|
|
49
49
|
|
|
50
|
-
body = { model: model,
|
|
50
|
+
body = { model: model, token_field_for(model) => max_tokens, messages: api_messages }
|
|
51
51
|
|
|
52
52
|
if tools&.any?
|
|
53
53
|
if caching_enabled
|
|
@@ -59,9 +59,7 @@ module Clacky
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
body[:reasoning_effort] = reasoning_effort.to_s
|
|
64
|
-
end
|
|
62
|
+
apply_reasoning_params(body, model, reasoning_effort)
|
|
65
63
|
|
|
66
64
|
body
|
|
67
65
|
end
|
|
@@ -196,6 +194,73 @@ module Clacky
|
|
|
196
194
|
|
|
197
195
|
# ── Private helpers ───────────────────────────────────────────────────────
|
|
198
196
|
|
|
197
|
+
# Returns the token-limit field name for the given model.
|
|
198
|
+
# Most OpenAI-compatible APIs use :max_tokens; MiMo-V2.5 (Xiaomi) uses
|
|
199
|
+
# :max_completion_tokens to cap the combined thinking + answer length.
|
|
200
|
+
private_class_method def self.token_field_for(model)
|
|
201
|
+
model.to_s.match?(/^mimo-v2/i) ? :max_completion_tokens : :max_tokens
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Apply model-specific reasoning / thinking parameters to the request body.
|
|
205
|
+
#
|
|
206
|
+
# Different model families expose extended reasoning through different API
|
|
207
|
+
# fields. We map the shared internal reasoning_effort value to each
|
|
208
|
+
# family's native parameters in-place.
|
|
209
|
+
#
|
|
210
|
+
# Zero side-effects: when reasoning_effort is nil/empty the body is
|
|
211
|
+
# unchanged, preserving the provider default for all models.
|
|
212
|
+
private_class_method def self.apply_reasoning_params(body, model, reasoning_effort)
|
|
213
|
+
effort_str = reasoning_effort.to_s
|
|
214
|
+
|
|
215
|
+
if model.to_s.match?(/^glm-[45]/i)
|
|
216
|
+
# GLM (Zhipu / Z.ai) supports a native top-level "thinking" field
|
|
217
|
+
# ({type: "enabled"|"disabled"}) plus a restricted reasoning_effort
|
|
218
|
+
# that only accepts "max" or "high". Other effort levels collapse
|
|
219
|
+
# to "high". Send both fields so GLM activates thinking correctly.
|
|
220
|
+
if %w[off nothink disabled].include?(effort_str)
|
|
221
|
+
body[:thinking] = { type: "disabled" }
|
|
222
|
+
elsif !effort_str.empty?
|
|
223
|
+
glm_effort =
|
|
224
|
+
case effort_str
|
|
225
|
+
when "max", "xhigh" then "max"
|
|
226
|
+
when "high" then "high"
|
|
227
|
+
when "medium", "low" then "high" # GLM collapses these to "high"
|
|
228
|
+
else "max"
|
|
229
|
+
end
|
|
230
|
+
body[:thinking] = { type: "enabled" }
|
|
231
|
+
body[:reasoning_effort] = glm_effort
|
|
232
|
+
end
|
|
233
|
+
elsif model.to_s.match?(/^kimi-k3/i)
|
|
234
|
+
# Kimi K3 reasoning_effort only accepts low/high/max (default max).
|
|
235
|
+
# Thinking is always enabled and cannot be turned off; "off" maps
|
|
236
|
+
# to the lowest intensity "low". Unsupported effort levels (medium,
|
|
237
|
+
# xhigh) would be rejected or ignored by the server.
|
|
238
|
+
if %w[off nothink disabled].include?(effort_str)
|
|
239
|
+
body[:reasoning_effort] = "low"
|
|
240
|
+
elsif !effort_str.empty?
|
|
241
|
+
body[:reasoning_effort] =
|
|
242
|
+
case effort_str
|
|
243
|
+
when "max", "xhigh" then "max"
|
|
244
|
+
when "high" then "high"
|
|
245
|
+
when "medium", "low" then "low"
|
|
246
|
+
else "max"
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
elsif model.to_s.match?(/^mimo-v2/i)
|
|
250
|
+
# MiMo-V2.5 (Xiaomi) controls reasoning via a top-level
|
|
251
|
+
# thinking:{type:...} field rather than reasoning_effort. Map the
|
|
252
|
+
# internal reasoning_effort value to thinking on/off so MiMo does
|
|
253
|
+
# not receive an unsupported parameter.
|
|
254
|
+
if %w[off nothink disabled].include?(effort_str)
|
|
255
|
+
body[:thinking] = { type: "disabled" }
|
|
256
|
+
elsif !effort_str.empty?
|
|
257
|
+
body[:thinking] = { type: "enabled" }
|
|
258
|
+
end
|
|
259
|
+
elsif reasoning_effort && !effort_str.empty?
|
|
260
|
+
body[:reasoning_effort] = effort_str
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
199
264
|
private_class_method def self.parse_tool_calls(raw)
|
|
200
265
|
return nil if raw.nil? || raw.empty?
|
|
201
266
|
|
|
@@ -10,7 +10,7 @@ module Clacky
|
|
|
10
10
|
# Fields that are internal to the agent and must not be sent to the API.
|
|
11
11
|
INTERNAL_FIELDS = %i[
|
|
12
12
|
task_id created_at system_injected session_context memory_update
|
|
13
|
-
subagent_instructions subagent_result token_usage
|
|
13
|
+
subagent_instructions subagent_result subagent_transcript token_usage
|
|
14
14
|
compressed_summary chunk_path truncated transient
|
|
15
15
|
chunk_index chunk_count
|
|
16
16
|
].freeze
|
|
@@ -71,6 +71,20 @@ module Clacky
|
|
|
71
71
|
self
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# Attach a key/value onto the most recent tool result message matching the
|
|
75
|
+
# given tool_call_id. Handles both OpenAI-style (role:"tool", tool_call_id)
|
|
76
|
+
# and Anthropic-style (role:"user" with tool_result blocks) messages.
|
|
77
|
+
# No-op if no matching message is found.
|
|
78
|
+
def attach_to_tool_result(tool_call_id, key, value)
|
|
79
|
+
msg = @messages.reverse.find do |m|
|
|
80
|
+
(m[:role] == "tool" && m[:tool_call_id] == tool_call_id) ||
|
|
81
|
+
(m[:role] == "user" && m[:content].is_a?(Array) &&
|
|
82
|
+
m[:content].any? { |b| b.is_a?(Hash) && b[:type] == "tool_result" && b[:tool_use_id] == tool_call_id })
|
|
83
|
+
end
|
|
84
|
+
msg[key] = value if msg
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
74
88
|
# Mutate the last message matching the predicate lambda in-place.
|
|
75
89
|
# Used by execute_skill_with_subagent to update instruction messages.
|
|
76
90
|
def mutate_last_matching(predicate, &block)
|
data/lib/clacky/providers.rb
CHANGED
|
@@ -148,6 +148,18 @@ module Clacky
|
|
|
148
148
|
"abs-claude-fable-5" => "abs-claude-opus-4-8",
|
|
149
149
|
"abs-claude-sonnet-4-6" => "abs-claude-sonnet-4-5"
|
|
150
150
|
},
|
|
151
|
+
# Secondary gateway URL used when the primary base_url is unreachable
|
|
152
|
+
# after max retries. The model name stays the same — only the endpoint
|
|
153
|
+
# changes. Nil / absent means no URL fallback for this provider.
|
|
154
|
+
"fallback_base_url" => "https://llm.1024code.com",
|
|
155
|
+
# Two selectable endpoints exposed in the Base URL dropdown:
|
|
156
|
+
# Primary — global CDN, lowest latency for most regions.
|
|
157
|
+
# Secondary — China-optimised relay (1024code.com), useful when
|
|
158
|
+
# the primary is unreachable from mainland China.
|
|
159
|
+
"endpoint_variants" => [
|
|
160
|
+
{ "label" => "Primary (Global)", "label_key" => "settings.models.baseurl.variant.openclacky_primary", "base_url" => "https://api.openclacky.com" }.freeze,
|
|
161
|
+
{ "label" => "Secondary (China)", "label_key" => "settings.models.baseurl.variant.openclacky_secondary", "base_url" => "https://llm.1024code.com" }.freeze
|
|
162
|
+
].freeze,
|
|
151
163
|
"website_url" => "https://www.openclacky.com/ai-keys"
|
|
152
164
|
}.freeze,
|
|
153
165
|
|
|
@@ -336,13 +348,30 @@ module Clacky
|
|
|
336
348
|
"base_url" => "https://api.xiaomimimo.com/v1",
|
|
337
349
|
"api" => "openai-completions",
|
|
338
350
|
"default_model" => "mimo-v2.5-pro",
|
|
339
|
-
|
|
340
|
-
#
|
|
351
|
+
# The MiMo-V2 family (mimo-v2-pro / mimo-v2-omni) was retired on
|
|
352
|
+
# 2026-06-30 and the model ids are no longer accepted by the API. The
|
|
353
|
+
# current lineup is the V2.5 series:
|
|
354
|
+
# - mimo-v2.5-pro: text reasoning flagship, no vision
|
|
355
|
+
# - mimo-v2.5: native omni-modal (image/video/audio/text), vision-capable
|
|
356
|
+
# Source: https://platform.xiaomimimo.com/docs/zh-CN/model
|
|
357
|
+
"models" => ["mimo-v2.5-pro", "mimo-v2.5"],
|
|
341
358
|
"capabilities" => { "vision" => false }.freeze,
|
|
342
359
|
"model_capabilities" => {
|
|
343
|
-
"mimo-v2
|
|
360
|
+
"mimo-v2.5" => { "vision" => true }.freeze
|
|
344
361
|
}.freeze,
|
|
345
|
-
"default_ocr_model" => "mimo-v2
|
|
362
|
+
"default_ocr_model" => "mimo-v2.5",
|
|
363
|
+
# Xiaomi serves the same V2.5 lineup from two billing endpoints: the
|
|
364
|
+
# pay-as-you-go API (api.xiaomimimo.com) and the Token Plan subscription
|
|
365
|
+
# endpoint (token-plan-cn.xiaomimimo.com). Both accept identical model
|
|
366
|
+
# ids and share one capability profile, so a single preset with
|
|
367
|
+
# endpoint_variants recognises both. Without this, users on the Token
|
|
368
|
+
# Plan endpoint fell through to "unknown provider" and the conservative
|
|
369
|
+
# vision=true default applied to every model — sending images to the
|
|
370
|
+
# text-only mimo-v2.5-pro, which rejects image input.
|
|
371
|
+
"endpoint_variants" => [
|
|
372
|
+
{ "label" => "Pay-as-you-go", "label_key" => "settings.models.baseurl.variant.mimo_payg", "base_url" => "https://api.xiaomimimo.com/v1", "region" => "cn" }.freeze,
|
|
373
|
+
{ "label" => "Token Plan", "label_key" => "settings.models.baseurl.variant.mimo_token_plan", "base_url" => "https://token-plan-cn.xiaomimimo.com/v1", "region" => "cn" }.freeze
|
|
374
|
+
].freeze,
|
|
346
375
|
"website_url" => "https://platform.xiaomimimo.com/"
|
|
347
376
|
}.freeze,
|
|
348
377
|
|
|
@@ -350,8 +379,8 @@ module Clacky
|
|
|
350
379
|
"name" => "GLM (Z.ai / Zhipu)",
|
|
351
380
|
"base_url" => "https://open.bigmodel.cn/api/paas/v4",
|
|
352
381
|
"api" => "openai-completions",
|
|
353
|
-
"default_model" => "glm-5.
|
|
354
|
-
"models" => ["glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
|
|
382
|
+
"default_model" => "glm-5.2",
|
|
383
|
+
"models" => ["glm-5.2", "glm-5.1", "glm-5", "glm-5-turbo", "glm-5v-turbo", "glm-4.7"],
|
|
355
384
|
# Zhipu / Z.ai expose four functionally-equivalent endpoints:
|
|
356
385
|
# two regional sites (mainland open.bigmodel.cn + international api.z.ai)
|
|
357
386
|
# each with a general-billing and a Coding-Plan subpath. They share the
|
|
@@ -508,6 +537,22 @@ module Clacky
|
|
|
508
537
|
|
|
509
538
|
MEDIA_KINDS = %w[image video audio stt video_understanding].freeze
|
|
510
539
|
|
|
540
|
+
# Per-model maximum output token limits.
|
|
541
|
+
#
|
|
542
|
+
# The Agent global default (@max_tokens = 16_384) is tuned for the lowest
|
|
543
|
+
# common denominator. Strong reasoning models (GLM-5.2, Kimi-K3, MiMo-V2.5)
|
|
544
|
+
# support 64K–128K output but are artificially throttled to 16K, causing
|
|
545
|
+
# truncation on long reasoning chains and code generation.
|
|
546
|
+
#
|
|
547
|
+
# Entries are matched top-to-bottom; the first match wins. Models not
|
|
548
|
+
# listed here fall back to the global default.
|
|
549
|
+
MODEL_MAX_OUTPUT = [
|
|
550
|
+
{ pattern: /glm/i, limit: 65_536 }, # GLM-5.2: 128K output ceiling; 64K ample for reasoning+answer
|
|
551
|
+
{ pattern: /kimi-k3/i, limit: 65_536 }, # Kimi K3: max_completion_tokens=131072 (max 1M); 64K ample
|
|
552
|
+
{ pattern: /mimo-v2\.5-pro/i, limit: 65_536 }, # MiMo-V2.5-Pro: max_completion_tokens=131072; 64K ample
|
|
553
|
+
{ pattern: /mimo/i, limit: 32_768 } # MiMo-V2.5: max_completion_tokens=32768; full default ceiling
|
|
554
|
+
].freeze
|
|
555
|
+
|
|
511
556
|
class << self
|
|
512
557
|
# Check if a provider preset exists
|
|
513
558
|
# @param provider_id [String] The provider identifier (e.g., "anthropic", "openrouter")
|
|
@@ -547,6 +592,18 @@ module Clacky
|
|
|
547
592
|
preset&.dig("api")
|
|
548
593
|
end
|
|
549
594
|
|
|
595
|
+
# Resolve the per-model maximum output token limit.
|
|
596
|
+
# Returns nil when no MODEL_MAX_OUTPUT entry matches — callers should
|
|
597
|
+
# then fall back to the global default (@max_tokens).
|
|
598
|
+
#
|
|
599
|
+
# @param model_name [String] The model name (e.g. "glm-5.2")
|
|
600
|
+
# @return [Integer, nil] The max output limit, or nil if undeclared
|
|
601
|
+
def max_output_for(model_name)
|
|
602
|
+
return nil if model_name.nil? || model_name.to_s.empty?
|
|
603
|
+
entry = MODEL_MAX_OUTPUT.find { |e| model_name.to_s.match?(e[:pattern]) }
|
|
604
|
+
entry&.[](:limit)
|
|
605
|
+
end
|
|
606
|
+
|
|
550
607
|
# Resolve the API type for a specific provider+model pair.
|
|
551
608
|
#
|
|
552
609
|
# Resolution order:
|
|
@@ -779,6 +836,16 @@ module Clacky
|
|
|
779
836
|
preset&.dig("fallback_models", model)
|
|
780
837
|
end
|
|
781
838
|
|
|
839
|
+
# Get the fallback base URL for a provider (used when primary endpoint is unreachable).
|
|
840
|
+
# Returns nil if the provider has no secondary gateway configured.
|
|
841
|
+
# @param provider_id [String] The provider identifier
|
|
842
|
+
# @return [String, nil] The fallback base URL or nil
|
|
843
|
+
def fallback_base_url(provider_id)
|
|
844
|
+
preset = PRESETS[provider_id]
|
|
845
|
+
url = preset&.dig("fallback_base_url")
|
|
846
|
+
url&.empty? ? nil : url
|
|
847
|
+
end
|
|
848
|
+
|
|
782
849
|
# Find provider ID by base URL.
|
|
783
850
|
# Matches if the given URL starts with the provider's base_url (after normalisation),
|
|
784
851
|
# so both exact matches and sub-path variants (e.g. "/v1") are recognised.
|