llm-switcher 1.3.0 → 1.3.1
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 +5 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/package.json +1 -1
- package/plugin.mjs +3 -3
- package/tests/agy.e2e.test.mjs +7 -2
- package/tests/formats.test.mjs +16 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog — LLM Switcher
|
|
2
2
|
|
|
3
|
+
## Release 1.3.1
|
|
4
|
+
|
|
5
|
+
- **agy note:** `switch plugin status` now names agy 1.2.14. This release of agy also loads `~/.gemini/antigravity-cli/hooks.json` but does not run its hooks (measured 2026-10-03), so the shim toast is still the notice for agy.
|
|
6
|
+
- **Tests:** A test covers an error in a converted agy stream. The error goes to agy bare, as Code Assist sends it. The agy end-to-end tests now pass in any order.
|
|
7
|
+
|
|
3
8
|
## Release 1.3.0
|
|
4
9
|
|
|
5
10
|
- **Antigravity CLI (`agy`):** A third tool, beside Claude Code and Codex. A profile with `"tool": "agy"` routes it, and `switch agy <profile>` turns it on. `switch off agy` turns it off and leaves the other tools alone. The dashboard has an agy row and an **agy on intact** template.
|
package/README.md
CHANGED
|
@@ -621,7 +621,7 @@ This writes one file for each tool, and it opens no configuration file of either
|
|
|
621
621
|
| --- | --- | --- |
|
|
622
622
|
| Claude Code | `~/.claude/skills/llm-switcher-status/` | A folder with `.claude-plugin/plugin.json` under a skills directory loads as a plugin on the next session. There is no marketplace and no install step. |
|
|
623
623
|
| Codex | `~/.codex/hooks.json` | Codex reads this file by itself. `config.toml` stays closed. |
|
|
624
|
-
| Antigravity CLI | `~/.gemini/antigravity-cli/hooks.json` | agy reads this file at the start. agy 1.2.
|
|
624
|
+
| Antigravity CLI | `~/.gemini/antigravity-cli/hooks.json` | agy reads this file at the start. agy 1.2.14 loads it, but it does not run global hooks yet. Only `.agents/hooks.json` in a workspace runs. Until a release runs global hooks, the shim toast is the notice for agy. agy cannot run a quoted path, so if the path of `hook-status.mjs` holds a space, the install skips agy and prints `[Skip]`. |
|
|
625
625
|
|
|
626
626
|
An existing `~/.codex/hooks.json` is merged. Your own hooks stay, and `switch plugin uninstall` takes
|
|
627
627
|
only ours away. If the file does not parse, the install refuses it and changes nothing, because that
|
package/README.vi.md
CHANGED
|
@@ -614,7 +614,7 @@ Lệnh này ghi một file cho mỗi tool, và không mở file cấu hình nào
|
|
|
614
614
|
| --- | --- | --- |
|
|
615
615
|
| Claude Code | `~/.claude/skills/llm-switcher-status/` | Một thư mục có `.claude-plugin/plugin.json` nằm dưới skills directory sẽ được nạp như một plugin ở phiên kế tiếp. Không cần marketplace, không cần bước install. |
|
|
616
616
|
| Codex | `~/.codex/hooks.json` | Codex tự đọc file này. `config.toml` không bị mở. |
|
|
617
|
-
| Antigravity CLI | `~/.gemini/antigravity-cli/hooks.json` | agy đọc file này khi khởi động. agy 1.2.
|
|
617
|
+
| Antigravity CLI | `~/.gemini/antigravity-cli/hooks.json` | agy đọc file này khi khởi động. agy 1.2.14 nạp file này nhưng chưa chạy hook toàn cục. Chỉ `.agents/hooks.json` trong workspace mới chạy. Cho tới khi có bản chạy hook toàn cục, toast của shim là nhắc nhở cho agy. agy không chạy được đường dẫn có dấu nháy, nên nếu đường dẫn của `hook-status.mjs` có dấu cách, lệnh cài bỏ qua agy và in `[Skip]`. |
|
|
618
618
|
|
|
619
619
|
Nếu `~/.codex/hooks.json` đã có sẵn thì lệnh sẽ **merge**. Hook của bạn được giữ nguyên, và
|
|
620
620
|
`switch plugin uninstall` chỉ lấy đi phần của switcher. Nếu file không parse được thì lệnh từ chối và
|
package/package.json
CHANGED
package/plugin.mjs
CHANGED
|
@@ -181,11 +181,11 @@ function uninstallCodex(opts) {
|
|
|
181
181
|
// with its quotes kept literally, so the path goes unquoted, with forward slashes. A path with a
|
|
182
182
|
// space cannot be written that way, and then agy is skipped.
|
|
183
183
|
//
|
|
184
|
-
// agy 1.2.
|
|
185
|
-
// runs (measured 2026-10-
|
|
184
|
+
// agy 1.2.14 loads this global file but does not run its hooks; only a workspace's .agents/hooks.json
|
|
185
|
+
// runs (measured 2026-10-03). The entry is ready for a release that runs global hooks. Until then the
|
|
186
186
|
// shim toast is the launch notice for agy.
|
|
187
187
|
export const AGY_HOOK_NAME = 'llm-switcher-status';
|
|
188
|
-
export const AGY_GLOBAL_HOOKS_NOTE = 'agy 1.2.
|
|
188
|
+
export const AGY_GLOBAL_HOOKS_NOTE = 'agy 1.2.14 loads global hooks but does not run them yet; the shim toast is the notice';
|
|
189
189
|
|
|
190
190
|
function agyHooksPath(opts) {
|
|
191
191
|
return path.join(opts.agyHome || agyHome(), 'hooks.json');
|
package/tests/agy.e2e.test.mjs
CHANGED
|
@@ -489,6 +489,7 @@ test('the inspector reads the usage of a non-streaming Bifrost agy turn', async
|
|
|
489
489
|
});
|
|
490
490
|
|
|
491
491
|
test('a failed Bifrost lookup is retried soon, not pinned for ten minutes', async () => {
|
|
492
|
+
flakyLookups = 0; // the first lookup of this test fails, whatever ran before
|
|
492
493
|
const port = await startGateway({ LLM_SWITCHER_CODE_ASSIST_URL: `${base}/google`, LLM_SWITCHER_BIFROST_RETRY_MS: '200' });
|
|
493
494
|
const send = async () => (await fetch(`http://127.0.0.1:${port}/v1internal:streamGenerateContent?alt=sse`, { method: 'POST', headers: agyHeaders({ 'x-llm-profile': 'agyf' }), body: JSON.stringify(envelope()) })).text();
|
|
494
495
|
assert.match(await send(), /flaky converted/);
|
|
@@ -497,6 +498,7 @@ test('a failed Bifrost lookup is retried soon, not pinned for ten minutes', asyn
|
|
|
497
498
|
});
|
|
498
499
|
|
|
499
500
|
test('a Bifrost lookup that answers 404 is retried soon too', async () => {
|
|
501
|
+
notFoundLookups = 0;
|
|
500
502
|
const port = await startGateway({ LLM_SWITCHER_CODE_ASSIST_URL: `${base}/google`, LLM_SWITCHER_BIFROST_RETRY_MS: '200' });
|
|
501
503
|
const send = async () => (await fetch(`http://127.0.0.1:${port}/v1internal:streamGenerateContent?alt=sse`, { method: 'POST', headers: agyHeaders({ 'x-llm-profile': 'agynf' }), body: JSON.stringify(envelope()) })).text();
|
|
502
504
|
assert.match(await send(), /flaky converted/);
|
|
@@ -518,9 +520,12 @@ test('a Bifrost stream that breaks ends the answer to agy with an error, not a c
|
|
|
518
520
|
await assert.rejects(r.text());
|
|
519
521
|
});
|
|
520
522
|
|
|
521
|
-
test('with agy switched off, its agent turns go back to Google', async () => {
|
|
523
|
+
test('with agy switched off, its agent turns go back to Google', async (t) => {
|
|
522
524
|
const token = fs.readFileSync(path.join(tmpDir, 'admin.token'), 'utf8').trim();
|
|
523
|
-
const
|
|
525
|
+
const switchAgy = (profile) => fetch(url('/api/switch'), { method: 'POST', headers: { 'content-type': 'application/json', 'x-llm-switcher-token': token }, body: JSON.stringify({ target: 'agy', profile }) });
|
|
526
|
+
// Put agy back, so a test added after this one still finds it routed.
|
|
527
|
+
t.after(async () => { await switchAgy('agyb'); });
|
|
528
|
+
const off = await switchAgy(null);
|
|
524
529
|
assert.equal(off.status, 200, await off.text());
|
|
525
530
|
const before = received.length;
|
|
526
531
|
const r = await fetch(url('/v1internal:streamGenerateContent?alt=sse'), { method: 'POST', headers: agyHeaders(), body: JSON.stringify(envelope()) });
|
package/tests/formats.test.mjs
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
buildResponsesMessage, createUpstreamNormalizer as makeNormalizer, emitUpstreamBody,
|
|
11
11
|
isAntigravityModel, smartUsage,
|
|
12
12
|
createChatStream, buildChatMessage, chatFinish, anthropicStopReason,
|
|
13
|
-
smartText, smartReasoning, sanitizeJsonSchema, normalizeUpstream, buildVertexMessage
|
|
13
|
+
smartText, smartReasoning, sanitizeJsonSchema, normalizeUpstream, buildVertexMessage,
|
|
14
|
+
createCodeAssistStream
|
|
14
15
|
} from '../formats.mjs';
|
|
15
16
|
import { assertValidAnthropicEvents } from './helpers.mjs';
|
|
16
17
|
|
|
@@ -898,3 +899,17 @@ test('codeAssistToIR: generation parameters map to ir.params', () => {
|
|
|
898
899
|
assert.equal(ir.params.topP, 0.9);
|
|
899
900
|
assert.deepEqual(ir.params.stop, ['END']);
|
|
900
901
|
});
|
|
902
|
+
|
|
903
|
+
test('Code Assist stream: a chunk is wrapped with a trace id, an error is sent bare as Code Assist does', () => {
|
|
904
|
+
const out = [];
|
|
905
|
+
const stream = createCodeAssistStream((event, data) => out.push(data), 'gemini-3.8-flash-high');
|
|
906
|
+
stream.text('hi');
|
|
907
|
+
stream.error('upstream broke');
|
|
908
|
+
const chunk = out.find(d => d.response);
|
|
909
|
+
assert.equal(typeof chunk.traceId, 'string');
|
|
910
|
+
assert.match(chunk.response.responseId, /^agy-/);
|
|
911
|
+
const err = out.at(-1);
|
|
912
|
+
assert.deepEqual(Object.keys(err), ['error'], 'an error is not wrapped in response/traceId');
|
|
913
|
+
assert.equal(err.error.status, 'INTERNAL');
|
|
914
|
+
assert.equal(err.error.message, 'upstream broke');
|
|
915
|
+
});
|