Skip to content

Commit ae8d451

Browse files
committed
fix(clickclack): defer unset token budget to runtime
1 parent e7e55e1 commit ae8d451

6 files changed

Lines changed: 55 additions & 30 deletions

File tree

docs/channels/clickclack.md

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,20 @@ An account counts as configured only when `baseUrl`, `token`, and `workspace` ar
5252

5353
### Account config keys
5454

55-
| Key | Default | Notes |
56-
| ----------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
57-
| `baseUrl` | none (required) | ClickClack server URL. |
58-
| `token` | none (required) | Plain string or secret ref (`source: "env" \| "file" \| "exec"`). |
59-
| `workspace` | none (required) | Workspace id, slug, or name. |
60-
| `replyMode` | `"agent"` | `"agent"` runs the full agent pipeline; `"model"` sends short direct model completions. |
61-
| `defaultTo` | `"channel:general"` | Target used when an outbound path gives no target. |
62-
| `allowFrom` | `["*"]` | User-id allowlist for inbound DMs and channel messages. |
63-
| `botUserId` | auto-detected | Resolved from the bot token identity at startup. |
64-
| `agentId` | route default | Pin this account's inbound messages to one agent. |
65-
| `toolsAllow` | none | Tool allowlist for agent replies from this account. |
66-
| `model`, `systemPrompt` | none | Used by `replyMode: "model"` completions. |
67-
| `maxTokens` | `96` | Model-mode output cap (1 to 32768); higher values can increase latency and provider cost. |
68-
| `reconnectMs` | `1500` | Realtime reconnect delay (100 to 60000). |
55+
| Key | Default | Notes |
56+
| ----------------------- | ------------------- | -------------------------------------------------------------------------------------------------- |
57+
| `baseUrl` | none (required) | ClickClack server URL. |
58+
| `token` | none (required) | Plain string or secret ref (`source: "env" \| "file" \| "exec"`). |
59+
| `workspace` | none (required) | Workspace id, slug, or name. |
60+
| `replyMode` | `"agent"` | `"agent"` runs the full agent pipeline; `"model"` sends short direct model completions. |
61+
| `defaultTo` | `"channel:general"` | Target used when an outbound path gives no target. |
62+
| `allowFrom` | `["*"]` | User-id allowlist for inbound DMs and channel messages. |
63+
| `botUserId` | auto-detected | Resolved from the bot token identity at startup. |
64+
| `agentId` | route default | Pin this account's inbound messages to one agent. |
65+
| `toolsAllow` | none | Tool allowlist for agent replies from this account. |
66+
| `model`, `systemPrompt` | none | Used by `replyMode: "model"` completions. |
67+
| `maxTokens` | model default | Optional model-mode output cap (1 to 32768); higher values can increase latency and provider cost. |
68+
| `reconnectMs` | `1500` | Realtime reconnect delay (100 to 60000). |
6969

7070
If `plugins.allow` is a non-empty restrictive list, explicitly selecting
7171
ClickClack in channel setup or running `openclaw plugins enable clickclack`
@@ -110,10 +110,9 @@ Each account opens its own ClickClack realtime connection and uses its own bot t
110110
- `replyMode: "agent"` (default) dispatches inbound messages through the normal agent pipeline, including session recording and tool policy.
111111
- `replyMode: "model"` skips the agent pipeline and uses the plugin runtime's `llm.complete` for direct bot replies (optionally shaped by `model`, `systemPrompt`, and `maxTokens`).
112112

113-
The compatibility default remains `96` tokens for accounts that omit the new
114-
setting. Set `maxTokens` per account when a different response-length, latency,
115-
or provider-usage tradeoff is appropriate; reasoning-heavy models may need a
116-
substantially larger explicit budget before they produce sendable text.
113+
When `maxTokens` is unset, ClickClack omits the cap and lets the selected runtime
114+
and model choose their normal output budget. Set `maxTokens` per account to
115+
impose an explicit response-length, latency, or provider-usage bound.
117116

118117
Model mode runs completions against the resolved bot agent id, which requires
119118
the explicit `plugins.entries.clickclack.llm.allowAgentIdOverride: true` trust

extensions/clickclack/src/accounts.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ describe("ClickClack account resolution", () => {
107107
defaultTo: "channel:general",
108108
enabled: true,
109109
agentActivity: false,
110-
maxTokens: 96,
110+
maxTokens: undefined,
111111
model: undefined,
112112
name: undefined,
113113
reconnectMs: 1_500,
@@ -176,7 +176,7 @@ describe("ClickClack account resolution", () => {
176176
});
177177
});
178178

