llm-switcher 1.1.10 → 1.2.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 +51 -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/config.example.json +12 -34
- package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
- 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/docs/response-matrix.json +1130 -1130
- package/ensure-ca-bundle.mjs +28 -0
- package/formats.mjs +13 -155
- package/mcp.mjs +39 -11
- package/package.json +1 -1
- package/proxy.mjs +92 -27
- package/shim.mjs +200 -57
- package/skills/llm-switcher/SKILL.md +93 -88
- package/state.mjs +1100 -191
- package/switch +0 -0
- 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/contract-lab.test.mjs +22 -7
- package/tests/formats.test.mjs +33 -46
- package/tests/gateway.e2e.test.mjs +136 -36
- package/tests/helpers.mjs +24 -24
- package/tests/lifecycle.test.mjs +16 -10
- package/tests/live-optimizer-interop.mjs +205 -205
- 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 +61 -154
package/ui.html
CHANGED
|
@@ -836,7 +836,7 @@
|
|
|
836
836
|
</div>
|
|
837
837
|
<div class="cli-slots-grid">
|
|
838
838
|
<!-- Slot: Claude Code -->
|
|
839
|
-
<div id="card-slot-
|
|
839
|
+
<div id="card-slot-claude" class="cli-slot-card">
|
|
840
840
|
<div class="cli-slot-header">
|
|
841
841
|
<div class="cli-slot-title-group">
|
|
842
842
|
<span class="cli-slot-icon" aria-hidden="true">CC</span>
|
|
@@ -845,16 +845,16 @@
|
|
|
845
845
|
<div class="cli-slot-route">/v1/messages</div>
|
|
846
846
|
</div>
|
|
847
847
|
</div>
|
|
848
|
-
<span id="badge-slot-
|
|
848
|
+
<span id="badge-slot-claude" class="badge">OFF</span>
|
|
849
849
|
</div>
|
|
850
|
-
<label class="sr-only" for="select-slot-
|
|
851
|
-
<select id="select-slot-
|
|
850
|
+
<label class="sr-only" for="select-slot-claude">Profile for Claude Code</label>
|
|
851
|
+
<select id="select-slot-claude" class="cli-slot-select" onchange="switchCliTarget('claude', this.value)">
|
|
852
852
|
<option value="">Official endpoint</option>
|
|
853
853
|
</select>
|
|
854
854
|
</div>
|
|
855
855
|
|
|
856
856
|
<!-- Slot: Codex -->
|
|
857
|
-
<div id="card-slot-
|
|
857
|
+
<div id="card-slot-codex" class="cli-slot-card">
|
|
858
858
|
<div class="cli-slot-header">
|
|
859
859
|
<div class="cli-slot-title-group">
|
|
860
860
|
<span class="cli-slot-icon" aria-hidden="true">CX</span>
|
|
@@ -863,49 +863,14 @@
|
|
|
863
863
|
<div class="cli-slot-route">/v1/responses</div>
|
|
864
864
|
</div>
|
|
865
865
|
</div>
|
|
866
|
-
<span id="badge-slot-
|
|
866
|
+
<span id="badge-slot-codex" class="badge">OFF</span>
|
|
867
867
|
</div>
|
|
868
|
-
<label class="sr-only" for="select-slot-
|
|
869
|
-
<select id="select-slot-
|
|
868
|
+
<label class="sr-only" for="select-slot-codex">Profile for Codex</label>
|
|
869
|
+
<select id="select-slot-codex" class="cli-slot-select" onchange="switchCliTarget('codex', this.value)">
|
|
870
870
|
<option value="">Official endpoint</option>
|
|
871
871
|
</select>
|
|
872
872
|
</div>
|
|
873
873
|
|
|
874
|
-
<!-- Slot: OpenAI Chat -->
|
|
875
|
-
<div id="card-slot-openai-chat" class="cli-slot-card">
|
|
876
|
-
<div class="cli-slot-header">
|
|
877
|
-
<div class="cli-slot-title-group">
|
|
878
|
-
<span class="cli-slot-icon" aria-hidden="true">OA</span>
|
|
879
|
-
<div>
|
|
880
|
-
<div class="cli-slot-name">OpenAI Chat</div>
|
|
881
|
-
<div class="cli-slot-route">/v1/chat/completions</div>
|
|
882
|
-
</div>
|
|
883
|
-
</div>
|
|
884
|
-
<span id="badge-slot-openai-chat" class="badge">OFF</span>
|
|
885
|
-
</div>
|
|
886
|
-
<label class="sr-only" for="select-slot-openai-chat">Profile for OpenAI Chat</label>
|
|
887
|
-
<select id="select-slot-openai-chat" class="cli-slot-select" onchange="switchCliTarget('openai-chat', this.value)">
|
|
888
|
-
<option value="">Official endpoint</option>
|
|
889
|
-
</select>
|
|
890
|
-
</div>
|
|
891
|
-
|
|
892
|
-
<!-- Slot: Vertex -->
|
|
893
|
-
<div id="card-slot-vertex" class="cli-slot-card">
|
|
894
|
-
<div class="cli-slot-header">
|
|
895
|
-
<div class="cli-slot-title-group">
|
|
896
|
-
<span class="cli-slot-icon" aria-hidden="true">VX</span>
|
|
897
|
-
<div>
|
|
898
|
-
<div class="cli-slot-name">Vertex / Gemini</div>
|
|
899
|
-
<div class="cli-slot-route">/v1beta/models/*</div>
|
|
900
|
-
</div>
|
|
901
|
-
</div>
|
|
902
|
-
<span id="badge-slot-vertex" class="badge">OFF</span>
|
|
903
|
-
</div>
|
|
904
|
-
<label class="sr-only" for="select-slot-vertex">Profile for Vertex or Gemini</label>
|
|
905
|
-
<select id="select-slot-vertex" class="cli-slot-select" onchange="switchCliTarget('vertex', this.value)">
|
|
906
|
-
<option value="">Official endpoint</option>
|
|
907
|
-
</select>
|
|
908
|
-
</div>
|
|
909
874
|
</div>
|
|
910
875
|
|
|
911
876
|
<!-- Providers & Profiles List -->
|
|
@@ -971,14 +936,13 @@
|
|
|
971
936
|
<div id="tab-routing" class="tab-pane" role="tabpanel" aria-labelledby="tab-button-routing" hidden>
|
|
972
937
|
<div class="form-grid">
|
|
973
938
|
<div class="form-group">
|
|
974
|
-
<label for="p-
|
|
975
|
-
<select id="p-
|
|
976
|
-
<option value="auto">
|
|
977
|
-
<option value="anthropic">
|
|
978
|
-
<option value="
|
|
979
|
-
<option value="responses">OpenAI Responses — Codex (/v1/responses)</option>
|
|
980
|
-
<option value="vertex">Vertex generateContent (/v1beta/models/*)</option>
|
|
939
|
+
<label for="p-tool">Serves which tool</label>
|
|
940
|
+
<select id="p-tool" onchange="onToolChange(this.value)">
|
|
941
|
+
<option value="auto">Both — Claude Code and Codex</option>
|
|
942
|
+
<option value="anthropic">Claude Code only (/v1/messages)</option>
|
|
943
|
+
<option value="responses">Codex only (/v1/responses)</option>
|
|
981
944
|
</select>
|
|
945
|
+
<span style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">Saved as <code>tool</code> in config.json. OpenAI Chat and Vertex are no longer client inputs: the gateway stands between Claude Code, Codex and the provider.</span>
|
|
982
946
|
</div>
|
|
983
947
|
<div class="form-group">
|
|
984
948
|
<label for="p-outformat">Upstream protocol</label>
|
|
@@ -1022,7 +986,7 @@
|
|
|
1022
986
|
<div class="warning-callout" id="warn-1m-callout">
|
|
1023
987
|
<span class="badge badge-convert" id="warn-1m-icon">NOTE</span>
|
|
1024
988
|
<div id="warn-1m-text">
|
|
1025
|
-
<strong>1M
|
|
989
|
+
<strong>1M context notice:</strong> The <code>1M context</code> checkbox only records that a tier may use a 1M window. The gateway unlocks the context on the request itself; it never rewrites <code>settings.json</code> or the launcher environment. A model without native 1M support still answers <code>HTTP 400 (Context Exceeded)</code>.
|
|
1026
990
|
</div>
|
|
1027
991
|
</div>
|
|
1028
992
|
|
|
@@ -1083,34 +1047,14 @@
|
|
|
1083
1047
|
<div id="tab-codex" class="tab-pane" role="tabpanel" aria-labelledby="tab-button-codex" hidden>
|
|
1084
1048
|
<div class="form-grid" style="border-top: 1px solid var(--border); padding-top: 12px; margin-top: 12px;">
|
|
1085
1049
|
<div class="form-group full">
|
|
1086
|
-
<
|
|
1087
|
-
|
|
1088
|
-
Blindfold mode — hide the gateway address from the CLI
|
|
1089
|
-
</label>
|
|
1050
|
+
<span class="field-heading">Interceptor</span>
|
|
1051
|
+
<span id="blindfold-port-display" style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">not configured</span>
|
|
1090
1052
|
<span style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
Build the certificates first with <code>bash blindfold/make-certs.sh <host></code>.
|
|
1053
|
+
The interceptor is one switcher-wide setting (<code>blindfold</code> in config.json), not a per-profile one.
|
|
1054
|
+
Build the certificates with <code>bash blindfold/make-certs.sh</code> — one certificate names all three hosts this switcher routes.
|
|
1094
1055
|
</span>
|
|
1095
1056
|
</div>
|
|
1096
1057
|
</div>
|
|
1097
|
-
|
|
1098
|
-
<div class="form-grid" id="blindfold-fields" hidden>
|
|
1099
|
-
<div class="form-group">
|
|
1100
|
-
<label for="p-blindfold-host">Intercepted host</label>
|
|
1101
|
-
<input type="text" id="p-blindfold-host" placeholder="chatgpt.com">
|
|
1102
|
-
<span style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">ChatGPT sign-in: <code>chatgpt.com</code>. API key: <code>api.openai.com</code>.</span>
|
|
1103
|
-
</div>
|
|
1104
|
-
<div class="form-group">
|
|
1105
|
-
<label for="p-blindfold-port">Interceptor port</label>
|
|
1106
|
-
<input type="number" id="p-blindfold-port" min="1" max="65535" placeholder="3457">
|
|
1107
|
-
</div>
|
|
1108
|
-
<div class="form-group full">
|
|
1109
|
-
<label for="p-blindfold-prefix">API path prefix</label>
|
|
1110
|
-
<input type="text" id="p-blindfold-prefix" placeholder="/backend-api/codex">
|
|
1111
|
-
<span style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">Requests under this path reach the gateway. Every other path goes to the real host. API key accounts use <code>/v1</code>.</span>
|
|
1112
|
-
</div>
|
|
1113
|
-
</div>
|
|
1114
1058
|
</div>
|
|
1115
1059
|
|
|
1116
1060
|
<div id="tab-inspector" class="tab-pane" role="tabpanel" aria-labelledby="tab-button-inspector" hidden>
|
|
@@ -1201,8 +1145,6 @@
|
|
|
1201
1145
|
let currentProfileBuffer = null;
|
|
1202
1146
|
let renderedSlotFormat = 'auto';
|
|
1203
1147
|
const MASKED_KEY = '__LLM_SWITCHER_KEEP_KEY__';
|
|
1204
|
-
// Same list as TARGETS in state.mjs. The page is served as a static file and cannot import it.
|
|
1205
|
-
const TARGETS = ['anthropic', 'responses', 'openai-chat', 'vertex'];
|
|
1206
1148
|
// Order matters: publicModels is read by position when codexRoles has no entry.
|
|
1207
1149
|
const CODEX_ROLE_SLOTS = ['main', 'review', 'subagent'];
|
|
1208
1150
|
|
|
@@ -1222,7 +1164,7 @@
|
|
|
1222
1164
|
{ id: 'haiku', label: 'Haiku Model', placeholder: 'e.g. ag/gemini-3.7-flash-high', allow1M: true },
|
|
1223
1165
|
{ id: 'fable', label: 'Fable Model', placeholder: 'e.g. ag/gemini-3.8-flash-high', allow1M: true }
|
|
1224
1166
|
],
|
|
1225
|
-
warning: '<strong>1M
|
|
1167
|
+
warning: '<strong>1M context notice:</strong> The <code>1M context</code> checkbox only records that a tier may use a 1M window. The gateway unlocks the context on the request itself; it never rewrites <code>settings.json</code> or the launcher environment. A model without native 1M support still answers <code>HTTP 400 (Context Exceeded)</code>.',
|
|
1226
1168
|
icon: 'NOTE'
|
|
1227
1169
|
},
|
|
1228
1170
|
responses: {
|
|
@@ -1235,22 +1177,6 @@
|
|
|
1235
1177
|
warning: '<strong>Context notice:</strong> The launcher applies <code>model</code>, <code>review_model</code>, and <code>agents.default_subagent_model</code> through Codex config overrides. The 1M option also sets <code>model_context_window</code> and the auto-compact limit.',
|
|
1236
1178
|
icon: 'NOTE'
|
|
1237
1179
|
},
|
|
1238
|
-
'openai-chat': {
|
|
1239
|
-
heading: 'Default Model (OpenAI Chat)',
|
|
1240
|
-
slots: [
|
|
1241
|
-
{ id: 'default', label: 'Default Chat Model', placeholder: 'e.g. gpt-4o or ag/gemini-3.8-flash', allow1M: true }
|
|
1242
|
-
],
|
|
1243
|
-
warning: '<strong>1M Context Notice:</strong> When enabled, sets <code>OPENAI_MAX_CONTEXT_TOKENS=1000000</code>. Verify that upstream model supports 1M context before enabling.',
|
|
1244
|
-
icon: 'NOTE'
|
|
1245
|
-
},
|
|
1246
|
-
vertex: {
|
|
1247
|
-
heading: 'Default Model (Vertex / Gemini)',
|
|
1248
|
-
slots: [
|
|
1249
|
-
{ id: 'default', label: 'Default Model (Vertex/Gemini)', placeholder: 'e.g. gemini-2.5-pro or gemini-2.5-flash', allow1M: false }
|
|
1250
|
-
],
|
|
1251
|
-
warning: '<strong>Context Window Notice:</strong> Vertex/Gemini SDK context window is managed directly by upstream Google models. No client-side 1M flag is needed.',
|
|
1252
|
-
icon: 'INFO'
|
|
1253
|
-
}
|
|
1254
1180
|
};
|
|
1255
1181
|
SLOT_CONFIG.auto = SLOT_CONFIG.anthropic;
|
|
1256
1182
|
|
|
@@ -1367,7 +1293,7 @@
|
|
|
1367
1293
|
update1mWarnings();
|
|
1368
1294
|
}
|
|
1369
1295
|
|
|
1370
|
-
function
|
|
1296
|
+
function onToolChange(newFmt) {
|
|
1371
1297
|
collectCurrentSlotValues(renderedSlotFormat);
|
|
1372
1298
|
renderModelSlots(newFmt);
|
|
1373
1299
|
renderCodexTab(newFmt);
|
|
@@ -1398,29 +1324,13 @@
|
|
|
1398
1324
|
if (input) input.value = Object.hasOwn(roles, slot) ? (roles[slot] || '') : (published[i] || '');
|
|
1399
1325
|
});
|
|
1400
1326
|
|
|
1401
|
-
const chk = document.getElementById('p-blindfold');
|
|
1402
|
-
if (chk) chk.checked = Boolean(p.blindfold);
|
|
1403
|
-
const host = document.getElementById('p-blindfold-host');
|
|
1404
|
-
if (host) host.value = p.blindfoldHost || '';
|
|
1405
|
-
const port = document.getElementById('p-blindfold-port');
|
|
1406
|
-
if (port) port.value = p.blindfoldPort || '';
|
|
1407
|
-
const prefix = document.getElementById('p-blindfold-prefix');
|
|
1408
|
-
if (prefix) prefix.value = p.blindfoldPrefix || '';
|
|
1409
|
-
onBlindfoldToggle(Boolean(p.blindfold));
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
// The form already marks itself dirty through one input/change listener, so this
|
|
1413
|
-
// only reveals the fields that the toggle governs.
|
|
1414
|
-
function onBlindfoldToggle(enabled) {
|
|
1415
|
-
const fields = document.getElementById('blindfold-fields');
|
|
1416
|
-
if (fields) fields.hidden = !enabled;
|
|
1417
1327
|
}
|
|
1418
1328
|
|
|
1419
1329
|
// One set of inputs feeds both keys: codexRoles is the role mapping the gateway
|
|
1420
1330
|
// resolves, publicModels is the flat list /v1/models serves. Writing only one of
|
|
1421
1331
|
// them lets the two drift apart.
|
|
1422
1332
|
function collectCodexValues(profile) {
|
|
1423
|
-
if (!servesCodex(
|
|
1333
|
+
if (!servesCodex(document.getElementById('p-tool')?.value || 'auto')) return;
|
|
1424
1334
|
|
|
1425
1335
|
const roles = {};
|
|
1426
1336
|
const published = [];
|
|
@@ -1432,13 +1342,6 @@
|
|
|
1432
1342
|
profile.codexRoles = roles;
|
|
1433
1343
|
profile.publicModels = published;
|
|
1434
1344
|
|
|
1435
|
-
profile.blindfold = Boolean(document.getElementById('p-blindfold')?.checked);
|
|
1436
|
-
const host = document.getElementById('p-blindfold-host')?.value.trim() || '';
|
|
1437
|
-
const prefix = document.getElementById('p-blindfold-prefix')?.value.trim() || '';
|
|
1438
|
-
const port = parseInt(document.getElementById('p-blindfold-port')?.value, 10);
|
|
1439
|
-
profile.blindfoldHost = host;
|
|
1440
|
-
profile.blindfoldPrefix = prefix;
|
|
1441
|
-
profile.blindfoldPort = Number.isInteger(port) && port > 0 && port <= 65535 ? port : '';
|
|
1442
1345
|
}
|
|
1443
1346
|
|
|
1444
1347
|
function collectCurrentSlotValues(format = renderedSlotFormat) {
|
|
@@ -1456,7 +1359,7 @@
|
|
|
1456
1359
|
}
|
|
1457
1360
|
|
|
1458
1361
|
function update1mWarnings() {
|
|
1459
|
-
const currentFmt = document.getElementById('p-
|
|
1362
|
+
const currentFmt = document.getElementById('p-tool')?.value || 'auto';
|
|
1460
1363
|
const cfg = getActiveSlotConfig(currentFmt);
|
|
1461
1364
|
cfg.slots.forEach(s => {
|
|
1462
1365
|
const chk = document.getElementById(`p-${s.id}-1m`);
|
|
@@ -1516,10 +1419,8 @@
|
|
|
1516
1419
|
dot.className = 'status-dot active';
|
|
1517
1420
|
title.innerText = `Gateway active for ${activeTargets.length} client${activeTargets.length > 1 ? 's' : ''}`;
|
|
1518
1421
|
const parts = [];
|
|
1519
|
-
if (activeMap.
|
|
1520
|
-
if (activeMap.
|
|
1521
|
-
if (activeMap['openai-chat']) parts.push(`OpenAI: ${activeMap['openai-chat']}`);
|
|
1522
|
-
if (activeMap.vertex) parts.push(`Vertex: ${activeMap.vertex}`);
|
|
1422
|
+
if (activeMap.claude) parts.push(`Claude: ${activeMap.claude}`);
|
|
1423
|
+
if (activeMap.codex) parts.push(`Codex: ${activeMap.codex}`);
|
|
1523
1424
|
sub.innerText = `Port ${state.port} | ${parts.join(' | ')}`;
|
|
1524
1425
|
toggleBtn.className = 'btn btn-danger';
|
|
1525
1426
|
toggleBtn.innerText = 'Use official endpoints';
|
|
@@ -1531,12 +1432,18 @@
|
|
|
1531
1432
|
toggleBtn.innerText = 'Activate compatible routes';
|
|
1532
1433
|
}
|
|
1533
1434
|
|
|
1534
|
-
|
|
1435
|
+
const bfEl = document.getElementById('blindfold-port-display');
|
|
1436
|
+
if (bfEl) {
|
|
1437
|
+
const bf = state.config?.blindfold;
|
|
1438
|
+
bfEl.textContent = (bf && bf.port)
|
|
1439
|
+
? `port ${bf.port} — host ${bf.host || 'api.anthropic.com, api.openai.com, chatgpt.com'}, prefix ${bf.prefix || 'default'}`
|
|
1440
|
+
: 'not configured (the tools reach the provider directly)';
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Update the two tool slots
|
|
1535
1444
|
const slots = [
|
|
1536
|
-
{ id: '
|
|
1537
|
-
{ id: '
|
|
1538
|
-
{ id: 'openai-chat', name: 'OpenAI Chat', key: 'openai-chat' },
|
|
1539
|
-
{ id: 'vertex', name: 'Vertex / Gemini', key: 'vertex' }
|
|
1445
|
+
{ id: 'claude', name: 'Claude Code', key: 'claude' },
|
|
1446
|
+
{ id: 'codex', name: 'Codex', key: 'codex' }
|
|
1540
1447
|
];
|
|
1541
1448
|
|
|
1542
1449
|
for (const slot of slots) {
|
|
@@ -1548,8 +1455,8 @@
|
|
|
1548
1455
|
if (select) {
|
|
1549
1456
|
select.innerHTML = '<option value="">Official endpoint</option>';
|
|
1550
1457
|
for (const [key, p] of Object.entries(state.config.profiles)) {
|
|
1551
|
-
const
|
|
1552
|
-
if (
|
|
1458
|
+
const tool = p.tool || 'auto';
|
|
1459
|
+
if (tool === 'auto' || tool === slot.key) {
|
|
1553
1460
|
const opt = document.createElement('option');
|
|
1554
1461
|
opt.value = key;
|
|
1555
1462
|
opt.textContent = `${p.name} (${key})`;
|
|
@@ -1580,14 +1487,12 @@
|
|
|
1580
1487
|
|
|
1581
1488
|
for (const [key, p] of Object.entries(state.config.profiles)) {
|
|
1582
1489
|
const activeCLIs = [];
|
|
1583
|
-
if (activeMap.
|
|
1584
|
-
if (activeMap.
|
|
1585
|
-
if (activeMap['openai-chat'] === key) activeCLIs.push('OpenAI');
|
|
1586
|
-
if (activeMap.vertex === key) activeCLIs.push('Vertex');
|
|
1490
|
+
if (activeMap.claude === key) activeCLIs.push('Claude');
|
|
1491
|
+
if (activeMap.codex === key) activeCLIs.push('Codex');
|
|
1587
1492
|
const isActiveAny = activeCLIs.length > 0;
|
|
1588
1493
|
const isEditing = (key === currentlyEditingKey);
|
|
1589
|
-
const shortFmt = (f) => ({ auto: 'auto',
|
|
1590
|
-
const routeBadge = `${shortFmt(p.
|
|
1494
|
+
const shortFmt = (f) => ({ auto: 'auto', claude: 'claude', codex: 'codex' }[f] || f || 'auto');
|
|
1495
|
+
const routeBadge = `${shortFmt(p.tool || 'auto')}→${shortFmt(p.outFormat || p.mode)}`;
|
|
1591
1496
|
|
|
1592
1497
|
const card = document.createElement('div');
|
|
1593
1498
|
card.className = `profile-card ${isEditing ? 'editing' : ''} ${isActiveAny ? 'active' : ''}`;
|
|
@@ -1599,7 +1504,8 @@
|
|
|
1599
1504
|
selectProfileForEdit(key);
|
|
1600
1505
|
});
|
|
1601
1506
|
|
|
1602
|
-
const
|
|
1507
|
+
const slotFmt = p.tool === 'claude' ? 'anthropic' : p.tool === 'codex' ? 'responses' : 'auto';
|
|
1508
|
+
const pCfg = getActiveSlotConfig(slotFmt);
|
|
1603
1509
|
const modelsSummaryHtml = pCfg.slots.map(s => {
|
|
1604
1510
|
const val = valueForSlot(p.defaultModels, s.id) || '-';
|
|
1605
1511
|
const has1m = flagForSlot(p.model1M, s.id);
|
|
@@ -1725,14 +1631,15 @@
|
|
|
1725
1631
|
document.getElementById('p-key').disabled = true;
|
|
1726
1632
|
document.getElementById('p-name').value = p.name;
|
|
1727
1633
|
document.getElementById('p-mode').value = p.mode || 'hybrid';
|
|
1728
|
-
|
|
1634
|
+
const slotFmt = p.tool === 'claude' ? 'anthropic' : p.tool === 'codex' ? 'responses' : 'auto';
|
|
1635
|
+
document.getElementById('p-tool').value = slotFmt;
|
|
1729
1636
|
document.getElementById('p-thinking-mode').value = p.thinkingMode || 'auto';
|
|
1730
1637
|
document.getElementById('p-outformat').value = p.outFormat || '';
|
|
1731
1638
|
document.getElementById('p-url').value = p.baseURL;
|
|
1732
1639
|
document.getElementById('p-key-val').value = p.apiKey || '';
|
|
1733
1640
|
|
|
1734
|
-
renderModelSlots(
|
|
1735
|
-
renderCodexTab(
|
|
1641
|
+
renderModelSlots(slotFmt);
|
|
1642
|
+
renderCodexTab(slotFmt);
|
|
1736
1643
|
|
|
1737
1644
|
document.getElementById('btn-delete').style.display = Object.keys(state.config.profiles).length > 1 ? 'block' : 'none';
|
|
1738
1645
|
const usedBy = Object.entries(state.activeProfiles || {}).filter(([, v]) => v === key).map(([t]) => t);
|
|
@@ -1754,7 +1661,7 @@
|
|
|
1754
1661
|
currentProfileBuffer = {
|
|
1755
1662
|
name: '',
|
|
1756
1663
|
mode: 'hybrid',
|
|
1757
|
-
|
|
1664
|
+
tool: null,
|
|
1758
1665
|
outFormat: '',
|
|
1759
1666
|
thinkingMode: 'auto',
|
|
1760
1667
|
baseURL: 'https://',
|
|
@@ -1769,7 +1676,7 @@
|
|
|
1769
1676
|
document.getElementById('p-key').disabled = false;
|
|
1770
1677
|
document.getElementById('p-name').value = '';
|
|
1771
1678
|
document.getElementById('p-mode').value = 'hybrid';
|
|
1772
|
-
document.getElementById('p-
|
|
1679
|
+
document.getElementById('p-tool').value = 'auto';
|
|
1773
1680
|
document.getElementById('p-outformat').value = '';
|
|
1774
1681
|
document.getElementById('p-thinking-mode').value = 'auto';
|
|
1775
1682
|
document.getElementById('p-url').value = 'https://';
|
|
@@ -1796,7 +1703,7 @@
|
|
|
1796
1703
|
setKeyIfNew('9router');
|
|
1797
1704
|
document.getElementById('p-name').value = '9Router Cloud';
|
|
1798
1705
|
document.getElementById('p-mode').value = 'convert';
|
|
1799
|
-
document.getElementById('p-
|
|
1706
|
+
document.getElementById('p-tool').value = 'auto';
|
|
1800
1707
|
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1801
1708
|
document.getElementById('p-url').value = 'https://api.9router.com/v1';
|
|
1802
1709
|
document.getElementById('p-thinking-mode').value = 'auto';
|
|
@@ -1814,7 +1721,7 @@
|
|
|
1814
1721
|
setKeyIfNew('openrouter');
|
|
1815
1722
|
document.getElementById('p-name').value = 'OpenRouter';
|
|
1816
1723
|
document.getElementById('p-mode').value = 'convert';
|
|
1817
|
-
document.getElementById('p-
|
|
1724
|
+
document.getElementById('p-tool').value = 'auto';
|
|
1818
1725
|
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1819
1726
|
document.getElementById('p-url').value = 'https://openrouter.ai/api/v1';
|
|
1820
1727
|
currentProfileBuffer = {
|
|
@@ -1831,7 +1738,7 @@
|
|
|
1831
1738
|
setKeyIfNew('codex-vertex');
|
|
1832
1739
|
document.getElementById('p-name').value = 'Codex to Vertex';
|
|
1833
1740
|
document.getElementById('p-mode').value = 'convert';
|
|
1834
|
-
document.getElementById('p-
|
|
1741
|
+
document.getElementById('p-tool').value = 'responses';
|
|
1835
1742
|
document.getElementById('p-outformat').value = 'vertex';
|
|
1836
1743
|
document.getElementById('p-url').value = 'https://api.9router.com/v1';
|
|
1837
1744
|
document.getElementById('p-thinking-mode').value = 'auto';
|
|
@@ -1848,7 +1755,7 @@
|
|
|
1848
1755
|
setKeyIfNew('selfhost');
|
|
1849
1756
|
document.getElementById('p-name').value = 'Selfhost Hybrid';
|
|
1850
1757
|
document.getElementById('p-mode').value = 'hybrid';
|
|
1851
|
-
document.getElementById('p-
|
|
1758
|
+
document.getElementById('p-tool').value = 'auto';
|
|
1852
1759
|
document.getElementById('p-outformat').value = '';
|
|
1853
1760
|
document.getElementById('p-url').value = 'https://llm.example.com/v1';
|
|
1854
1761
|
currentProfileBuffer = {
|
|
@@ -1865,15 +1772,15 @@
|
|
|
1865
1772
|
setKeyIfNew('local-openai');
|
|
1866
1773
|
document.getElementById('p-name').value = 'Local OpenAI Server';
|
|
1867
1774
|
document.getElementById('p-mode').value = 'convert';
|
|
1868
|
-
document.getElementById('p-
|
|
1775
|
+
document.getElementById('p-tool').value = 'auto';
|
|
1869
1776
|
document.getElementById('p-outformat').value = 'openai-chat';
|
|
1870
1777
|
document.getElementById('p-url').value = 'http://127.0.0.1:8000/v1';
|
|
1871
1778
|
document.getElementById('p-thinking-mode').value = 'native';
|
|
1872
1779
|
currentProfileBuffer = {
|
|
1873
|
-
defaultModels: {
|
|
1874
|
-
model1M: {
|
|
1780
|
+
defaultModels: { opus: 'gpt-4o' },
|
|
1781
|
+
model1M: { opus: false }
|
|
1875
1782
|
};
|
|
1876
|
-
renderModelSlots('
|
|
1783
|
+
renderModelSlots('auto');
|
|
1877
1784
|
}
|
|
1878
1785
|
formDirty = true;
|
|
1879
1786
|
showToast(`Applied ${type} template!`);
|
|
@@ -1939,7 +1846,7 @@
|
|
|
1939
1846
|
return;
|
|
1940
1847
|
}
|
|
1941
1848
|
|
|
1942
|
-
const currentFmt = document.getElementById('p-
|
|
1849
|
+
const currentFmt = document.getElementById('p-tool')?.value || 'auto';
|
|
1943
1850
|
const cfg = getActiveSlotConfig(currentFmt);
|
|
1944
1851
|
|
|
1945
1852
|
for (const m of models) {
|
|
@@ -2022,7 +1929,7 @@
|
|
|
2022
1929
|
const profile = {
|
|
2023
1930
|
name: document.getElementById('p-name').value.trim(),
|
|
2024
1931
|
mode: document.getElementById('p-mode').value,
|
|
2025
|
-
|
|
1932
|
+
tool: ({ anthropic: 'claude', responses: 'codex' }[document.getElementById('p-tool').value]) || null,
|
|
2026
1933
|
thinkingMode: document.getElementById('p-thinking-mode').value || 'auto',
|
|
2027
1934
|
baseURL: document.getElementById('p-url').value.trim(),
|
|
2028
1935
|
apiKey: document.getElementById('p-key-val').value.trim(),
|
|
@@ -2092,7 +1999,7 @@
|
|
|
2092
1999
|
box.style.display = 'none';
|
|
2093
2000
|
|
|
2094
2001
|
try {
|
|
2095
|
-
const currentFmt = document.getElementById('p-
|
|
2002
|
+
const currentFmt = document.getElementById('p-tool')?.value || 'auto';
|
|
2096
2003
|
const cfg = getActiveSlotConfig(currentFmt);
|
|
2097
2004
|
const primarySlotId = cfg.slots[0]?.id || 'default';
|
|
2098
2005
|
const modelInput = document.getElementById(`p-${primarySlotId}`) || document.getElementById('p-sonnet') || document.getElementById('p-opus');
|