skimpyclaw 0.1.5 → 0.1.7

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.
@@ -57,25 +57,12 @@ describe('ToolCallGuard', () => {
57
57
  }
58
58
  });
59
59
  });
60
- describe('token budget', () => {
61
- it('does not exceed with small usage', () => {
62
- const guard = new ToolCallGuard(100_000);
63
- const r = guard.recordTokens(1000, 500);
64
- expect(r.exceeded).toBe(false);
65
- expect(r.warning).toBeUndefined();
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
- const tokenResult = guard.recordTokens(response.usage?.input_tokens ?? 0, response.usage?.output_tokens ?? 0);
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);
@@ -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
- const tokenResult = guard.recordTokens(parsed.response?.usage?.input_tokens ?? 0, parsed.response?.usage?.output_tokens ?? 0);
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);
@@ -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
- opts.baseURL = providerConfig.baseURL;
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' };
@@ -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
- const inputTokens = typeof usage?.prompt_tokens === 'number' ? usage.prompt_tokens : 0;
34
- const outputTokens = typeof usage?.completion_tokens === 'number' ? usage.completion_tokens : 0;
35
- if (inputTokens === 0 && outputTokens === 0)
36
- return;
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
- const tokenResult = guard.recordTokens(completion.usage?.prompt_tokens ?? 0, completion.usage?.completion_tokens ?? 0);
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
- if (message.reasoning_content !== undefined) {
231
- assistantToolCallMessage.reasoning_content = message.reasoning_content;
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
- assistantToolCallMessage.reasoning_content = '';
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. Returns exceeded flag if over budget. */
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, token budget
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 ?? DEFAULT_MAX_TURN_TOKENS;
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. Returns exceeded flag if over budget. */
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.1';
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.1';
310
+ aliases.minimax = 'minimax/MiniMax-M2.5';
311
311
  }
312
312
  if (providers.has('kimi-api')) {
313
313
  aliases.kimi = 'kimi/kimi-for-coding';
@@ -398,7 +398,7 @@ export function buildSetupConfig(input) {
398
398
  },
399
399
  heartbeat: {
400
400
  intervalMs: 1800000,
401
- prompt: 'Read HEARTBEAT.md. Follow it strictly. If nothing needs attention, reply HEARTBEAT_OK.',
401
+ prompt: 'Read ~/.skimpyclaw/agents/main/HEARTBEAT.md. Follow it strictly. If nothing needs attention, reply HEARTBEAT_OK.',
402
402
  tools: {
403
403
  enabled: true,
404
404
  allowedPaths: [
@@ -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.1', max_tokens: 8, messages: [{ role: 'user', content: 'ping' }] }),
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
  }
package/dist/voice.js CHANGED
@@ -187,11 +187,19 @@ function getSTTProvider(config) {
187
187
  if (!providers || Object.keys(providers).length === 0) {
188
188
  return null;
189
189
  }
190
- const providerName = config.defaultProvider || Object.keys(providers)[0];
191
- const provider = providers[providerName];
192
- if (!provider)
193
- return null;
194
- return { name: providerName, provider };
190
+ const isApiBackedSttProvider = (provider) => Boolean(provider && (provider.stt || provider.apiKey));
191
+ if (config.defaultProvider) {
192
+ const preferred = providers[config.defaultProvider];
193
+ if (isApiBackedSttProvider(preferred)) {
194
+ return { name: config.defaultProvider, provider: preferred };
195
+ }
196
+ }
197
+ for (const [name, provider] of Object.entries(providers)) {
198
+ if (isApiBackedSttProvider(provider)) {
199
+ return { name, provider };
200
+ }
201
+ }
202
+ return null;
195
203
  }
196
204
  /**
197
205
  * Resolve the API key, supporting ${ENV_VAR} syntax.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skimpyclaw",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Lightweight personal AI assistant with Telegram and Discord integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",