llm-switcher 1.6.17 → 1.6.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog — LLM Switcher
2
2
 
3
+ ## Release 1.6.18 — 2026-10-22
4
+
5
+ - **DeepSeek and Kimi on a turn with no reasoning.** `hasNativeReasoning` now matches `deepseek` and `kimi` model ids. Codex sends a continue or compaction turn with no reasoning item, and the proxy took such a model for a non-thinking one. The tool-call turns then went out with no `reasoning_content`, and DeepSeek thinking mode answered 400 "reasoning_content must be passed back". A model that is not a thinking model still gets no field, and `thinkingMode: off` still sends none.
6
+ - **Tool names in the debug log.** With `debug: true`, the proxy now logs the names of the tools it declares upstream and the `tool_choice`, on both the HTTP and the WebSocket route. Before, the log held the first 300 to 500 characters of the body, which never reached `tools`. This is how to tell whether an upstream "undeclared tool" error comes from a missing declaration.
7
+
3
8
  ## Release 1.6.17 — 2026-10-13
4
9
 
5
10
  - **Documents in a tool result.** A document with a text source, a content source or a URL goes to the model as its text or its URL. Before 1.6.17, it went as a bare `[document]` label. Only base64 data becomes a label, for example `[file: a.pdf]`.
package/formats.mjs CHANGED
@@ -1049,7 +1049,11 @@ function withUpstreamToolNames(ir, names) {
1049
1049
  function hasNativeReasoning(model) {
1050
1050
  // model IDs change often, but all current Opus variants support reasoning.
1051
1051
  const m = String(model || '').toLowerCase();
1052
+ // DeepSeek and Kimi run in thinking mode by default and refuse the next tool step when an earlier
1053
+ // tool-call turn lacks reasoning_content, even if the client (Codex on a continue/compaction turn)
1054
+ // sent no reasoning item with the request.
1052
1055
  return m.includes('thinking') || m.includes('reasoning') || m.includes('reasoner') || m.includes('opus')
1056
+ || m.includes('deepseek') || m.includes('kimi')
1053
1057
  || /(^|[/_.:-])(o[134]|r1|qwq)([/_.:-]|$)/.test(m);
1054
1058
  }
1055
1059
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-switcher",
3
- "version": "1.6.17",
3
+ "version": "1.6.18",
4
4
  "description": "Zero-dependency multi-protocol edge gateway & provider switcher for Claude Code and Codex, with OpenAI-compatible, Anthropic and Vertex upstreams",
5
5
  "keywords": [
6
6
  "llm",
@@ -0,0 +1,11 @@
1
+ {
2
+ "claude": {
3
+ "cacheKey": "claude-intact:zencore/space-bunny-free,zencore/muse-spark-1.3-contributor-free",
4
+ "time": 1792631963371,
5
+ "lines": [
6
+ "[llm-switcher] claude -> claude-intact | 127.0.0.1:20142 | zencore/space-bunny-free",
7
+ "[llm-switcher:preflight] ⚠️ CẢNH BÁO: Model đã setup không còn khả dụng trên provider: 'zencore/space-bunny-free' [slot sonnet], 'zencore/muse-spark-1.3-contributor-free' [slot haiku].",
8
+ "[llm-switcher:preflight] Gợi ý: Chạy 'switch ui' để cập nhật lại model hợp lệ."
9
+ ]
10
+ }
11
+ }
package/proxy.mjs CHANGED
@@ -1320,6 +1320,7 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
1320
1320
  const { url, headers, upBody } = buildUpstreamRequest(profile, outFormat, ir, mappedModel, req, slot, profileKey);
1321
1321
  if (traceId) headers['x-intact-trace'] = traceId;
1322
1322
  debugLog(`[${profileKey}] ${clientFormat} -> ${outFormat} ${url} ::`, JSON.stringify(upBody).slice(0, 500));
1323
+ debugLog(`[${profileKey}] tools declared upstream:`, JSON.stringify((upBody.tools || []).map(t => t.function?.name ?? t.name ?? t.type)), 'tool_choice:', JSON.stringify(upBody.tool_choice ?? null));
1323
1324
 
1324
1325
  let upstreamRes;
1325
1326
  try {
@@ -2592,6 +2593,7 @@ async function handleWsResponseCreate(socket, payload, req, ac, history = null)
2592
2593
  const { url, headers, upBody } = buildUpstreamRequest(profile, outFormat, ir, mappedModel, req, slot, profileKey);
2593
2594
  if (traceId) headers['x-intact-trace'] = traceId;
2594
2595
  debugLog(`[${profileKey}:ws] ${clientFormat} -> ${outFormat} ${url} ::`, JSON.stringify(upBody).slice(0, 300));
2596
+ debugLog(`[${profileKey}:ws] tools declared upstream:`, JSON.stringify((upBody.tools || []).map(t => t.function?.name ?? t.name ?? t.type)), 'tool_choice:', JSON.stringify(upBody.tool_choice ?? null));
2595
2597
 
2596
2598
  let upstreamRes;
2597
2599
  try {
@@ -1263,6 +1263,26 @@ test('irToChatBody replays thinking as reasoning_content on tool-call turns', ()
1263
1263
  assert.equal(rb.messages.find(m => m.tool_calls).reasoning_content, 'Plan: run ls');
1264
1264
  });
1265
1265
 
1266
+ // Codex continue/compaction turns carry no reasoning item; DeepSeek still answers 400 "reasoning_content must be passed back".
1267
+ test('irToChatBody auto mode replays reasoning_content for DeepSeek/Kimi even when the request has no reasoning', () => {
1268
+ const ir = responsesToIR({ model: 'x', input: [
1269
+ { type: 'message', role: 'user', content: 'go' },
1270
+ { type: 'function_call', call_id: 'c1', name: 'exec', arguments: '{}' },
1271
+ { type: 'function_call_output', call_id: 'c1', output: 'ok' },
1272
+ { type: 'message', role: 'user', content: 'continue' }
1273
+ ] });
1274
+ for (const model of ['zencore/deepseek-v4.1-flash-free', 'buddycore/deepseek-v4.1-flash', 'zencore/kimi-k2.7']) {
1275
+ const call = irToChatBody(ir, model, { thinkingMode: 'auto' }).messages.find(m => m.tool_calls);
1276
+ assert.equal(call.reasoning_content, '', model);
1277
+ }
1278
+ for (const model of ['gpt-5', 'zencore/glm-5.3-flash-free']) {
1279
+ const body = irToChatBody(ir, model, { thinkingMode: 'auto' });
1280
+ assert.ok(body.messages.every(m => !('reasoning_content' in m)), model);
1281
+ }
1282
+ const off = irToChatBody(ir, 'zencore/deepseek-v4.1-flash-free', { thinkingMode: 'off' });
1283
+ assert.ok(off.messages.every(m => !('reasoning_content' in m)));
1284
+ });
1285
+
1266
1286
  // Strict OpenAI-compatible servers may refuse the extra key, so native mode sends it only on the profile opt-in.
1267
1287
  test('irToChatBody native mode sends no reasoning_content without the profile opt-in', () => {
1268
1288
  const ir = anthropicToIR({ model: 'x', max_tokens: 8000, thinking: { type: 'enabled', budget_tokens: 4000 }, messages: [