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.
Files changed (51) hide show
  1. package/CHANGELOG.md +51 -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/config.example.json +12 -34
  8. package/docs/TOKEN-OPTIMIZER-INTEROP.md +110 -110
  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/docs/response-matrix.json +1130 -1130
  24. package/ensure-ca-bundle.mjs +28 -0
  25. package/formats.mjs +13 -155
  26. package/mcp.mjs +39 -11
  27. package/package.json +1 -1
  28. package/proxy.mjs +92 -27
  29. package/shim.mjs +200 -57
  30. package/skills/llm-switcher/SKILL.md +93 -88
  31. package/state.mjs +1100 -191
  32. package/switch +0 -0
  33. package/switch.cmd +2 -2
  34. package/switch.mjs +228 -53
  35. package/tests/blindfold-e2e.test.mjs +380 -0
  36. package/tests/blindfold-task5.test.mjs +429 -0
  37. package/tests/blindfold.task3.test.mjs +700 -0
  38. package/tests/blindfold.test.mjs +10 -5
  39. package/tests/catalog.test.mjs +147 -0
  40. package/tests/contract-lab.test.mjs +22 -7
  41. package/tests/formats.test.mjs +33 -46
  42. package/tests/gateway.e2e.test.mjs +136 -36
  43. package/tests/helpers.mjs +24 -24
  44. package/tests/lifecycle.test.mjs +16 -10
  45. package/tests/live-optimizer-interop.mjs +205 -205
  46. package/tests/mcp.test.mjs +78 -2
  47. package/tests/real-user-sim.test.mjs +464 -0
  48. package/tests/shim.test.mjs +159 -66
  49. package/tests/state.test.mjs +975 -193
  50. package/tests/switch.test.mjs +446 -2
  51. 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-anthropic" class="cli-slot-card">
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-anthropic" class="badge">OFF</span>
848
+ <span id="badge-slot-claude" class="badge">OFF</span>
849
849
  </div>
850
- <label class="sr-only" for="select-slot-anthropic">Profile for Claude Code</label>
851
- <select id="select-slot-anthropic" class="cli-slot-select" onchange="switchCliTarget('anthropic', this.value)">
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-responses" class="cli-slot-card">
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-responses" class="badge">OFF</span>
866
+ <span id="badge-slot-codex" class="badge">OFF</span>
867
867
  </div>
868
- <label class="sr-only" for="select-slot-responses">Profile for Codex</label>
869
- <select id="select-slot-responses" class="cli-slot-select" onchange="switchCliTarget('responses', this.value)">
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-informat">Client protocol</label>
975
- <select id="p-informat" onchange="onInFormatChange(this.value)">
976
- <option value="auto">AUTO (Accepts Claude / OpenAI / Codex / Vertex)</option>
977
- <option value="anthropic">Anthropic Messages (/v1/messages)</option>
978
- <option value="openai-chat">OpenAI Chat (/v1/chat/completions)</option>
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 Context Warning:</strong> Enabling <code>[1M Context]</code> for a tier sets <code>ANTHROPIC_DEFAULT_&lt;TIER&gt;_MODEL=&lt;tier&gt;[1m]</code> for that tier. If the upstream model lacks native 1M token support, long conversations will fail with <code>HTTP 400 (Context Exceeded)</code>.
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
- <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; text-transform: none;">
1087
- <input type="checkbox" id="p-blindfold" style="width: 13px; height: 13px;" onchange="onBlindfoldToggle(this.checked)">
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
- Off: the CLI is given a base URL override, and it prints a warning on its own <code>/model</code> screen.
1092
- On: the CLI keeps its official endpoint and reaches the gateway through a local interceptor.
1093
- Build the certificates first with <code>bash blindfold/make-certs.sh &lt;host&gt;</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 Context Warning:</strong> Enabling <code>[1M Context]</code> for a tier sets <code>ANTHROPIC_DEFAULT_&lt;TIER&gt;_MODEL=&lt;tier&gt;[1m]</code> for that tier. If the upstream model lacks native 1M token support, long conversations will fail with <code>HTTP 400 (Context Exceeded)</code>.',
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 onInFormatChange(newFmt) {
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(profile.inFormat)) return;
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-informat')?.value || 'auto';
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.anthropic) parts.push(`Claude: ${activeMap.anthropic}`);
1520
- if (activeMap.responses) parts.push(`Codex: ${activeMap.responses}`);
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
- // Update 4 CLI Slots
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: 'anthropic', name: 'Claude Code', key: 'anthropic' },
1537
- { id: 'responses', name: 'Codex', key: 'responses' },
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 inFmt = p.inFormat || 'auto';
1552
- if (inFmt === 'auto' || inFmt === slot.key) {
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.anthropic === key) activeCLIs.push('Claude');
1584
- if (activeMap.responses === key) activeCLIs.push('Codex');
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', anthropic: 'anth', 'openai-chat': 'chat', responses: 'resp', vertex: 'vtx' }[f] || f || 'auto');
1590
- const routeBadge = `${shortFmt(p.inFormat)}→${shortFmt(p.outFormat || p.mode)}`;
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 pCfg = getActiveSlotConfig(p.inFormat || 'auto');
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
- document.getElementById('p-informat').value = p.inFormat || 'auto';
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(p.inFormat || 'auto');
1735
- renderCodexTab(p.inFormat || 'auto');
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
- inFormat: 'auto',
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-informat').value = 'auto';
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-informat').value = 'auto';
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-informat').value = 'auto';
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-informat').value = 'responses';
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-informat').value = 'auto';
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-informat').value = 'openai-chat';
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: { default: 'gpt-4o' },
1874
- model1M: { default: false }
1780
+ defaultModels: { opus: 'gpt-4o' },
1781
+ model1M: { opus: false }
1875
1782
  };
1876
- renderModelSlots('openai-chat');
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-informat')?.value || 'auto';
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
- inFormat: document.getElementById('p-informat').value || 'auto',
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-informat')?.value || 'auto';
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');