llm-switcher 1.6.0 → 1.6.3
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 +30 -0
- package/README.md +42 -34
- package/README.vi.md +38 -33
- package/docs/session-2026-10-07-compaction.md +224 -0
- package/formats.mjs +85 -25
- package/idlecache.mjs +159 -0
- package/idlecall.mjs +46 -54
- package/idlecompact.mjs +171 -238
- package/package.json +1 -1
- package/proxy.mjs +111 -135
- package/shim.mjs +29 -0
- package/state.mjs +48 -2
- package/switch.mjs +12 -5
- package/tests/certs.test.mjs +7 -1
- package/tests/codex-daemon.test.mjs +57 -3
- package/tests/compact-cli.test.mjs +34 -0
- package/tests/dashboard.e2e.test.mjs +7 -1
- package/tests/formats.test.mjs +44 -1
- package/tests/idlecompact.e2e.test.mjs +203 -0
- package/tests/idlecompact.test.mjs +233 -279
- package/ui.html +657 -85
- package/claudesession.mjs +0 -196
- package/codexsession.mjs +0 -141
package/ui.html
CHANGED
|
@@ -551,6 +551,7 @@
|
|
|
551
551
|
|
|
552
552
|
/* Model slot combobox: one listbox per slot, provider prefix dimmed, match in the accent. */
|
|
553
553
|
.combo { position: relative; }
|
|
554
|
+
.combo[data-open] { z-index: 50; }
|
|
554
555
|
.slot-note { font-size: 0.72rem; color: var(--faint); } /* Kept in the page when empty: a live region added later is often not read. */
|
|
555
556
|
.slot-note:empty { margin-top: -0.25rem; }
|
|
556
557
|
.combo > input { padding-right: 2.25rem; font-family: var(--font-mono); font-size: 0.8rem; }
|
|
@@ -905,6 +906,10 @@
|
|
|
905
906
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 12h16M12 4v16"/><circle cx="12" cy="12" r="8"/></svg>
|
|
906
907
|
Routes & Profiles<span class="ct" id="ct-routes"></span>
|
|
907
908
|
</a>
|
|
909
|
+
<a class="nav-link" href="#/compact" data-nav="compact">
|
|
910
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M4 14h6m0 0v6m0-6L3 21"/><path d="M20 10h-6m0 0V4m0 6l7-7"/><circle cx="12" cy="12" r="2"/></svg>
|
|
911
|
+
Idle Compaction<span class="ct" id="ct-compact"></span>
|
|
912
|
+
</a>
|
|
908
913
|
<a class="nav-link" href="#/models" data-nav="models">
|
|
909
914
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/></svg>
|
|
910
915
|
Model Catalog<span class="ct" id="ct-models"></span>
|
|
@@ -952,57 +957,6 @@
|
|
|
952
957
|
<!-- VIEW 1: Routes & Profiles -->
|
|
953
958
|
<div id="view-routes" class="view-panel" style="display:none;">
|
|
954
959
|
<div class="sec">
|
|
955
|
-
<!-- Idle compaction -->
|
|
956
|
-
<div class="card">
|
|
957
|
-
<div class="cardh">
|
|
958
|
-
<h2 style="font-size:1.02rem; font-weight:600;">Idle compaction</h2>
|
|
959
|
-
<span class="badge outline" id="badge-idle-compact">OFF</span>
|
|
960
|
-
</div>
|
|
961
|
-
<p style="margin:.2rem 0 .7rem; color:var(--muted); font-size:.86rem;">
|
|
962
|
-
A Claude Code conversation that comes back after a pause has lost the provider's prompt
|
|
963
|
-
cache, so its history is paid for at full price. Shorten it then, and write the
|
|
964
|
-
compaction into the session file so the next resume starts short.
|
|
965
|
-
</p>
|
|
966
|
-
<div class="row">
|
|
967
|
-
<label class="cb" style="display:flex; align-items:center; gap:.5rem;">
|
|
968
|
-
<input type="checkbox" id="idle-compact-enabled" onchange="saveIdleCompact()">
|
|
969
|
-
<span>Shorten a conversation that was idle too long</span>
|
|
970
|
-
</label>
|
|
971
|
-
</div>
|
|
972
|
-
<div class="row">
|
|
973
|
-
<div class="who">
|
|
974
|
-
<div class="l">Pause that counts as losing the cache</div>
|
|
975
|
-
<div class="meta">minutes · Anthropic 5m default, OpenAI 30m</div>
|
|
976
|
-
</div>
|
|
977
|
-
<div style="min-width:9rem;">
|
|
978
|
-
<input type="number" id="idle-compact-minutes" min="1" max="1440" class="inp"
|
|
979
|
-
style="width:100%;" onchange="saveIdleCompact()">
|
|
980
|
-
</div>
|
|
981
|
-
</div>
|
|
982
|
-
<div class="row">
|
|
983
|
-
<div class="who">
|
|
984
|
-
<div class="l">Context it must be larger than</div>
|
|
985
|
-
<div class="meta">KB · below this nothing is touched</div>
|
|
986
|
-
</div>
|
|
987
|
-
<div style="min-width:9rem;">
|
|
988
|
-
<input type="number" id="idle-compact-kb" min="8" max="4096" class="inp"
|
|
989
|
-
style="width:100%;" onchange="saveIdleCompact()">
|
|
990
|
-
</div>
|
|
991
|
-
</div>
|
|
992
|
-
<div class="row">
|
|
993
|
-
<div class="who">
|
|
994
|
-
<div class="l">Model that writes the summary</div>
|
|
995
|
-
<div class="meta">a cheaper model writes a cheaper and worse summary</div>
|
|
996
|
-
</div>
|
|
997
|
-
<div style="min-width:16rem; flex:1;">
|
|
998
|
-
<input type="text" id="idle-compact-model" class="inp" style="width:100%;"
|
|
999
|
-
placeholder="empty = the model of the conversation itself"
|
|
1000
|
-
onchange="saveIdleCompact()">
|
|
1001
|
-
</div>
|
|
1002
|
-
</div>
|
|
1003
|
-
<span id="idle-compact-status" class="meta" aria-live="polite"></span>
|
|
1004
|
-
</div>
|
|
1005
|
-
|
|
1006
960
|
<!-- Active Coding CLI Targets -->
|
|
1007
961
|
<div class="card">
|
|
1008
962
|
<div class="cardh">
|
|
@@ -1067,6 +1021,195 @@
|
|
|
1067
1021
|
</div>
|
|
1068
1022
|
</div>
|
|
1069
1023
|
|
|
1024
|
+
<!-- VIEW: Idle Prefix Cache & Compaction -->
|
|
1025
|
+
<div id="view-compact" class="view-panel" style="display:none;">
|
|
1026
|
+
<div class="sec">
|
|
1027
|
+
<!-- Overview Stats -->
|
|
1028
|
+
<div class="stats">
|
|
1029
|
+
<div class="stat">
|
|
1030
|
+
<div class="k">Compaction Engine</div>
|
|
1031
|
+
<div class="v" id="stat-compact-status" style="color:var(--muted); font-size:1.4rem;">Disabled</div>
|
|
1032
|
+
<div class="status" id="stat-compact-indicator" style="margin-top:0.3rem;">Prefix caching inactive</div>
|
|
1033
|
+
</div>
|
|
1034
|
+
|
|
1035
|
+
<div class="stat">
|
|
1036
|
+
<div class="k">Inactivity Pause</div>
|
|
1037
|
+
<div class="v" id="stat-compact-pause" style="color:var(--accent); font-size:1.4rem;">15 min</div>
|
|
1038
|
+
<div class="hint" style="margin-top:0.3rem;">Trigger pause threshold</div>
|
|
1039
|
+
</div>
|
|
1040
|
+
|
|
1041
|
+
<div class="stat">
|
|
1042
|
+
<div class="k">Min Size Threshold</div>
|
|
1043
|
+
<div class="v" id="stat-compact-size" style="color:var(--mint); font-size:1.4rem;">64 KB</div>
|
|
1044
|
+
<div class="hint" style="margin-top:0.3rem;">Smaller payloads bypassed</div>
|
|
1045
|
+
</div>
|
|
1046
|
+
|
|
1047
|
+
<div class="stat">
|
|
1048
|
+
<div class="k">Cache Lookback</div>
|
|
1049
|
+
<div class="v" id="stat-compact-lookback" style="color:var(--code); font-size:1.4rem;">72 hrs</div>
|
|
1050
|
+
<div class="hint" style="margin-top:0.3rem;">Storage retention window</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
|
|
1054
|
+
<!-- Hero Master Card -->
|
|
1055
|
+
<div class="card pad">
|
|
1056
|
+
<div class="sech">
|
|
1057
|
+
<div>
|
|
1058
|
+
<div style="display:flex; align-items:center; gap:0.6rem;">
|
|
1059
|
+
<h2 style="font-size:1.05rem; font-weight:600;">Idle Prefix Cache & Compaction</h2>
|
|
1060
|
+
<span class="badge outline" id="badge-idle-compact">OFF</span>
|
|
1061
|
+
</div>
|
|
1062
|
+
<p class="hint" style="margin-top:0.35rem; max-width:54rem; line-height:1.45;">
|
|
1063
|
+
Shortens long conversation histories when an agent session remains inactive past a configured pause.
|
|
1064
|
+
The edge gateway maintains a canonical shortened prefix for resumed turns, cutting context tokens while keeping client transcripts 100% untouched.
|
|
1065
|
+
</p>
|
|
1066
|
+
</div>
|
|
1067
|
+
<div style="display:flex; align-items:center; gap:0.8rem;">
|
|
1068
|
+
<label class="switch" title="Toggle Idle Compaction">
|
|
1069
|
+
<input type="checkbox" id="idle-compact-enabled" onchange="saveIdleCompact()">
|
|
1070
|
+
<span></span>
|
|
1071
|
+
</label>
|
|
1072
|
+
</div>
|
|
1073
|
+
</div>
|
|
1074
|
+
<div id="idle-compact-status" style="margin-top:0.75rem; font-size:0.82rem; min-height:1.2rem;" aria-live="polite"></div>
|
|
1075
|
+
</div>
|
|
1076
|
+
|
|
1077
|
+
<!-- Configuration Section (Hidden when inactive) -->
|
|
1078
|
+
<div id="idle-compact-config-section" style="display:none; flex-direction:column; gap:1.25rem;">
|
|
1079
|
+
<!-- Card 1: Core Policy & Trigger Thresholds -->
|
|
1080
|
+
<div class="card">
|
|
1081
|
+
<div class="cardh">
|
|
1082
|
+
<h3 style="font-size:0.95rem; font-weight:600;">Compaction Policy & Triggers</h3>
|
|
1083
|
+
<span class="badge ok">Gateway Invariant</span>
|
|
1084
|
+
</div>
|
|
1085
|
+
|
|
1086
|
+
<div class="row">
|
|
1087
|
+
<div class="who">
|
|
1088
|
+
<div class="l">Inactivity Pause Threshold</div>
|
|
1089
|
+
<div class="meta">Elapsed idle minutes after which the conversation prefix is compacted</div>
|
|
1090
|
+
</div>
|
|
1091
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1092
|
+
<input type="number" id="idle-compact-minutes" min="1" max="1440" step="any" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Inactivity pause threshold in minutes (1 – 1440 min)" placeholder="15">
|
|
1093
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.2rem; user-select:none;">min</span>
|
|
1094
|
+
</div>
|
|
1095
|
+
</div>
|
|
1096
|
+
|
|
1097
|
+
<div class="row">
|
|
1098
|
+
<div class="who">
|
|
1099
|
+
<div class="l">Minimum Context Payload</div>
|
|
1100
|
+
<div class="meta">Context threshold; conversations smaller than this are kept untouched</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1103
|
+
<input type="number" id="idle-compact-kb" min="8" max="4096" step="any" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Minimum context payload size in kilobytes (8 – 4096 KB)" placeholder="64">
|
|
1104
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.2rem; user-select:none;">KB</span>
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
|
|
1108
|
+
<div class="row" style="align-items:flex-start;">
|
|
1109
|
+
<div class="who" style="padding-top:0.25rem;">
|
|
1110
|
+
<div class="l">Summarization Model</div>
|
|
1111
|
+
<div class="meta">Select from discovered/profile models or enter custom ID (empty = deterministic truncation)</div>
|
|
1112
|
+
</div>
|
|
1113
|
+
<div style="min-width:18rem; flex:1; max-width:28rem; margin-left:auto; display:flex; flex-direction:column; gap:0.4rem;">
|
|
1114
|
+
<div class="combo" id="combo-compact-model">
|
|
1115
|
+
<input type="text" id="idle-compact-model" placeholder="empty = deterministic truncation" maxlength="512" onchange="saveIdleCompact()" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="idle-compact-model-list" autocomplete="off" spellcheck="false" style="font-family:var(--font-mono); font-size:0.8rem;" title="Upstream model used to generate turn summary text">
|
|
1116
|
+
<button type="button" class="combo-toggle" tabindex="-1" aria-label="Show available summary models">
|
|
1117
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6l4 4 4-4"/></svg>
|
|
1118
|
+
</button>
|
|
1119
|
+
<ul class="combo-list" id="idle-compact-model-list" role="listbox" aria-label="Available summarization models" hidden></ul>
|
|
1120
|
+
</div>
|
|
1121
|
+
<div id="compact-model-presets" style="display:flex; flex-wrap:wrap; align-items:center; gap:0.35rem;">
|
|
1122
|
+
<!-- Quick Presets rendered dynamically -->
|
|
1123
|
+
</div>
|
|
1124
|
+
</div>
|
|
1125
|
+
</div>
|
|
1126
|
+
|
|
1127
|
+
<div class="row">
|
|
1128
|
+
<div class="who">
|
|
1129
|
+
<div class="l">Shorten Codex CLI Sessions</div>
|
|
1130
|
+
<div class="meta">Off by default; Codex retains native turn history in local SQLite storage</div>
|
|
1131
|
+
</div>
|
|
1132
|
+
<label class="switch" title="Toggle Codex compaction" style="margin-left:auto;">
|
|
1133
|
+
<input type="checkbox" id="idle-compact-codex" onchange="saveIdleCompact()">
|
|
1134
|
+
<span></span>
|
|
1135
|
+
</label>
|
|
1136
|
+
</div>
|
|
1137
|
+
</div>
|
|
1138
|
+
|
|
1139
|
+
<!-- Card 2: Context Retention & Budget Controls -->
|
|
1140
|
+
<div class="card">
|
|
1141
|
+
<div class="cardh">
|
|
1142
|
+
<h3 style="font-size:0.95rem; font-weight:600;">Context Retention & Cache Budgets</h3>
|
|
1143
|
+
<span class="badge accent">Turn Budgets</span>
|
|
1144
|
+
</div>
|
|
1145
|
+
|
|
1146
|
+
<div class="row">
|
|
1147
|
+
<div class="who">
|
|
1148
|
+
<div class="l">Verbatim Recent Turns</div>
|
|
1149
|
+
<div class="meta">Number of most recent turns always preserved byte-for-byte</div>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1152
|
+
<input type="number" id="idle-compact-keep" min="1" max="100" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Number of most recent turns to retain verbatim (1 – 100 turns)" placeholder="6">
|
|
1153
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">turns</span>
|
|
1154
|
+
</div>
|
|
1155
|
+
</div>
|
|
1156
|
+
|
|
1157
|
+
<div class="row">
|
|
1158
|
+
<div class="who">
|
|
1159
|
+
<div class="l">Character Budget Per Earlier Turn</div>
|
|
1160
|
+
<div class="meta">Maximum character budget preserved per earlier conversation turn</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1163
|
+
<input type="number" id="idle-compact-user-chars" min="128" max="24000" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Character budget limit per earlier text turn (128 – 24000 chars)" placeholder="3000">
|
|
1164
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">chars</span>
|
|
1165
|
+
</div>
|
|
1166
|
+
</div>
|
|
1167
|
+
|
|
1168
|
+
<div class="row">
|
|
1169
|
+
<div class="who">
|
|
1170
|
+
<div class="l">Maximum Summary Length</div>
|
|
1171
|
+
<div class="meta">Upper character limit allocated for generated turn summary blocks</div>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1174
|
+
<input type="number" id="idle-compact-summary-chars" min="256" max="128000" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Upper character limit allocated for generated summary (256 – 128000 chars)" placeholder="24000">
|
|
1175
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">chars</span>
|
|
1176
|
+
</div>
|
|
1177
|
+
</div>
|
|
1178
|
+
|
|
1179
|
+
<div class="row">
|
|
1180
|
+
<div class="who">
|
|
1181
|
+
<div class="l">Cache Prefix Retention Window</div>
|
|
1182
|
+
<div class="meta">Duration in hours before idle cached prefixes are pruned from state disk</div>
|
|
1183
|
+
</div>
|
|
1184
|
+
<div style="display:flex; align-items:center; gap:0.45rem; margin-left:auto;">
|
|
1185
|
+
<input type="number" id="idle-compact-lookback" min="1" max="168" step="any" onchange="saveIdleCompact()" style="width:7.5rem; text-align:right;" title="Duration in hours before idle cached prefixes are pruned (1 – 168 hours)" placeholder="72">
|
|
1186
|
+
<span style="font-size:0.8rem; color:var(--muted); min-width:2.4rem; user-select:none;">hours</span>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
</div>
|
|
1190
|
+
|
|
1191
|
+
<!-- Card 3: Architecture & Safety Invariants -->
|
|
1192
|
+
<div class="card pad">
|
|
1193
|
+
<h3 style="font-size:0.95rem; font-weight:600; margin-bottom:0.75rem;">Edge Gateway Invariants & Storage Safety</h3>
|
|
1194
|
+
<div class="sec">
|
|
1195
|
+
<div class="copyline">
|
|
1196
|
+
<code>Zero Client Mutation: Transcripts on client disk (Claude Code / Codex) are never modified.</code>
|
|
1197
|
+
</div>
|
|
1198
|
+
<div class="copyline">
|
|
1199
|
+
<code>Atomic CAS Cache: Compare-and-swap generation counters prevent stale compaction races.</code>
|
|
1200
|
+
</div>
|
|
1201
|
+
<div class="copyline">
|
|
1202
|
+
<code>Private State Storage: Cached prefixes saved under ~/.llm-switcher/state/idle-compact/ (0600 permissions).</code>
|
|
1203
|
+
</div>
|
|
1204
|
+
<div class="copyline">
|
|
1205
|
+
<code>Deterministic Safety: If no summary model is configured, gateway uses pure heuristic truncation without external calls.</code>
|
|
1206
|
+
</div>
|
|
1207
|
+
</div>
|
|
1208
|
+
</div>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
</div>
|
|
1212
|
+
|
|
1070
1213
|
<!-- VIEW 2: Discovered Models Catalog -->
|
|
1071
1214
|
<div id="view-models" class="view-panel" style="display:none;">
|
|
1072
1215
|
<div class="sec">
|
|
@@ -1242,13 +1385,6 @@
|
|
|
1242
1385
|
</div>
|
|
1243
1386
|
|
|
1244
1387
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
1245
|
-
<label class="f">Thinking Mode
|
|
1246
|
-
<select id="p-thinking-mode">
|
|
1247
|
-
<option value="auto">AUTO (restores reasoning, strips tags safely)</option>
|
|
1248
|
-
<option value="native">NATIVE (strict reasoning_effort)</option>
|
|
1249
|
-
<option value="off">OFF (never send thinking)</option>
|
|
1250
|
-
</select>
|
|
1251
|
-
</label>
|
|
1252
1388
|
<label class="f">Routing Mode
|
|
1253
1389
|
<select id="p-mode">
|
|
1254
1390
|
<option value="convert">CONVERT (Bi-directional translation)</option>
|
|
@@ -1262,11 +1398,46 @@
|
|
|
1262
1398
|
<!-- TAB 3: Models -->
|
|
1263
1399
|
<div id="tab-models" class="tab-content" style="display:none; gap:0.9rem; flex-direction:column;">
|
|
1264
1400
|
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.75rem;">
|
|
1265
|
-
<
|
|
1401
|
+
<div>
|
|
1402
|
+
<h3 id="model-slot-heading" style="font-size:0.92rem; font-weight:600; margin:0;">Model Slots Mapping</h3>
|
|
1403
|
+
<span class="hint" style="font-size:0.75rem;">Configure target model and individual thinking effort for each slot</span>
|
|
1404
|
+
</div>
|
|
1266
1405
|
<div id="models-status" class="models-status" aria-live="polite"></div>
|
|
1267
1406
|
</div>
|
|
1268
1407
|
|
|
1269
1408
|
<div id="slot-fields" style="display:grid; gap:0.75rem;"></div>
|
|
1409
|
+
|
|
1410
|
+
<!-- Global Reasoning Policy -->
|
|
1411
|
+
<div style="border-top:1px solid var(--line); padding-top:0.85rem; margin-top:0.35rem; display:grid; gap:0.85rem;">
|
|
1412
|
+
<div style="display:flex; justify-content:space-between; align-items:center;">
|
|
1413
|
+
<h4 style="font-size:0.88rem; font-weight:600; margin:0;">Reasoning Mode & Policy</h4>
|
|
1414
|
+
<span class="hint">Controls reasoning behavior across all model slots</span>
|
|
1415
|
+
</div>
|
|
1416
|
+
|
|
1417
|
+
<div style="display:grid; grid-template-columns:1fr 1fr; gap:0.9rem;">
|
|
1418
|
+
<label class="f">Thinking Mode
|
|
1419
|
+
<select id="p-thinking-mode">
|
|
1420
|
+
<option value="auto">AUTO (restores reasoning, strips tags safely)</option>
|
|
1421
|
+
<option value="native">NATIVE (strict reasoning_effort)</option>
|
|
1422
|
+
<option value="off">OFF (never send thinking to any model)</option>
|
|
1423
|
+
</select>
|
|
1424
|
+
</label>
|
|
1425
|
+
<label class="f" id="f-thinking-effort" style="display:none;">Global Fallback Effort
|
|
1426
|
+
<select id="p-thinking-effort">
|
|
1427
|
+
<option value="auto">Auto (inherit from client tool)</option>
|
|
1428
|
+
<option value="low">Low (fast, light reasoning)</option>
|
|
1429
|
+
<option value="medium">Medium (balanced reasoning)</option>
|
|
1430
|
+
<option value="high">High (deep reasoning)</option>
|
|
1431
|
+
<option value="xhigh">Extra High (complex problem solving)</option>
|
|
1432
|
+
<option value="max">Max (maximum reasoning depth)</option>
|
|
1433
|
+
</select>
|
|
1434
|
+
</label>
|
|
1435
|
+
</div>
|
|
1436
|
+
|
|
1437
|
+
<div id="bifrost-hint" style="display:none; padding:0.6rem 0.85rem; border-radius:6px; background:rgba(59,130,246,0.08); border:1px solid rgba(59,130,246,0.25); font-size:0.82rem; color:#60a5fa; line-height:1.4;">
|
|
1438
|
+
⚡ <strong>Bifrost Passthrough:</strong> Byte-for-byte routing to intact without format conversion. Thinking effort is controlled natively by your client tool.
|
|
1439
|
+
</div>
|
|
1440
|
+
</div>
|
|
1270
1441
|
</div>
|
|
1271
1442
|
|
|
1272
1443
|
<!-- Test Connection Box -->
|
|
@@ -1474,7 +1645,7 @@
|
|
|
1474
1645
|
function handleRoute() {
|
|
1475
1646
|
const hash = window.location.hash || '#/routes';
|
|
1476
1647
|
const route = hash.replace(/^#\/?/, '').split('/')[0] || 'routes';
|
|
1477
|
-
const validRoutes = ['routes', 'models', 'logs', 'doctor'];
|
|
1648
|
+
const validRoutes = ['routes', 'compact', 'models', 'logs', 'doctor'];
|
|
1478
1649
|
const activeRoute = validRoutes.includes(route) ? route : 'routes';
|
|
1479
1650
|
|
|
1480
1651
|
// Update active nav link
|
|
@@ -1495,6 +1666,7 @@
|
|
|
1495
1666
|
updateHeader(activeRoute);
|
|
1496
1667
|
|
|
1497
1668
|
// Lazy load view content
|
|
1669
|
+
if (activeRoute === 'compact') loadIdleCompact();
|
|
1498
1670
|
if (activeRoute === 'models') renderCatalogView();
|
|
1499
1671
|
if (activeRoute === 'logs') loadInspectorLogs();
|
|
1500
1672
|
}
|
|
@@ -1512,7 +1684,6 @@
|
|
|
1512
1684
|
const activeTargets = Object.entries(activeMap).filter(([_, v]) => Boolean(v));
|
|
1513
1685
|
|
|
1514
1686
|
if (route === 'routes') {
|
|
1515
|
-
loadIdleCompact();
|
|
1516
1687
|
titleEl.textContent = 'Routes & Profiles';
|
|
1517
1688
|
if (activeTargets.length > 0) {
|
|
1518
1689
|
const parts = [];
|
|
@@ -1528,6 +1699,14 @@
|
|
|
1528
1699
|
<button class="btn btn-outline" id="btn-toggle-routes" onclick="toggleProxy()">${activeTargets.length > 0 ? 'Use official endpoints' : 'Activate compatible routes'}</button>
|
|
1529
1700
|
<button class="btn btn-outline" onclick="loadStatus()">Refresh</button>
|
|
1530
1701
|
`;
|
|
1702
|
+
} else if (route === 'compact') {
|
|
1703
|
+
loadIdleCompact();
|
|
1704
|
+
titleEl.textContent = 'Idle Compaction';
|
|
1705
|
+
subEl.textContent = 'Edge gateway prefix compaction and cache pruning for long sessions.';
|
|
1706
|
+
actionsEl.innerHTML = `
|
|
1707
|
+
<button class="btn btn-outline" onclick="loadIdleCompact()">Refresh</button>
|
|
1708
|
+
<button class="btn btn-primary" onclick="saveIdleCompact()">Save Configuration</button>
|
|
1709
|
+
`;
|
|
1531
1710
|
} else if (route === 'models') {
|
|
1532
1711
|
titleEl.textContent = 'Dynamic Model Catalog';
|
|
1533
1712
|
subEl.textContent = 'Auto-polled models from upstream providers to guarantee zero 404/drift errors.';
|
|
@@ -1675,6 +1854,11 @@
|
|
|
1675
1854
|
return defaultModels[slotId] || '';
|
|
1676
1855
|
}
|
|
1677
1856
|
|
|
1857
|
+
function effortForSlot(modelEfforts, slotId) {
|
|
1858
|
+
if (!modelEfforts) return 'auto';
|
|
1859
|
+
return modelEfforts[slotId] || 'auto';
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1678
1862
|
function renderModelSlots(slotFmt, p) {
|
|
1679
1863
|
const container = document.getElementById('slot-fields');
|
|
1680
1864
|
const heading = document.getElementById('model-slot-heading');
|
|
@@ -1687,11 +1871,27 @@
|
|
|
1687
1871
|
|
|
1688
1872
|
for (const s of cfg.slots) {
|
|
1689
1873
|
const val = currentProfileBuffer ? valueForSlot(currentProfileBuffer.defaultModels, s.id) : valueForSlot(p?.defaultModels, s.id);
|
|
1874
|
+
const effVal = currentProfileBuffer ? effortForSlot(currentProfileBuffer.modelEfforts, s.id) : effortForSlot(p?.modelEfforts, s.id);
|
|
1690
1875
|
|
|
1691
1876
|
const slotRow = document.createElement('div');
|
|
1692
|
-
slotRow.
|
|
1877
|
+
slotRow.className = 'slot-row-card';
|
|
1878
|
+
slotRow.style.cssText = 'display:flex; flex-direction:column; gap:0.35rem; padding:0.6rem 0.75rem; border-radius:6px; background:var(--card-bg, rgba(255,255,255,0.02)); border:1px solid var(--line);';
|
|
1693
1879
|
slotRow.innerHTML = `
|
|
1694
|
-
<
|
|
1880
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem;">
|
|
1881
|
+
<label for="p-${s.id}" style="font-size:0.82rem; font-weight:600; margin:0;">${escapeHtml(s.name)}</label>
|
|
1882
|
+
<div style="display:flex; align-items:center; gap:0.35rem;">
|
|
1883
|
+
<label for="p-effort-${s.id}" style="font-size:0.75rem; color:var(--muted); margin:0;">Target Effort:</label>
|
|
1884
|
+
<select id="p-effort-${s.id}" class="slot-effort inp" onchange="formDirty = true" style="height:26px; padding:0 0.4rem; font-size:0.78rem; border-radius:4px; min-width:6.8rem;">
|
|
1885
|
+
<option value="auto">Auto (Default)</option>
|
|
1886
|
+
<option value="none">None (0)</option>
|
|
1887
|
+
<option value="low">Low</option>
|
|
1888
|
+
<option value="medium">Medium</option>
|
|
1889
|
+
<option value="high">High</option>
|
|
1890
|
+
<option value="xhigh">Extra High</option>
|
|
1891
|
+
<option value="max">Max</option>
|
|
1892
|
+
</select>
|
|
1893
|
+
</div>
|
|
1894
|
+
</div>
|
|
1695
1895
|
<div class="combo">
|
|
1696
1896
|
<input type="text" id="p-${s.id}" oninput="updateWindowNotes()" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="p-${s.id}-list" autocomplete="off" spellcheck="false" placeholder="${escapeHtml(s.placeholder)}" value="${escapeHtml(val)}">
|
|
1697
1897
|
<button type="button" class="combo-toggle" tabindex="-1" aria-label="Show models for ${escapeHtml(s.name)}">
|
|
@@ -1699,10 +1899,15 @@
|
|
|
1699
1899
|
</button>
|
|
1700
1900
|
<ul class="combo-list" id="p-${s.id}-list" role="listbox" aria-label="${escapeHtml(s.name)} models" hidden></ul>
|
|
1701
1901
|
</div>
|
|
1702
|
-
<div
|
|
1902
|
+
<div style="display:flex; justify-content:space-between; align-items:center; gap:0.5rem; min-height:1rem;">
|
|
1903
|
+
<div class="slot-note" id="p-${s.id}-note" role="status" aria-live="polite"></div>
|
|
1904
|
+
<div class="slot-bifrost-hint" id="p-bifrost-${s.id}" style="display:none; font-size:0.74rem; color:#60a5fa;">⚡ Bifrost Passthrough (native effort)</div>
|
|
1905
|
+
</div>
|
|
1703
1906
|
`;
|
|
1704
1907
|
container.appendChild(slotRow);
|
|
1705
1908
|
wireCombo(slotRow.querySelector('.combo'));
|
|
1909
|
+
const effSelect = slotRow.querySelector('#p-effort-' + s.id);
|
|
1910
|
+
if (effSelect) effSelect.value = effVal || 'auto';
|
|
1706
1911
|
}
|
|
1707
1912
|
updateWindowNotes();
|
|
1708
1913
|
}
|
|
@@ -1716,6 +1921,7 @@
|
|
|
1716
1921
|
const cfg = getActiveSlotConfig(renderedSlotFormat);
|
|
1717
1922
|
if (!currentProfileBuffer) currentProfileBuffer = {};
|
|
1718
1923
|
const defaultModels = { ...(currentProfileBuffer.defaultModels || {}) };
|
|
1924
|
+
const modelEfforts = { ...(currentProfileBuffer.modelEfforts || {}) };
|
|
1719
1925
|
for (const s of cfg.slots) {
|
|
1720
1926
|
const inp = document.getElementById('p-' + s.id);
|
|
1721
1927
|
if (inp) {
|
|
@@ -1723,13 +1929,19 @@
|
|
|
1723
1929
|
if (val) defaultModels[s.id] = val;
|
|
1724
1930
|
else delete defaultModels[s.id];
|
|
1725
1931
|
}
|
|
1932
|
+
const effSelect = document.getElementById('p-effort-' + s.id);
|
|
1933
|
+
if (effSelect) {
|
|
1934
|
+
const effVal = effSelect.value;
|
|
1935
|
+
if (effVal && effVal !== 'auto') modelEfforts[s.id] = effVal;
|
|
1936
|
+
else delete modelEfforts[s.id];
|
|
1937
|
+
}
|
|
1726
1938
|
}
|
|
1727
1939
|
currentProfileBuffer.defaultModels = defaultModels;
|
|
1940
|
+
currentProfileBuffer.modelEfforts = modelEfforts;
|
|
1728
1941
|
// An old config can still carry the retired 1M flags. They are dropped on the next save
|
|
1729
1942
|
// instead of being written back: the window is the model's own now.
|
|
1730
1943
|
delete currentProfileBuffer.model1M;
|
|
1731
|
-
return { defaultModels };
|
|
1732
|
-
|
|
1944
|
+
return { defaultModels, modelEfforts };
|
|
1733
1945
|
}
|
|
1734
1946
|
|
|
1735
1947
|
// 128000 reads as 128K and 262144 as 256K: a round decimal count keeps base 1000.
|
|
@@ -1756,6 +1968,64 @@
|
|
|
1756
1968
|
const model = document.getElementById('p-' + s.id)?.value.trim() || '';
|
|
1757
1969
|
note.textContent = model ? windowLine(model) : '';
|
|
1758
1970
|
}
|
|
1971
|
+
syncEffortOptions();
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function syncEffortOptions() {
|
|
1975
|
+
const mode = document.getElementById('p-thinking-mode')?.value;
|
|
1976
|
+
const url = (document.getElementById('p-url')?.value || '').toLowerCase();
|
|
1977
|
+
const tool = document.getElementById('p-tool')?.value || 'auto';
|
|
1978
|
+
const isIntact = url.includes('intact') || url.includes(':20130') || url.includes(':8080') ||
|
|
1979
|
+
url.includes(':20142') || url.includes(':20150') || url.includes(':20152');
|
|
1980
|
+
const isThinkingOff = mode === 'off';
|
|
1981
|
+
|
|
1982
|
+
const cfg = getActiveSlotConfig(renderedSlotFormat);
|
|
1983
|
+
let anyBifrost = false;
|
|
1984
|
+
|
|
1985
|
+
for (const s of cfg.slots) {
|
|
1986
|
+
const inp = document.getElementById('p-' + s.id);
|
|
1987
|
+
const effSelect = document.getElementById('p-effort-' + s.id);
|
|
1988
|
+
const bifrostHint = document.getElementById('p-bifrost-' + s.id);
|
|
1989
|
+
if (!effSelect) continue;
|
|
1990
|
+
|
|
1991
|
+
const modelVal = (inp?.value || '').trim().toLowerCase();
|
|
1992
|
+
let slotIsBifrost = false;
|
|
1993
|
+
if (isIntact && modelVal) {
|
|
1994
|
+
if (tool === 'claude') slotIsBifrost = modelVal.startsWith('claude/') || modelVal.startsWith('anthropic/');
|
|
1995
|
+
else if (tool === 'codex') slotIsBifrost = modelVal.startsWith('codex/');
|
|
1996
|
+
else if (tool === 'agy') slotIsBifrost = modelVal.startsWith('antigravity/');
|
|
1997
|
+
else if (tool === 'auto') slotIsBifrost = modelVal.startsWith('claude/') || modelVal.startsWith('anthropic/') || modelVal.startsWith('codex/') || modelVal.startsWith('antigravity/');
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
if (slotIsBifrost) {
|
|
2001
|
+
anyBifrost = true;
|
|
2002
|
+
if (bifrostHint) bifrostHint.style.display = 'block';
|
|
2003
|
+
effSelect.disabled = true;
|
|
2004
|
+
effSelect.style.opacity = '0.6';
|
|
2005
|
+
effSelect.title = 'Bifrost passthrough active: effort controlled natively by client tool';
|
|
2006
|
+
} else {
|
|
2007
|
+
if (bifrostHint) bifrostHint.style.display = 'none';
|
|
2008
|
+
if (isThinkingOff) {
|
|
2009
|
+
effSelect.disabled = true;
|
|
2010
|
+
effSelect.style.opacity = '0.5';
|
|
2011
|
+
effSelect.title = 'Thinking mode is OFF globally';
|
|
2012
|
+
} else {
|
|
2013
|
+
effSelect.disabled = false;
|
|
2014
|
+
effSelect.style.opacity = '1';
|
|
2015
|
+
effSelect.title = 'Target thinking effort for this model';
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
const effortEl = document.getElementById('p-thinking-effort');
|
|
2021
|
+
const globalBifrostHint = document.getElementById('bifrost-hint');
|
|
2022
|
+
if (effortEl) {
|
|
2023
|
+
effortEl.disabled = isThinkingOff;
|
|
2024
|
+
effortEl.style.opacity = isThinkingOff ? '0.5' : '1';
|
|
2025
|
+
}
|
|
2026
|
+
if (globalBifrostHint) {
|
|
2027
|
+
globalBifrostHint.style.display = anyBifrost ? 'block' : 'none';
|
|
2028
|
+
}
|
|
1759
2029
|
}
|
|
1760
2030
|
|
|
1761
2031
|
// Prefetch Counts for Nav Badges
|
|
@@ -1783,6 +2053,8 @@
|
|
|
1783
2053
|
if (ctLogs && lastLogsCount > 0) ctLogs.textContent = lastLogsCount;
|
|
1784
2054
|
}
|
|
1785
2055
|
} catch {}
|
|
2056
|
+
|
|
2057
|
+
loadIdleCompact();
|
|
1786
2058
|
}
|
|
1787
2059
|
|
|
1788
2060
|
// Load Status
|
|
@@ -1790,16 +2062,125 @@
|
|
|
1790
2062
|
// dashboard and the terminal cannot show two different settings.
|
|
1791
2063
|
function showIdleCompact(c) {
|
|
1792
2064
|
c = c || {};
|
|
1793
|
-
|
|
1794
|
-
document.getElementById('idle-compact-
|
|
1795
|
-
|
|
1796
|
-
document.getElementById('idle-compact-
|
|
2065
|
+
const enabled = !!c.enabled;
|
|
2066
|
+
const elEnabled = document.getElementById('idle-compact-enabled');
|
|
2067
|
+
if (elEnabled) elEnabled.checked = enabled;
|
|
2068
|
+
const configSec = document.getElementById('idle-compact-config-section');
|
|
2069
|
+
if (configSec) configSec.style.display = enabled ? 'flex' : 'none';
|
|
2070
|
+
const elMinutes = document.getElementById('idle-compact-minutes');
|
|
2071
|
+
if (elMinutes) elMinutes.value = c.idleMinutes ?? 15;
|
|
2072
|
+
const elKb = document.getElementById('idle-compact-kb');
|
|
2073
|
+
if (elKb) elKb.value = (c.minBytes ?? 65536) / 1024;
|
|
2074
|
+
const elModel = document.getElementById('idle-compact-model');
|
|
2075
|
+
if (elModel) {
|
|
2076
|
+
elModel.value = c.model || '';
|
|
2077
|
+
updateCompactModelPresets(c.model || '');
|
|
2078
|
+
}
|
|
2079
|
+
const elCodex = document.getElementById('idle-compact-codex');
|
|
2080
|
+
if (elCodex) elCodex.checked = !!c.codex;
|
|
2081
|
+
const elKeep = document.getElementById('idle-compact-keep');
|
|
2082
|
+
if (elKeep) elKeep.value = c.keepRecent ?? 6;
|
|
2083
|
+
const elUserChars = document.getElementById('idle-compact-user-chars');
|
|
2084
|
+
if (elUserChars) elUserChars.value = c.userChars ?? 3000;
|
|
2085
|
+
const elSummaryChars = document.getElementById('idle-compact-summary-chars');
|
|
2086
|
+
if (elSummaryChars) elSummaryChars.value = c.summaryMaxChars ?? 24000;
|
|
2087
|
+
const elLookback = document.getElementById('idle-compact-lookback');
|
|
2088
|
+
if (elLookback) elLookback.value = c.sessionLookbackHours ?? 72;
|
|
2089
|
+
|
|
2090
|
+
// Badge on hero card
|
|
1797
2091
|
const badge = document.getElementById('badge-idle-compact');
|
|
1798
|
-
badge
|
|
1799
|
-
|
|
2092
|
+
if (badge) {
|
|
2093
|
+
badge.textContent = enabled ? 'ACTIVE' : 'OFF';
|
|
2094
|
+
badge.className = 'badge ' + (enabled ? 'ok' : 'outline');
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
// Nav link count
|
|
2098
|
+
const ctCompact = document.getElementById('ct-compact');
|
|
2099
|
+
if (ctCompact) {
|
|
2100
|
+
ctCompact.textContent = enabled ? 'ON' : '';
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
// Stats cards
|
|
2104
|
+
const statStatus = document.getElementById('stat-compact-status');
|
|
2105
|
+
if (statStatus) {
|
|
2106
|
+
statStatus.textContent = enabled ? 'Active' : 'Disabled';
|
|
2107
|
+
statStatus.style.color = enabled ? 'var(--ok)' : 'var(--muted)';
|
|
2108
|
+
}
|
|
2109
|
+
const statInd = document.getElementById('stat-compact-indicator');
|
|
2110
|
+
if (statInd) {
|
|
2111
|
+
statInd.textContent = enabled ? 'Prefix caching active' : 'Prefix caching inactive';
|
|
2112
|
+
statInd.className = 'status ' + (enabled ? 'on' : '');
|
|
2113
|
+
}
|
|
2114
|
+
const statPause = document.getElementById('stat-compact-pause');
|
|
2115
|
+
if (statPause) {
|
|
2116
|
+
statPause.textContent = (c.idleMinutes ?? 15) + ' min';
|
|
2117
|
+
}
|
|
2118
|
+
const statSize = document.getElementById('stat-compact-size');
|
|
2119
|
+
if (statSize) {
|
|
2120
|
+
statSize.textContent = ((c.minBytes ?? 65536) / 1024) + ' KB';
|
|
2121
|
+
}
|
|
2122
|
+
const statLookback = document.getElementById('stat-compact-lookback');
|
|
2123
|
+
if (statLookback) {
|
|
2124
|
+
statLookback.textContent = (c.sessionLookbackHours ?? 72) + ' hrs';
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
async function ensureCatalogLoaded() {
|
|
2129
|
+
if (cachedCatalog) return cachedCatalog;
|
|
2130
|
+
try {
|
|
2131
|
+
const res = await api('/api/catalog');
|
|
2132
|
+
if (res.ok) cachedCatalog = await res.json();
|
|
2133
|
+
} catch {}
|
|
2134
|
+
return cachedCatalog;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
function setCompactModel(model) {
|
|
2138
|
+
const inp = document.getElementById('idle-compact-model');
|
|
2139
|
+
if (inp) {
|
|
2140
|
+
inp.value = model;
|
|
2141
|
+
saveIdleCompact();
|
|
2142
|
+
updateCompactModelPresets(model);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
function updateCompactModelPresets(currentModel) {
|
|
2147
|
+
const container = document.getElementById('compact-model-presets');
|
|
2148
|
+
if (!container) return;
|
|
2149
|
+
if (currentModel === undefined) {
|
|
2150
|
+
currentModel = document.getElementById('idle-compact-model')?.value.trim() || '';
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
const presets = [
|
|
2154
|
+
{ id: '', label: 'Deterministic (none)' },
|
|
2155
|
+
{ id: 'haiku', label: 'haiku' },
|
|
2156
|
+
{ id: 'subagent', label: 'subagent' }
|
|
2157
|
+
];
|
|
2158
|
+
|
|
2159
|
+
const profiles = state.config?.profiles || {};
|
|
2160
|
+
let added = 0;
|
|
2161
|
+
for (const p of Object.values(profiles)) {
|
|
2162
|
+
if (p.defaultModels) {
|
|
2163
|
+
for (const [slot, m] of Object.entries(p.defaultModels)) {
|
|
2164
|
+
if (m && added < 2 && !presets.some(x => x.id === m)) {
|
|
2165
|
+
presets.push({ id: m, label: m.includes('/') ? m.split('/')[1] : m });
|
|
2166
|
+
added++;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
container.innerHTML = `<span class="hint" style="font-size:0.75rem; align-self:center; margin-right:0.2rem;">Quick Presets:</span>` +
|
|
2173
|
+
presets.map(p => {
|
|
2174
|
+
const active = (p.id === currentModel);
|
|
2175
|
+
const style = active
|
|
2176
|
+
? 'padding:0.18rem 0.5rem; font-size:0.75rem; border-color:var(--accent); color:var(--accent); background:var(--accent-soft); font-weight:600;'
|
|
2177
|
+
: 'padding:0.18rem 0.5rem; font-size:0.75rem;';
|
|
2178
|
+
return `<button type="button" class="btn btn-outline btn-sm" style="${style}" onclick="setCompactModel('${escapeHtml(p.id)}')">${escapeHtml(p.label)}</button>`;
|
|
2179
|
+
}).join('');
|
|
1800
2180
|
}
|
|
1801
2181
|
|
|
1802
2182
|
async function loadIdleCompact() {
|
|
2183
|
+
ensureCatalogLoaded();
|
|
1803
2184
|
try {
|
|
1804
2185
|
const r = await api('/api/idle-compact');
|
|
1805
2186
|
if (r.ok) showIdleCompact((await r.json()).idleCompact);
|
|
@@ -1807,14 +2188,35 @@
|
|
|
1807
2188
|
}
|
|
1808
2189
|
|
|
1809
2190
|
async function saveIdleCompact() {
|
|
2191
|
+
const isEnabled = document.getElementById('idle-compact-enabled').checked;
|
|
2192
|
+
const configSec = document.getElementById('idle-compact-config-section');
|
|
2193
|
+
if (configSec) configSec.style.display = isEnabled ? 'flex' : 'none';
|
|
1810
2194
|
const status = document.getElementById('idle-compact-status');
|
|
2195
|
+
for (const input of document.querySelectorAll('[id^="idle-compact-"][type="number"]')) {
|
|
2196
|
+
if (!input.value || !input.checkValidity()) {
|
|
2197
|
+
input.reportValidity();
|
|
2198
|
+
if (status) {
|
|
2199
|
+
status.textContent = 'Not saved: enter a value within the displayed bounds.';
|
|
2200
|
+
status.style.color = 'var(--err)';
|
|
2201
|
+
}
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
1811
2205
|
const patch = {
|
|
2206
|
+
codex: document.getElementById('idle-compact-codex').checked,
|
|
2207
|
+
keepRecent: Number(document.getElementById('idle-compact-keep').value),
|
|
2208
|
+
userChars: Number(document.getElementById('idle-compact-user-chars').value),
|
|
2209
|
+
summaryMaxChars: Number(document.getElementById('idle-compact-summary-chars').value),
|
|
2210
|
+
sessionLookbackHours: Number(document.getElementById('idle-compact-lookback').value),
|
|
1812
2211
|
enabled: document.getElementById('idle-compact-enabled').checked,
|
|
1813
|
-
idleMinutes: Number(document.getElementById('idle-compact-minutes').value)
|
|
1814
|
-
minBytes: (Number(document.getElementById('idle-compact-kb').value)
|
|
2212
|
+
idleMinutes: Number(document.getElementById('idle-compact-minutes').value),
|
|
2213
|
+
minBytes: Math.round(Number(document.getElementById('idle-compact-kb').value) * 1024),
|
|
1815
2214
|
model: document.getElementById('idle-compact-model').value.trim()
|
|
1816
2215
|
};
|
|
1817
|
-
status
|
|
2216
|
+
if (status) {
|
|
2217
|
+
status.textContent = 'Saving...';
|
|
2218
|
+
status.style.color = 'var(--muted)';
|
|
2219
|
+
}
|
|
1818
2220
|
try {
|
|
1819
2221
|
const r = await api('/api/idle-compact', {
|
|
1820
2222
|
method: 'POST',
|
|
@@ -1823,9 +2225,18 @@
|
|
|
1823
2225
|
});
|
|
1824
2226
|
if (!r.ok) throw new Error((await r.json()).error || r.status);
|
|
1825
2227
|
showIdleCompact((await r.json()).idleCompact);
|
|
1826
|
-
status
|
|
2228
|
+
if (status) {
|
|
2229
|
+
status.textContent = 'Saved. Applies to subsequent conversations that resume after a pause.';
|
|
2230
|
+
status.style.color = 'var(--ok)';
|
|
2231
|
+
setTimeout(() => { if (status) status.textContent = ''; }, 4500);
|
|
2232
|
+
}
|
|
2233
|
+
showToast('Idle compaction configuration saved');
|
|
1827
2234
|
} catch (e) {
|
|
1828
|
-
status
|
|
2235
|
+
if (status) {
|
|
2236
|
+
status.textContent = 'Not saved: ' + e.message;
|
|
2237
|
+
status.style.color = 'var(--err)';
|
|
2238
|
+
}
|
|
2239
|
+
showToast('Failed to save: ' + e.message, true);
|
|
1829
2240
|
}
|
|
1830
2241
|
}
|
|
1831
2242
|
|
|
@@ -2036,6 +2447,7 @@
|
|
|
2036
2447
|
formLoadedKey = key;
|
|
2037
2448
|
formDirty = false;
|
|
2038
2449
|
currentProfileBuffer = JSON.parse(JSON.stringify(p));
|
|
2450
|
+
if (!currentProfileBuffer.modelEfforts) currentProfileBuffer.modelEfforts = {};
|
|
2039
2451
|
|
|
2040
2452
|
document.getElementById('form-title').textContent = 'Edit Profile: ' + p.name;
|
|
2041
2453
|
document.getElementById('p-key').value = key;
|
|
@@ -2048,6 +2460,11 @@
|
|
|
2048
2460
|
document.getElementById('p-outformat').value = p.outFormat || '';
|
|
2049
2461
|
document.getElementById('p-mode').value = p.mode || 'convert';
|
|
2050
2462
|
document.getElementById('p-thinking-mode').value = p.thinkingMode || 'auto';
|
|
2463
|
+
if (document.getElementById('p-thinking-effort')) {
|
|
2464
|
+
document.getElementById('p-thinking-effort').value = p.thinkingEffort || 'auto';
|
|
2465
|
+
}
|
|
2466
|
+
renderModelSlots(toolVal, p);
|
|
2467
|
+
syncEffortOptions();
|
|
2051
2468
|
document.getElementById('btn-delete').style.display = 'inline-flex';
|
|
2052
2469
|
|
|
2053
2470
|
const activeMap = state.activeProfiles || (state.config?.activeProfiles || {});
|
|
@@ -2056,8 +2473,6 @@
|
|
|
2056
2473
|
if (activeTag) {
|
|
2057
2474
|
activeTag.textContent = activeTargets.length ? '● Active: ' + activeTargets.join(', ') : '';
|
|
2058
2475
|
}
|
|
2059
|
-
|
|
2060
|
-
renderModelSlots(toolVal, p);
|
|
2061
2476
|
}
|
|
2062
2477
|
|
|
2063
2478
|
function initNewProfile() {
|
|
@@ -2072,7 +2487,8 @@
|
|
|
2072
2487
|
outFormat: 'openai-chat',
|
|
2073
2488
|
mode: 'convert',
|
|
2074
2489
|
thinkingMode: 'auto',
|
|
2075
|
-
defaultModels: {}
|
|
2490
|
+
defaultModels: {},
|
|
2491
|
+
modelEfforts: {}
|
|
2076
2492
|
};
|
|
2077
2493
|
|
|
2078
2494
|
document.getElementById('form-title').textContent = 'Add New Provider Profile';
|
|
@@ -2085,10 +2501,14 @@
|
|
|
2085
2501
|
document.getElementById('p-outformat').value = 'openai-chat';
|
|
2086
2502
|
document.getElementById('p-mode').value = 'convert';
|
|
2087
2503
|
document.getElementById('p-thinking-mode').value = 'auto';
|
|
2504
|
+
if (document.getElementById('p-thinking-effort')) {
|
|
2505
|
+
document.getElementById('p-thinking-effort').value = 'auto';
|
|
2506
|
+
}
|
|
2507
|
+
renderModelSlots('auto', currentProfileBuffer);
|
|
2508
|
+
syncEffortOptions();
|
|
2088
2509
|
document.getElementById('btn-delete').style.display = 'none';
|
|
2089
2510
|
document.getElementById('active-tag').textContent = '';
|
|
2090
2511
|
|
|
2091
|
-
renderModelSlots('auto', currentProfileBuffer);
|
|
2092
2512
|
switchTab('tab-general');
|
|
2093
2513
|
}
|
|
2094
2514
|
|
|
@@ -2163,6 +2583,8 @@
|
|
|
2163
2583
|
document.getElementById('p-outformat').value = t.outFormat;
|
|
2164
2584
|
document.getElementById('p-mode').value = t.mode;
|
|
2165
2585
|
document.getElementById('p-thinking-mode').value = t.thinkingMode;
|
|
2586
|
+
document.getElementById('p-thinking-effort').value = t.thinkingEffort || 'auto';
|
|
2587
|
+
syncEffortOptions();
|
|
2166
2588
|
currentProfileBuffer = { ...currentProfileBuffer, ...t };
|
|
2167
2589
|
formDirty = true;
|
|
2168
2590
|
renderModelSlots(t.tool || 'auto', t);
|
|
@@ -2218,6 +2640,13 @@
|
|
|
2218
2640
|
// already had the key.
|
|
2219
2641
|
const thinkingMode = document.getElementById('p-thinking-mode').value;
|
|
2220
2642
|
if (thinkingMode !== 'auto' || existing.thinkingMode) profilePayload.thinkingMode = thinkingMode;
|
|
2643
|
+
const thinkingEffort = document.getElementById('p-thinking-effort')?.value;
|
|
2644
|
+
if (thinkingEffort && (thinkingEffort !== 'auto' || existing.thinkingEffort)) {
|
|
2645
|
+
profilePayload.thinkingEffort = thinkingEffort;
|
|
2646
|
+
}
|
|
2647
|
+
if (currentProfileBuffer?.modelEfforts && Object.keys(currentProfileBuffer.modelEfforts).length > 0) {
|
|
2648
|
+
profilePayload.modelEfforts = { ...currentProfileBuffer.modelEfforts };
|
|
2649
|
+
}
|
|
2221
2650
|
|
|
2222
2651
|
if (keyVal === MASKED_KEY) {
|
|
2223
2652
|
profilePayload.apiKey = existing.apiKey || '';
|
|
@@ -2428,7 +2857,14 @@
|
|
|
2428
2857
|
function wireCombo(combo) {
|
|
2429
2858
|
const input = combo.querySelector('input');
|
|
2430
2859
|
combo.activeIndex = -1;
|
|
2431
|
-
input.addEventListener('input', () => {
|
|
2860
|
+
input.addEventListener('input', () => {
|
|
2861
|
+
combo.dataset.filter = '1';
|
|
2862
|
+
combo.activeIndex = -1;
|
|
2863
|
+
openCombo(combo);
|
|
2864
|
+
if (input.id === 'idle-compact-model') {
|
|
2865
|
+
updateCompactModelPresets(input.value.trim());
|
|
2866
|
+
}
|
|
2867
|
+
});
|
|
2432
2868
|
input.addEventListener('keydown', onComboKeydown);
|
|
2433
2869
|
input.addEventListener('blur', () => closeCombo(combo));
|
|
2434
2870
|
// Keep the focus in the field while the list or the arrow is used.
|
|
@@ -2492,6 +2928,12 @@
|
|
|
2492
2928
|
const list = combo.querySelector('.combo-list');
|
|
2493
2929
|
const q = combo.dataset.filter ? input.value.trim().toLowerCase() : '';
|
|
2494
2930
|
const current = input.value.trim();
|
|
2931
|
+
|
|
2932
|
+
if (input.id === 'idle-compact-model') {
|
|
2933
|
+
renderCompactModelOptions(combo, q, current);
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2495
2937
|
const matches = loadedModels.filter(m => !q || m.toLowerCase().includes(q)).slice(0, 200);
|
|
2496
2938
|
combo.matches = matches;
|
|
2497
2939
|
if (!loadedModels.length) {
|
|
@@ -2515,6 +2957,119 @@
|
|
|
2515
2957
|
setComboActive(combo, combo.activeIndex);
|
|
2516
2958
|
}
|
|
2517
2959
|
|
|
2960
|
+
function getAvailableCompactModels() {
|
|
2961
|
+
const items = [];
|
|
2962
|
+
const seen = new Set();
|
|
2963
|
+
|
|
2964
|
+
const add = (id, label, desc, category) => {
|
|
2965
|
+
if (!seen.has(id)) {
|
|
2966
|
+
seen.add(id);
|
|
2967
|
+
items.push({ id, label: label || id, desc: desc || '', category });
|
|
2968
|
+
}
|
|
2969
|
+
};
|
|
2970
|
+
|
|
2971
|
+
// 1. Deterministic option (empty string)
|
|
2972
|
+
add('', 'Deterministic Truncation (no external call)', 'Pure heuristic context truncation · Zero latency & zero tokens', 'Deterministic');
|
|
2973
|
+
|
|
2974
|
+
// 2. Profile role/slot aliases (recommended for routing)
|
|
2975
|
+
add('haiku', 'haiku (Claude Haiku Slot)', 'Auto-resolves to active profile Claude Haiku model', 'Slot Aliases');
|
|
2976
|
+
add('subagent', 'subagent (Codex Subagent Slot)', 'Auto-resolves to active profile Codex Subagent model', 'Slot Aliases');
|
|
2977
|
+
add('sonnet', 'sonnet (Claude Sonnet Slot)', 'Auto-resolves to active profile Claude Sonnet model', 'Slot Aliases');
|
|
2978
|
+
add('main', 'main (Codex Main Slot)', 'Auto-resolves to active profile Codex Main model', 'Slot Aliases');
|
|
2979
|
+
|
|
2980
|
+
// 3. Models configured in current profiles
|
|
2981
|
+
const profiles = state.config?.profiles || {};
|
|
2982
|
+
for (const [profKey, p] of Object.entries(profiles)) {
|
|
2983
|
+
const profName = p.name || profKey;
|
|
2984
|
+
if (p.defaultModels) {
|
|
2985
|
+
for (const [slot, m] of Object.entries(p.defaultModels)) {
|
|
2986
|
+
if (m) add(m, m, `${profName} · ${slot} slot`, 'Profile Models');
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (Array.isArray(p.publicModels)) {
|
|
2990
|
+
for (const m of p.publicModels) {
|
|
2991
|
+
if (m) add(m, m, `${profName}`, 'Profile Models');
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
// 4. Discovered Catalog models
|
|
2997
|
+
if (cachedCatalog) {
|
|
2998
|
+
if (Array.isArray(cachedCatalog.claude?.models)) {
|
|
2999
|
+
for (const m of cachedCatalog.claude.models) {
|
|
3000
|
+
if (m.id) add(m.id, m.id, `Claude Catalog · ${m.tier || ''}`, 'Catalog');
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
if (Array.isArray(cachedCatalog.codex?.models)) {
|
|
3004
|
+
for (const m of cachedCatalog.codex.models) {
|
|
3005
|
+
if (m.id) add(m.id, m.id, `Codex Catalog · ${m.role || ''}`, 'Catalog');
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// 5. Popular lightweight models
|
|
3011
|
+
const popular = [
|
|
3012
|
+
{ id: 'zencore/muse-spark-1.3-contributor-free', desc: 'ZenCore contributor fast model' },
|
|
3013
|
+
{ id: 'zencore/exo-free', desc: 'ZenCore free pool fast model' },
|
|
3014
|
+
{ id: 'claude-3-5-haiku-20241022', desc: 'Anthropic lightweight summarizer' },
|
|
3015
|
+
{ id: 'gpt-4o-mini', desc: 'OpenAI low-cost summarizer' },
|
|
3016
|
+
{ id: 'gemini-2.5-flash', desc: 'Google Gemini fast summarizer' }
|
|
3017
|
+
];
|
|
3018
|
+
for (const p of popular) {
|
|
3019
|
+
add(p.id, p.id, p.desc, 'Recommended');
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
return items;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
function renderCompactModelOptions(combo, q, current) {
|
|
3026
|
+
const input = combo.querySelector('input');
|
|
3027
|
+
const list = combo.querySelector('.combo-list');
|
|
3028
|
+
const allItems = getAvailableCompactModels();
|
|
3029
|
+
|
|
3030
|
+
const filtered = allItems.filter(item => {
|
|
3031
|
+
if (!q) return true;
|
|
3032
|
+
if (item.id === '' && ('deterministic'.includes(q) || 'none'.includes(q) || 'empty'.includes(q) || 'heuristic'.includes(q))) return true;
|
|
3033
|
+
return item.id.toLowerCase().includes(q) || item.label.toLowerCase().includes(q) || item.desc.toLowerCase().includes(q);
|
|
3034
|
+
}).slice(0, 150);
|
|
3035
|
+
|
|
3036
|
+
combo.matches = filtered.map(item => item.id);
|
|
3037
|
+
|
|
3038
|
+
if (!filtered.length) {
|
|
3039
|
+
list.innerHTML = '<li class="combo-empty">No matching model found. The typed model will be used directly.</li>';
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
if (!(combo.activeIndex >= 0 && combo.activeIndex < filtered.length)) {
|
|
3044
|
+
combo.activeIndex = filtered.findIndex(item => item.id === current);
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
list.innerHTML = filtered.map((item, i) => {
|
|
3048
|
+
const isSelected = item.id === current;
|
|
3049
|
+
let badgeClass = 'badge outline';
|
|
3050
|
+
if (item.category === 'Deterministic') badgeClass = 'badge ok';
|
|
3051
|
+
else if (item.category === 'Slot Aliases') badgeClass = 'badge accent';
|
|
3052
|
+
else if (item.category === 'Profile Models') badgeClass = 'badge';
|
|
3053
|
+
|
|
3054
|
+
const cut = item.id.lastIndexOf('/') + 1;
|
|
3055
|
+
const prefix = cut ? `<span class="pfx">${markMatch(item.id.slice(0, cut), q)}</span>` : '';
|
|
3056
|
+
const mainText = item.id ? `${prefix}${markMatch(item.id.slice(cut), q)}` : markMatch(item.label, q);
|
|
3057
|
+
|
|
3058
|
+
return `<li class="combo-opt" role="option" id="${input.id}-opt-${i}" data-i="${i}" aria-selected="${i === combo.activeIndex}" style="display:flex; flex-direction:column; gap:0.18rem; padding:0.45rem 0.6rem; border-bottom:1px solid rgba(255,255,255,0.04);" onmousedown="event.preventDefault(); chooseComboOption(this)">`
|
|
3059
|
+
+ `<div style="display:flex; align-items:center; justify-content:space-between; gap:0.5rem; width:100%;">`
|
|
3060
|
+
+ `<span class="id" style="font-weight:600;">${mainText}</span>`
|
|
3061
|
+
+ `<div style="display:flex; align-items:center; gap:0.4rem; flex:none;">`
|
|
3062
|
+
+ `<span class="${badgeClass}" style="font-size:0.68rem; padding:0.1rem 0.35rem;">${escapeHtml(item.category)}</span>`
|
|
3063
|
+
+ (isSelected ? '<span class="cur" style="font-weight:600;">active</span>' : '')
|
|
3064
|
+
+ `</div>`
|
|
3065
|
+
+ `</div>`
|
|
3066
|
+
+ (item.desc && item.id ? `<div style="font-size:0.72rem; color:var(--muted); font-family:var(--font-sans);">${escapeHtml(item.desc)}</div>` : '')
|
|
3067
|
+
+ `</li>`;
|
|
3068
|
+
}).join('');
|
|
3069
|
+
|
|
3070
|
+
setComboActive(combo, combo.activeIndex);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
2518
3073
|
function setComboActive(combo, i) {
|
|
2519
3074
|
const input = combo.querySelector('input');
|
|
2520
3075
|
combo.activeIndex = i;
|
|
@@ -2533,12 +3088,18 @@
|
|
|
2533
3088
|
|
|
2534
3089
|
function chooseComboOption(li) {
|
|
2535
3090
|
const combo = li.closest('.combo');
|
|
2536
|
-
const
|
|
2537
|
-
|
|
2538
|
-
|
|
3091
|
+
const idx = Number(li.dataset.i);
|
|
3092
|
+
const model = combo?.matches?.[idx];
|
|
3093
|
+
if (model === undefined) return;
|
|
3094
|
+
const input = combo.querySelector('input');
|
|
3095
|
+
input.value = model;
|
|
2539
3096
|
formDirty = true;
|
|
2540
3097
|
closeCombo(combo);
|
|
2541
3098
|
updateWindowNotes();
|
|
3099
|
+
if (input.id === 'idle-compact-model') {
|
|
3100
|
+
saveIdleCompact();
|
|
3101
|
+
updateCompactModelPresets(model);
|
|
3102
|
+
}
|
|
2542
3103
|
}
|
|
2543
3104
|
|
|
2544
3105
|
function onComboKeydown(e) {
|
|
@@ -2873,6 +3434,17 @@
|
|
|
2873
3434
|
// Setup log auto-refresh
|
|
2874
3435
|
toggleLogAutoRefresh();
|
|
2875
3436
|
|
|
3437
|
+
// Setup profile modal effort & bifrost change triggers
|
|
3438
|
+
document.getElementById('p-thinking-mode')?.addEventListener('change', syncEffortOptions);
|
|
3439
|
+
document.getElementById('p-mode')?.addEventListener('change', syncEffortOptions);
|
|
3440
|
+
document.getElementById('p-outformat')?.addEventListener('change', syncEffortOptions);
|
|
3441
|
+
document.getElementById('p-url')?.addEventListener('input', syncEffortOptions);
|
|
3442
|
+
document.getElementById('p-tool')?.addEventListener('change', syncEffortOptions);
|
|
3443
|
+
|
|
3444
|
+
// Wire Idle Compaction summarization model combobox
|
|
3445
|
+
const compactCombo = document.getElementById('combo-compact-model');
|
|
3446
|
+
if (compactCombo) wireCombo(compactCombo);
|
|
3447
|
+
|
|
2876
3448
|
// Trigger initial route
|
|
2877
3449
|
handleRoute();
|
|
2878
3450
|
|