skimpyclaw 0.1.5 → 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/providers/anthropic.js +2 -10
- package/dist/providers/codex.js +2 -10
- package/dist/providers/index.js +8 -2
- package/dist/providers/openai.js +33 -18
- 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
|
@@ -57,25 +57,12 @@ describe('ToolCallGuard', () => {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
-
describe('token
|
|
61
|
-
it('
|
|
62
|
-
const guard = new ToolCallGuard(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expect(
|
|
66
|
-
});
|
|
67
|
-
it('warns at 80% usage', () => {
|
|
68
|
-
const guard = new ToolCallGuard(10_000);
|
|
69
|
-
const r = guard.recordTokens(4000, 4100);
|
|
70
|
-
expect(r.exceeded).toBe(false);
|
|
71
|
-
expect(r.warning).toBeDefined();
|
|
72
|
-
expect(r.warning).toContain('warning');
|
|
73
|
-
});
|
|
74
|
-
it('exceeds at 100% usage', () => {
|
|
75
|
-
const guard = new ToolCallGuard(10_000);
|
|
76
|
-
const r = guard.recordTokens(5000, 5000);
|
|
77
|
-
expect(r.exceeded).toBe(true);
|
|
78
|
-
expect(r.warning).toContain('exceeded');
|
|
60
|
+
describe('token tracking', () => {
|
|
61
|
+
it('tracks tokens without enforcement', () => {
|
|
62
|
+
const guard = new ToolCallGuard();
|
|
63
|
+
guard.recordTokens(5000, 5000);
|
|
64
|
+
const stats = guard.getStats();
|
|
65
|
+
expect(stats.totalTokens).toBe(10000);
|
|
79
66
|
});
|
|
80
67
|
});
|
|
81
68
|
describe('reset', () => {
|
|
@@ -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)
|
|
@@ -119,6 +131,9 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
119
131
|
// Inject Kimi $web_search builtin tool when using Moonshot/Kimi provider
|
|
120
132
|
const providerBaseURL = config.models.providers[provider]?.baseURL || '';
|
|
121
133
|
const requiresReasoningContent = providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai');
|
|
134
|
+
const kimiRequestExtras = requiresReasoningContent
|
|
135
|
+
? { extra_body: { interleaved: { field: 'reasoning_content' } } }
|
|
136
|
+
: {};
|
|
122
137
|
if (providerBaseURL.includes('kimi.com') || providerBaseURL.includes('moonshot.ai')) {
|
|
123
138
|
openaiTools.push({ type: 'builtin_function', function: { name: '$web_search' } });
|
|
124
139
|
console.log('[agent:openai-tools] Injected Kimi $web_search builtin tool');
|
|
@@ -157,6 +172,7 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
157
172
|
tools: openaiTools,
|
|
158
173
|
max_tokens: options.maxTokens || 4096,
|
|
159
174
|
temperature: options.temperature,
|
|
175
|
+
...kimiRequestExtras,
|
|
160
176
|
});
|
|
161
177
|
recordOpenAIUsage({
|
|
162
178
|
model: modelId,
|
|
@@ -171,16 +187,8 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
171
187
|
costDetails: toCostDetails(modelId, completion.usage),
|
|
172
188
|
});
|
|
173
189
|
genObs?.end();
|
|
174
|
-
// Guard: track token usage
|
|
175
|
-
|
|
176
|
-
if (tokenResult.warning)
|
|
177
|
-
console.warn(`[agent:openai-tools:guard] ${tokenResult.warning}`);
|
|
178
|
-
if (tokenResult.exceeded) {
|
|
179
|
-
return {
|
|
180
|
-
response: `[Stopped: ${tokenResult.warning}]`,
|
|
181
|
-
toolCalls: toolLog,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
190
|
+
// Guard: track token usage (stats only, no enforcement)
|
|
191
|
+
guard.recordTokens(completion.usage?.prompt_tokens ?? 0, completion.usage?.completion_tokens ?? 0);
|
|
184
192
|
}
|
|
185
193
|
catch (err) {
|
|
186
194
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -224,14 +232,21 @@ export async function chatWithToolsOpenAI(params, provider) {
|
|
|
224
232
|
// Kimi requires reasoning_content when thinking mode is enabled.
|
|
225
233
|
const assistantToolCallMessage = {
|
|
226
234
|
role: 'assistant',
|
|
227
|
-
content: message.content ??
|
|
235
|
+
content: message.content ?? null,
|
|
228
236
|
tool_calls: message.tool_calls,
|
|
229
237
|
};
|
|
230
|
-
|
|
231
|
-
|
|
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);
|
|
232
245
|
}
|
|
233
246
|
else if (requiresReasoningContent) {
|
|
234
|
-
|
|
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 = ' ';
|
|
235
250
|
}
|
|
236
251
|
apiMessages.push(assistantToolCallMessage);
|
|
237
252
|
// Execute each tool call
|
|
@@ -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
|
}
|