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/claudesession.mjs DELETED
@@ -1,196 +0,0 @@
1
- // Claude Code's session file.
2
- //
3
- // Claude Code keeps a conversation as a JSONL file, one entry per line, and builds every request
4
- // it sends from the chain of entries linked by parentUuid. That is a fact about the file, not about
5
- // any API, and it is the reason a compaction written here lasts: the next request is built from
6
- // whatever the chain says, so an entry that starts a new chain shortens every request after it.
7
- //
8
- // A compaction is two entries, and this is the shape Claude Code itself writes. It was read off a
9
- // real session on this machine, from Claude Code's own automatic compaction:
10
- //
11
- // {"type":"system","subtype":"compact_boundary","content":"Conversation compacted",
12
- // "parentUuid":null,"logicalParentUuid":"<the last entry before>",
13
- // "compactMetadata":{"trigger":"auto","preTokens":968627,"postTokens":19320, ...}}
14
- //
15
- // {"type":"user","isCompactSummary":true,
16
- // "message":{"role":"user","content":"This session is being continued from a previous
17
- // conversation that ran out of context. The summary below covers the earlier portion..."}}
18
- //
19
- // The boundary is what makes the old entries unreachable rather than deleted. That is Claude Code's
20
- // own behaviour: its automatic compaction drops ~949k tokens of history and leaves the entries in
21
- // the file, because a transcript the person can still read is worth more than a small file. This
22
- // writes the same thing for the same reason, so nothing here destroys anything a resume would
23
- // otherwise show.
24
- //
25
- // Verified against Claude Code 2.1.292 on this machine: with twelve synthetic turns and a boundary
26
- // before them, the next resume sent four messages instead of the thirteen, and none of the twelve.
27
-
28
- import crypto from 'node:crypto';
29
- import fs from 'node:fs';
30
- import os from 'node:os';
31
- import path from 'node:path';
32
-
33
- const UUID = () => crypto.randomUUID();
34
-
35
- // The line Claude Code writes, kept verbatim so a summary written here is read exactly like one
36
- // Claude Code wrote itself.
37
- const SUMMARY_PREFIX =
38
- 'This session is being continued from a previous conversation. The summary below covers the ' +
39
- 'earlier portion of the conversation.\n\nSummary:\n';
40
-
41
- export function claudeConfigDir(env = process.env) {
42
- return env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
43
- }
44
-
45
- /**
46
- * The file a session lives in. Claude Code names the directory after the working directory, with
47
- * the separators folded into dashes, and the file after the session id.
48
- */
49
- export function sessionFileFor(sessionId, cwd, env = process.env) {
50
- const dir = path.join(claudeConfigDir(env), 'projects', encodeCwd(cwd));
51
- return { dir, file: path.join(dir, `${sessionId}.jsonl`) };
52
- }
53
-
54
- // A gateway does not know the client's working directory: nothing in the request says so, and
55
- // guessing it would write the compaction somewhere nothing reads it. The session id is enough,
56
- // though, because Claude Code names the file after it and never reuses one. So the file is found
57
- // by looking for it, which also means a session started somewhere else is found too.
58
- export function findSessionFile(sessionId, env = process.env) {
59
- if (!sessionId || !/^[A-Za-z0-9._-]{1,128}$/.test(sessionId)) return '';
60
- const projects = path.join(claudeConfigDir(env), 'projects');
61
- let dirs;
62
- try {
63
- dirs = fs.readdirSync(projects, { withFileTypes: true });
64
- } catch {
65
- return '';
66
- }
67
- for (const d of dirs) {
68
- if (!d.isDirectory()) continue;
69
- const f = path.join(projects, d.name, `${sessionId}.jsonl`);
70
- try {
71
- fs.accessSync(f, fs.constants.R_OK);
72
- return f;
73
- } catch { /* another project directory, keep looking */ }
74
- }
75
- return '';
76
- }
77
-
78
- function encodeCwd(cwd) {
79
- return String(cwd || '').replace(/[\\/:]/g, '-');
80
- }
81
-
82
- /** Reads the session id out of the headers or body Claude Code sends. */
83
- export function claudeSessionId(req, payload) {
84
- const h = req?.headers?.['x-claude-code-session-id'];
85
- if (h && typeof h === 'string' && h.trim()) return h.trim();
86
- const uid = payload?.metadata?.user_id;
87
- if (typeof uid === 'string') {
88
- const m = /_session_([0-9a-f-]{8,})/i.exec(uid);
89
- if (m) return m[1];
90
- if (/^[0-9a-f-]{36}$/i.test(uid.trim())) return uid.trim();
91
- }
92
- return '';
93
- }
94
-
95
- /** The last entry in the chain, which is where a compaction cuts. */
96
- export function readLeaf(file) {
97
- let last = null;
98
- let text;
99
- try {
100
- text = fs.readFileSync(file, 'utf8');
101
- } catch {
102
- return null;
103
- }
104
- for (const line of text.split('\n')) {
105
- if (!line.trim()) continue;
106
- let e;
107
- try { e = JSON.parse(line); } catch { continue; }
108
- if (e && typeof e.uuid === 'string') last = e;
109
- }
110
- return last;
111
- }
112
-
113
- /**
114
- * Writes the two entries. Appends, because the chain is append-only and the old entries stay: the
115
- * boundary makes them unreachable, and a person who resumes an old session id by hand still finds
116
- * the transcript.
117
- *
118
- * Returns what it wrote, or null when it could not: a session file this cannot write is not a
119
- * reason to fail the request. The turn is still shortened on the wire either way.
120
- */
121
- export function writeCompaction({ file, summary, sessionId, cwd, preTokens, postTokens, version }) {
122
- if (!file || !summary) return null;
123
- const leaf = readLeaf(file);
124
- if (!leaf) return null;
125
- // A file that already ends in a boundary has nothing to cut again, and writing a second one
126
- // would drop the summary that is already in place.
127
- if (isBoundaryTail(file)) return null;
128
-
129
- const sid = sessionId || leaf.sessionId || 'unknown';
130
- const at = new Date().toISOString();
131
- const base = {
132
- isSidechain: false,
133
- cwd: cwd || '',
134
- version: version || '',
135
- sessionId: sid,
136
- timestamp: at,
137
- };
138
- const boundaryUuid = UUID();
139
- const boundary = {
140
- ...base,
141
- parentUuid: null,
142
- logicalParentUuid: leaf.uuid,
143
- type: 'system',
144
- subtype: 'compact_boundary',
145
- content: 'Conversation compacted',
146
- level: 'info',
147
- uuid: boundaryUuid,
148
- compactMetadata: {
149
- trigger: 'auto',
150
- preTokens: preTokens || 0,
151
- postTokens: postTokens || 0,
152
- cumulativeDroppedTokens: Math.max(0, (preTokens || 0) - (postTokens || 0)),
153
- durationMs: 0,
154
- },
155
- };
156
- const summaryEntry = {
157
- ...base,
158
- parentUuid: boundaryUuid,
159
- uuid: UUID(),
160
- promptId: UUID(),
161
- type: 'user',
162
- isCompactSummary: true,
163
- message: { role: 'user', content: SUMMARY_PREFIX + summary },
164
- };
165
-
166
- const lines = [JSON.stringify(boundary), JSON.stringify(summaryEntry)].join('\n') + '\n';
167
- try {
168
- // Appended in place, never rewritten through a rename. Claude Code holds this file open while
169
- // a session runs, so a rename would replace the file it is writing to: its next append would
170
- // land on the old inode and every entry written in between would be lost. One O_APPEND write
171
- // puts both entries at the end together.
172
- fs.appendFileSync(file, lines, { mode: 0o600 });
173
- } catch {
174
- return null;
175
- }
176
- return { boundaryUuid, summaryUuid: summaryEntry.uuid };
177
- }
178
-
179
- function isBoundaryTail(file) {
180
- let text;
181
- try {
182
- text = fs.readFileSync(file, 'utf8');
183
- } catch {
184
- return false;
185
- }
186
- const lines = text.split('\n').filter(l => l.trim());
187
- for (let i = lines.length - 1; i >= 0 && i >= lines.length - 40; i--) {
188
- let e;
189
- try { e = JSON.parse(lines[i]); } catch { continue; }
190
- if (e?.type === 'system' && e?.subtype === 'compact_boundary') return true;
191
- if (e?.type === 'user' && e?.isCompactSummary === true) return true;
192
- }
193
- return false;
194
- }
195
-
196
- export { SUMMARY_PREFIX };
package/codexsession.mjs DELETED
@@ -1,141 +0,0 @@
1
- // Codex's thread store.
2
- //
3
- // Codex compacts by writing three things that have to agree, and all three were read off a real
4
- // thread that had compacted seven times:
5
- //
6
- // 1. a `compacted` entry in the rollout file, carrying the summary and the history that replaces
7
- // the old one;
8
- // 2. a row of type contextCompaction in thread_items, whose item_json is nothing but a type and
9
- // an id -- a marker, not a summary;
10
- // 3. thread_history_projection_state moved forward to the byte offset and ordinal of the entry
11
- // just written.
12
- //
13
- // The third is the one that is easy to miss, and it is why writing the first two looks like it
14
- // worked and then changes nothing. thread_history_projection_state tracks how far the store has
15
- // consumed the rollout file: next_rollout_byte_offset is the size of the file, exactly, and
16
- // next_rollout_ordinal is the ordinal after the last entry read. The rollout file is the record;
17
- // the store is a projection of it. Append to the file without moving the projection and the entry
18
- // is simply unread.
19
- //
20
- // This writes all three, and it is opt-in, and it is opt-in because it opens Codex's database. It
21
- // never deletes: earlier items stay, as they stay after Codex's own compaction, and the marker is
22
- // what makes them unreachable.
23
-
24
- import crypto from 'node:crypto';
25
- import fs from 'node:fs';
26
- import path from 'node:path';
27
-
28
- const uuid = () => crypto.randomUUID();
29
-
30
- function codexHome(env = process.env) {
31
- return env.CODEX_HOME || path.join(env.HOME || '', '.codex');
32
- }
33
-
34
- /** The rollout file of the most recent thread: sessions/YYYY/MM/DD/rollout-<stamp>-<id>.jsonl */
35
- export function latestRollout(env = process.env, now = new Date()) {
36
- const root = codexHome(env);
37
- let newest = { file: '', mtime: 0 };
38
- const walk = (dir, depth) => {
39
- if (depth > 4) return;
40
- let entries;
41
- try {
42
- entries = fs.readdirSync(dir, { withFileTypes: true });
43
- } catch {
44
- return;
45
- }
46
- for (const e of entries) {
47
- const p = path.join(dir, e.name);
48
- if (e.isDirectory()) walk(p, depth + 1);
49
- else if (e.name.endsWith('.jsonl')) {
50
- try {
51
- const m = fs.statSync(p).mtimeMs;
52
- if (m > newest.mtime) newest = { file: p, mtime: m };
53
- } catch { /* vanished under us, keep looking */ }
54
- }
55
- }
56
- };
57
- walk(path.join(root, 'sessions'), 0);
58
- return newest.file;
59
- }
60
-
61
- /**
62
- * Writes a compaction into the thread Codex is working on. Returns what it wrote, or null when it
63
- * wrote nothing -- which is never an error: the turn is still shortened on the wire either way.
64
- */
65
- export async function writeCodexCompaction({ summary, env = process.env, now = Date.now() }) {
66
- const text = String(summary || '').trim();
67
- if (!text) return null;
68
-
69
- let db;
70
- try {
71
- const { DatabaseSync } = await import('node:sqlite');
72
- db = new DatabaseSync(path.join(codexHome(env), 'thread_history_1.sqlite'));
73
- } catch {
74
- return null;
75
- }
76
-
77
- try {
78
- const thread = db.prepare(
79
- 'SELECT thread_id FROM thread_items GROUP BY thread_id ORDER BY MAX(rollout_ordinal) DESC LIMIT 1'
80
- ).get();
81
- if (!thread) return null;
82
- const tid = thread.thread_id;
83
-
84
- const turn = db.prepare(
85
- 'SELECT turn_id FROM thread_items WHERE thread_id = ? ORDER BY rollout_ordinal DESC LIMIT 1'
86
- ).get(tid);
87
- const turnId = turn?.turn_id || uuid();
88
-
89
- // The ordinal follows the projection, not MAX(rollout_ordinal): an entry appended to the file
90
- // moves the projection's ordinal on, and the two are not the same number.
91
- const proj = db.prepare(
92
- 'SELECT next_rollout_byte_offset AS off, next_rollout_ordinal AS ord FROM thread_history_projection_state WHERE thread_id = ?'
93
- ).get(tid);
94
- const ordinal = proj?.ord ?? (db.prepare('SELECT MAX(rollout_ordinal) AS m FROM thread_items WHERE thread_id = ?').get(tid)?.m ?? 0) + 1;
95
-
96
- const file = latestRollout(env);
97
- if (!file) return null;
98
-
99
- const itemId = uuid();
100
- const entry = {
101
- timestamp: new Date(now).toISOString(),
102
- ordinal,
103
- type: 'compacted',
104
- payload: {
105
- message: text,
106
- // The summary doubles as the one message that survives, so the thread keeps something to
107
- // answer from even if nothing else is carried over.
108
- replacement_history: [{
109
- type: 'message',
110
- id: 'msg_' + uuid().replace(/-/g, '').slice(0, 24),
111
- role: 'user',
112
- content: [{ type: 'input_text', text }],
113
- }],
114
- window_number: 1,
115
- },
116
- };
117
- const line = JSON.stringify(entry) + '\n';
118
-
119
- // Appended in place. A rename would swap the file Codex holds open, and its next append
120
- // would land on the old inode, taking everything written since with it.
121
- fs.appendFileSync(file, line);
122
- const size = fs.statSync(file).size;
123
-
124
- db.prepare(
125
- 'INSERT INTO thread_items (thread_id, turn_id, item_id, rollout_ordinal, created_at_ms, item_json, item_type, updated_at_ordinal, started_at_ms, completed_at_ms) ' +
126
- 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
127
- ).run(tid, turnId, itemId, ordinal, now, JSON.stringify({ type: 'contextCompaction', id: itemId }),
128
- 'contextCompaction', ordinal, now, now);
129
-
130
- // The part that is easy to leave out, and without which the first two are unread.
131
- db.prepare(
132
- 'UPDATE thread_history_projection_state SET next_rollout_byte_offset = ?, next_rollout_ordinal = ? WHERE thread_id = ?'
133
- ).run(size, ordinal, tid);
134
-
135
- db.close();
136
- return { threadId: tid, ordinal, bytes: size, chars: text.length };
137
- } catch {
138
- try { db?.close(); } catch { /* already closed */ }
139
- return null;
140
- }
141
- }