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.
Files changed (61) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +202 -257
  3. package/README.vi.md +200 -256
  4. package/blindfold/blindfold.mjs +200 -53
  5. package/blindfold/make-certs.sh +26 -7
  6. package/catalog.mjs +246 -0
  7. package/classifier.mjs +238 -0
  8. package/config.example.json +12 -34
  9. package/docs/codex-blindfold.md +28 -17
  10. package/docs/cross-platform.md +16 -7
  11. package/docs/diagrams/ir-healer-pipeline.mmd +16 -0
  12. package/docs/diagrams/ir-healer-pipeline.png +0 -0
  13. package/docs/diagrams/ir-healer-pipeline.svg +90 -0
  14. package/docs/diagrams/ir-translation-pipeline.html +14925 -0
  15. package/docs/diagrams/ir-translation-pipeline.sequence.json +31 -0
  16. package/docs/diagrams/ir-translation-pipeline.svg +5128 -0
  17. package/docs/diagrams/system-architecture.architecture.json +76 -0
  18. package/docs/diagrams/system-architecture.html +14978 -0
  19. package/docs/diagrams/system-architecture.svg +5147 -0
  20. package/docs/diagrams/system-topology.mmd +30 -0
  21. package/docs/diagrams/system-topology.png +0 -0
  22. package/docs/diagrams/system-topology.svg +125 -0
  23. package/ensure-ca-bundle.mjs +28 -0
  24. package/formats.mjs +43 -156
  25. package/icons/antigravity.png +0 -0
  26. package/icons/claude.png +0 -0
  27. package/icons/codex.png +0 -0
  28. package/icons/deepseek.png +0 -0
  29. package/icons/gemini.png +0 -0
  30. package/icons/github.png +0 -0
  31. package/icons/groq.png +0 -0
  32. package/icons/intact.svg +1 -0
  33. package/icons/ollama.png +0 -0
  34. package/icons/openai.png +0 -0
  35. package/icons/openrouter.png +0 -0
  36. package/icons/qwen.png +0 -0
  37. package/icons/vertex.png +0 -0
  38. package/mcp.mjs +39 -11
  39. package/package.json +1 -1
  40. package/proxy.mjs +114 -37
  41. package/shim.mjs +200 -57
  42. package/skills/llm-switcher/SKILL.md +15 -10
  43. package/state.mjs +1100 -191
  44. package/switch.cmd +2 -2
  45. package/switch.mjs +228 -53
  46. package/tests/blindfold-e2e.test.mjs +380 -0
  47. package/tests/blindfold-task5.test.mjs +429 -0
  48. package/tests/blindfold.task3.test.mjs +700 -0
  49. package/tests/blindfold.test.mjs +10 -5
  50. package/tests/catalog.test.mjs +147 -0
  51. package/tests/classifier.test.mjs +210 -0
  52. package/tests/contract-lab.test.mjs +22 -7
  53. package/tests/formats.test.mjs +63 -46
  54. package/tests/gateway.e2e.test.mjs +136 -36
  55. package/tests/lifecycle.test.mjs +16 -10
  56. package/tests/mcp.test.mjs +78 -2
  57. package/tests/real-user-sim.test.mjs +464 -0
  58. package/tests/shim.test.mjs +159 -66
  59. package/tests/state.test.mjs +975 -193
  60. package/tests/switch.test.mjs +446 -2
  61. package/ui.html +1710 -1726
@@ -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 chatgpt.com
113
+ bash blindfold/make-certs.sh
114
114
  ```
115
115
 
116
- The subject of the certificate must match the host your Codex account calls:
116
+ The leaf names exactly three hosts, and the interceptor routes exactly those three:
117
117
 
118
- | Codex sign-in | Host | Profile keys |
118
+ | CONNECT host | Paths to this switcher's gateway | Everything else |
119
119
  | --- | --- | --- |
120
- | ChatGPT account | `chatgpt.com` | The defaults. Add nothing. |
121
- | API key | `api.openai.com` | `"blindfoldHost": "api.openai.com"`, `"blindfoldPrefix": "/v1"` |
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
- `switch` compares the host in the profile against the subject alternative names of the
124
- leaf certificate. If they differ it refuses the activation, names the host, and prints
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)
@@ -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&amp;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) &amp; 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) &amp; 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 &amp; 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 &amp; 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 &amp; 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>