179-
it("preserves the legacy token fallback while honoring explicit account overrides", () => {
179+
it("uses the runtime model budget when unset while honoring explicit account overrides", () => {
180180
const cfg = {
181181
channels: {
182182
clickclack: {
@@ -192,8 +192,8 @@ describe("ClickClack account resolution", () => {
192192
},
193193
} satisfies CoreConfig;
194194

195-
expect(resolveClickClackAccount({ cfg }).maxTokens).toBe(96);
196-
expect(resolveClickClackAccount({ cfg, accountId: "legacy" }).maxTokens).toBe(96);
195+
expect(resolveClickClackAccount({ cfg }).maxTokens).toBeUndefined();
196+
expect(resolveClickClackAccount({ cfg, accountId: "legacy" }).maxTokens).toBeUndefined();
197197
expect(resolveClickClackAccount({ cfg, accountId: "tuned" }).maxTokens).toBe(8_192);
198198
});
199199

extensions/clickclack/src/accounts.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import {
88
} from "openclaw/plugin-sdk/account-helpers";
99
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
1010
import { resolveMergedAccountConfig } from "openclaw/plugin-sdk/account-resolution";
11-
import { resolveIntegerOption } from "openclaw/plugin-sdk/number-runtime";
11+
import {
12+
resolveIntegerOption,
13+
resolveOptionalIntegerOption,
14+
} from "openclaw/plugin-sdk/number-runtime";
1215
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
1316
import {
1417
normalizeSecretInputString,
@@ -21,9 +24,6 @@ import type { ClickClackAccountConfig, CoreConfig, ResolvedClickClackAccount } f
2124
const DEFAULT_RECONNECT_MS = 1_500;
2225
const MIN_RECONNECT_MS = 100;
2326
const MAX_RECONNECT_MS = 60_000;
24-
// Preserve the pre-config compatibility fallback for accounts that omit the
25-
// new option; operators can opt into a larger model-specific budget.
26-
const DEFAULT_MAX_TOKENS = 96;
2727
const MIN_MAX_TOKENS = 1;
2828
const MAX_MAX_TOKENS = 32_768;
2929

@@ -140,7 +140,7 @@ export function resolveClickClackAccount(params: {
140140
replyMode: merged.replyMode === "model" ? "model" : "agent",
141141
model: normalizeOptionalString(merged.model),
142142
systemPrompt: normalizeOptionalString(merged.systemPrompt),
143-
maxTokens: resolveIntegerOption(merged.maxTokens, DEFAULT_MAX_TOKENS, {
143+
maxTokens: resolveOptionalIntegerOption(merged.maxTokens, {
144144
min: MIN_MAX_TOKENS,
145145
max: MAX_MAX_TOKENS,
146146
}),

extensions/clickclack/src/inbound.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,32 @@ describe("handleClickClackInbound", () => {
191191
expect(sendRequest?.correlationId).toBe("fakeco.case_1");
192192
});
193193

194+
it("omits the completion cap and delivers the reply when maxTokens is unset", async () => {
195+
const runtime = createRuntime();
196+
setClickClackRuntime(runtime);
197+
const account = createAgentAccount({
198+
accountId: "service",
199+
agentId: "service-bot",
200+
replyMode: "model",
201+
maxTokens: undefined,
202+
});
203+
204+
await handleClickClackInbound({
205+
account,
206+
config: {} satisfies CoreConfig,
207+
message: createMessage({
208+
body: "hello without a clickclack cap",
209+
author_id: "usr_human",
210+
}),
211+
});
212+
213+
const completionRequest = (runtime.llm.complete as LlmCompleteMock).mock.calls[0]?.[0];
214+
expect(completionRequest).not.toHaveProperty("maxTokens");
215+
expect(sendClickClackTextMock).toHaveBeenCalledWith(
216+
expect.objectContaining({ accountId: "service", text: "service bot online" }),
217+
);
218+
});
219+
194220
it("logs and skips delivery when model mode produces no sendable text", async () => {
195221
const runtime = createRuntime();
196222
vi.mocked(runtime.llm.complete).mockResolvedValue({

extensions/clickclack/src/inbound.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async function dispatchModelReply(params: {
8989
const result = await runtime.llm.complete({
9090
agentId: params.route.agentId,
9191
model: params.account.model,
92-
maxTokens: params.account.maxTokens,
92+
...(params.account.maxTokens === undefined ? {} : { maxTokens: params.account.maxTokens }),
9393
purpose: "clickclack bot reply",
9494
systemPrompt: params.account.systemPrompt,
9595
messages: [
@@ -104,7 +104,7 @@ async function dispatchModelReply(params: {
104104
runtime.logging
105105
.getChildLogger({ plugin: "clickclack", feature: "model-reply" })
106106
.warn(
107-
`[${params.account.accountId}] ClickClack model reply produced no sendable text (maxTokens=${params.account.maxTokens})`,
107+
`[${params.account.accountId}] ClickClack model reply produced no sendable text (maxTokens=${params.account.maxTokens ?? "runtime default"})`,
108108
);
109109
return;
110110
}

extensions/clickclack/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export type ResolvedClickClackAccount = {
5252
replyMode: "agent" | "model";
5353
model?: string;
5454
systemPrompt?: string;
55-
maxTokens: number;
55+
maxTokens?: number;
5656
timeoutSeconds?: number;
5757
toolsAllow?: string[];
5858
defaultTo: string;

0 commit comments

Comments
 (0)