llm-switcher 1.1.7 → 1.1.8

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/README.md CHANGED
@@ -191,7 +191,12 @@ flowchart LR
191
191
 
192
192
  ---
193
193
 
194
- ## Changes in 1.1.7
194
+ ## Changes in 1.1.8
195
+
196
+ - **Claude Code tools.** A tool schema value of `0`, `false`, `""` or `null` (for example `minimum: 0`) became an empty object schema. Gemini refused every Claude Code request with HTTP 400 "Starting an object on a scalar field". These values now stay as they are.
197
+ - **Shim PATH order.** If the shim folder is on `PATH` but after the real `claude` or `codex`, `switch shim status` and `switch doctor` now tell you to put the export line last in your shell files.
198
+
199
+ ### Changes in 1.1.7
195
200
 
196
201
  - **Codex tools.** With `publicModels` set, Codex lost its tools and ended after one answer. The model catalog copied the metadata of a real OpenAI model, which puts Codex in the "Responses Lite" form. The catalog now keeps Codex in its direct tool mode, and the gateway also reads tools that arrive as an `additional_tools` input item.
197
202
 
package/README.vi.md CHANGED
@@ -191,7 +191,12 @@ flowchart LR
191
191
 
192
192
  ---
193
193
 
194
- ## Thay đổi trong bản 1.1.7
194
+ ## Thay đổi trong bản 1.1.8
195
+
196
+ - **Tool của Claude Code.** Giá trị `0`, `false`, `""` hoặc `null` trong schema của tool (ví dụ `minimum: 0`) bị đổi thành schema object rỗng. Gemini từ chối mọi request của Claude Code với HTTP 400 "Starting an object on a scalar field". Giờ các giá trị này được giữ nguyên.
197
+ - **Thứ tự PATH của shim.** Nếu thư mục shim có trong `PATH` nhưng đứng sau `claude` hoặc `codex` thật, `switch shim status` và `switch doctor` giờ chỉ cách sửa: đặt dòng export ở cuối các file cấu hình shell.
198
+
199
+ ### Thay đổi trong bản 1.1.7
195
200
 
196
201
  - **Tool của Codex.** Khi có `publicModels`, Codex mất hết tool và dừng sau một câu trả lời. Model catalog chép metadata của một model OpenAI thật, và metadata này đưa Codex sang dạng "Responses Lite". Giờ catalog giữ Codex ở chế độ tool trực tiếp, và gateway cũng đọc tool gửi đến dưới dạng input item `additional_tools`.
197
202
 
