llm-switcher 1.6.5 → 1.6.7
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 +12 -0
- package/compaction.mjs +72 -0
- package/docs/goal-check.md +75 -0
- package/formats.mjs +107 -35
- package/goalcheck-call.mjs +208 -0
- package/goalcheck.mjs +498 -0
- package/maintenance/gateway-upstream-watch/REPOSITORIES.md +64 -0
- package/maintenance/gateway-upstream-watch/SKILL.md +18 -0
- package/maintenance/gateway-upstream-watch/prompt.vi.md +11 -0
- package/maintenance/gateway-upstream-watch/references/repos.json +570 -0
- package/maintenance/gateway-upstream-watch/watch.py +222 -0
- package/package.json +1 -1
- package/preflight-check.mjs +188 -0
- package/proxy.mjs +393 -53
- package/shim.mjs +13 -2
- package/state.mjs +5 -0
- package/tests/codex-daemon.test.mjs +1 -0
- package/tests/compaction.e2e.test.mjs +79 -0
- package/tests/gateway-contract.test.mjs +59 -0
- package/tests/gateway.e2e.test.mjs +47 -0
- package/tests/goalcheck.e2e.test.mjs +316 -0
- package/tests/goalcheck.test.mjs +312 -0
- package/tests/preflight.test.mjs +119 -0
- package/tests/tool-repair.test.mjs +119 -2
- package/tests/update.test.mjs +2 -2
- package/tests/upstream-stream.test.mjs +30 -0
- package/toolvocab.mjs +59 -11
- package/ui.html +676 -1
- package/upstream-stream.mjs +37 -0
package/proxy.mjs
CHANGED
|
@@ -27,16 +27,32 @@ import {
|
|
|
27
27
|
contractLabSettings, STATE_DIR, stopRecordedBlindfold
|
|
28
28
|
} from './state.mjs';
|
|
29
29
|
import { parseModelWindows, resolveProfileWindows, readLocalCodexWindows } from './windows.mjs';
|
|
30
|
-
import { createToolVocab, NULL_TOOL_VOCAB } from './toolvocab.mjs';
|
|
30
|
+
import { createToolVocab, NULL_TOOL_VOCAB, toolNameMapForIR } from './toolvocab.mjs';
|
|
31
|
+
import { readUpstreamPayloads } from './upstream-stream.mjs';
|
|
32
|
+
import { compactionResponse, emitCompaction, expandGatewayCompactions, collectCompaction } from './compaction.mjs';
|
|
31
33
|
import { classifyCodexRole, classifyClaudeTier, syncLocalCatalog, refreshCatalog, checkVersionAndRefresh } from './catalog.mjs';
|
|
32
34
|
import { checkForUpdate } from './version.mjs';
|
|
33
35
|
import { applyUpdate } from './update.mjs';
|
|
34
36
|
import { createContractLab, createHalfTap, tapClientWrites, capText, capJson, toolVersionFromUA, finishHalf, PROBE_HEADER, TRACE_ID_RE } from './contract.mjs';
|
|
35
37
|
import {
|
|
36
|
-
idleCompactPolicy, validateIdleCompactPatch, compactRawHistory, summaryReplacement, IDLE_COMPACT_KEY,
|
|
38
|
+
idleCompactPolicy, validateIdleCompactPatch, compactRawHistory, summaryReplacement, IDLE_COMPACT_KEY, conversationKey
|
|
37
39
|
} from './idlecompact.mjs';
|
|
38
40
|
import { askSummary } from './idlecall.mjs';
|
|
39
41
|
import { IdlePrefixCache, compactCacheKey, policyFingerprint } from './idlecache.mjs';
|
|
42
|
+
import {
|
|
43
|
+
GOAL_CHECK_KEY, goalCheckPolicy, validateGoalCheckPatch, redactGoalCheck,
|
|
44
|
+
isGoalEvaluatorRequest, buildBoundedEvidence, buildJevQuestions,
|
|
45
|
+
combineVerdicts, parseEvaluatorVerdict
|
|
46
|
+
} from './goalcheck.mjs';
|
|
47
|
+
import { callJev, testJevConnection, resolveJevTarget } from './goalcheck-call.mjs';
|
|
48
|
+
|
|
49
|
+
const MAX_GOAL_CHECK_LOGS = 64;
|
|
50
|
+
const goalCheckLogs = [];
|
|
51
|
+
const randStr = (n = 6) => crypto.randomBytes(Math.ceil(n / 2)).toString('hex').slice(0, n);
|
|
52
|
+
function logGoalCheck(entry) {
|
|
53
|
+
if (goalCheckLogs.length >= MAX_GOAL_CHECK_LOGS) goalCheckLogs.shift();
|
|
54
|
+
goalCheckLogs.push({ id: `gc_${Date.now()}_${randStr(4)}`, timestamp: Date.now(), ...entry });
|
|
55
|
+
}
|
|
40
56
|
|
|
41
57
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
58
|
const uiHtmlPath = path.join(__dirname, 'ui.html');
|
|
@@ -454,53 +470,13 @@ function upstreamEndpoint(profile, outFormat, model, stream, req) {
|
|
|
454
470
|
return { url: ov['openai-chat'] || `${base}/chat/completions`, headers };
|
|
455
471
|
}
|
|
456
472
|
|
|
457
|
-
// Read the upstream stream: accept both SSE `data:` and raw JSON lines (Vertex framing).
|
|
458
|
-
async function* readUpstreamPayloads(upstreamRes) {
|
|
459
|
-
const reader = upstreamRes.body.getReader();
|
|
460
|
-
const decoder = new TextDecoder('utf8');
|
|
461
|
-
let buffer = '';
|
|
462
|
-
let finished = false;
|
|
463
|
-
try {
|
|
464
|
-
while (true) {
|
|
465
|
-
const { done, value } = await reader.read();
|
|
466
|
-
if (done) {
|
|
467
|
-
finished = true;
|
|
468
|
-
break;
|
|
469
|
-
}
|
|
470
|
-
buffer += decoder.decode(value, { stream: true });
|
|
471
|
-
const lines = buffer.split('\n');
|
|
472
|
-
buffer = lines.pop();
|
|
473
|
-
for (const line of lines) {
|
|
474
|
-
const parsed = parsePayloadLine(line);
|
|
475
|
-
if (parsed) yield parsed;
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
buffer += decoder.decode();
|
|
479
|
-
const tail = parsePayloadLine(buffer);
|
|
480
|
-
if (tail) yield tail;
|
|
481
|
-
} finally {
|
|
482
|
-
// A consumer that stops early (an in-band error) must close the upstream connection too.
|
|
483
|
-
if (!finished) await reader.cancel().catch(() => {});
|
|
484
|
-
try { reader.releaseLock(); } catch {}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function parsePayloadLine(line) {
|
|
489
|
-
let s = line.trim();
|
|
490
|
-
if (!s) return null;
|
|
491
|
-
if (s.startsWith('data:')) s = s.slice(5).trim();
|
|
492
|
-
else if (s.startsWith('event:') || s.startsWith(':') || s.startsWith('id:') || s.startsWith('retry:')) return null;
|
|
493
|
-
if (!s || s === '[DONE]' || !s.startsWith('{')) return null;
|
|
494
|
-
try { return JSON.parse(s); } catch { return null; }
|
|
495
|
-
}
|
|
496
|
-
|
|
497
473
|
// The tool vocabulary the caller declared, so a model that answers with a name from its own
|
|
498
474
|
// training is written back in the caller's words instead of ending the turn with "No such tool
|
|
499
475
|
// available". Built once per request from the tools the caller actually sent, so a request that
|
|
500
476
|
// declared none pays nothing and a turn where the model agreed with the list changes nothing.
|
|
501
|
-
function toolVocabFor(ir) {
|
|
477
|
+
function toolVocabFor(ir, outFormat) {
|
|
502
478
|
if (!ir || typeof ir !== 'object') return NULL_TOOL_VOCAB;
|
|
503
|
-
if (!ir._vocab) ir._vocab = createToolVocab(ir.tools);
|
|
479
|
+
if (!ir._vocab) ir._vocab = createToolVocab(ir.tools, { allowHyphens: outFormat !== 'vertex', names: [...toolNameMapForIR(ir).forward.keys()] });
|
|
504
480
|
return ir._vocab;
|
|
505
481
|
}
|
|
506
482
|
|
|
@@ -887,6 +863,206 @@ async function pumpStream(upstreamRes, { normalize, col, renderer, splitter, sig
|
|
|
887
863
|
// once, so a slow or absent intact cannot reach the answer the client is waiting for.
|
|
888
864
|
const contractLab = createContractLab({ settings: () => contractLabSettings(loadConfig()) });
|
|
889
865
|
|
|
866
|
+
function sendEvaluatorResponse(res, stream, verdict, model) {
|
|
867
|
+
const jsonText = JSON.stringify({
|
|
868
|
+
ok: Boolean(verdict.ok),
|
|
869
|
+
reason: String(verdict.reason || ''),
|
|
870
|
+
...(verdict.impossible ? { impossible: true } : {})
|
|
871
|
+
});
|
|
872
|
+
const modelName = model || 'evaluator';
|
|
873
|
+
if (stream) {
|
|
874
|
+
res.writeHead(200, {
|
|
875
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
876
|
+
'Cache-Control': 'no-cache',
|
|
877
|
+
'Connection': 'keep-alive'
|
|
878
|
+
});
|
|
879
|
+
const msgId = `msg_${Date.now()}_${randStr(12)}`;
|
|
880
|
+
const emit = (event, data) => res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
881
|
+
|
|
882
|
+
emit('message_start', {
|
|
883
|
+
type: 'message_start',
|
|
884
|
+
message: {
|
|
885
|
+
id: msgId,
|
|
886
|
+
type: 'message',
|
|
887
|
+
role: 'assistant',
|
|
888
|
+
model: modelName,
|
|
889
|
+
content: [],
|
|
890
|
+
stop_reason: null,
|
|
891
|
+
stop_sequence: null,
|
|
892
|
+
usage: { input_tokens: 0, output_tokens: 0 }
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
emit('content_block_start', {
|
|
896
|
+
type: 'content_block_start',
|
|
897
|
+
index: 0,
|
|
898
|
+
content_block: { type: 'text', text: '' }
|
|
899
|
+
});
|
|
900
|
+
emit('content_block_delta', {
|
|
901
|
+
type: 'content_block_delta',
|
|
902
|
+
index: 0,
|
|
903
|
+
delta: { type: 'text_delta', text: jsonText }
|
|
904
|
+
});
|
|
905
|
+
emit('content_block_stop', {
|
|
906
|
+
type: 'content_block_stop',
|
|
907
|
+
index: 0
|
|
908
|
+
});
|
|
909
|
+
emit('message_delta', {
|
|
910
|
+
type: 'message_delta',
|
|
911
|
+
delta: { stop_reason: 'end_turn', stop_sequence: null },
|
|
912
|
+
usage: { output_tokens: Math.max(1, Math.ceil(jsonText.length / 4)) }
|
|
913
|
+
});
|
|
914
|
+
emit('message_stop', {
|
|
915
|
+
type: 'message_stop'
|
|
916
|
+
});
|
|
917
|
+
res.end();
|
|
918
|
+
} else {
|
|
919
|
+
const msgId = `msg_${Date.now()}_${randStr(12)}`;
|
|
920
|
+
sendJson(res, 200, {
|
|
921
|
+
id: msgId,
|
|
922
|
+
type: 'message',
|
|
923
|
+
role: 'assistant',
|
|
924
|
+
model: modelName,
|
|
925
|
+
content: [{ type: 'text', text: jsonText }],
|
|
926
|
+
stop_reason: 'end_turn',
|
|
927
|
+
stop_sequence: null,
|
|
928
|
+
usage: { input_tokens: 0, output_tokens: Math.max(1, Math.ceil(jsonText.length / 4)) }
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async function executeGoalCheckGate({ req, res, clientFormat, payload, ir, profile, profileKey, policy, condition, opts, log }) {
|
|
934
|
+
const startTime = Date.now();
|
|
935
|
+
const sessionId = conversationKey(clientFormat, req, ir) || req.headers['x-claude-code-session-id'] || '';
|
|
936
|
+
const requestedModel = ir.model || payload.model || '';
|
|
937
|
+
const mappedModel = mapModel(requestedModel, profile, clientFormat);
|
|
938
|
+
const bifrost = BIFROST_FORMATS.has(clientFormat) && await crossesBifrost(req, profile, mappedModel);
|
|
939
|
+
const outFormat = resolveOutFormat(profile, mappedModel);
|
|
940
|
+
|
|
941
|
+
const gateAC = new AbortController();
|
|
942
|
+
const totalTimeout = Number.isFinite(policy.totalTimeoutMs) ? policy.totalTimeoutMs : 25000;
|
|
943
|
+
const gateTimer = setTimeout(() => gateAC.abort(), totalTimeout);
|
|
944
|
+
req.on('aborted', () => gateAC.abort());
|
|
945
|
+
|
|
946
|
+
const target = resolveJevTarget(policy, (loadConfig() || {}).profiles || {});
|
|
947
|
+
const evidence = buildBoundedEvidence(payload, policy.maxStateChars);
|
|
948
|
+
const questions = buildJevQuestions(condition);
|
|
949
|
+
|
|
950
|
+
let nativeVerdict = null;
|
|
951
|
+
let jevResult = null;
|
|
952
|
+
|
|
953
|
+
try {
|
|
954
|
+
const nativePromise = (async () => {
|
|
955
|
+
try {
|
|
956
|
+
let url, headers, body;
|
|
957
|
+
if (bifrost) {
|
|
958
|
+
url = bifrostURL(profile, clientFormat, req, opts);
|
|
959
|
+
headers = bifrostHeaders(req, profile);
|
|
960
|
+
const nativePayload = clientFormat === 'responses' ? expandGatewayCompactions(payload) : payload;
|
|
961
|
+
const p = { ...nativePayload, model: mappedModel };
|
|
962
|
+
body = Buffer.from(JSON.stringify(p), 'utf8');
|
|
963
|
+
} else if (outFormat === 'anthropic') {
|
|
964
|
+
const ep = upstreamEndpoint(profile, 'anthropic', mappedModel, ir.stream, req);
|
|
965
|
+
url = ep.url;
|
|
966
|
+
headers = ep.headers;
|
|
967
|
+
let json = { ...payload, model: mappedModel };
|
|
968
|
+
const healed = healAnthropicPayload(json);
|
|
969
|
+
if (healed.changed) json = healed.payload;
|
|
970
|
+
if (profile?.thinkingMode === 'off' && json.thinking) delete json.thinking;
|
|
971
|
+
body = Buffer.from(JSON.stringify(json), 'utf8');
|
|
972
|
+
} else {
|
|
973
|
+
const slot = resolveSlot(requestedModel, mappedModel, profile, clientFormat);
|
|
974
|
+
const reqData = buildUpstreamRequest(profile, outFormat, ir, mappedModel, req, slot);
|
|
975
|
+
url = reqData.url;
|
|
976
|
+
headers = reqData.headers;
|
|
977
|
+
body = JSON.stringify(reqData.upBody);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const traceId = clientFormat === 'codeassist' ? null : (probeTraceId(req) || contractLab.traceFor(mappedModel));
|
|
981
|
+
if (traceId) headers['x-intact-trace'] = traceId;
|
|
982
|
+
|
|
983
|
+
const upRes = await fetch(url, { method: 'POST', headers, body, signal: gateAC.signal });
|
|
984
|
+
if (!upRes.ok) {
|
|
985
|
+
const errText = await upRes.text().catch(() => '');
|
|
986
|
+
return { malformed: true, error: `Upstream HTTP ${upRes.status}: ${errText.slice(0, 150)}` };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const normalize = createUpstreamNormalizer(outFormat);
|
|
990
|
+
const col = createCollector();
|
|
991
|
+
const ctype = upRes.headers.get('content-type') || '';
|
|
992
|
+
if (ctype.includes('application/json')) {
|
|
993
|
+
const parsed = await upRes.json();
|
|
994
|
+
const ev = normalize(parsed);
|
|
995
|
+
col.add(ev);
|
|
996
|
+
if (ev.error) return { malformed: true, error: ev.error };
|
|
997
|
+
} else {
|
|
998
|
+
for await (const parsed of readUpstreamPayloads(upRes)) {
|
|
999
|
+
const ev = normalize(parsed);
|
|
1000
|
+
col.add(ev);
|
|
1001
|
+
if (ev.error) return { malformed: true, error: ev.error };
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const collectedText = col.text.join('');
|
|
1006
|
+
return parseEvaluatorVerdict(collectedText);
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
return { malformed: true, error: err.message || 'native fetch error' };
|
|
1009
|
+
}
|
|
1010
|
+
})();
|
|
1011
|
+
|
|
1012
|
+
const jevPromise = (async () => {
|
|
1013
|
+
if (evidence.stateTooLarge) {
|
|
1014
|
+
return { stateTooLarge: true, chars: evidence.chars, maxChars: evidence.maxChars };
|
|
1015
|
+
}
|
|
1016
|
+
try {
|
|
1017
|
+
return await callJev({
|
|
1018
|
+
url: target.url,
|
|
1019
|
+
model: target.model,
|
|
1020
|
+
apiKey: target.apiKey,
|
|
1021
|
+
state: evidence.state,
|
|
1022
|
+
questions,
|
|
1023
|
+
timeoutMs: policy.timeoutMs,
|
|
1024
|
+
signal: gateAC.signal
|
|
1025
|
+
});
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
return { ok: false, reason: 'error', message: err.message };
|
|
1028
|
+
}
|
|
1029
|
+
})();
|
|
1030
|
+
|
|
1031
|
+
const [nativeSettled, jevSettled] = await Promise.all([nativePromise, jevPromise]);
|
|
1032
|
+
nativeVerdict = nativeSettled;
|
|
1033
|
+
jevResult = jevSettled;
|
|
1034
|
+
} finally {
|
|
1035
|
+
clearTimeout(gateTimer);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const finalVerdict = combineVerdicts({ nativeVerdict, jevResult, thresholds: policy });
|
|
1039
|
+
const latencyMs = Date.now() - startTime;
|
|
1040
|
+
|
|
1041
|
+
logGoalCheck({
|
|
1042
|
+
sessionId,
|
|
1043
|
+
condition: condition.slice(0, 300),
|
|
1044
|
+
nativeVerdict: nativeVerdict?.malformed ? { ok: false, malformed: true, error: nativeVerdict.error } : { ok: nativeVerdict?.ok, reason: nativeVerdict?.reason, impossible: nativeVerdict?.impossible },
|
|
1045
|
+
jevScores: jevResult?.scores || null,
|
|
1046
|
+
verdict: finalVerdict.ok,
|
|
1047
|
+
verdictType: finalVerdict.verdictType,
|
|
1048
|
+
reason: finalVerdict.reason,
|
|
1049
|
+
latencyMs
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
console.log(`[llm-switcher:goal-check] session=${sessionId.slice(0, 16)} native=${nativeVerdict?.ok} jev=${finalVerdict.ok} verdict=${finalVerdict.verdictType} (${latencyMs}ms)`);
|
|
1053
|
+
|
|
1054
|
+
if (gateAC.signal.aborted && req.destroyed) {
|
|
1055
|
+
return log({ status: 499, error: 'client disconnected during goal check' });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
sendEvaluatorResponse(res, ir.stream, finalVerdict, mappedModel);
|
|
1059
|
+
return log({
|
|
1060
|
+
status: 200,
|
|
1061
|
+
responsePreview: `[goal-check:${finalVerdict.verdictType}] ${finalVerdict.reason.slice(0, 100)}`,
|
|
1062
|
+
tokens: { prompt: 0, completion: 0 }
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
890
1066
|
// ----------------------------------------------------
|
|
891
1067
|
// LLM Switcher generic pipeline: client --parse--> IR --emit--> upstream
|
|
892
1068
|
// Client (input) formats : anthropic | openai-chat | responses (Codex) | vertex
|
|
@@ -937,7 +1113,7 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
937
1113
|
|
|
938
1114
|
let ir;
|
|
939
1115
|
try {
|
|
940
|
-
ir = parseToIR(clientFormat, payload);
|
|
1116
|
+
ir = parseToIR(clientFormat, payload, { allowOpaqueCompaction: true });
|
|
941
1117
|
} catch (e) {
|
|
942
1118
|
sendClientError(res, clientFormat, 400, `Cannot parse ${clientFormat} request: ${e.message}`);
|
|
943
1119
|
return;
|
|
@@ -949,13 +1125,46 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
949
1125
|
// Code Assist names streaming in the path (:streamGenerateContent), not in the body.
|
|
950
1126
|
if (clientFormat === 'codeassist') ir.stream = Boolean(opts.codeAssistStream);
|
|
951
1127
|
|
|
1128
|
+
const evalCheck = isGoalEvaluatorRequest(clientFormat, payload, req.headers);
|
|
1129
|
+
const gcPolicy = goalCheckPolicy(loadConfig());
|
|
1130
|
+
if (evalCheck.isEvaluator && gcPolicy.enabled) {
|
|
1131
|
+
const requestedModel = ir.model || payload.model || '';
|
|
1132
|
+
const mappedModel = mapModel(requestedModel, profile, clientFormat);
|
|
1133
|
+
const reqStartTime = Date.now();
|
|
1134
|
+
const logBase = { clientFormat, outFormat: 'goal-check', profile: profileKey, model: mappedModel, stream: ir.stream, requestPreview: previewOf(ir) };
|
|
1135
|
+
const log = (extra) => logInspection({ ...logBase, duration: Date.now() - reqStartTime, tokens: { prompt: 0, completion: 0 }, ...extra });
|
|
1136
|
+
|
|
1137
|
+
if (!evalCheck.isSupported) {
|
|
1138
|
+
logGoalCheck({
|
|
1139
|
+
sessionId: conversationKey(clientFormat, req, ir) || req.headers['x-claude-code-session-id'] || '',
|
|
1140
|
+
condition: evalCheck.condition || '',
|
|
1141
|
+
nativeVerdict: null,
|
|
1142
|
+
jevScores: null,
|
|
1143
|
+
verdict: false,
|
|
1144
|
+
verdictType: 'Unsupported evaluator',
|
|
1145
|
+
reason: `Unsupported evaluator format: ${evalCheck.reason || 'unrecognized shape'}`,
|
|
1146
|
+
latencyMs: 0
|
|
1147
|
+
});
|
|
1148
|
+
sendEvaluatorResponse(res, ir.stream, {
|
|
1149
|
+
ok: false,
|
|
1150
|
+
reason: `Unsupported evaluator format: ${evalCheck.reason || 'unrecognized shape'}`
|
|
1151
|
+
}, mappedModel);
|
|
1152
|
+
return log({ status: 200, responsePreview: '[goal-check:Unsupported evaluator]' });
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
return await executeGoalCheckGate({
|
|
1156
|
+
req, res, clientFormat, payload, ir, profile, profileKey,
|
|
1157
|
+
policy: gcPolicy, condition: evalCheck.condition, opts, log
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
|
|
952
1161
|
// Compaction rewrites the request itself, not just the intermediate form: when the client and
|
|
953
1162
|
// the provider speak the same shape this gateway forwards the client's own bytes, so a change
|
|
954
1163
|
// made only to the parsed form would shorten nothing that leaves the building.
|
|
955
1164
|
const shrunk = await maybeCompactIdle(req, clientFormat, payload, ir, profile, profileKey, bodyBuffer);
|
|
956
1165
|
if (shrunk) {
|
|
957
1166
|
try {
|
|
958
|
-
const compactedIR = parseToIR(clientFormat, shrunk.payload);
|
|
1167
|
+
const compactedIR = parseToIR(clientFormat, shrunk.payload, { allowOpaqueCompaction: true });
|
|
959
1168
|
payload = shrunk.payload;
|
|
960
1169
|
bodyBuffer = shrunk.bodyBuffer;
|
|
961
1170
|
ir = compactedIR;
|
|
@@ -1008,7 +1217,9 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
1008
1217
|
const headers = bifrostHeaders(req, profile);
|
|
1009
1218
|
if (traceId) headers['x-intact-trace'] = traceId;
|
|
1010
1219
|
try {
|
|
1011
|
-
const
|
|
1220
|
+
const nativePayload = clientFormat === 'responses' ? expandGatewayCompactions(payload) : payload;
|
|
1221
|
+
const nativeBytes = nativePayload === payload ? bodyBuffer : Buffer.from(JSON.stringify(nativePayload));
|
|
1222
|
+
const r = await forwardAnthropicDirect(res, nativePayload, nativeBytes, bifrostURL(profile, clientFormat, req, opts), headers, mappedModel, ac.signal, profile, true);
|
|
1012
1223
|
answered = !r.error && r.status >= 200 && r.status < 300;
|
|
1013
1224
|
log({ status: r.status, tokens: r.tokens, responsePreview: '(bifrost)', error: r.error || undefined });
|
|
1014
1225
|
} catch (err) {
|
|
@@ -1038,6 +1249,10 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
1038
1249
|
return;
|
|
1039
1250
|
}
|
|
1040
1251
|
|
|
1252
|
+
if (ir.opaqueCompaction) {
|
|
1253
|
+
sendClientError(res, clientFormat, 400, 'Cannot convert native or legacy compaction state; resume through its original Responses provider.');
|
|
1254
|
+
return log({ status: 400, error: 'unsupported compaction state' });
|
|
1255
|
+
}
|
|
1041
1256
|
const slot = resolveSlot(requestedModel, mappedModel, profile, clientFormat);
|
|
1042
1257
|
const { url, headers, upBody } = buildUpstreamRequest(profile, outFormat, ir, mappedModel, req, slot);
|
|
1043
1258
|
if (traceId) headers['x-intact-trace'] = traceId;
|
|
@@ -1056,13 +1271,37 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
1056
1271
|
if (!upstreamRes.ok) {
|
|
1057
1272
|
const errText = await upstreamRes.text().catch(() => '');
|
|
1058
1273
|
console.error(`[${profileKey}] Error HTTP ${upstreamRes.status}:`, errText.slice(0, 500));
|
|
1059
|
-
|
|
1274
|
+
let userFriendlyMsg = extractUpstreamMessage(errText) || `Upstream HTTP ${upstreamRes.status}`;
|
|
1275
|
+
if (upstreamRes.status === 404 && (errText.toLowerCase().includes('model') || userFriendlyMsg.toLowerCase().includes('model'))) {
|
|
1276
|
+
userFriendlyMsg = `Model '${mappedModel}' is not available or has been deprecated on profile '${profileKey}' (HTTP 404: ${userFriendlyMsg}). Open 'switch ui' to reconfigure your model.`;
|
|
1277
|
+
} else if (upstreamRes.status === 401 || upstreamRes.status === 403) {
|
|
1278
|
+
userFriendlyMsg = `API key for profile '${profileKey}' was rejected (HTTP ${upstreamRes.status}: ${userFriendlyMsg}). Check credentials in 'switch ui'.`;
|
|
1279
|
+
}
|
|
1280
|
+
sendClientError(res, clientFormat, upstreamRes.status, userFriendlyMsg, pickRetryHeaders(upstreamRes));
|
|
1060
1281
|
return log({ status: upstreamRes.status, error: errText.slice(0, 300) });
|
|
1061
1282
|
}
|
|
1062
1283
|
|
|
1063
1284
|
const normalize = createUpstreamNormalizer(outFormat);
|
|
1064
1285
|
const col = createCollector();
|
|
1065
1286
|
|
|
1287
|
+
if (ir.compaction) {
|
|
1288
|
+
try {
|
|
1289
|
+
await collectCompaction(upstreamRes, outFormat, ir.stream, normalize, col);
|
|
1290
|
+
if (ac.signal.aborted) return log({ status: 499, error: 'client disconnected' });
|
|
1291
|
+
const response = compactionResponse(col, requestedModel || mappedModel, text => splitThinkTags(text).text);
|
|
1292
|
+
if (ir.stream) {
|
|
1293
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8', 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no' });
|
|
1294
|
+
emitCompaction(response, (event, data) => sendSSE(res, event, data)); res.end();
|
|
1295
|
+
} else sendJson(res, 200, response);
|
|
1296
|
+
answered = true;
|
|
1297
|
+
return log({ status: 200, responsePreview: '(compaction)', tokens: { prompt: col.prompt, completion: col.completion() } });
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
if (ac.signal.aborted) return log({ status: 499, error: 'client disconnected' });
|
|
1300
|
+
sendClientError(res, clientFormat, 502, err.message);
|
|
1301
|
+
return log({ status: 502, error: err.message });
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1066
1305
|
// ---- non-stream ----
|
|
1067
1306
|
if (!ir.stream) {
|
|
1068
1307
|
let json;
|
|
@@ -1085,7 +1324,7 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
1085
1324
|
const out = clientMessage(clientFormat, {
|
|
1086
1325
|
model: requestedModel || mappedModel, think, text: [split.text], tools, toolMeta: ir.toolMeta,
|
|
1087
1326
|
finish: col.finish, prompt: col.prompt, completion, cached: col.cached,
|
|
1088
|
-
reasoning: col.reasoning, sig: col.sig, vocab: toolVocabFor(ir)
|
|
1327
|
+
reasoning: col.reasoning, sig: col.sig, vocab: toolVocabFor(ir, outFormat)
|
|
1089
1328
|
});
|
|
1090
1329
|
sendJson(res, 200, out);
|
|
1091
1330
|
answered = true;
|
|
@@ -1102,7 +1341,7 @@ async function handleConvert(clientFormat, req, res, bodyBuffer, opts = {}) {
|
|
|
1102
1341
|
'Connection': 'keep-alive',
|
|
1103
1342
|
'X-Accel-Buffering': 'no'
|
|
1104
1343
|
});
|
|
1105
|
-
const renderer = clientRenderer(clientFormat, res, requestedModel || mappedModel, { toolMeta: ir.toolMeta, vocab: toolVocabFor(ir) });
|
|
1344
|
+
const renderer = clientRenderer(clientFormat, res, requestedModel || mappedModel, { toolMeta: ir.toolMeta, vocab: toolVocabFor(ir, outFormat) });
|
|
1106
1345
|
renderer.start();
|
|
1107
1346
|
const splitter = createThinkTagSplitter(t => renderer.think(t), t => renderer.text(t));
|
|
1108
1347
|
const streamError = await pumpStream(upstreamRes, { normalize, col, renderer, splitter, signal: ac.signal, sink: res, tag: profileKey });
|
|
@@ -1735,6 +1974,92 @@ async function routeApi(req, res, method, pathname) {
|
|
|
1735
1974
|
return sendJson(res, 200, { idleCompact: idleCompactPolicy(loadConfig()) });
|
|
1736
1975
|
}
|
|
1737
1976
|
|
|
1977
|
+
// Goal Check
|
|
1978
|
+
if (method === 'GET' && pathname === '/api/goal-check') {
|
|
1979
|
+
const cfg = loadConfig() || {};
|
|
1980
|
+
const policy = goalCheckPolicy(cfg);
|
|
1981
|
+
const target = resolveJevTarget(policy, cfg.profiles || {});
|
|
1982
|
+
const configured = Boolean(target.url && (target.apiKey || policy.sourceProfile));
|
|
1983
|
+
return sendJson(res, 200, {
|
|
1984
|
+
goalCheck: redactGoalCheck(policy, MASKED_KEY),
|
|
1985
|
+
status: {
|
|
1986
|
+
enabled: policy.enabled,
|
|
1987
|
+
configured,
|
|
1988
|
+
model: policy.model,
|
|
1989
|
+
endpoint: target.url || '(none)'
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
if (method === 'POST' && pathname === '/api/goal-check') {
|
|
1994
|
+
let patch;
|
|
1995
|
+
try {
|
|
1996
|
+
patch = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8'));
|
|
1997
|
+
} catch (err) {
|
|
1998
|
+
return sendJson(res, err.status || 400, { error: err.message });
|
|
1999
|
+
}
|
|
2000
|
+
const currentCfg = loadConfig() || {};
|
|
2001
|
+
const currentPolicy = goalCheckPolicy(currentCfg);
|
|
2002
|
+
const invalid = validateGoalCheckPatch(patch, currentPolicy);
|
|
2003
|
+
if (invalid) return sendJson(res, 400, { error: invalid });
|
|
2004
|
+
|
|
2005
|
+
const currentStoredKey = (currentCfg[GOAL_CHECK_KEY]?.apiKey) || '';
|
|
2006
|
+
if (patch.apiKey === MASKED_KEY) {
|
|
2007
|
+
patch.apiKey = currentStoredKey;
|
|
2008
|
+
} else if (patch.apiKey === undefined && Object.hasOwn(currentCfg, GOAL_CHECK_KEY)) {
|
|
2009
|
+
patch.apiKey = currentStoredKey;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
currentCfg[GOAL_CHECK_KEY] = { ...(currentCfg[GOAL_CHECK_KEY] || {}), ...patch };
|
|
2013
|
+
try {
|
|
2014
|
+
saveConfig(currentCfg);
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
return sendJson(res, 500, { error: err.message });
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const updatedPolicy = goalCheckPolicy(loadConfig());
|
|
2020
|
+
const target = resolveJevTarget(updatedPolicy, (loadConfig() || {}).profiles || {});
|
|
2021
|
+
const configured = Boolean(target.url && (target.apiKey || updatedPolicy.sourceProfile));
|
|
2022
|
+
return sendJson(res, 200, {
|
|
2023
|
+
goalCheck: redactGoalCheck(updatedPolicy, MASKED_KEY),
|
|
2024
|
+
status: {
|
|
2025
|
+
enabled: updatedPolicy.enabled,
|
|
2026
|
+
configured,
|
|
2027
|
+
model: updatedPolicy.model,
|
|
2028
|
+
endpoint: target.url || '(none)'
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
if (method === 'POST' && pathname === '/api/goal-check/test') {
|
|
2033
|
+
let body;
|
|
2034
|
+
try {
|
|
2035
|
+
body = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8'));
|
|
2036
|
+
} catch (err) {
|
|
2037
|
+
return sendJson(res, err.status || 400, { error: err.message });
|
|
2038
|
+
}
|
|
2039
|
+
const cfg = loadConfig() || {};
|
|
2040
|
+
const currentGC = cfg[GOAL_CHECK_KEY] || {};
|
|
2041
|
+
const currentStoredKey = currentGC.apiKey || '';
|
|
2042
|
+
const apiKey = body.apiKey === MASKED_KEY ? currentStoredKey : (body.apiKey ?? currentStoredKey);
|
|
2043
|
+
const baseURL = body.baseURL !== undefined ? body.baseURL : (currentGC.baseURL || '');
|
|
2044
|
+
const decisionEndpoint = body.decisionEndpoint !== undefined ? body.decisionEndpoint : (currentGC.decisionEndpoint || '');
|
|
2045
|
+
const model = body.model !== undefined ? body.model : (currentGC.model || 'typesafe/jev-latest');
|
|
2046
|
+
const sourceProfile = body.sourceProfile !== undefined ? body.sourceProfile : (currentGC.sourceProfile || '');
|
|
2047
|
+
|
|
2048
|
+
const testRes = await testJevConnection({
|
|
2049
|
+
baseURL,
|
|
2050
|
+
decisionEndpoint,
|
|
2051
|
+
apiKey,
|
|
2052
|
+
model,
|
|
2053
|
+
sourceProfile,
|
|
2054
|
+
profiles: cfg.profiles || {},
|
|
2055
|
+
timeoutMs: body.timeoutMs || 8000
|
|
2056
|
+
});
|
|
2057
|
+
return sendJson(res, 200, testRes);
|
|
2058
|
+
}
|
|
2059
|
+
if (method === 'GET' && pathname === '/api/goal-check/logs') {
|
|
2060
|
+
return sendJson(res, 200, { logs: goalCheckLogs.slice().reverse() });
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1738
2063
|
if (method !== 'POST') {
|
|
1739
2064
|
req.resume();
|
|
1740
2065
|
return sendJson(res, 404, { error: `Not found: ${method} ${pathname}` });
|
|
@@ -2019,7 +2344,7 @@ function sendWsFailed(socket, model, message, status = 500, tap = null, code = n
|
|
|
2019
2344
|
// previous_response_id; the server keeps the rest. The gateway keeps it per socket, bounded.
|
|
2020
2345
|
const WS_HISTORY_MAX = 8;
|
|
2021
2346
|
// Only these output items are valid input for the next turn; reasoning items are not replayed.
|
|
2022
|
-
const WS_REPLAY_TYPES = new Set(['message', 'function_call', 'custom_tool_call', 'local_shell_call']);
|
|
2347
|
+
const WS_REPLAY_TYPES = new Set(['message', 'function_call', 'custom_tool_call', 'local_shell_call', 'compaction']);
|
|
2023
2348
|
|
|
2024
2349
|
function wsInputItems(input) {
|
|
2025
2350
|
if (typeof input === 'string') return input ? [{ type: 'message', role: 'user', content: [{ type: 'input_text', text: input }] }] : [];
|
|
@@ -2169,7 +2494,7 @@ async function handleWsResponseCreate(socket, payload, req, ac, history = null)
|
|
|
2169
2494
|
|
|
2170
2495
|
const bifrostModel = mapModel(payload.model || '', profile, clientFormat);
|
|
2171
2496
|
if (await crossesBifrost(req, profile, bifrostModel)) {
|
|
2172
|
-
return wsBifrostTurn({ socket, payload, req, ac, history, input, profile, profileKey, mappedModel: bifrostModel });
|
|
2497
|
+
return wsBifrostTurn({ socket, payload: expandGatewayCompactions(payload), req, ac, history, input, profile, profileKey, mappedModel: bifrostModel });
|
|
2173
2498
|
}
|
|
2174
2499
|
|
|
2175
2500
|
let ir;
|
|
@@ -2224,6 +2549,21 @@ async function handleWsResponseCreate(socket, payload, req, ac, history = null)
|
|
|
2224
2549
|
const normalize = createUpstreamNormalizer(outFormat);
|
|
2225
2550
|
const col = createCollector();
|
|
2226
2551
|
|
|
2552
|
+
if (ir.compaction) {
|
|
2553
|
+
try {
|
|
2554
|
+
await collectCompaction(upstreamRes, outFormat, true, normalize, col);
|
|
2555
|
+
if (ac.signal.aborted) return log({ status: 499, error: 'client disconnected' });
|
|
2556
|
+
const response = compactionResponse(col, requestedModel || mappedModel, text => splitThinkTags(text).text);
|
|
2557
|
+
emitCompaction(response, (event, data) => { tapWsEvent(halfTap, event, data); if (socket.writable) socket.write(encodeWsFrame(JSON.stringify(data))); });
|
|
2558
|
+
rememberWsTurn(history, response, []); answered = true;
|
|
2559
|
+
return log({ status: 200, responsePreview: '(compaction)', tokens: { prompt: col.prompt, completion: col.completion() } });
|
|
2560
|
+
} catch (err) {
|
|
2561
|
+
if (ac.signal.aborted) return log({ status: 499, error: 'client disconnected' });
|
|
2562
|
+
sendWsFailed(socket, mappedModel, err.message, 502, halfTap);
|
|
2563
|
+
return log({ status: 502, error: err.message });
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2227
2567
|
let completedResponse = null;
|
|
2228
2568
|
const renderer = createResponsesStream((e, d) => {
|
|
2229
2569
|
tapWsEvent(halfTap, e, d);
|
|
@@ -2231,7 +2571,7 @@ async function handleWsResponseCreate(socket, payload, req, ac, history = null)
|
|
|
2231
2571
|
if (socket.writable) {
|
|
2232
2572
|
socket.write(encodeWsFrame(JSON.stringify(d)));
|
|
2233
2573
|
}
|
|
2234
|
-
}, requestedModel || mappedModel, { toolMeta: ir.toolMeta, vocab: toolVocabFor(ir) });
|
|
2574
|
+
}, requestedModel || mappedModel, { toolMeta: ir.toolMeta, vocab: toolVocabFor(ir, outFormat) });
|
|
2235
2575
|
|
|
2236
2576
|
renderer.start();
|
|
2237
2577
|
const splitter = createThinkTagSplitter(t => renderer.think(t), t => renderer.text(t));
|
package/shim.mjs
CHANGED
|
@@ -43,6 +43,7 @@ const GATEWAY_CHECK = path.join(path.dirname(fileURLToPath(import.meta.url)), 'v
|
|
|
43
43
|
// The launch check covers the start only. A routed agy then runs behind this relay, which proves
|
|
44
44
|
// the gateway on every connection, so a port freed later never receives the token.
|
|
45
45
|
const AGY_RELAY = path.join(path.dirname(fileURLToPath(import.meta.url)), 'agy-relay.mjs');
|
|
46
|
+
const PREFLIGHT_CHECK = path.join(path.dirname(fileURLToPath(import.meta.url)), 'preflight-check.mjs');
|
|
46
47
|
|
|
47
48
|
// CLIs to wrap. `claude` is the most important case (--resume); codex and agy have their own files.
|
|
48
49
|
export const SHIMMED = ['claude', 'codex', 'agy'];
|
|
@@ -81,6 +82,7 @@ fi`;
|
|
|
81
82
|
# "${name}", from any shell — including one that never sourced anything (claude --resume).
|
|
82
83
|
SWITCHER_DIR="${STATE_DIR}"
|
|
83
84
|
STATE_HELPER="${STATE_HELPER}"
|
|
85
|
+
PREFLIGHT_CHECK="${PREFLIGHT_CHECK}"
|
|
84
86
|
SHIM_DIR="\${BASH_SOURCE%/*}"
|
|
85
87
|
|
|
86
88
|
# Captured BEFORE the tool's env file is sourced: the CA decision must see the value this shell
|
|
@@ -126,7 +128,12 @@ fi
|
|
|
126
128
|
# here. Only while THIS tool is routed: an empty route file means the tool is off, so the tool
|
|
127
129
|
# reaches its official endpoint and there is nothing to warn about.
|
|
128
130
|
if [ "$TOOL_ACTIVE" = "1" ] && [ -s "$SWITCHER_DIR/${routeFile}" ]; then
|
|
129
|
-
|
|
131
|
+
PREFLIGHT_OUT="$(node "$PREFLIGHT_CHECK" "${name}" "$SWITCHER_DIR" 2>/dev/null)" || PREFLIGHT_OUT=""
|
|
132
|
+
if [ -n "$PREFLIGHT_OUT" ]; then
|
|
133
|
+
printf '%s\\n' "$PREFLIGHT_OUT" >&2
|
|
134
|
+
else
|
|
135
|
+
printf '[llm-switcher] %s\\n' "$(cat "$SWITCHER_DIR/${routeFile}")" >&2
|
|
136
|
+
fi
|
|
130
137
|
fi
|
|
131
138
|
|
|
132
139
|
# R8: a variable is removed ONLY when its value is one an older switcher wrote. The user's own
|
|
@@ -248,6 +255,7 @@ set "NODE_EXTRA_CA_CERTS=%CA_BUNDLE%"
|
|
|
248
255
|
REM Auto-generated by LLM Switcher - DO NOT EDIT.
|
|
249
256
|
setlocal
|
|
250
257
|
set "SWITCHER_DIR=${STATE_DIR}"
|
|
258
|
+
set "PREFLIGHT_CHECK=${PREFLIGHT_CHECK}"
|
|
251
259
|
${!needsCaBlock(name) ? '' : `set "STATE_HELPER=${STATE_HELPER}"
|
|
252
260
|
|
|
253
261
|
REM Captured BEFORE the tool's env file runs: the CA decision must see the value this shell
|
|
@@ -280,7 +288,10 @@ if "%TOOL_ACTIVE%"=="1" call :CHECK_GATEWAY
|
|
|
280
288
|
REM Nothing on screen tells the person that the switcher took this tool's traffic, and both TUIs
|
|
281
289
|
REM can claim the whole screen, so a printed line would be hidden. A toast is not. It is detached:
|
|
282
290
|
REM the tool never waits for it. Only while THIS tool is routed (%TOOL_ACTIVE%).
|
|
283
|
-
if "%TOOL_ACTIVE%"=="1" if exist "%SWITCHER_DIR%\\${routeFile}"
|
|
291
|
+
if "%TOOL_ACTIVE%"=="1" if exist "%SWITCHER_DIR%\\${routeFile}" (
|
|
292
|
+
for /f "delims=" %%M in ('node "%PREFLIGHT_CHECK%" "${name}" "%SWITCHER_DIR%" 2^>nul') do echo %%M 1>&2
|
|
293
|
+
start "" /b powershell -NoProfile -ExecutionPolicy Bypass -File "${ROUTE_NOTIFIER}" "%SWITCHER_DIR%\\${routeFile}" >nul 2>&1
|
|
294
|
+
)
|
|
284
295
|
|
|
285
296
|
REM R8: a variable is removed ONLY when its value is one an older switcher wrote. The user's own
|
|
286
297
|
REM proxy, base URL, model or LLM_SWITCHER_* setting is theirs and is kept.
|
package/state.mjs
CHANGED
|
@@ -1680,6 +1680,11 @@ export function redactConfig(cfg) {
|
|
|
1680
1680
|
lab.hasApiKey = Boolean(lab.apiKey);
|
|
1681
1681
|
lab.apiKey = lab.apiKey ? MASKED_KEY : '';
|
|
1682
1682
|
}
|
|
1683
|
+
const gc = clone.goalCheck;
|
|
1684
|
+
if (gc && typeof gc === 'object' && !Array.isArray(gc)) {
|
|
1685
|
+
gc.hasApiKey = Boolean(gc.apiKey);
|
|
1686
|
+
gc.apiKey = gc.apiKey ? MASKED_KEY : '';
|
|
1687
|
+
}
|
|
1683
1688
|
return clone;
|
|
1684
1689
|
}
|
|
1685
1690
|
|
|
@@ -12,6 +12,7 @@ import { buildCerts } from '../blindfold/certs.mjs';
|
|
|
12
12
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'llmsw-codex-daemon-'));
|
|
13
13
|
process.env.HOME = path.join(root, 'home');
|
|
14
14
|
process.env.LLM_SWITCHER_STATE_DIR = path.join(root, 'state');
|
|
15
|
+
process.env.LLM_SWITCHER_BLINDFOLD_CERTS = path.join(root, 'certs');
|
|
15
16
|
delete process.env.CODEX_HOME;
|
|
16
17
|
fs.mkdirSync(process.env.LLM_SWITCHER_STATE_DIR, { recursive: true });
|
|
17
18
|
const { restartCodexDaemon, applyLaunchState, clearLaunchState, emptyToolEnvFiles, computeLaunchState, STATE_DIR, INTERCEPT_HOSTS, usableCACertificate } = await import('../state.mjs');
|