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.
@@ -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-CZJCvMSN.js"></script>
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
- 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)
@@ -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
- const tokenResult = guard.recordTokens(completion.usage?.prompt_tokens ?? 0, completion.usage?.completion_tokens ?? 0);
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
- apiMessages.push(message);
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. 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';
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skimpyclaw",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Lightweight personal AI assistant with Telegram and Discord integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",