skimpyclaw 0.1.4 → 0.1.6
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.
- package/dist/__tests__/tool-guard.test.js +6 -19
- package/dist/api.js +19 -2
- package/dist/dashboard/assets/{index-CZJCvMSN.js → index-CkonC7Cd.js} +15 -15
- package/dist/dashboard/index.html +1 -1
- package/dist/providers/anthropic.js +2 -10
- package/dist/providers/codex.js +2 -10
- package/dist/providers/index.js +8 -2
- package/dist/providers/openai.js +44 -16
- package/dist/providers/tool-guard.d.ts +2 -5
- package/dist/providers/tool-guard.js +4 -19
- package/dist/setup.js +3 -3
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>SkimpyClaw Dashboard</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
8
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700;800&display=swap" rel="stylesheet">
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CkonC7Cd.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-EAg6lqF5.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -191,16 +191,8 @@ export async function chatWithToolsAnthropic(params) {
|
|
|
191
191
|
costDetails: toCostDetails(modelId, usage),
|
|
192
192
|
});
|
|
193
193
|
genObs?.end();
|
|
194
|
-
// Guard: track token usage
|
|
195
|
-
|
|
196
|
-
if (tokenResult.warning)
|
|
197
|
-
console.warn(`[agent:tools:guard] ${tokenResult.warning}`);
|
|
198
|
-
if (tokenResult.exceeded) {
|
|
199
|
-
return {
|
|
200
|
-
response: `[Stopped: ${tokenResult.warning}]`,
|
|
201
|
-
toolCalls: toolLog,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
194
|
+
// Guard: track token usage (stats only, no enforcement)
|
|
195
|
+
guard.recordTokens(response.usage?.input_tokens ?? 0, response.usage?.output_tokens ?? 0);
|
|
204
196
|
}
|
|
205
197
|
catch (err) {
|
|
206
198
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
package/dist/providers/codex.js
CHANGED
|
@@ -339,16 +339,8 @@ export async function chatWithToolsCodex(params) {
|
|
|
339
339
|
costDetails: toCostDetails(modelId, parsed.response?.usage),
|
|
340
340
|
});
|
|
341
341
|
genObs?.end();
|
|
342
|
-
// Guard: track token usage
|
|
343
|
-
|
|
344
|
-
if (tokenResult.warning)
|
|
345
|
-
console.warn(`[codex:tools:guard] ${tokenResult.warning}`);
|
|
346
|
-
if (tokenResult.exceeded) {
|
|
347
|
-
return {
|
|
348
|
-
response: `[Stopped: ${tokenResult.warning}]`,
|
|
349
|
-
toolCalls: toolLog,
|
|
350
|
-
};
|
|
351
|
-
}
|
|
342
|
+
// Guard: track token usage (stats only, no enforcement)
|
|
343
|
+
guard.recordTokens(parsed.response?.usage?.input_tokens ?? 0, parsed.response?.usage?.output_tokens ?? 0);
|
|
352
344
|
}
|
|
353
345
|
catch (err) {
|
|
354
346
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
package/dist/providers/index.js
CHANGED
|
@@ -86,8 +86,14 @@ export async function initProviders(config) {
|
|
|
86
86
|
if (!apiKey)
|
|
87
87
|
continue;
|
|
88
88
|
const opts = { apiKey };
|
|
89
|
-
if (providerConfig.baseURL)
|
|
90
|
-
|
|
89
|
+
if (providerConfig.baseURL) {
|
|
90
|
+
let normalizedBaseURL = providerConfig.baseURL;
|
|
91
|
+
if (name === 'minimax') {
|
|
92
|
+
const trimmed = normalizedBaseURL.replace(/\/+$/, '');
|
|
93
|
+
normalizedBaseURL = trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
94
|
+
}
|
|
95
|
+
opts.baseURL = normalizedBaseURL;
|
|
96
|
+
}
|
|
91
97
|
// Kimi Code API requires a coding-agent User-Agent with version string
|
|
92
98
|
if (providerConfig.baseURL?.includes('kimi.com')) {
|
|
93
99
|
opts.defaultHeaders = { 'User-Agent': 'claude-code/2.1.42' };
|
package/dist/providers/openai.js
CHANGED
|
@@ -30,10 +30,16 @@ export function isOpenAIAvailable(provider) {
|
|
|
30
30
|
const LANGFUSE_APP_NAME = 'skimpyclaw';
|
|
31
31
|
function recordOpenAIUsage(params) {
|
|
32
32
|
const usage = params.usage;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let inputTokens = typeof usage?.prompt_tokens === 'number'
|
|
34
|
+
? usage.prompt_tokens
|
|
35
|
+
: (typeof usage?.input_tokens === 'number' ? usage.input_tokens : 0);
|
|
36
|
+
let outputTokens = typeof usage?.completion_tokens === 'number'
|
|
37
|
+
? usage.completion_tokens
|
|
38
|
+
: (typeof usage?.output_tokens === 'number' ? usage.output_tokens : 0);
|
|
39
|
+
// Some OpenAI-compatible providers only return total_tokens.
|
|
40
|
+
if (inputTokens === 0 && outputTokens === 0 && typeof usage?.total_tokens === 'number') {
|
|
41
|
+
inputTokens = usage.total_tokens;
|
|
42
|
+
}
|
|
37
43
|
const cost = toCostDetails(params.model, usage);
|
|
38
44
|
recordUsage(buildUsageRecord({
|
|
39
45
|
model: params.model,
|
|
@@ -61,6 +67,11 @@ export async function chatOpenAI(params, provider) {
|
|
|
61
67
|
}
|
|
62
68
|
const { messages, options, config } = params;
|
|
63
69
|
const modelId = stripProvider(options.model, openaiClients);
|
|
70
|
+
const providerBaseURL = config.models.providers[provider]?.baseURL || '';
|
|
71
|
+
const isKimiLike = providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai');
|
|
72
|
+
const kimiRequestExtras = isKimiLike
|
|
73
|
+
? { extra_body: { interleaved: { field: 'reasoning_content' } } }
|
|
74
|
+
: {};
|
|
64
75
|
const openaiMessages = messages.map(m => ({
|
|
65
76
|
role: m.role,
|
|
66
77
|
content: toOpenAIContent(m.content),
|
|
@@ -80,6 +91,7 @@ export async function chatOpenAI(params, provider) {
|
|
|
80
91
|
messages: openaiMessages,
|
|
81
92
|
max_tokens: options.maxTokens || 4096,
|
|
82
93
|
temperature: options.temperature,
|
|
94
|
+
...kimiRequestExtras,
|
|
83
95
|
});
|
|
84
96
|
let content = response.choices[0]?.message?.content || '';
|
|
85
97
|
// Strip <think>...</think> reasoning blocks (e.g. MiniMax M2.x)
|
|
@@ -118,6 +130,10 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
118
130
|
const openaiTools = toOpenAITools(toolDefs);
|
|
119
131
|
// Inject Kimi $web_search builtin tool when using Moonshot/Kimi provider
|
|
120
132
|
const providerBaseURL = config.models.providers[provider]?.baseURL || '';
|
|
133
|
+
const requiresReasoningContent = providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai');
|
|
134
|
+
const kimiRequestExtras = requiresReasoningContent
|
|
135
|
+
? { extra_body: { interleaved: { field: 'reasoning_content' } } }
|
|
136
|
+
: {};
|
|
121
137
|
if (providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai')) {
|
|
122
138
|
openaiTools.push({ type: 'builtin_function', function: { name: '$web_search' } });
|
|
123
139
|
console.log('[agent:openai-tools] Injected Kimi $web_search builtin tool');
|
|
@@ -156,6 +172,7 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
156
172
|
tools: openaiTools,
|
|
157
173
|
max_tokens: options.maxTokens || 4096,
|
|
158
174
|
temperature: options.temperature,
|
|
175
|
+
...kimiRequestExtras,
|
|
159
176
|
});
|
|
160
177
|
recordOpenAIUsage({
|
|
161
178
|
model: modelId,
|
|
@@ -170,16 +187,8 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
170
187
|
costDetails: toCostDetails(modelId, completion.usage),
|
|
171
188
|
});
|
|
172
189
|
genObs?.end();
|
|
173
|
-
// Guard: track token usage
|
|
174
|
-
|
|
175
|
-
if (tokenResult.warning)
|
|
176
|
-
console.warn(`[agent:openai-tools:guard] ${tokenResult.warning}`);
|
|
177
|
-
if (tokenResult.exceeded) {
|
|
178
|
-
return {
|
|
179
|
-
response: `[Stopped: ${tokenResult.warning}]`,
|
|
180
|
-
toolCalls: toolLog,
|
|
181
|
-
};
|
|
182
|
-
}
|
|
190
|
+
// Guard: track token usage (stats only, no enforcement)
|
|
191
|
+
guard.recordTokens(completion.usage?.prompt_tokens ?? 0, completion.usage?.completion_tokens ?? 0);
|
|
183
192
|
}
|
|
184
193
|
catch (err) {
|
|
185
194
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -219,8 +228,27 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
219
228
|
cost: toCostDetails(modelId, completion.usage),
|
|
220
229
|
};
|
|
221
230
|
}
|
|
222
|
-
// Append assistant message with tool_calls to conversation
|
|
223
|
-
|
|
231
|
+
// Append assistant message with tool_calls to conversation.
|
|
232
|
+
// Kimi requires reasoning_content when thinking mode is enabled.
|
|
233
|
+
const assistantToolCallMessage = {
|
|
234
|
+
role: 'assistant',
|
|
235
|
+
content: message.content ?? null,
|
|
236
|
+
tool_calls: message.tool_calls,
|
|
237
|
+
};
|
|
238
|
+
const rawReasoning = message.reasoning_content
|
|
239
|
+
?? message.additional_kwargs?.reasoning_content
|
|
240
|
+
?? message.reasoning?.content;
|
|
241
|
+
if (rawReasoning !== undefined && rawReasoning !== null) {
|
|
242
|
+
assistantToolCallMessage.reasoning_content = Array.isArray(rawReasoning)
|
|
243
|
+
? rawReasoning.join('\n')
|
|
244
|
+
: String(rawReasoning);
|
|
245
|
+
}
|
|
246
|
+
else if (requiresReasoningContent) {
|
|
247
|
+
// Some Kimi responses omit reasoning_content despite thinking mode.
|
|
248
|
+
// Send a placeholder to satisfy strict tool-call replay validation.
|
|
249
|
+
assistantToolCallMessage.reasoning_content = ' ';
|
|
250
|
+
}
|
|
251
|
+
apiMessages.push(assistantToolCallMessage);
|
|
224
252
|
// Execute each tool call
|
|
225
253
|
for (const toolCall of message.tool_calls) {
|
|
226
254
|
const fnName = toolCall.function.name;
|
|
@@ -15,11 +15,8 @@ export declare class ToolCallGuard {
|
|
|
15
15
|
recordResult(result: string): {
|
|
16
16
|
nudge?: string;
|
|
17
17
|
};
|
|
18
|
-
/** Record token usage
|
|
19
|
-
recordTokens(inputTokens: number, outputTokens: number):
|
|
20
|
-
exceeded: boolean;
|
|
21
|
-
warning?: string;
|
|
22
|
-
};
|
|
18
|
+
/** Record token usage for stats tracking. */
|
|
19
|
+
recordTokens(inputTokens: number, outputTokens: number): void;
|
|
23
20
|
/** Reset guard state (for testing or between turns). */
|
|
24
21
|
reset(): void;
|
|
25
22
|
/** Get current stats. */
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// ToolCallGuard — Spin detection, no-progress detection
|
|
1
|
+
// ToolCallGuard — Spin detection, no-progress detection
|
|
2
2
|
import { createHash } from 'crypto';
|
|
3
3
|
const SPIN_WARN_THRESHOLD = 3;
|
|
4
4
|
const SPIN_BLOCK_THRESHOLD = 5;
|
|
5
5
|
const NO_PROGRESS_THRESHOLD = 5;
|
|
6
|
-
const DEFAULT_MAX_TURN_TOKENS = 200_000;
|
|
7
6
|
export class ToolCallGuard {
|
|
8
7
|
callHistory = [];
|
|
9
8
|
resultHashes = [];
|
|
10
9
|
totalInputTokens = 0;
|
|
11
10
|
totalOutputTokens = 0;
|
|
11
|
+
// Kept for future use — not currently enforced
|
|
12
12
|
maxTurnTokens;
|
|
13
13
|
constructor(maxTurnTokens) {
|
|
14
|
-
this.maxTurnTokens = maxTurnTokens
|
|
14
|
+
this.maxTurnTokens = maxTurnTokens;
|
|
15
15
|
}
|
|
16
16
|
hash(data) {
|
|
17
17
|
return createHash('md5').update(data).digest('hex').slice(0, 16);
|
|
@@ -52,25 +52,10 @@ export class ToolCallGuard {
|
|
|
52
52
|
}
|
|
53
53
|
return {};
|
|
54
54
|
}
|
|
55
|
-
/** Record token usage
|
|
55
|
+
/** Record token usage for stats tracking. */
|
|
56
56
|
recordTokens(inputTokens, outputTokens) {
|
|
57
57
|
this.totalInputTokens += inputTokens;
|
|
58
58
|
this.totalOutputTokens += outputTokens;
|
|
59
|
-
const total = this.totalInputTokens + this.totalOutputTokens;
|
|
60
|
-
if (total >= this.maxTurnTokens) {
|
|
61
|
-
return {
|
|
62
|
-
exceeded: true,
|
|
63
|
-
warning: `Token budget exceeded: ${total} tokens used (limit: ${this.maxTurnTokens})`
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
// Warn at 80%
|
|
67
|
-
if (total >= this.maxTurnTokens * 0.8) {
|
|
68
|
-
return {
|
|
69
|
-
exceeded: false,
|
|
70
|
-
warning: `Token budget warning: ${total}/${this.maxTurnTokens} tokens used (${Math.round(total / this.maxTurnTokens * 100)}%)`
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
return { exceeded: false };
|
|
74
59
|
}
|
|
75
60
|
/** Reset guard state (for testing or between turns). */
|
|
76
61
|
reset() {
|
package/dist/setup.js
CHANGED
|
@@ -284,7 +284,7 @@ function buildDefaultModel(providers) {
|
|
|
284
284
|
if (providers.has('kimi-api'))
|
|
285
285
|
return 'kimi/kimi-for-coding';
|
|
286
286
|
if (providers.has('minimax-api'))
|
|
287
|
-
return 'minimax/MiniMax-M2.
|
|
287
|
+
return 'minimax/MiniMax-M2.5';
|
|
288
288
|
return 'openai/gpt-4o';
|
|
289
289
|
}
|
|
290
290
|
function buildAliases(providers) {
|
|
@@ -307,7 +307,7 @@ function buildAliases(providers) {
|
|
|
307
307
|
aliases.codex = 'codex/gpt-5.3-codex';
|
|
308
308
|
}
|
|
309
309
|
if (providers.has('minimax-api')) {
|
|
310
|
-
aliases.minimax = 'minimax/MiniMax-M2.
|
|
310
|
+
aliases.minimax = 'minimax/MiniMax-M2.5';
|
|
311
311
|
}
|
|
312
312
|
if (providers.has('kimi-api')) {
|
|
313
313
|
aliases.kimi = 'kimi/kimi-for-coding';
|
|
@@ -510,7 +510,7 @@ async function validateProviderAuth(providers, secrets) {
|
|
|
510
510
|
'content-type': 'application/json',
|
|
511
511
|
'anthropic-version': '2023-06-01',
|
|
512
512
|
},
|
|
513
|
-
body: JSON.stringify({ model: 'MiniMax-M2.
|
|
513
|
+
body: JSON.stringify({ model: 'MiniMax-M2.5', max_tokens: 8, messages: [{ role: 'user', content: 'ping' }] }),
|
|
514
514
|
});
|
|
515
515
|
checks.push({ name: 'MiniMax API', ok: res.ok, detail: res.ok ? 'auth ok' : `HTTP ${res.status}` });
|
|
516
516
|
}
|