llm-switcher 1.1.11 → 1.2.2
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 +63 -0
- package/README.md +202 -257
- package/README.vi.md +200 -256
- package/blindfold/blindfold.mjs +200 -53
- package/blindfold/make-certs.sh +26 -7
- package/catalog.mjs +246 -0
- package/classifier.mjs +238 -0
- package/config.example.json +12 -34
- package/docs/codex-blindfold.md +28 -17
- package/docs/cross-platform.md +16 -7
- package/docs/diagrams/ir-healer-pipeline.mmd +16 -0
- package/docs/diagrams/ir-healer-pipeline.png +0 -0
- package/docs/diagrams/ir-healer-pipeline.svg +90 -0
- package/docs/diagrams/ir-translation-pipeline.html +14925 -0
- package/docs/diagrams/ir-translation-pipeline.sequence.json +31 -0
- package/docs/diagrams/ir-translation-pipeline.svg +5128 -0
- package/docs/diagrams/system-architecture.architecture.json +76 -0
- package/docs/diagrams/system-architecture.html +14978 -0
- package/docs/diagrams/system-architecture.svg +5147 -0
- package/docs/diagrams/system-topology.mmd +30 -0
- package/docs/diagrams/system-topology.png +0 -0
- package/docs/diagrams/system-topology.svg +125 -0
- package/ensure-ca-bundle.mjs +28 -0
- package/formats.mjs +43 -156
- package/icons/antigravity.png +0 -0
- package/icons/claude.png +0 -0
- package/icons/codex.png +0 -0
- package/icons/deepseek.png +0 -0
- package/icons/gemini.png +0 -0
- package/icons/github.png +0 -0
- package/icons/groq.png +0 -0
- package/icons/intact.svg +1 -0
- package/icons/ollama.png +0 -0
- package/icons/openai.png +0 -0
- package/icons/openrouter.png +0 -0
- package/icons/qwen.png +0 -0
- package/icons/vertex.png +0 -0
- package/mcp.mjs +39 -11
- package/package.json +1 -1
- package/proxy.mjs +114 -37
- package/shim.mjs +200 -57
- package/skills/llm-switcher/SKILL.md +15 -10
- package/state.mjs +1100 -191
- package/switch.cmd +2 -2
- package/switch.mjs +228 -53
- package/tests/blindfold-e2e.test.mjs +380 -0
- package/tests/blindfold-task5.test.mjs +429 -0
- package/tests/blindfold.task3.test.mjs +700 -0
- package/tests/blindfold.test.mjs +10 -5
- package/tests/catalog.test.mjs +147 -0
- package/tests/classifier.test.mjs +210 -0
- package/tests/contract-lab.test.mjs +22 -7
- package/tests/formats.test.mjs +63 -46
- package/tests/gateway.e2e.test.mjs +136 -36
- package/tests/lifecycle.test.mjs +16 -10
- package/tests/mcp.test.mjs +78 -2
- package/tests/real-user-sim.test.mjs +464 -0
- package/tests/shim.test.mjs +159 -66
- package/tests/state.test.mjs +975 -193
- package/tests/switch.test.mjs +446 -2
- package/ui.html +1710 -1726
package/docs/cross-platform.md
CHANGED
|
@@ -110,18 +110,27 @@ no `hosts` file change and no certificate in a system trust store. See
|
|
|
110
110
|
Build the certificates once. On Windows, run this line in Git Bash, not in PowerShell:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
bash blindfold/make-certs.sh
|
|
113
|
+
bash blindfold/make-certs.sh
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
The
|
|
116
|
+
The leaf names exactly three hosts, and the interceptor routes exactly those three:
|
|
117
117
|
|
|
118
|
-
|
|
|
118
|
+
| CONNECT host | Paths to this switcher's gateway | Everything else |
|
|
119
119
|
| --- | --- | --- |
|
|
120
|
-
|
|
|
121
|
-
|
|
|
120
|
+
| `api.anthropic.com` | `/v1/messages`, `/v1/messages/...` | to `api.anthropic.com`, unchanged |
|
|
121
|
+
| `api.openai.com` | `/v1/responses`, `/v1/responses/...`, `/v1/models`, `/v1/models/...` | to `api.openai.com`, unchanged |
|
|
122
|
+
| `chatgpt.com` | `/backend-api/codex/...`, forwarded as `/v1` | to `chatgpt.com`, unchanged |
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
A CONNECT host outside the table is tunneled untouched, with no TLS termination. A request
|
|
125
|
+
whose `Host` header names a different host than its CONNECT target gets
|
|
126
|
+
`421 Misdirected Request` and opens no upstream connection; the comparison ignores case and
|
|
127
|
+
a default `:443`.
|
|
128
|
+
|
|
129
|
+
There is no `blindfoldHost` and no `blindfoldPrefix` any more: this table is the routing, and
|
|
130
|
+
it is not a profile setting.
|
|
131
|
+
|
|
132
|
+
`switch` compares those three hosts against the subject alternative names of the leaf
|
|
133
|
+
certificate. If one is missing it refuses the activation, names the host, and prints
|
|
125
134
|
the command that rebuilds the certificate. No file is written on that path.
|
|
126
135
|
|
|
127
136
|
## Check that the platform work succeeded
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
sequenceDiagram
|
|
2
|
+
autonumber
|
|
3
|
+
actor CLI as Client Agent (Claude Code / Codex)
|
|
4
|
+
participant GW as LLM Switcher (:3456)
|
|
5
|
+
participant IR as Healer & IR Engine
|
|
6
|
+
participant UP as Upstream (intact / 9Router / Vertex)
|
|
7
|
+
|
|
8
|
+
CLI->>GW: POST /v1/messages or /v1/responses
|
|
9
|
+
GW->>IR: parseToIR(clientFormat, payload)
|
|
10
|
+
Note over IR: Repair orphan results, thinking budget & 1M context
|
|
11
|
+
IR-->>GW: emitUpstreamBody(outFormat, healedIR)
|
|
12
|
+
GW->>UP: POST upstream API (with AbortSignal)
|
|
13
|
+
UP-->>GW: Raw stream chunks (OpenAI / Vertex / Anthropic)
|
|
14
|
+
GW->>IR: normalizeUpstream(chunk) & parse reasoning
|
|
15
|
+
IR-->>GW: Native client event stream chunks
|
|
16
|
+
GW->>CLI: Synthesized genuine SSE (thinking_delta + text)
|
|
Binary file
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 949.355 455" width="949.355" height="455" style="--bg:#1a1b26;--fg:#a9b1d6;--line:#3d59a1;--accent:#7aa2f7;--muted:#565f89;background:var(--bg)">
|
|
2
|
+
<style>
|
|
3
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
|
|
4
|
+
text { font-family: 'Inter', system-ui, sans-serif; }
|
|
5
|
+
svg {
|
|
6
|
+
/* Derived from --bg and --fg (overridable via --line, --accent, etc.) */
|
|
7
|
+
--_text: var(--fg);
|
|
8
|
+
--_text-sec: var(--muted, color-mix(in srgb, var(--fg) 60%, var(--bg)));
|
|
9
|
+
--_text-muted: var(--muted, color-mix(in srgb, var(--fg) 40%, var(--bg)));
|
|
10
|
+
--_text-faint: color-mix(in srgb, var(--fg) 25%, var(--bg));
|
|
11
|
+
--_line: var(--line, color-mix(in srgb, var(--fg) 50%, var(--bg)));
|
|
12
|
+
--_arrow: var(--accent, color-mix(in srgb, var(--fg) 85%, var(--bg)));
|
|
13
|
+
--_node-fill: var(--surface, color-mix(in srgb, var(--fg) 3%, var(--bg)));
|
|
14
|
+
--_node-stroke: var(--border, color-mix(in srgb, var(--fg) 20%, var(--bg)));
|
|
15
|
+
--_group-fill: var(--bg);
|
|
16
|
+
--_group-hdr: color-mix(in srgb, var(--fg) 5%, var(--bg));
|
|
17
|
+
--_inner-stroke: color-mix(in srgb, var(--fg) 12%, var(--bg));
|
|
18
|
+
--_key-badge: color-mix(in srgb, var(--fg) 10%, var(--bg));
|
|
19
|
+
}
|
|
20
|
+
</style>
|
|
21
|
+
<defs>
|
|
22
|
+
<marker id="seq-arrow" markerWidth="8" markerHeight="5" refX="8" refY="2.5" orient="auto-start-reverse">
|
|
23
|
+
<polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" />
|
|
24
|
+
</marker>
|
|
25
|
+
<marker id="seq-arrow-open" markerWidth="8" markerHeight="5" refX="8" refY="2.5" orient="auto-start-reverse">
|
|
26
|
+
<polyline points="0 0, 8 2.5, 0 5" fill="none" stroke="var(--_arrow)" stroke-width="1" />
|
|
27
|
+
</marker>
|
|
28
|
+
</defs>
|
|
29
|
+
<line class="lifeline" data-actor="CLI" x1="147.01" y1="70" x2="147.01" y2="425" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
|
|
30
|
+
<line class="lifeline" data-actor="GW" x1="383.6095" y1="70" x2="383.6095" y2="425" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
|
|
31
|
+
<line class="lifeline" data-actor="IR" x1="573.8965000000001" y1="70" x2="573.8965000000001" y2="425" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
|
|
32
|
+
<line class="lifeline" data-actor="UP" x1="801.9745" y1="70" x2="801.9745" y2="425" stroke="var(--_line)" stroke-width="0.75" stroke-dasharray="6 4" />
|
|
33
|
+
<g class="message" data-from="CLI" data-to="GW" data-label="POST /v1/messages or /v1/responses" data-line-style="solid" data-arrow-head="filled" data-self="false">
|
|
34
|
+
<line x1="147.01" y1="90" x2="383.6095" y2="90" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
|
|
35
|
+
<text x="265.30975" y="80" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">POST /v1/messages or /v1/responses</text>
|
|
36
|
+
</g>
|
|
37
|
+
<g class="message" data-from="GW" data-to="IR" data-label="parseToIR(clientFormat, payload)" data-line-style="solid" data-arrow-head="filled" data-self="false">
|
|
38
|
+
<line x1="383.6095" y1="130" x2="573.8965000000001" y2="130" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
|
|
39
|
+
<text x="478.75300000000004" y="120" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">parseToIR(clientFormat, payload)</text>
|
|
40
|
+
</g>
|
|
41
|
+
<g class="message" data-from="IR" data-to="GW" data-label="emitUpstreamBody(outFormat, healedIR)" data-line-style="dashed" data-arrow-head="filled" data-self="false">
|
|
42
|
+
<line x1="573.8965000000001" y1="185" x2="383.6095" y2="185" stroke="var(--_line)" stroke-width="1" stroke-dasharray="6 4" marker-end="url(#seq-arrow)" />
|
|
43
|
+
<text x="478.75300000000004" y="175" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">emitUpstreamBody(outFormat, healedIR)</text>
|
|
44
|
+
</g>
|
|
45
|
+
<g class="message" data-from="GW" data-to="UP" data-label="POST upstream API (with AbortSignal)" data-line-style="solid" data-arrow-head="filled" data-self="false">
|
|
46
|
+
<line x1="383.6095" y1="225" x2="801.9745" y2="225" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
|
|
47
|
+
<text x="592.792" y="215" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">POST upstream API (with AbortSignal)</text>
|
|
48
|
+
</g>
|
|
49
|
+
<g class="message" data-from="UP" data-to="GW" data-label="Raw stream chunks (OpenAI / Vertex / Anthropic)" data-line-style="dashed" data-arrow-head="filled" data-self="false">
|
|
50
|
+
<line x1="801.9745" y1="265" x2="383.6095" y2="265" stroke="var(--_line)" stroke-width="1" stroke-dasharray="6 4" marker-end="url(#seq-arrow)" />
|
|
51
|
+
<text x="592.792" y="255" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Raw stream chunks (OpenAI / Vertex / Anthropic)</text>
|
|
52
|
+
</g>
|
|
53
|
+
<g class="message" data-from="GW" data-to="IR" data-label="normalizeUpstream(chunk) & parse reasoning" data-line-style="solid" data-arrow-head="filled" data-self="false">
|
|
54
|
+
<line x1="383.6095" y1="305" x2="573.8965000000001" y2="305" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
|
|
55
|
+
<text x="478.75300000000004" y="295" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">normalizeUpstream(chunk) & parse reasoning</text>
|
|
56
|
+
</g>
|
|
57
|
+
<g class="message" data-from="IR" data-to="GW" data-label="Native client event stream chunks" data-line-style="dashed" data-arrow-head="filled" data-self="false">
|
|
58
|
+
<line x1="573.8965000000001" y1="345" x2="383.6095" y2="345" stroke="var(--_line)" stroke-width="1" stroke-dasharray="6 4" marker-end="url(#seq-arrow)" />
|
|
59
|
+
<text x="478.75300000000004" y="335" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Native client event stream chunks</text>
|
|
60
|
+
</g>
|
|
61
|
+
<g class="message" data-from="GW" data-to="CLI" data-label="Synthesized genuine SSE (thinking_delta + text)" data-line-style="solid" data-arrow-head="filled" data-self="false">
|
|
62
|
+
<line x1="383.6095" y1="385" x2="147.01" y2="385" stroke="var(--_line)" stroke-width="1" marker-end="url(#seq-arrow)" />
|
|
63
|
+
<text x="265.30975" y="375" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Synthesized genuine SSE (thinking_delta + text)</text>
|
|
64
|
+
</g>
|
|
65
|
+
<g class="note" data-position="over" data-actors="IR">
|
|
66
|
+
<polygon points="443.4595000000001,138 698.3335,138 704.3335,144 704.3335,161 443.4595000000001,161" fill="var(--bg)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
67
|
+
<polygon points="698.3335,138 704.3335,144 698.3335,144" fill="var(--_inner-stroke)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
68
|
+
<text x="573.8965000000001" y="149.5" font-size="11" text-anchor="middle" font-weight="400" fill="var(--_text-muted)" dy="3.8499999999999996">Repair orphan results, thinking budget & 1M context</text>
|
|
69
|
+
</g>
|
|
70
|
+
<g class="actor" data-id="CLI" data-label="Client Agent (Claude Code / Codex)" data-type="actor">
|
|
71
|
+
<g transform="translate(129.01,32) scale(1.5)">
|
|
72
|
+
<path d="M21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3C16.9706 3 21 7.02944 21 12Z" fill="none" stroke="var(--_line)" stroke-width="0.6666666666666666" />
|
|
73
|
+
<path d="M15 10C15 11.6569 13.6569 13 12 13C10.3431 13 9 11.6569 9 10C9 8.34315 10.3431 7 12 7C13.6569 7 15 8.34315 15 10Z" fill="none" stroke="var(--_line)" stroke-width="0.6666666666666666" />
|
|
74
|
+
<path d="M5.62842 18.3563C7.08963 17.0398 9.39997 16 12 16C14.6 16 16.9104 17.0398 18.3716 18.3563" fill="none" stroke="var(--_line)" stroke-width="0.6666666666666666" />
|
|
75
|
+
</g>
|
|
76
|
+
<text x="147.01" y="84" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">Client Agent (Claude Code / Codex)</text>
|
|
77
|
+
</g>
|
|
78
|
+
<g class="actor" data-id="GW" data-label="LLM Switcher (:3456)" data-type="participant">
|
|
79
|
+
<rect x="304.02" y="30" width="159.17900000000003" height="40" rx="4" ry="4" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
80
|
+
<text x="383.6095" y="50" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">LLM Switcher (:3456)</text>
|
|
81
|
+
</g>
|
|
82
|
+
<g class="actor" data-id="IR" data-label="Healer & IR Engine" data-type="participant">
|
|
83
|
+
<rect x="503.19900000000007" y="30" width="141.39499999999998" height="40" rx="4" ry="4" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
84
|
+
<text x="573.8965000000001" y="50" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">Healer & IR Engine</text>
|
|
85
|
+
</g>
|
|
86
|
+
<g class="actor" data-id="UP" data-label="Upstream (intact / 9Router / Vertex)" data-type="participant">
|
|
87
|
+
<rect x="684.594" y="30" width="234.76099999999997" height="40" rx="4" ry="4" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
88
|
+
<text x="801.9745" y="50" font-size="13" text-anchor="middle" font-weight="500" fill="var(--_text)" dy="4.55">Upstream (intact / 9Router / Vertex)</text>
|
|
89
|
+
</g>
|
|
90
|
+
</svg>
|