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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
flowchart LR
|
|
2
|
+
subgraph Clients[" Dev Clients & Coding CLIs "]
|
|
3
|
+
CC["Claude Code CLI\n(/v1/messages)"]
|
|
4
|
+
CDX["OpenAI Codex CLI\n(/v1/responses)"]
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
subgraph Optimizers[" Optional Middle-Layer "]
|
|
8
|
+
OPT["Token Optimizers\n(Headroom / RTK)"]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
subgraph Gateway[" LLM Switcher Edge Gateway (:3456) "]
|
|
12
|
+
ROUTER["Edge Router\n(Zero-Mutation)"]
|
|
13
|
+
HEALER["Healer Engine\n(Auto-Fix Schemas)"]
|
|
14
|
+
IR["Bi-Directional IR\n(Event Stream Synth)"]
|
|
15
|
+
ROUTER --> HEALER --> IR
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
subgraph Upstreams[" Upstream Providers "]
|
|
19
|
+
INTACT["intact Gateway\n(Recommended Pooler)"]
|
|
20
|
+
OTHER["9Router / Vertex / Other"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
CC -->|direct /v1/messages| ROUTER
|
|
24
|
+
CDX -->|direct /v1/responses| ROUTER
|
|
25
|
+
CC -.->|prune prompt| OPT
|
|
26
|
+
CDX -.->|prune prompt| OPT
|
|
27
|
+
OPT -->|forward :3456| ROUTER
|
|
28
|
+
|
|
29
|
+
IR -->|contract & pool| INTACT
|
|
30
|
+
IR -->|standard call| OTHER
|
|
Binary file
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1712.757 298.51666666666665" width="1712.757" height="298.51666666666665" 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="arrowhead" markerWidth="8" markerHeight="5" refX="7" refY="2.5" orient="auto">
|
|
23
|
+
<polygon points="0 0, 8 2.5, 0 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
|
|
24
|
+
</marker>
|
|
25
|
+
<marker id="arrowhead-start" markerWidth="8" markerHeight="5" refX="1" refY="2.5" orient="auto-start-reverse">
|
|
26
|
+
<polygon points="8 0, 0 2.5, 8 5" fill="var(--_arrow)" stroke="var(--_arrow)" stroke-width="0.75" stroke-linejoin="round" />
|
|
27
|
+
</marker>
|
|
28
|
+
</defs>
|
|
29
|
+
<g class="subgraph" data-id="Clients" data-label=" Dev Clients & Coding CLIs ">
|
|
30
|
+
<rect x="40" y="40" width="180.654" height="191.60000000000002" rx="0" ry="0" fill="var(--_group-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
31
|
+
<rect x="40" y="40" width="180.654" height="28" rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
32
|
+
<text x="52" y="54" font-size="12" font-weight="600" fill="var(--_text-sec)" dy="4.199999999999999"> Dev Clients & Coding CLIs </text>
|
|
33
|
+
</g>
|
|
34
|
+
<g class="subgraph" data-id="Optimizers" data-label=" Optional Middle-Layer ">
|
|
35
|
+
<rect x="384.674" y="90.56666666666666" width="176.949" height="113.80000000000001" rx="0" ry="0" fill="var(--_group-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
36
|
+
<rect x="384.674" y="90.56666666666666" width="176.949" height="28" rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
37
|
+
<text x="396.674" y="104.56666666666666" font-size="12" font-weight="600" fill="var(--_text-sec)" dy="4.199999999999999"> Optional Middle-Layer </text>
|
|
38
|
+
</g>
|
|
39
|
+
<g class="subgraph" data-id="Gateway" data-label=" LLM Switcher Edge Gateway (:3456) ">
|
|
40
|
+
<rect x="728.0190000000001" y="92.55000000000001" width="563.89" height="113.80000000000001" rx="0" ry="0" fill="var(--_group-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
41
|
+
<rect x="728.0190000000001" y="92.55000000000001" width="563.89" height="28" rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
42
|
+
<text x="740.0190000000001" y="106.55000000000001" font-size="12" font-weight="600" fill="var(--_text-sec)" dy="4.199999999999999"> LLM Switcher Edge Gateway (:3456) </text>
|
|
43
|
+
</g>
|
|
44
|
+
<g class="subgraph" data-id="Upstreams" data-label=" Upstream Providers ">
|
|
45
|
+
<rect x="1462.4630000000002" y="48.45" width="210.29399999999998" height="174.70000000000002" rx="0" ry="0" fill="var(--_group-fill)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
46
|
+
<rect x="1462.4630000000002" y="48.45" width="210.29399999999998" height="28" rx="0" ry="0" fill="var(--_group-hdr)" stroke="var(--_node-stroke)" stroke-width="1" />
|
|
47
|
+
<text x="1474.4630000000002" y="62.45" font-size="12" font-weight="600" fill="var(--_text-sec)" dy="4.199999999999999"> Upstream Providers </text>
|
|
48
|
+
</g>
|
|
49
|
+
<polyline class="edge" data-from="CC" data-to="ROUTER" data-style="solid" data-arrow-start="false" data-arrow-end="true" data-label="direct /v1/messages" points="200.208,101.93333333333334 366.674,101.93333333333334 366.674,56.26666666666667 710.0190000000001,56.26666666666667 710.0190000000001,150 744.0190000000001,150" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
50
|
+
<polyline class="edge" data-from="CDX" data-to="ROUTER" data-style="solid" data-arrow-start="false" data-arrow-end="true" data-label="direct /v1/responses" points="209.1,197.66666666666669 366.674,197.66666666666669 366.674,239.36666666666667 710.0190000000001,239.36666666666667 710.0190000000001,176.9 744.0190000000001,176.9" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
51
|
+
<polyline class="edge" data-from="CC" data-to="OPT" data-style="dotted" data-arrow-start="false" data-arrow-end="true" data-label="prune prompt" points="200.208,119.86666666666667 254.654,119.86666666666667 254.654,144.81666666666666 366.674,144.81666666666666 366.674,152.5 400.674,152.5" fill="none" stroke="var(--_line)" stroke-width="1" stroke-dasharray="4 4" marker-end="url(#arrowhead)" />
|
|
52
|
+
<polyline class="edge" data-from="CDX" data-to="OPT" data-style="dotted" data-arrow-start="false" data-arrow-end="true" data-label="prune prompt" points="209.1,179.73333333333335 254.654,179.73333333333335 254.654,178.11666666666667 366.674,178.11666666666667 366.674,170.43333333333334 400.674,170.43333333333334" fill="none" stroke="var(--_line)" stroke-width="1" stroke-dasharray="4 4" marker-end="url(#arrowhead)" />
|
|
53
|
+
<polyline class="edge" data-from="OPT" data-to="ROUTER" data-style="solid" data-arrow-start="false" data-arrow-end="true" data-label="forward :3456" points="545.623,161.46666666666667 710.019,161.4666666666667 710.019,163.45000000000002 744.0190000000001,163.45000000000002" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
54
|
+
<polyline class="edge" data-from="IR" data-to="INTACT" data-style="solid" data-arrow-start="false" data-arrow-end="true" data-label="contract & pool" points="1275.909,172.41666666666669 1325.909,172.41666666666669 1325.909,180.10000000000002 1444.4630000000002,180.10000000000002 1444.4630000000002,180.25 1478.4630000000002,180.25000000000003" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
55
|
+
<polyline class="edge" data-from="IR" data-to="OTHER" data-style="solid" data-arrow-start="false" data-arrow-end="true" data-label="standard call" points="1275.909,154.48333333333335 1333.631,154.48333333333335 1333.631,128.925 1444.4630000000002,128.925 1444.4630000000002,110.9 1478.4630000000002,110.90000000000002" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
56
|
+
<polyline class="edge" data-from="ROUTER" data-to="HEALER" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="876.3710000000001,163.45000000000002 916.3710000000001,163.45000000000002" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
57
|
+
<polyline class="edge" data-from="HEALER" data-to="IR" data-style="solid" data-arrow-start="false" data-arrow-end="true" points="1072.4350000000002,163.45000000000002 1112.4350000000002,163.45000000000002" fill="none" stroke="var(--_line)" stroke-width="1" marker-end="url(#arrowhead)" />
|
|
58
|
+
<g class="edge-label" data-from="CC" data-to="ROUTER" data-label="direct /v1/messages">
|
|
59
|
+
<rect x="592.2990000000001" y="40.26666666666667" width="109.72000000000001" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
60
|
+
<text x="647.1590000000001" y="55.41666666666667" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">direct /v1/messages</text>
|
|
61
|
+
</g>
|
|
62
|
+
<g class="edge-label" data-from="CDX" data-to="ROUTER" data-label="direct /v1/responses">
|
|
63
|
+
<rect x="588.7350000000001" y="223.36666666666667" width="113.28400000000002" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
64
|
+
<text x="645.3770000000002" y="238.51666666666668" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">direct /v1/responses</text>
|
|
65
|
+
</g>
|
|
66
|
+
<g class="edge-label" data-from="CC" data-to="OPT" data-label="prune prompt">
|
|
67
|
+
<rect x="262.654" y="128.81666666666666" width="80.02000000000001" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
68
|
+
<text x="302.664" y="143.96666666666667" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">prune prompt</text>
|
|
69
|
+
</g>
|
|
70
|
+
<g class="edge-label" data-from="CDX" data-to="OPT" data-label="prune prompt">
|
|
71
|
+
<rect x="262.654" y="162.11666666666667" width="80.02000000000001" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
72
|
+
<text x="302.664" y="177.26666666666668" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">prune prompt</text>
|
|
73
|
+
</g>
|
|
74
|
+
<g class="edge-label" data-from="OPT" data-to="ROUTER" data-label="forward :3456">
|
|
75
|
+
<rect x="603.623" y="145.46666666666667" width="82.39600000000002" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
76
|
+
<text x="644.821" y="160.61666666666667" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">forward :3456</text>
|
|
77
|
+
</g>
|
|
78
|
+
<g class="edge-label" data-from="IR" data-to="INTACT" data-label="contract & pool">
|
|
79
|
+
<rect x="1333.909" y="164.10000000000002" width="86.55400000000002" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
80
|
+
<text x="1377.1860000000001" y="179.25000000000003" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">contract & pool</text>
|
|
81
|
+
</g>
|
|
82
|
+
<g class="edge-label" data-from="IR" data-to="OTHER" data-label="standard call">
|
|
83
|
+
<rect x="1341.631" y="112.92500000000001" width="78.83200000000001" height="30.3" rx="2" ry="2" fill="var(--bg)" stroke="var(--_inner-stroke)" stroke-width="1" />
|
|
84
|
+
<text x="1381.047" y="128.07500000000002" text-anchor="middle" font-size="11" font-weight="400" fill="var(--_text-sec)" dy="3.8499999999999996">standard call</text>
|
|
85
|
+
</g>
|
|
86
|
+
<g class="node" data-id="CC" data-label="Claude Code CLI
|
|
87
|
+
(/v1/messages)" data-shape="rectangle">
|
|
88
|
+
<rect x="60.446" y="84" width="139.762" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
89
|
+
<text x="130.327" y="110.9" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="130.327" dy="-3.9000000000000012">Claude Code CLI</tspan><tspan x="130.327" dy="16.900000000000002">(/v1/messages)</tspan></text>
|
|
90
|
+
</g>
|
|
91
|
+
<g class="node" data-id="CDX" data-label="OpenAI Codex CLI
|
|
92
|
+
(/v1/responses)" data-shape="rectangle">
|
|
93
|
+
<rect x="60.446" y="161.8" width="148.654" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
94
|
+
<text x="134.773" y="188.70000000000002" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="134.773" dy="-3.9000000000000012">OpenAI Codex CLI</tspan><tspan x="134.773" dy="16.900000000000002">(/v1/responses)</tspan></text>
|
|
95
|
+
</g>
|
|
96
|
+
<g class="node" data-id="OPT" data-label="Token Optimizers
|
|
97
|
+
(Headroom / RTK)" data-shape="rectangle">
|
|
98
|
+
<rect x="400.674" y="134.56666666666666" width="144.949" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
99
|
+
<text x="473.1485" y="161.46666666666667" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="473.1485" dy="-3.9000000000000012">Token Optimizers</tspan><tspan x="473.1485" dy="16.900000000000002">(Headroom / RTK)</tspan></text>
|
|
100
|
+
</g>
|
|
101
|
+
<g class="node" data-id="ROUTER" data-label="Edge Router
|
|
102
|
+
(Zero-Mutation)" data-shape="rectangle">
|
|
103
|
+
<rect x="744.0190000000001" y="136.55" width="132.352" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
104
|
+
<text x="810.1950000000002" y="163.45000000000002" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="810.1950000000002" dy="-3.9000000000000012">Edge Router</tspan><tspan x="810.1950000000002" dy="16.900000000000002">(Zero-Mutation)</tspan></text>
|
|
105
|
+
</g>
|
|
106
|
+
<g class="node" data-id="HEALER" data-label="Healer Engine
|
|
107
|
+
(Auto-Fix Schemas)" data-shape="rectangle">
|
|
108
|
+
<rect x="916.3710000000001" y="136.55" width="156.064" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
109
|
+
<text x="994.4030000000001" y="163.45000000000002" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="994.4030000000001" dy="-3.9000000000000012">Healer Engine</tspan><tspan x="994.4030000000001" dy="16.900000000000002">(Auto-Fix Schemas)</tspan></text>
|
|
110
|
+
</g>
|
|
111
|
+
<g class="node" data-id="IR" data-label="Bi-Directional IR
|
|
112
|
+
(Event Stream Synth)" data-shape="rectangle">
|
|
113
|
+
<rect x="1112.4350000000002" y="136.55" width="163.474" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
114
|
+
<text x="1194.1720000000003" y="163.45000000000002" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="1194.1720000000003" dy="-3.9000000000000012">Bi-Directional IR</tspan><tspan x="1194.1720000000003" dy="16.900000000000002">(Event Stream Synth)</tspan></text>
|
|
115
|
+
</g>
|
|
116
|
+
<g class="node" data-id="INTACT" data-label="intact Gateway
|
|
117
|
+
(Recommended Pooler)" data-shape="rectangle">
|
|
118
|
+
<rect x="1478.4630000000002" y="153.35000000000002" width="177.55299999999997" height="53.800000000000004" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
119
|
+
<text x="1567.2395000000001" y="180.25000000000003" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)"><tspan x="1567.2395000000001" dy="-3.9000000000000012">intact Gateway</tspan><tspan x="1567.2395000000001" dy="16.900000000000002">(Recommended Pooler)</tspan></text>
|
|
120
|
+
</g>
|
|
121
|
+
<g class="node" data-id="OTHER" data-label="9Router / Vertex / Other" data-shape="rectangle">
|
|
122
|
+
<rect x="1478.4630000000002" y="92.45" width="178.29399999999998" height="36.900000000000006" rx="0" ry="0" fill="var(--_node-fill)" stroke="var(--_node-stroke)" stroke-width="0.75" />
|
|
123
|
+
<text x="1567.6100000000001" y="110.9" text-anchor="middle" font-size="13" font-weight="500" fill="var(--_text)" dy="4.55">9Router / Vertex / Other</text>
|
|
124
|
+
</g>
|
|
125
|
+
</svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// ensure-ca-bundle.mjs — the shim's one call into Node (R2, R4)
|
|
3
|
+
//
|
|
4
|
+
// The shim must point NODE_EXTRA_CA_CERTS at a bundle holding BOTH the user's own CA and the
|
|
5
|
+
// switcher's ca.pem, so trusting the interceptor never costs the user a certificate they
|
|
6
|
+
// already had. Building that bundle means reading files, comparing content byte for byte and
|
|
7
|
+
// renaming atomically — none of which belongs in a shell script, and none at all in cmd.exe.
|
|
8
|
+
//
|
|
9
|
+
// It is a FILE rather than `node -e "..."` on purpose: cmd.exe re-parses quotes and reads %2
|
|
10
|
+
// inside a percent-encoded URL, so a space anywhere in the checkout path would corrupt the
|
|
11
|
+
// program or its arguments.
|
|
12
|
+
//
|
|
13
|
+
// argv: <inherited NODE_EXTRA_CA_CERTS> <switcher ca.pem> <state dir>
|
|
14
|
+
// stdout: the bundle path, and nothing else — the shim compares it against -f before exporting.
|
|
15
|
+
// exit 1 on any failure: the reason is already on stderr, and the shim then keeps the inherited
|
|
16
|
+
// value and exports no path to a file that does not exist.
|
|
17
|
+
// ============================================================
|
|
18
|
+
|
|
19
|
+
import { ensureClaudeCaBundle } from './state.mjs';
|
|
20
|
+
|
|
21
|
+
const [, , inheritedCa, switcherCaPath, stateDir] = process.argv;
|
|
22
|
+
const result = ensureClaudeCaBundle(inheritedCa, switcherCaPath, stateDir);
|
|
23
|
+
|
|
24
|
+
if (result && result.ok && result.path) {
|
|
25
|
+
process.stdout.write(result.path);
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
process.exit(1);
|
package/formats.mjs
CHANGED
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
// params: { maxTokens, temperature, topP, topK, stop[],
|
|
20
20
|
// presencePenalty?, frequencyPenalty? },
|
|
21
21
|
// thinking: { type:'enabled'|'disabled'|'adaptive', budget?, effort? } | null,
|
|
22
|
+
// responseFormat: { type:'json_object' } | { type:'json_schema', name?, schema, strict? } | null,
|
|
22
23
|
// stream: bool
|
|
23
24
|
// }
|
|
24
25
|
// ============================================================
|
|
25
26
|
|
|
26
|
-
export const IN_FORMATS = ['anthropic', 'openai-chat', 'responses', 'vertex'];
|
|
27
27
|
export const OUT_FORMATS = ['openai-chat', 'anthropic', 'vertex'];
|
|
28
28
|
|
|
29
29
|
// ---------------- stop reasons ----------------
|
|
@@ -272,7 +272,10 @@ function sanitizeSchemaNode(schema) {
|
|
|
272
272
|
}
|
|
273
273
|
clean[k] = sanitizeSchemaNode(v);
|
|
274
274
|
}
|
|
275
|
-
|
|
275
|
+
// An empty schema carries no shape, and Vertex reads that as a missing `type`.
|
|
276
|
+
// Heal it the way a missing schema is healed — but only when nothing else is here,
|
|
277
|
+
// so an `anyOf`/`$ref` shape is never handed a type it did not ask for.
|
|
278
|
+
if (!clean.type && (clean.properties || Object.keys(clean).length === 0)) {
|
|
276
279
|
clean.type = 'object';
|
|
277
280
|
}
|
|
278
281
|
if (clean.type === 'object' && !clean.properties) {
|
|
@@ -325,10 +328,23 @@ function baseIR() {
|
|
|
325
328
|
return {
|
|
326
329
|
model: '', system: '', messages: [], tools: [], toolChoice: null,
|
|
327
330
|
params: { maxTokens: null, temperature: null, topP: null, topK: null, stop: [] },
|
|
328
|
-
thinking: null, stream: false
|
|
331
|
+
thinking: null, responseFormat: null, stream: false
|
|
329
332
|
};
|
|
330
333
|
}
|
|
331
334
|
|
|
335
|
+
// Structured output (Anthropic output_config.format, Responses text.format) -> IR responseFormat.
|
|
336
|
+
// `text` asks for nothing.
|
|
337
|
+
function responseFormatIR(f) {
|
|
338
|
+
if (!f || typeof f !== 'object') return null;
|
|
339
|
+
if (f.type === 'json_object') return { type: 'json_object' };
|
|
340
|
+
if (f.type !== 'json_schema') return null;
|
|
341
|
+
if (!f.schema || typeof f.schema !== 'object') return { type: 'json_object' };
|
|
342
|
+
const out = { type: 'json_schema', schema: f.schema };
|
|
343
|
+
if (typeof f.name === 'string' && f.name) out.name = f.name;
|
|
344
|
+
if (typeof f.strict === 'boolean') out.strict = f.strict;
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
|
|
332
348
|
// Anthropic Messages API -> IR (logic ported from the old transformAnthropicToOpenAI).
|
|
333
349
|
function anthropicToIR(payload) {
|
|
334
350
|
const ir = baseIR();
|
|
@@ -421,6 +437,7 @@ function anthropicToIR(payload) {
|
|
|
421
437
|
if (Array.isArray(payload.stop_sequences)) ir.params.stop = stopList(payload.stop_sequences);
|
|
422
438
|
|
|
423
439
|
ir.thinking = thinkingFromAnthropicParam(payload.thinking, payload.output_config?.effort);
|
|
440
|
+
ir.responseFormat = responseFormatIR(payload.output_config?.format || payload.output_format);
|
|
424
441
|
return ir;
|
|
425
442
|
}
|
|
426
443
|
|
|
@@ -455,73 +472,6 @@ function applyAllowedTools(ir, mode, list, nameOf) {
|
|
|
455
472
|
ir.toolChoice = ir.tools.length ? (mode === 'required' ? 'required' : 'auto') : null;
|
|
456
473
|
}
|
|
457
474
|
|
|
458
|
-
function chatToIR(payload) {
|
|
459
|
-
const ir = baseIR();
|
|
460
|
-
ir.model = payload.model || '';
|
|
461
|
-
ir.stream = payload.stream === true;
|
|
462
|
-
|
|
463
|
-
if (Array.isArray(payload.messages)) {
|
|
464
|
-
for (const m of payload.messages) {
|
|
465
|
-
const role = m.role;
|
|
466
|
-
if (role === 'system' || role === 'developer') {
|
|
467
|
-
const t = typeof m.content === 'string' ? m.content : asText(m.content);
|
|
468
|
-
if (t) ir.system = ir.system ? `${ir.system}\n\n${t}` : t;
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
if (role === 'tool' || role === 'function') {
|
|
472
|
-
ir.messages.push({
|
|
473
|
-
role: 'tool', toolCallId: m.tool_call_id || m.id,
|
|
474
|
-
name: m.name, content: typeof m.content === 'string' ? m.content : asText(m.content)
|
|
475
|
-
});
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
478
|
-
const out = { role: role === 'assistant' ? 'assistant' : 'user' };
|
|
479
|
-
if (typeof m.content === 'string' || Array.isArray(m.content)) out.content = m.content;
|
|
480
|
-
else if (m.content != null) out.content = asText(m.content);
|
|
481
|
-
if (Array.isArray(m.tool_calls)) {
|
|
482
|
-
out.toolCalls = m.tool_calls.map((tc, i) => ({
|
|
483
|
-
id: tc.id || null,
|
|
484
|
-
name: tc.function?.name || tc.name || '',
|
|
485
|
-
args: parseArgs(tc.function?.arguments ?? tc.function?.args ?? tc.args)
|
|
486
|
-
})).filter(t => t.name);
|
|
487
|
-
}
|
|
488
|
-
if (out.content !== undefined || out.toolCalls) ir.messages.push(out);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const toolDefs = payload.tools;
|
|
493
|
-
if (Array.isArray(toolDefs) && toolDefs.length) {
|
|
494
|
-
ir.tools = toolDefs.map(t => {
|
|
495
|
-
const fn = t.function || t;
|
|
496
|
-
return { name: fn.name, description: fn.description || '', parameters: sanitizeJsonSchema(fn.parameters || fn.input_schema || {}) };
|
|
497
|
-
}).filter(t => t.name);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const tc = payload.tool_choice;
|
|
501
|
-
if (typeof tc === 'string') ir.toolChoice = tc;
|
|
502
|
-
else if (tc?.type === 'function' && tc.function?.name) ir.toolChoice = { name: tc.function.name };
|
|
503
|
-
else if (tc?.type === 'allowed_tools') applyAllowedTools(ir, tc.allowed_tools?.mode, tc.allowed_tools?.tools, t => t?.function?.name);
|
|
504
|
-
|
|
505
|
-
ir.params.maxTokens = payload.max_tokens ?? payload.max_completion_tokens ?? null;
|
|
506
|
-
if (typeof payload.temperature === 'number') ir.params.temperature = payload.temperature;
|
|
507
|
-
if (typeof payload.top_p === 'number') ir.params.topP = payload.top_p;
|
|
508
|
-
if (typeof payload.presence_penalty === 'number') ir.params.presencePenalty = payload.presence_penalty;
|
|
509
|
-
if (typeof payload.frequency_penalty === 'number') ir.params.frequencyPenalty = payload.frequency_penalty;
|
|
510
|
-
if (payload.stop !== undefined && payload.stop !== null) ir.params.stop = stopList(payload.stop);
|
|
511
|
-
if (payload.parallel_tool_calls === false) ir.params.parallelToolCalls = false;
|
|
512
|
-
|
|
513
|
-
if (payload.thinking && typeof payload.thinking === 'object') {
|
|
514
|
-
ir.thinking = thinkingFromAnthropicParam(payload.thinking, payload.reasoning_effort);
|
|
515
|
-
} else if (typeof payload.reasoning_effort === 'string') {
|
|
516
|
-
ir.thinking = isNoReasoningEffort(payload.reasoning_effort)
|
|
517
|
-
? { type: 'disabled' }
|
|
518
|
-
: { type: 'enabled', budget: effortToBudget(payload.reasoning_effort), effort: payload.reasoning_effort };
|
|
519
|
-
} else if (payload.reasoning && typeof payload.reasoning === 'object') {
|
|
520
|
-
ir.thinking = thinkingFromReasoningParam(payload.reasoning);
|
|
521
|
-
}
|
|
522
|
-
return ir;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
475
|
// OpenAI/OpenRouter `reasoning` object -> IR thinking.
|
|
526
476
|
function thinkingFromReasoningParam(r) {
|
|
527
477
|
if (!r || typeof r !== 'object') return null;
|
|
@@ -709,98 +659,20 @@ function responsesToIR(payload) {
|
|
|
709
659
|
if (payload.reasoning && typeof payload.reasoning === 'object') {
|
|
710
660
|
ir.thinking = thinkingFromReasoningParam(payload.reasoning);
|
|
711
661
|
}
|
|
662
|
+
ir.responseFormat = responseFormatIR(payload.text?.format);
|
|
712
663
|
return ir;
|
|
713
664
|
}
|
|
714
665
|
|
|
715
|
-
//
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
const sys = payload.systemInstruction?.parts;
|
|
722
|
-
if (Array.isArray(sys)) {
|
|
723
|
-
ir.system = sys.map(p => p.text || '').filter(Boolean).join('\n\n');
|
|
724
|
-
} else if (typeof payload.system_instruction === 'string') {
|
|
725
|
-
ir.system = payload.system_instruction;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// Vertex has no tool call ids: auto-generate ids for functionCalls then match functionResponses by name (FIFO),
|
|
729
|
-
// so upstream OpenAI/Anthropic gets the correct tool_call <-> tool result pairs instead of "healing" them into text.
|
|
730
|
-
const pendingByName = new Map();
|
|
731
|
-
let callSeq = 0;
|
|
732
|
-
|
|
733
|
-
if (Array.isArray(payload.contents)) {
|
|
734
|
-
for (const c of payload.contents) {
|
|
735
|
-
const parts = Array.isArray(c?.parts) ? c.parts : [];
|
|
736
|
-
const texts = [];
|
|
737
|
-
const toolCalls = [];
|
|
738
|
-
for (const p of parts) {
|
|
739
|
-
if (!p || typeof p !== 'object') continue;
|
|
740
|
-
if (typeof p.text === 'string' && p.text && p.thought !== true) texts.push(p.text);
|
|
741
|
-
if (p.functionCall) {
|
|
742
|
-
const name = p.functionCall.name || '';
|
|
743
|
-
const id = p.functionCall.id || `call_vtx_${callSeq++}_${name}`;
|
|
744
|
-
if (!pendingByName.has(name)) pendingByName.set(name, []);
|
|
745
|
-
pendingByName.get(name).push(id);
|
|
746
|
-
const sig = p.thoughtSignature || p.thought_signature || null;
|
|
747
|
-
rememberToolSignature(id, sig);
|
|
748
|
-
toolCalls.push({ id, name, args: p.functionCall.args ?? {}, sig });
|
|
749
|
-
}
|
|
750
|
-
if (p.functionResponse) {
|
|
751
|
-
const fr = p.functionResponse;
|
|
752
|
-
const name = fr.name || '';
|
|
753
|
-
const queue = pendingByName.get(name);
|
|
754
|
-
const id = fr.id || (queue && queue.length ? queue.shift() : `call_vtx_orphan_${name}`);
|
|
755
|
-
ir.messages.push({
|
|
756
|
-
role: 'tool', toolCallId: id, name: name || null,
|
|
757
|
-
content: typeof fr.response === 'string' ? fr.response : JSON.stringify(fr.response ?? '')
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
if (c.role === 'model') {
|
|
762
|
-
const out = { role: 'assistant' };
|
|
763
|
-
if (texts.length) out.content = texts.join('');
|
|
764
|
-
if (toolCalls.length) out.toolCalls = toolCalls;
|
|
765
|
-
if (out.content !== undefined || out.toolCalls) ir.messages.push(out);
|
|
766
|
-
} else if (c.role === 'function') {
|
|
767
|
-
// functionResponse already pushed above
|
|
768
|
-
} else {
|
|
769
|
-
if (texts.length) ir.messages.push({ role: 'user', content: texts.join('') });
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
const fns = payload.tools?.flatMap(t => t.functionDeclarations || t.function_declarations || []);
|
|
775
|
-
if (Array.isArray(fns) && fns.length) {
|
|
776
|
-
ir.tools = fns.filter(f => f.name).map(f => ({
|
|
777
|
-
name: f.name, description: f.description || '',
|
|
778
|
-
parameters: sanitizeJsonSchema(f.parameters || {})
|
|
779
|
-
}));
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const gc = payload.generationConfig || payload.generation_config || {};
|
|
783
|
-
if (typeof gc.maxOutputTokens === 'number') ir.params.maxTokens = gc.maxOutputTokens;
|
|
784
|
-
if (typeof gc.temperature === 'number') ir.params.temperature = gc.temperature;
|
|
785
|
-
if (typeof gc.topP === 'number') ir.params.topP = gc.topP;
|
|
786
|
-
if (Array.isArray(gc.stopSequences)) ir.params.stop = stopList(gc.stopSequences);
|
|
787
|
-
const th = gc.thinkingConfig || gc.thinking_config;
|
|
788
|
-
if (th && typeof th === 'object') {
|
|
789
|
-
if (th.thinkingBudget === 0) ir.thinking = { type: 'disabled' };
|
|
790
|
-
else if (th.thinkingBudget > 0) ir.thinking = { type: 'enabled', budget: clampBudget(th.thinkingBudget) };
|
|
791
|
-
else if (th.thinkingLevel) ir.thinking = { type: 'enabled', budget: effortToBudget(th.thinkingLevel), effort: th.thinkingLevel };
|
|
792
|
-
else if (th.includeThoughts) ir.thinking = { type: 'enabled', budget: 2048 };
|
|
793
|
-
}
|
|
794
|
-
return ir;
|
|
795
|
-
}
|
|
796
|
-
|
|
666
|
+
// R5: two inputs, one per tool — `anthropic` for Claude Code, `responses` for Codex. The
|
|
667
|
+
// openai-chat and vertex routes were removed with their parsers; a format that reaches here
|
|
668
|
+
// anyway is an error, never a silent fallback to another protocol (a different API key!).
|
|
669
|
+
// The UPSTREAM formats are a different axis and are untouched: emitUpstreamBody still speaks
|
|
670
|
+
// openai-chat, anthropic and vertex to whatever provider the profile points at.
|
|
797
671
|
function parseToIR(clientFormat, payload) {
|
|
798
672
|
switch (clientFormat) {
|
|
799
673
|
case 'anthropic': return anthropicToIR(payload);
|
|
800
|
-
case 'openai-chat': return chatToIR(payload);
|
|
801
674
|
case 'responses': return responsesToIR(payload);
|
|
802
|
-
|
|
803
|
-
default: throw new Error(`Unknown client format: ${clientFormat}`);
|
|
675
|
+
default: throw new Error(`Unsupported input format: ${clientFormat}. Only anthropic (Claude Code) and responses (Codex) accept requests.`);
|
|
804
676
|
}
|
|
805
677
|
}
|
|
806
678
|
|
|
@@ -966,6 +838,13 @@ function irToChatBody(ir, model, opts = {}) {
|
|
|
966
838
|
if (typeof ir.params.frequencyPenalty === 'number') body.frequency_penalty = ir.params.frequencyPenalty;
|
|
967
839
|
if (ir.params.stop.length) body.stop = ir.params.stop;
|
|
968
840
|
if (ir.params.parallelToolCalls === false && body.tools) body.parallel_tool_calls = false;
|
|
841
|
+
if (ir.responseFormat?.type === 'json_schema') {
|
|
842
|
+
const js = { name: ir.responseFormat.name || 'response', schema: ir.responseFormat.schema };
|
|
843
|
+
if (typeof ir.responseFormat.strict === 'boolean') js.strict = ir.responseFormat.strict;
|
|
844
|
+
body.response_format = { type: 'json_schema', json_schema: js };
|
|
845
|
+
} else if (ir.responseFormat?.type === 'json_object') {
|
|
846
|
+
body.response_format = { type: 'json_object' };
|
|
847
|
+
}
|
|
969
848
|
|
|
970
849
|
if (wantsThinking && mode === 'native') {
|
|
971
850
|
body.reasoning_effort = ir.thinking?.effort && !['max', 'xhigh'].includes(ir.thinking.effort)
|
|
@@ -1085,6 +964,8 @@ function irToAnthropicBody(ir, model) {
|
|
|
1085
964
|
if (typeof ir.params.topP === 'number') body.top_p = ir.params.topP;
|
|
1086
965
|
if (typeof ir.params.topK === 'number') body.top_k = ir.params.topK;
|
|
1087
966
|
if (ir.params.stop.length) body.stop_sequences = ir.params.stop;
|
|
967
|
+
// Anthropic has no JSON mode without a schema.
|
|
968
|
+
if (ir.responseFormat?.type === 'json_schema') body.output_config = { format: { type: 'json_schema', schema: ir.responseFormat.schema } };
|
|
1088
969
|
if (ir.thinking && ir.thinking.type === 'adaptive') {
|
|
1089
970
|
body.thinking = { type: 'adaptive' };
|
|
1090
971
|
} else if (ir.thinking && ir.thinking.type !== 'disabled' && body.max_tokens > 1024) {
|
|
@@ -1206,6 +1087,10 @@ function irToVertexBody(ir, model) {
|
|
|
1206
1087
|
gc.thinkingConfig.thinkingBudget = clampBudget(ir.thinking.budget ?? (ir.thinking.effort ? effortToBudget(ir.thinking.effort) : 2048));
|
|
1207
1088
|
}
|
|
1208
1089
|
}
|
|
1090
|
+
if (ir.responseFormat) {
|
|
1091
|
+
gc.responseMimeType = 'application/json';
|
|
1092
|
+
if (ir.responseFormat.type === 'json_schema') gc.responseSchema = toGeminiSchema(ir.responseFormat.schema);
|
|
1093
|
+
}
|
|
1209
1094
|
if (Object.keys(gc).length) body.generationConfig = gc;
|
|
1210
1095
|
return body;
|
|
1211
1096
|
}
|
|
@@ -1255,6 +1140,8 @@ function toGeminiSchema(schema, root, seen) {
|
|
|
1255
1140
|
return schema === 'object' ? { type: 'object', properties: {} } : { type: schema };
|
|
1256
1141
|
}
|
|
1257
1142
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema;
|
|
1143
|
+
// The same hole as the string shorthand above: an empty schema has no type Vertex can read.
|
|
1144
|
+
if (Object.keys(schema).length === 0) return { type: 'object', properties: {} };
|
|
1258
1145
|
root = root || schema;
|
|
1259
1146
|
seen = seen || new Set();
|
|
1260
1147
|
if (seen.has(schema)) return {};
|
|
@@ -2305,7 +2192,7 @@ export {
|
|
|
2305
2192
|
smartReasoning, smartText, smartToolCalls, smartUsage, smartFinish,
|
|
2306
2193
|
firstChoice, smartDelta, sanitizeJsonSchema, toGeminiSchema, splitParts,
|
|
2307
2194
|
budgetToEffort, effortToBudget, clampBudget, parseArgs, stringifyArgs,
|
|
2308
|
-
anthropicToIR,
|
|
2195
|
+
anthropicToIR, responsesToIR, parseToIR,
|
|
2309
2196
|
healToolPairs, healAnthropicPayload, rememberToolSignature, lookupToolSignature, estimateTokens, irToChatBody, irToAnthropicBody, irToVertexBody, emitUpstreamBody,
|
|
2310
2197
|
normalizeUpstream, createUpstreamNormalizer, createCollector,
|
|
2311
2198
|
createThinkTagSplitter, splitThinkTags,
|
|
Binary file
|
package/icons/claude.png
ADDED
|
Binary file
|
package/icons/codex.png
ADDED
|
Binary file
|
|
Binary file
|
package/icons/gemini.png
ADDED
|
Binary file
|
package/icons/github.png
ADDED
|
Binary file
|
package/icons/groq.png
ADDED
|
Binary file
|
package/icons/intact.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="lg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7c6cf6"/><stop offset="1" stop-color="#2dd4bf"/></linearGradient></defs><rect width="32" height="32" rx="9" fill="url(#lg)"/><path d="M16 6.5 24 9.6v6.1c0 5-3.4 8.6-8 10-4.6-1.4-8-5-8-10V9.6Z" fill="none" stroke="#fff" stroke-width="2.2" stroke-linejoin="round"/><path d="m12.4 16.2 2.6 2.6 4.8-5" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
package/icons/ollama.png
ADDED
|
Binary file
|
package/icons/openai.png
ADDED
|
Binary file
|
|
Binary file
|
package/icons/qwen.png
ADDED
|
Binary file
|
package/icons/vertex.png
ADDED
|
Binary file
|
package/mcp.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// ============================================================
|
|
12
12
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
|
-
import { claudeSettingsPath, loadConfig as loadSharedConfig, resolvePort, readAdminToken,
|
|
14
|
+
import { claudeSettingsPath, loadConfig as loadSharedConfig, resolvePort, readAdminToken, STATE_DIR } from './state.mjs';
|
|
15
15
|
|
|
16
16
|
const VERSION = (() => {
|
|
17
17
|
try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version; } catch { return '0.0.0'; }
|
|
@@ -80,7 +80,7 @@ async function postSwitch(port, target, profile) {
|
|
|
80
80
|
const TOOLS = [
|
|
81
81
|
{
|
|
82
82
|
name: 'switcher_status',
|
|
83
|
-
description: 'Get live status of LLM Switcher gateway: port,
|
|
83
|
+
description: 'Get live status of LLM Switcher gateway: port, the two tool targets (Claude Code, Codex), and the active profile of each.',
|
|
84
84
|
inputSchema: {
|
|
85
85
|
type: 'object',
|
|
86
86
|
properties: {}
|
|
@@ -98,14 +98,14 @@ const TOOLS = [
|
|
|
98
98
|
},
|
|
99
99
|
{
|
|
100
100
|
name: 'switcher_switch_profile',
|
|
101
|
-
description: 'Programmatically change the active profile for
|
|
101
|
+
description: 'Programmatically change the active profile for one tool: Claude Code (claude) or Codex (codex).',
|
|
102
102
|
inputSchema: {
|
|
103
103
|
type: 'object',
|
|
104
104
|
properties: {
|
|
105
105
|
target: {
|
|
106
106
|
type: 'string',
|
|
107
|
-
description: '
|
|
108
|
-
enum:
|
|
107
|
+
description: 'Tool to switch: "claude" (Claude Code) or "codex" (Codex)',
|
|
108
|
+
enum: ['claude', 'codex']
|
|
109
109
|
},
|
|
110
110
|
profile: {
|
|
111
111
|
type: 'string',
|
|
@@ -124,6 +124,17 @@ const TOOLS = [
|
|
|
124
124
|
limit: { type: 'number', description: 'Maximum number of recent logs to return (default: 5)' }
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'switcher_models',
|
|
130
|
+
description: 'Get the latest model catalog for Claude Code and Codex, including discovered upstream models and slot assignments.',
|
|
131
|
+
inputSchema: {
|
|
132
|
+
type: 'object',
|
|
133
|
+
properties: {
|
|
134
|
+
tool: { type: 'string', enum: ['claude', 'codex'], description: 'Optional: filter by tool ("claude" or "codex")' },
|
|
135
|
+
refresh: { type: 'boolean', description: 'Query official upstream endpoints for new models before returning' }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
127
138
|
}
|
|
128
139
|
];
|
|
129
140
|
|
|
@@ -131,17 +142,29 @@ async function handleToolCall(name, args) {
|
|
|
131
142
|
const cfg = loadConfig();
|
|
132
143
|
const port = getMcpPort();
|
|
133
144
|
|
|
145
|
+
if (name === 'switcher_models') {
|
|
146
|
+
const { loadCatalogCache, refreshCatalog } = await import('./catalog.mjs');
|
|
147
|
+
if (args?.refresh) {
|
|
148
|
+
await refreshCatalog(STATE_DIR);
|
|
149
|
+
}
|
|
150
|
+
const cache = loadCatalogCache(STATE_DIR);
|
|
151
|
+
const t = args?.tool?.toLowerCase();
|
|
152
|
+
const res = {};
|
|
153
|
+
if (!t || t === 'claude') res.claude = cache.claude;
|
|
154
|
+
if (!t || t === 'codex') res.codex = cache.codex;
|
|
155
|
+
res.updatedAt = cache.updatedAt;
|
|
156
|
+
return { content: [{ type: 'text', text: JSON.stringify(res, null, 2) }] };
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
if (name === 'switcher_status') {
|
|
135
160
|
const live = await fetchStatus(port);
|
|
136
161
|
const activeMap = live?.activeProfiles || cfg.activeProfiles || {};
|
|
137
162
|
const text = [
|
|
138
163
|
'=== LLM Switcher Gateway Status ===',
|
|
139
164
|
`Service Running: ${live ? `YES (http://127.0.0.1:${port})` : 'NO / UNREACHABLE'}`,
|
|
140
|
-
`Active Profiles by
|
|
141
|
-
` - Claude Code (/v1/messages)
|
|
142
|
-
` - Codex CLI (/v1/responses)
|
|
143
|
-
` - OpenAI Chat (/v1/chat/completions): [${activeMap['openai-chat'] || 'OFF'}]`,
|
|
144
|
-
` - Vertex (/v1beta/models/*) : [${activeMap.vertex || 'OFF'}]`,
|
|
165
|
+
`Active Profiles by tool:`,
|
|
166
|
+
` - Claude Code (/v1/messages) : [${activeMap.claude || 'OFF'}]`,
|
|
167
|
+
` - Codex CLI (/v1/responses) : [${activeMap.codex || 'OFF'}]`,
|
|
145
168
|
'',
|
|
146
169
|
`Available Profiles in config: ${Object.keys(cfg.profiles || {}).join(', ')}`,
|
|
147
170
|
`Dashboard Web UI: http://127.0.0.1:${port}/ui`
|
|
@@ -220,7 +243,12 @@ async function handleToolCall(name, args) {
|
|
|
220
243
|
if (res.success) {
|
|
221
244
|
return { content: [{ type: 'text', text: `Successfully set ${target || 'global'} active profile to "${profile || 'OFF'}".` }] };
|
|
222
245
|
}
|
|
223
|
-
|
|
246
|
+
// A 409 answer carries the clashing keys. Without them the agent cannot tell which two
|
|
247
|
+
// pointers of config.json disagree, and the only advice left is "go read the file".
|
|
248
|
+
const detail = Array.isArray(res.clashingKeys) && res.clashingKeys.length
|
|
249
|
+
? ` Clashing keys: ${res.clashingKeys.join(', ')}.`
|
|
250
|
+
: '';
|
|
251
|
+
return { content: [{ type: 'text', text: `Switch failed: ${res.error || 'Unknown error'}.${detail}` }], isError: true };
|
|
224
252
|
} catch (err) {
|
|
225
253
|
return { content: [{ type: 'text', text: `Gateway connection failed: ${err.message}` }], isError: true };
|
|
226
254
|
}
|