package/formats.mjs CHANGED
@@ -253,22 +253,24 @@ function smartDelta(choice) {
253
253
  const SCHEMA_MAPS = new Set(['properties', 'patternProperties', '$defs', 'definitions']);
254
254
 
255
255
  function sanitizeJsonSchema(schema) {
256
- if (!schema) return { type: 'object', properties: {} };
257
- if (typeof schema === 'string') {
258
- return schema;
259
- }
260
- if (typeof schema !== 'object') return schema;
261
- if (Array.isArray(schema)) return schema.map(sanitizeJsonSchema);
256
+ if (schema === null || schema === undefined) return { type: 'object', properties: {} };
257
+ return sanitizeSchemaNode(schema);
258
+ }
259
+
260
+ // Inside a schema 0, false, "" and null are values (minimum: 0, additionalProperties: false), not a missing schema.
261
+ function sanitizeSchemaNode(schema) {
262
+ if (schema === null || typeof schema !== 'object') return schema;
263
+ if (Array.isArray(schema)) return schema.map(sanitizeSchemaNode);
262
264
  const clean = {};
263
265
  for (const [k, v] of Object.entries(schema)) {
264
266
  if (k === '$schema' || k === 'cache_control' || k === 'encrypted') continue;
265
267
  if (k === 'format' && ['uri', 'uri-reference'].includes(v)) continue;
266
268
  // A name map: each value is a schema, the map itself is not. A parameter may be named "properties".
267
269
  if (SCHEMA_MAPS.has(k) && v && typeof v === 'object' && !Array.isArray(v)) {
268
- clean[k] = Object.fromEntries(Object.entries(v).map(([name, sub]) => [name, sanitizeJsonSchema(sub)]));
270
+ clean[k] = Object.fromEntries(Object.entries(v).map(([name, sub]) => [name, sanitizeSchemaNode(sub)]));
269
271
  continue;
270
272
  }
271
- clean[k] = sanitizeJsonSchema(v);
273
+ clean[k] = sanitizeSchemaNode(v);
272
274
  }
273
275
  if (!clean.type && clean.properties) {
274
276
  clean.type = 'object';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "llm-switcher",
3
- "version": "1.1.7",
3
+ "version": "1.1.8",
4
4
  "description": "Zero-dependency multi-protocol edge gateway & provider switcher for Claude Code, Codex, OpenAI and Gemini clients",
5
5
  "keywords": [
6
6
  "llm",
package/shim.mjs CHANGED
@@ -233,6 +233,15 @@ export function suggestedRcFiles(platform = process.platform) {
233
233
  return [path.join(home, '.profile')];
234
234
  }
235
235
 
236
+ // The shim is on PATH but behind the real binary: a line that runs later prepends another directory
237
+ // (npm-global, Homebrew). Login shells read the profile file, interactive shells the rc file.
238
+ export function pathOrderHint(platform = process.platform) {
239
+ const files = suggestedRcFiles(platform);
240
+ if (!files.length) return [`Run this command, then open a new terminal: ${pathExportLine(platform)}`];
241
+ return [`Put this line LAST in ${files.join(' and ')}, after every other PATH line, then open a new terminal:`,
242
+ ` ${pathExportLine(platform)}`];
243
+ }
244
+
236
245
  /**
237
246
  * Check whether running CLI processes have the gateway env.
238
247
  * This is the hardest failure to spot: a `claude` session opened BEFORE the gateway
package/switch.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  } from './state.mjs';
14
14
  import { runProbe, runCheck } from './contract.mjs';
15
15
  import {
16
- SHIM_DIR, installShims, uninstallShims, shimStatus, pathExportLine,
16
+ SHIM_DIR, installShims, uninstallShims, shimStatus, pathExportLine, pathOrderHint,
17
17
  suggestedRcFiles, auditRunningProcesses
18
18
  } from './shim.mjs';
19
19
  import {
@@ -739,6 +739,7 @@ async function manageShim(action = 'status') {
739
739
  if (s.active) console.log(`[PASS] ${s.name}: shim active → ${s.effective}`);
740
740
  else console.log(`[WARN] ${s.name}: shim installed but '${s.name}' resolves to ${s.effective || '(not found)'} — PATH order wrong`);
741
741
  }
742
+ if (st.onPath && st.shims.some(s => s.installed && !s.active)) for (const line of pathOrderHint()) console.log(` ${line}`);
742
743
 
743
744
  const audit = auditActiveClis();
744
745
  if (!audit.supported) console.log('\n[INFO] The running-session check is not supported on Windows.');
@@ -832,6 +833,7 @@ async function runDoctor() {
832
833
  else if (!s.active) warn(`[WARN] '${s.name}' resolves to ${s.effective || '(not found)'} instead of the shim — PATH order wrong.`);
833
834
  else console.log(`[PASS] '${s.name}' routed through shim.`);
834
835
  }
836
+ if (sh.onPath && sh.shims.some(s => s.installed && !s.active)) for (const line of pathOrderHint()) console.log(` ${line}`);
835
837
 
836
838
  // 7. Running processes missing env => those sessions call the provider directly
837
839
  const audit = auditActiveClis();
@@ -799,3 +799,19 @@ test('responsesToIR reads additional_tools items and sends a tool once', () => {
799
799
  assert.deepEqual(ir.tools.map(t => t.name), ['exec_command', 'apply_patch']);
800
800
  assert.equal(ir.messages.length, 0, 'the item is not a message');
801
801
  });
802
+
803
+ // ISS-CC-LS-001: a falsy value inside a schema (minimum: 0, additionalProperties: false) is a value,
804
+ // not a missing schema. Gemini refuses an object where it expects a number.
805
+ test('tool schemas keep 0, false, "" and null values', () => {
806
+ const schema = { type: 'object', additionalProperties: false, properties: {
807
+ total: { type: 'integer', minimum: 0, maximum: 9007199254740991 },
808
+ tags: { type: 'array', minItems: 0, items: { type: 'string', default: '' } },
809
+ note: { type: ['string', 'null'], default: null } } };
810
+ const ir = responsesToIR({ model: 'm', input: 'hi', tools: [{ type: 'function', name: 'f', parameters: schema }] });
811
+ const p = ir.tools[0].parameters;
812
+ assert.equal(p.additionalProperties, false);
813
+ assert.equal(p.properties.total.minimum, 0);
814
+ assert.equal(p.properties.tags.minItems, 0);
815
+ assert.equal(p.properties.tags.items.default, '');
816
+ assert.equal(p.properties.note.default, null);
817
+ });
@@ -19,7 +19,7 @@ const ROOT = path.resolve(__dirname, '..');
19
19
  const STATE = fs.mkdtempSync(path.join(os.tmpdir(), 'shimstate-'));
20
20
  process.env.LLM_SWITCHER_STATE_DIR = STATE;
21
21
 
22
- const { SHIM_DIR, SHIMMED, pathExportLine, suggestedRcFiles, shimStatus, renderShim } =
22
+ const { SHIM_DIR, SHIMMED, pathExportLine, pathOrderHint, suggestedRcFiles, shimStatus, renderShim } =
23
23
  await import(pathToFileURL(path.join(ROOT, 'shim.mjs')).href);
24
24
 
25
25
  // The behavioural tests run the CURRENT template, rendered into a temp dir. Running the copy
@@ -226,3 +226,19 @@ test('the POSIX shims source launch files only when this account owns them', ()
226
226
  }
227
227
  }
228
228
  });
229
+
230
+ // ISS-CC-LS-002: on zsh a later line in .zshrc or .zprofile can prepend npm-global or Homebrew
231
+ // ahead of the shim. The hint names both files and says the line must come last.
232
+ test('pathOrderHint tells a zsh user to put the export last in .zshrc and .zprofile', () => {
233
+ const shell = process.env.SHELL;
234
+ process.env.SHELL = '/bin/zsh';
235
+ try {
236
+ const text = pathOrderHint('darwin').join('\n');
237
+ assert.match(text, /LAST/);
238
+ assert.match(text, /\.zshrc/);
239
+ assert.match(text, /\.zprofile/);
240
+ assert.ok(text.includes(pathExportLine('darwin')));
241
+ } finally {
242
+ if (shell === undefined) delete process.env.SHELL; else process.env.SHELL = shell;
243
+ }
244
+ });