llm-switcher 1.2.11 → 1.3.0
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 +31 -0
- package/README.md +124 -20
- package/README.vi.md +123 -20
- package/config.example.json +13 -1
- package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
- package/docs/response-matrix.json +1130 -1130
- package/formats.mjs +128 -12
- package/hook-status.mjs +5 -4
- package/mcp.mjs +6 -5
- package/package.json +1 -1
- package/plugin.mjs +80 -9
- package/proxy.mjs +211 -20
- package/service.mjs +12 -6
- package/shim.mjs +62 -20
- package/skills/llm-switcher/SKILL.md +93 -93
- package/state.mjs +50 -13
- package/switch +0 -0
- package/switch.cmd +2 -2
- package/switch.mjs +115 -24
- package/tests/agy.e2e.test.mjs +529 -0
- package/tests/agy.unit.test.mjs +190 -0
- package/tests/dashboard.e2e.test.mjs +18 -17
- package/tests/formats.test.mjs +67 -1
- package/tests/gateway.e2e.test.mjs +20 -2
- package/tests/helpers.mjs +24 -24
- package/tests/live-optimizer-interop.mjs +205 -205
- package/tests/mcp.test.mjs +5 -5
- package/tests/plugin.test.mjs +4 -2
- package/tests/service.test.mjs +27 -1
- package/tests/shim.test.mjs +165 -0
- package/tests/state.test.mjs +8 -8
- package/tests/update.e2e.test.mjs +197 -0
- package/tests/update.test.mjs +145 -0
- package/ui.html +121 -8
- package/update.mjs +98 -0
- package/verify-gateway.mjs +35 -0
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: llm-switcher
|
|
3
|
-
description: Mandatory guidance for AI coding agents and token compression tools (Headroom, RTK, Ponytail, Repomix). Enforces routing all LLM traffic through the local LLM Switcher edge gateway (http://127.0.0.1:3456) instead of directly calling provider endpoints.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# LLM Switcher Edge Gateway Skill
|
|
7
|
-
|
|
8
|
-
This skill governs how AI coding agents (Claude Code, Codex, Cursor, Windsurf, Opencode) and prompt optimization/compression tools (Headroom, RTK, Ponytail, Repomix) must interact with LLM providers on this workstation.
|
|
9
|
-
|
|
10
|
-
## 1. The Core Directive
|
|
11
|
-
|
|
12
|
-
> **ALL OUTBOUND LLM REQUESTS MUST ROUTE THROUGH LLM SWITCHER (`http://127.0.0.1:3456`).**
|
|
13
|
-
>
|
|
14
|
-
> Never configure any tool, script, sub-agent, or token compression proxy to call external provider endpoints directly (such as `https://api.anthropic.com`, `https://api.openai.com`, or `https://generativelanguage.googleapis.com`).
|
|
15
|
-
>
|
|
16
|
-
> If a token compression tool (Headroom, RTK, Ponytail) is used, it MUST be positioned as an **inner processing layer**, and its **upstream destination MUST be configured to point to LLM Switcher** at `http://127.0.0.1:3456`.
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
[Client CLI / Agent]
|
|
20
|
-
│
|
|
21
|
-
▼
|
|
22
|
-
[Token Compressor: RTK / Headroom / Ponytail] <-- Inner Layer
|
|
23
|
-
│
|
|
24
|
-
▼ (MANDATORY: Upstream MUST point to http://127.0.0.1:3456)
|
|
25
|
-
[LLM Switcher Edge Gateway (:3456)] <-- Outermost Gatekeeper
|
|
26
|
-
│
|
|
27
|
-
▼ (Heals schemas, maps 1M context, extracts thinking, converts protocol)
|
|
28
|
-
[Internet / intact (Recommended) / 9Router / Upstream LLM]
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## 2. Why This is Mandatory
|
|
32
|
-
|
|
33
|
-
Direct outbound calls from token compressors will break the developer environment for several reasons:
|
|
34
|
-
|
|
35
|
-
1. **Schema Breakage & The Healer Engine:**
|
|
36
|
-
Token optimizers frequently prune conversation history to save tokens. In doing so, they often delete an `assistant` turn containing a `tool_use`, while leaving the subsequent `user` turn containing a `tool_result`.
|
|
37
|
-
- Anthropic and OpenAI APIs strictly reject this with `HTTP 400 (orphaned tool_use_id)`.
|
|
38
|
-
- **LLM Switcher** intercepts these malformed requests and runs its **Healer Engine**, automatically converting orphaned results into safe context text blocks and merging consecutive same-role turns so upstream providers accept the payload cleanly.
|
|
39
|
-
2. **Thinking / Reasoning Protection:**
|
|
40
|
-
Many token compressors naively strip `thinking` parameters or reasoning budgets to reduce token count.
|
|
41
|
-
- When routed through LLM Switcher, the gateway detects reasoning models (such as `ag/claude-opus-4-6-thinking` or `ag/gemini-3.8-flash`) and **automatically restores thinking parameters**, ensuring the model retains its full reasoning capabilities.
|
|
42
|
-
3. **Official Context Windows & Dynamic Model Discovery:**
|
|
43
|
-
Context windows follow the model you select without mutating permanent settings. The gateway auto-discovers official models from Anthropic and OpenAI, refreshing mappings on tool version upgrades (`switch models`).
|
|
44
|
-
4. **Server Gateway Synergy (intact / 9Router):**
|
|
45
|
-
LLM Switcher pairs directly with server-side gateways like **intact** (strongly recommended for credential proxying, account pooling, and contract lab self-healing) or 9Router, handling retries and quotas far better than any local tool.
|
|
46
|
-
|
|
47
|
-
## 3. Configuring Token Compression Tools
|
|
48
|
-
|
|
49
|
-
When setting up or assisting the user with token compression tools, apply these exact configurations:
|
|
50
|
-
|
|
51
|
-
### A. Headroom (Prompt Compression Proxy)
|
|
52
|
-
When configuring Headroom's upstream endpoint:
|
|
53
|
-
- In Headroom config (`headroom.yaml` or environment variables):
|
|
54
|
-
```bash
|
|
55
|
-
ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
|
|
56
|
-
OPENAI_BASE_URL="http://127.0.0.1:3456/v1"
|
|
57
|
-
```
|
|
58
|
-
- Point your CLI to Headroom's listening port (e.g. `http://127.0.0.1:8787`). Headroom compresses the prompt and forwards it to LLM Switcher on port `3456`.
|
|
59
|
-
|
|
60
|
-
### B. RTK (Rust Token Killer / Repomix)
|
|
61
|
-
- In RTK configuration or execution flags:
|
|
62
|
-
```bash
|
|
63
|
-
# Ensure RTK's target upstream proxy is LLM Switcher
|
|
64
|
-
export UPSTREAM_URL="http://127.0.0.1:3456"
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### C. Ponytail / Custom Proxies
|
|
68
|
-
- Always set the proxy's upstream host to:
|
|
69
|
-
`http://127.0.0.1:3456` (or `http://127.0.0.1:3456/v1` for OpenAI-compatible tools).
|
|
70
|
-
|
|
71
|
-
### D. Sub-Agents & Spawned Terminal Commands
|
|
72
|
-
When an agent spawns sub-processes that call LLM CLIs:
|
|
73
|
-
- **Never advise sourcing `env.sh` or `env.cmd`.** Those files carry no proxy variables any
|
|
74
|
-
more, so sourcing one changes nothing — and a stale variable an older version once wrote
|
|
75
|
-
would still point the tool at a port where nothing listens.
|
|
76
|
-
- Run `switch claude <profile>` or `switch
|
|
77
|
-
`~/.llm-switcher/bin` inject the
|
|
78
|
-
anything stale first.
|
|
79
|
-
- `claude` and `
|
|
80
|
-
wrap them in another script.
|
|
81
|
-
|
|
82
|
-
## 4. Operational Rules for AI Agents
|
|
83
|
-
|
|
84
|
-
1. **NEVER edit `~/.claude/settings.json` directly.**
|
|
85
|
-
LLM Switcher uses zero-mutation launcher flags (`active.flag`). Writing custom base URLs or auth tokens into `settings.json` triggers warning banners in Claude Code and risks breaking the user's official subscription. Use `switch` CLI commands instead.
|
|
86
|
-
2. **Check Gateway Health Before Complex Operations:**
|
|
87
|
-
Run `switch status` or call the `switcher_audit` MCP tool to confirm:
|
|
88
|
-
- LLM Switcher is active on port `3456`.
|
|
89
|
-
- The active profile matches the intended CLI target (Claude Code, Codex, or
|
|
90
|
-
3. **Verify Routing When Errors Occur:**
|
|
91
|
-
If a tool fails with `HTTP 400`, `HTTP 502`, or connection errors:
|
|
92
|
-
- Run `switch doctor` to audit port collisions and environment variables.
|
|
93
|
-
- Inspect recent request logs via `http://127.0.0.1:3456/ui` (Tab 4: Live Inspector) to see if an intermediary tool mangled the payload.
|
|
1
|
+
---
|
|
2
|
+
name: llm-switcher
|
|
3
|
+
description: Mandatory guidance for AI coding agents and token compression tools (Headroom, RTK, Ponytail, Repomix). Enforces routing all LLM traffic through the local LLM Switcher edge gateway (http://127.0.0.1:3456) instead of directly calling provider endpoints.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# LLM Switcher Edge Gateway Skill
|
|
7
|
+
|
|
8
|
+
This skill governs how AI coding agents (Claude Code, Codex, the Antigravity CLI `agy`, Cursor, Windsurf, Opencode) and prompt optimization/compression tools (Headroom, RTK, Ponytail, Repomix) must interact with LLM providers on this workstation.
|
|
9
|
+
|
|
10
|
+
## 1. The Core Directive
|
|
11
|
+
|
|
12
|
+
> **ALL OUTBOUND LLM REQUESTS MUST ROUTE THROUGH LLM SWITCHER (`http://127.0.0.1:3456`).**
|
|
13
|
+
>
|
|
14
|
+
> Never configure any tool, script, sub-agent, or token compression proxy to call external provider endpoints directly (such as `https://api.anthropic.com`, `https://api.openai.com`, or `https://generativelanguage.googleapis.com`).
|
|
15
|
+
>
|
|
16
|
+
> If a token compression tool (Headroom, RTK, Ponytail) is used, it MUST be positioned as an **inner processing layer**, and its **upstream destination MUST be configured to point to LLM Switcher** at `http://127.0.0.1:3456`.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
[Client CLI / Agent]
|
|
20
|
+
│
|
|
21
|
+
▼
|
|
22
|
+
[Token Compressor: RTK / Headroom / Ponytail] <-- Inner Layer
|
|
23
|
+
│
|
|
24
|
+
▼ (MANDATORY: Upstream MUST point to http://127.0.0.1:3456)
|
|
25
|
+
[LLM Switcher Edge Gateway (:3456)] <-- Outermost Gatekeeper
|
|
26
|
+
│
|
|
27
|
+
▼ (Heals schemas, maps 1M context, extracts thinking, converts protocol)
|
|
28
|
+
[Internet / intact (Recommended) / 9Router / Upstream LLM]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 2. Why This is Mandatory
|
|
32
|
+
|
|
33
|
+
Direct outbound calls from token compressors will break the developer environment for several reasons:
|
|
34
|
+
|
|
35
|
+
1. **Schema Breakage & The Healer Engine:**
|
|
36
|
+
Token optimizers frequently prune conversation history to save tokens. In doing so, they often delete an `assistant` turn containing a `tool_use`, while leaving the subsequent `user` turn containing a `tool_result`.
|
|
37
|
+
- Anthropic and OpenAI APIs strictly reject this with `HTTP 400 (orphaned tool_use_id)`.
|
|
38
|
+
- **LLM Switcher** intercepts these malformed requests and runs its **Healer Engine**, automatically converting orphaned results into safe context text blocks and merging consecutive same-role turns so upstream providers accept the payload cleanly.
|
|
39
|
+
2. **Thinking / Reasoning Protection:**
|
|
40
|
+
Many token compressors naively strip `thinking` parameters or reasoning budgets to reduce token count.
|
|
41
|
+
- When routed through LLM Switcher, the gateway detects reasoning models (such as `ag/claude-opus-4-6-thinking` or `ag/gemini-3.8-flash`) and **automatically restores thinking parameters**, ensuring the model retains its full reasoning capabilities.
|
|
42
|
+
3. **Official Context Windows & Dynamic Model Discovery:**
|
|
43
|
+
Context windows follow the model you select without mutating permanent settings. The gateway auto-discovers official models from Anthropic and OpenAI, refreshing mappings on tool version upgrades (`switch models`).
|
|
44
|
+
4. **Server Gateway Synergy (intact / 9Router):**
|
|
45
|
+
LLM Switcher pairs directly with server-side gateways like **intact** (strongly recommended for credential proxying, account pooling, and contract lab self-healing) or 9Router, handling retries and quotas far better than any local tool.
|
|
46
|
+
|
|
47
|
+
## 3. Configuring Token Compression Tools
|
|
48
|
+
|
|
49
|
+
When setting up or assisting the user with token compression tools, apply these exact configurations:
|
|
50
|
+
|
|
51
|
+
### A. Headroom (Prompt Compression Proxy)
|
|
52
|
+
When configuring Headroom's upstream endpoint:
|
|
53
|
+
- In Headroom config (`headroom.yaml` or environment variables):
|
|
54
|
+
```bash
|
|
55
|
+
ANTHROPIC_BASE_URL="http://127.0.0.1:3456"
|
|
56
|
+
OPENAI_BASE_URL="http://127.0.0.1:3456/v1"
|
|
57
|
+
```
|
|
58
|
+
- Point your CLI to Headroom's listening port (e.g. `http://127.0.0.1:8787`). Headroom compresses the prompt and forwards it to LLM Switcher on port `3456`.
|
|
59
|
+
|
|
60
|
+
### B. RTK (Rust Token Killer / Repomix)
|
|
61
|
+
- In RTK configuration or execution flags:
|
|
62
|
+
```bash
|
|
63
|
+
# Ensure RTK's target upstream proxy is LLM Switcher
|
|
64
|
+
export UPSTREAM_URL="http://127.0.0.1:3456"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### C. Ponytail / Custom Proxies
|
|
68
|
+
- Always set the proxy's upstream host to:
|
|
69
|
+
`http://127.0.0.1:3456` (or `http://127.0.0.1:3456/v1` for OpenAI-compatible tools).
|
|
70
|
+
|
|
71
|
+
### D. Sub-Agents & Spawned Terminal Commands
|
|
72
|
+
When an agent spawns sub-processes that call LLM CLIs:
|
|
73
|
+
- **Never advise sourcing `env.sh` or `env.cmd`.** Those files carry no proxy variables any
|
|
74
|
+
more, so sourcing one changes nothing — and a stale variable an older version once wrote
|
|
75
|
+
would still point the tool at a port where nothing listens.
|
|
76
|
+
- Run `switch claude <profile>`, `switch codex <profile>` or `switch agy <profile>` instead. The
|
|
77
|
+
shims in `~/.llm-switcher/bin` inject the gateway variables into the tool process itself and
|
|
78
|
+
clear anything stale first. For `agy` the variable is `CLOUD_CODE_URL`, not a proxy.
|
|
79
|
+
- `claude`, `codex` and `agy` on `PATH` already are those shims: invoke them normally, and never
|
|
80
|
+
wrap them in another script.
|
|
81
|
+
|
|
82
|
+
## 4. Operational Rules for AI Agents
|
|
83
|
+
|
|
84
|
+
1. **NEVER edit `~/.claude/settings.json` directly.**
|
|
85
|
+
LLM Switcher uses zero-mutation launcher flags (`active.flag`). Writing custom base URLs or auth tokens into `settings.json` triggers warning banners in Claude Code and risks breaking the user's official subscription. Use `switch` CLI commands instead.
|
|
86
|
+
2. **Check Gateway Health Before Complex Operations:**
|
|
87
|
+
Run `switch status` or call the `switcher_audit` MCP tool to confirm:
|
|
88
|
+
- LLM Switcher is active on port `3456`.
|
|
89
|
+
- The active profile matches the intended CLI target (Claude Code, Codex, or the Antigravity CLI).
|
|
90
|
+
3. **Verify Routing When Errors Occur:**
|
|
91
|
+
If a tool fails with `HTTP 400`, `HTTP 502`, or connection errors:
|
|
92
|
+
- Run `switch doctor` to audit port collisions and environment variables.
|
|
93
|
+
- Inspect recent request logs via `http://127.0.0.1:3456/ui` (Tab 4: Live Inspector) to see if an intermediary tool mangled the payload.
|
package/state.mjs
CHANGED
|
@@ -35,8 +35,8 @@ if (DATA_DIR !== ROOT_DIR) {
|
|
|
35
35
|
const LEGACY_TARGET_TOOL = { anthropic: 'claude', responses: 'codex' };
|
|
36
36
|
// Migration input only: the pointer keys an unmigrated config may still carry.
|
|
37
37
|
const LEGACY_TARGETS = ['anthropic', 'responses', 'openai-chat', 'vertex'];
|
|
38
|
-
export const TOOLS = ['claude', 'codex'];
|
|
39
|
-
export const COMMAND_WORDS = new Set(['claude', 'codex', 'on', 'off', 'status', 'doctor', 'ui']);
|
|
38
|
+
export const TOOLS = ['claude', 'codex', 'agy'];
|
|
39
|
+
export const COMMAND_WORDS = new Set(['claude', 'codex', 'agy', 'on', 'off', 'status', 'doctor', 'ui']);
|
|
40
40
|
export const DEFAULT_PORT = 3456;
|
|
41
41
|
export const DEFAULT_BLINDFOLD_PORT = 3457;
|
|
42
42
|
// Codex with ChatGPT sign-in calls https://chatgpt.com/backend-api/codex.
|
|
@@ -53,6 +53,9 @@ export const CLAUDE_MODEL_SLOTS = ['opus', 'sonnet', 'haiku', 'fable'];
|
|
|
53
53
|
// - subagent <-> `agents.default_subagent_model` (spawned agents)
|
|
54
54
|
// No fast/fallback in the docs — those are custom keys, read only for backward compatibility.
|
|
55
55
|
export const CODEX_MODEL_SLOTS = ['main', 'review', 'subagent'];
|
|
56
|
+
// The Antigravity CLI (agy) picks its own model per call. One slot names the upstream model of its
|
|
57
|
+
// agent calls; a '*' in the value stands for the model id that agy sent (antigravity/* = Bifrost).
|
|
58
|
+
export const AGY_MODEL_SLOTS = ['main'];
|
|
56
59
|
export const CHAT_MODEL_SLOTS = ['default'];
|
|
57
60
|
export const VERTEX_MODEL_SLOTS = ['default'];
|
|
58
61
|
|
|
@@ -134,10 +137,13 @@ export const paths = {
|
|
|
134
137
|
envClaudeSh: path.join(STATE_DIR, 'env-claude.sh'),
|
|
135
138
|
envCodexCmd: path.join(STATE_DIR, 'env-codex.cmd'),
|
|
136
139
|
envCodexSh: path.join(STATE_DIR, 'env-codex.sh'),
|
|
140
|
+
envAgyCmd: path.join(STATE_DIR, 'env-agy.cmd'),
|
|
141
|
+
envAgySh: path.join(STATE_DIR, 'env-agy.sh'),
|
|
137
142
|
// One line per tool, written with that tool's env file: what the launcher shows the person at
|
|
138
143
|
// launch. Empty exactly when the tool is not routed, so the launcher needs no second condition.
|
|
139
144
|
routeClaude: path.join(STATE_DIR, 'route-claude.txt'),
|
|
140
145
|
routeCodex: path.join(STATE_DIR, 'route-codex.txt'),
|
|
146
|
+
routeAgy: path.join(STATE_DIR, 'route-agy.txt'),
|
|
141
147
|
// The gateway port of the last switch on. The shim scrubs a stale loopback base URL
|
|
142
148
|
// against it, so a shell opened before a port change still reaches the right gateway.
|
|
143
149
|
gatewayPort: path.join(STATE_DIR, 'gateway.port'),
|
|
@@ -447,7 +453,7 @@ function createCodexHalf(p) {
|
|
|
447
453
|
|
|
448
454
|
export function validateProfileInput(p) {
|
|
449
455
|
if (!p || typeof p !== 'object' || Array.isArray(p)) return 'Profile must be an object';
|
|
450
|
-
if (p.inFormat !== undefined) return 'inFormat is not supported; use "tool": "claude"
|
|
456
|
+
if (p.inFormat !== undefined) return 'inFormat is not supported; use "tool": "claude", "codex" or "agy"';
|
|
451
457
|
if (p.blindfold !== undefined) return 'blindfold per-profile setting is deprecated';
|
|
452
458
|
if (p.blindfoldPort !== undefined) return 'blindfoldPort per-profile setting is deprecated';
|
|
453
459
|
if (p.blindfoldHost !== undefined) return 'blindfoldHost per-profile setting is deprecated';
|
|
@@ -637,9 +643,15 @@ export function migrateConfigInMemory(rawConfig) {
|
|
|
637
643
|
}
|
|
638
644
|
|
|
639
645
|
// Step 3: Pointer migration (R7b, Item 2)
|
|
646
|
+
// A present pointer follows its profile through a Step 1 rename: a profile keyed "agy" became
|
|
647
|
+
// "agy-profile" when agy turned into a command word, and a pointer left on "agy" would dangle.
|
|
648
|
+
const presentPointer = (tool) => {
|
|
649
|
+
const v = rawConfig.activeProfiles[tool];
|
|
650
|
+
return v ? (renameMap.get(v) || v) : v;
|
|
651
|
+
};
|
|
640
652
|
const newActiveProfiles = {};
|
|
641
653
|
if (rawConfig.activeProfiles && Object.hasOwn(rawConfig.activeProfiles, 'claude')) {
|
|
642
|
-
newActiveProfiles.claude =
|
|
654
|
+
newActiveProfiles.claude = presentPointer('claude');
|
|
643
655
|
} else {
|
|
644
656
|
let oldClaudeTarget = null;
|
|
645
657
|
if (rawConfig.activeProfiles && Object.hasOwn(rawConfig.activeProfiles, 'anthropic')) {
|
|
@@ -664,7 +676,7 @@ export function migrateConfigInMemory(rawConfig) {
|
|
|
664
676
|
}
|
|
665
677
|
|
|
666
678
|
if (rawConfig.activeProfiles && Object.hasOwn(rawConfig.activeProfiles, 'codex')) {
|
|
667
|
-
newActiveProfiles.codex =
|
|
679
|
+
newActiveProfiles.codex = presentPointer('codex');
|
|
668
680
|
} else {
|
|
669
681
|
let oldCodexTarget = null;
|
|
670
682
|
if (rawConfig.activeProfiles && Object.hasOwn(rawConfig.activeProfiles, 'responses')) {
|
|
@@ -688,6 +700,11 @@ export function migrateConfigInMemory(rawConfig) {
|
|
|
688
700
|
}
|
|
689
701
|
}
|
|
690
702
|
|
|
703
|
+
// agy has no legacy key and no single-pointer fallback: only its own pointer, and only to a
|
|
704
|
+
// profile made for agy.
|
|
705
|
+
const agyKey = rawConfig.activeProfiles && Object.hasOwn(rawConfig.activeProfiles, 'agy') ? presentPointer('agy') : null;
|
|
706
|
+
newActiveProfiles.agy = agyKey && cfg.profiles[agyKey]?.tool === 'agy' ? agyKey : null;
|
|
707
|
+
|
|
691
708
|
cfg.activeProfiles = newActiveProfiles;
|
|
692
709
|
delete cfg.activeProfile;
|
|
693
710
|
|
|
@@ -993,6 +1010,8 @@ export function findProfileKey(cfg, name) {
|
|
|
993
1010
|
}
|
|
994
1011
|
|
|
995
1012
|
export function profileAcceptsTarget(profile, target) {
|
|
1013
|
+
// agy has no legacy spelling and no inFormat: only a profile made for it serves it.
|
|
1014
|
+
if (target === 'agy') return profile?.tool === 'agy';
|
|
996
1015
|
if (profile?.tool) {
|
|
997
1016
|
if (target === 'claude' || target === 'anthropic') return profile.tool === 'claude';
|
|
998
1017
|
if (target === 'codex' || target === 'responses') return profile.tool === 'codex';
|
|
@@ -1008,6 +1027,7 @@ export function profileAcceptsTarget(profile, target) {
|
|
|
1008
1027
|
export function modelSlotsForProfile(profile) {
|
|
1009
1028
|
if (profile?.tool === 'claude') return CLAUDE_MODEL_SLOTS;
|
|
1010
1029
|
if (profile?.tool === 'codex') return CODEX_MODEL_SLOTS;
|
|
1030
|
+
if (profile?.tool === 'agy') return AGY_MODEL_SLOTS;
|
|
1011
1031
|
return MODEL_SLOTS_BY_FORMAT[profile?.inFormat] || MODEL_SLOTS_BY_FORMAT.auto;
|
|
1012
1032
|
}
|
|
1013
1033
|
|
|
@@ -1185,7 +1205,8 @@ export function getActiveMap(cfg) {
|
|
|
1185
1205
|
: Object.hasOwn(ap, legacy) ? ap[legacy]
|
|
1186
1206
|
: cfg?.activeProfile ?? null
|
|
1187
1207
|
);
|
|
1188
|
-
|
|
1208
|
+
// agy is newer than the single legacy pointer, so only its own key can turn it on.
|
|
1209
|
+
return { claude: pointer('claude', 'anthropic'), codex: pointer('codex', 'responses'), agy: ap.agy ?? null };
|
|
1189
1210
|
}
|
|
1190
1211
|
|
|
1191
1212
|
/**
|
|
@@ -1325,7 +1346,11 @@ export function computeLaunchState(cfg, port) {
|
|
|
1325
1346
|
const pick = (t) => (hasProfile(cfg, map[t]) ? cfg.profiles[map[t]] : null);
|
|
1326
1347
|
const claude = pick('claude');
|
|
1327
1348
|
const codex = pick('codex');
|
|
1328
|
-
const
|
|
1349
|
+
const agy = pick('agy');
|
|
1350
|
+
// The interceptor serves Claude Code and Codex only. agy takes its endpoint from CLOUD_CODE_URL, so
|
|
1351
|
+
// an agy-only switch on writes the launch files and starts no interceptor.
|
|
1352
|
+
const intercepted = Boolean(claude || codex);
|
|
1353
|
+
const active = Boolean(intercepted || agy);
|
|
1329
1354
|
|
|
1330
1355
|
// One interceptor serves both tools (R3) and its port lives at the top level of config.json
|
|
1331
1356
|
// (R3b). The per-profile port went with the per-profile host and prefix.
|
|
@@ -1354,7 +1379,7 @@ export function computeLaunchState(cfg, port) {
|
|
|
1354
1379
|
// and the tool set. The tool set travels sorted as one string (Finding 6), which is also what
|
|
1355
1380
|
// lets a running interceptor be updated in place instead of restarted.
|
|
1356
1381
|
// null when no tool is active, which is how reconcileBlindfold knows to stop it.
|
|
1357
|
-
blindfold:
|
|
1382
|
+
blindfold: intercepted
|
|
1358
1383
|
? {
|
|
1359
1384
|
port: bfPort,
|
|
1360
1385
|
activeTools: deriveActiveTools(cfg).join(','),
|
|
@@ -1367,15 +1392,20 @@ export function computeLaunchState(cfg, port) {
|
|
|
1367
1392
|
// of the other tool, and a restart cannot leave a stale override behind.
|
|
1368
1393
|
envClaude: [],
|
|
1369
1394
|
envCodex: [],
|
|
1395
|
+
envAgy: [],
|
|
1370
1396
|
// The launcher notice, one line per tool. Empty for a tool that is not routed (A1).
|
|
1371
1397
|
routeClaude: claude ? routeLine('claude', map.claude, claude) : '',
|
|
1372
|
-
routeCodex: codex ? routeLine('codex', map.codex, codex) : ''
|
|
1398
|
+
routeCodex: codex ? routeLine('codex', map.codex, codex) : '',
|
|
1399
|
+
routeAgy: agy ? routeLine('agy', map.agy, agy) : ''
|
|
1373
1400
|
};
|
|
1374
1401
|
|
|
1375
1402
|
// NODE_EXTRA_CA_CERTS is deliberately absent from env-claude: the shim decides it at launch,
|
|
1376
1403
|
// because only then does it know whether the user brought a CA of their own (R2).
|
|
1377
1404
|
if (claude) state.envClaude = proxyPairs();
|
|
1378
1405
|
if (codex) state.envCodex = [...proxyPairs(), ['CODEX_CA_CERTIFICATE', paths.blindfoldCA]];
|
|
1406
|
+
// agy reaches the gateway directly over plain http: no proxy and no CA (Go on Windows reads only
|
|
1407
|
+
// the system store). Remote control keeps its own host, so it never crosses the switcher.
|
|
1408
|
+
if (agy) state.envAgy = [['CLOUD_CODE_URL', `http://127.0.0.1:${port}`]];
|
|
1379
1409
|
return state;
|
|
1380
1410
|
}
|
|
1381
1411
|
|
|
@@ -1470,13 +1500,17 @@ function writeToolFiles(pairs, shPath, cmdPath) {
|
|
|
1470
1500
|
// tools from activeProfiles, and a colliding config is one this build will not migrate. This empties
|
|
1471
1501
|
// only the file pair of the tool that was switched off, under the same lock, so the other tool's
|
|
1472
1502
|
// launcher files are never opened (A13: emptied, never deleted).
|
|
1503
|
+
const TOOL_FILES = {
|
|
1504
|
+
claude: () => [paths.envClaudeCmd, paths.envClaudeSh, paths.routeClaude],
|
|
1505
|
+
codex: () => [paths.envCodexCmd, paths.envCodexSh, paths.routeCodex],
|
|
1506
|
+
agy: () => [paths.envAgyCmd, paths.envAgySh, paths.routeAgy]
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1473
1509
|
export function emptyToolEnvFiles(tool) {
|
|
1474
1510
|
const codex = tool === 'codex';
|
|
1475
1511
|
const codexRouteChanged = withLaunchLock(() => {
|
|
1476
1512
|
const before = codex ? readOrEmpty(paths.envCodexSh) : '';
|
|
1477
|
-
|
|
1478
|
-
writeAtomic(codex ? paths.envCodexSh : paths.envClaudeSh, '');
|
|
1479
|
-
writeAtomic(codex ? paths.routeCodex : paths.routeClaude, '');
|
|
1513
|
+
for (const f of (TOOL_FILES[tool] || TOOL_FILES.claude)()) writeAtomic(f, '');
|
|
1480
1514
|
return before !== '';
|
|
1481
1515
|
});
|
|
1482
1516
|
return { codexRouteChanged, codexDaemon: codexRouteChanged ? restartCodexDaemon([]) : undefined };
|
|
@@ -1544,9 +1578,12 @@ function writeLaunchState(cfg, port) {
|
|
|
1544
1578
|
try {
|
|
1545
1579
|
writeToolFiles(st.envClaude, paths.envClaudeSh, paths.envClaudeCmd);
|
|
1546
1580
|
writeToolFiles(st.envCodex, paths.envCodexSh, paths.envCodexCmd);
|
|
1581
|
+
writeToolFiles(st.envAgy, paths.envAgySh, paths.envAgyCmd);
|
|
1547
1582
|
writeAtomic(paths.routeClaude, st.routeClaude ? `${st.routeClaude}
|
|
1548
1583
|
` : '');
|
|
1549
1584
|
writeAtomic(paths.routeCodex, st.routeCodex ? `${st.routeCodex}
|
|
1585
|
+
` : '');
|
|
1586
|
+
writeAtomic(paths.routeAgy, st.routeAgy ? `${st.routeAgy}
|
|
1550
1587
|
` : '');
|
|
1551
1588
|
st.codexRouteChanged = readOrEmpty(paths.envCodexSh) !== codexBefore;
|
|
1552
1589
|
// Recorded on switch on so a shell opened while the gateway ran can still recognize its own
|
|
@@ -1573,7 +1610,7 @@ export function clearLaunchState(port) {
|
|
|
1573
1610
|
writeAtomic(paths.envCmd, STUB_CMD);
|
|
1574
1611
|
writeAtomic(paths.envSh, STUB_SH);
|
|
1575
1612
|
for (const f of [paths.envClaudeCmd, paths.envClaudeSh, paths.envCodexCmd, paths.envCodexSh,
|
|
1576
|
-
paths.routeClaude, paths.routeCodex]) {
|
|
1613
|
+
paths.envAgyCmd, paths.envAgySh, paths.routeClaude, paths.routeCodex, paths.routeAgy]) {
|
|
1577
1614
|
writeAtomic(f, '');
|
|
1578
1615
|
}
|
|
1579
1616
|
return codexBefore !== '';
|
package/switch
CHANGED
|
File without changes
|
package/switch.cmd
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
node "%~dp0switch.mjs" %*
|
|
1
|
+
@echo off
|
|
2
|
+
node "%~dp0switch.mjs" %*
|