@giovannijecha/jecode 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -6
- package/dist/batch.js +59 -5
- package/dist/cli-info.js +3 -0
- package/dist/commands.js +38 -10
- package/dist/config.js +12 -0
- package/dist/context/capacity.js +18 -0
- package/dist/context/compactor.js +62 -0
- package/dist/context/manual.js +69 -0
- package/dist/context/policy.js +125 -0
- package/dist/context/projection.js +49 -0
- package/dist/controller-request.js +34 -0
- package/dist/controller.js +25 -18
- package/dist/conversation.js +31 -3
- package/dist/providers/anthropic.js +33 -3
- package/dist/providers/catalog.js +11 -4
- package/dist/providers/ollama.js +75 -1
- package/dist/providers/openai-codex.js +46 -2
- package/dist/providers/openai.js +17 -0
- package/dist/sessions/codec.js +39 -7
- package/dist/sessions/store.js +5 -5
- package/dist/settings-command.js +42 -2
- package/dist/settings.js +9 -0
- package/dist/timeline.js +90 -0
- package/dist/tui/app-state.js +1 -0
- package/dist/tui/app-workflows.js +105 -8
- package/dist/tui/app.js +14 -7
- package/dist/tui/components/misc.js +4 -2
- package/dist/usage.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// One streamed provider request with a single safe context-overflow recovery.
|
|
2
|
+
export async function requestAssistant(history, current, specs, options, events, signal) {
|
|
3
|
+
const prepared = await events.onContext?.(history, current, "budget");
|
|
4
|
+
let context = prepared === undefined ? [...current] : clone(prepared);
|
|
5
|
+
let recovered = false;
|
|
6
|
+
for (;;) {
|
|
7
|
+
try {
|
|
8
|
+
const message = await options.provider.send({
|
|
9
|
+
model: options.model,
|
|
10
|
+
system: options.system,
|
|
11
|
+
messages: context,
|
|
12
|
+
tools: specs,
|
|
13
|
+
maxTokens: options.maxTokens,
|
|
14
|
+
effort: options.effort,
|
|
15
|
+
signal,
|
|
16
|
+
onStream: (event) => events.onStream(event),
|
|
17
|
+
onStatus: (status) => events.onStatus?.(status),
|
|
18
|
+
});
|
|
19
|
+
return { message, context };
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (recovered)
|
|
23
|
+
throw error;
|
|
24
|
+
const projected = await events.onContext?.(history, context, "overflow", error);
|
|
25
|
+
if (projected === undefined)
|
|
26
|
+
throw error;
|
|
27
|
+
context = clone(projected);
|
|
28
|
+
recovered = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function clone(messages) {
|
|
33
|
+
return structuredClone([...messages]);
|
|
34
|
+
}
|
package/dist/controller.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// loop iterates. That constraint is implemented here, not left as a product claim.
|
|
6
6
|
import { isToolCall } from "./types.js";
|
|
7
7
|
import { findTool, runTool, toolSpecs } from "./tools/index.js";
|
|
8
|
+
import { requestAssistant } from "./controller-request.js";
|
|
8
9
|
export const MAX_TOOL_CALLS_PER_STEP = 32;
|
|
9
10
|
/** Independent read calls share one bounded execution wave. */
|
|
10
11
|
export const MAX_CONCURRENT_TOOL_CALLS = 4;
|
|
@@ -13,24 +14,27 @@ export const MAX_CONCURRENT_TOOL_CALLS = 4;
|
|
|
13
14
|
* stops asking for tools. `history` is mutated in place, so an aborted turn
|
|
14
15
|
* still leaves the conversation in a consistent state.
|
|
15
16
|
*/
|
|
16
|
-
export async function runTurn(history, options, events, signal) {
|
|
17
|
+
export async function runTurn(history, options, events, signal, modelHistory = history) {
|
|
17
18
|
const specs = toolSpecs(options.tools);
|
|
19
|
+
let context = modelHistory;
|
|
20
|
+
const append = (message) => {
|
|
21
|
+
history.push(message);
|
|
22
|
+
if (context !== history)
|
|
23
|
+
context.push(message);
|
|
24
|
+
};
|
|
25
|
+
const checkpoint = async (settlement) => {
|
|
26
|
+
const projected = await events.onCheckpoint?.(history, settlement, context);
|
|
27
|
+
if (projected !== undefined)
|
|
28
|
+
context = clone([...projected]);
|
|
29
|
+
};
|
|
18
30
|
for (let step = 0; step < options.maxSteps; step++) {
|
|
19
31
|
throwIfAborted(signal);
|
|
20
32
|
events.onStep?.(step + 1, options.maxSteps);
|
|
21
33
|
// The message is displayed as it streams; what comes back here is the
|
|
22
34
|
// assembled version, which exists to be appended to the history.
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
messages: history,
|
|
27
|
-
tools: specs,
|
|
28
|
-
maxTokens: options.maxTokens,
|
|
29
|
-
effort: options.effort,
|
|
30
|
-
signal,
|
|
31
|
-
onStream: (event) => events.onStream(event),
|
|
32
|
-
onStatus: (status) => events.onStatus?.(status),
|
|
33
|
-
});
|
|
35
|
+
const response = await requestAssistant(history, context, specs, options, events, signal);
|
|
36
|
+
const assistant = response.message;
|
|
37
|
+
context = response.context;
|
|
34
38
|
throwIfAborted(signal);
|
|
35
39
|
const calls = assistant.content.filter(isToolCall);
|
|
36
40
|
if (assistant.content.length === 0) {
|
|
@@ -40,11 +44,11 @@ export async function runTurn(history, options, events, signal) {
|
|
|
40
44
|
throw new Error(`provider returned ${calls.length} tool calls in one step (maximum ${MAX_TOOL_CALLS_PER_STEP})`);
|
|
41
45
|
}
|
|
42
46
|
assertToolCallIds(calls);
|
|
43
|
-
|
|
47
|
+
append(assistant);
|
|
44
48
|
if (calls.length === 0) {
|
|
45
49
|
if (assistant.usage !== undefined)
|
|
46
50
|
events.onUsage?.(assistant.usage);
|
|
47
|
-
await
|
|
51
|
+
await checkpoint("completed");
|
|
48
52
|
return; // the model is done — hand back to the user
|
|
49
53
|
}
|
|
50
54
|
// Consecutive shared reads run together. An exclusive call is an ordered
|
|
@@ -88,7 +92,7 @@ export async function runTurn(history, options, events, signal) {
|
|
|
88
92
|
repairs.push({ call, run });
|
|
89
93
|
results.push(run.result);
|
|
90
94
|
}
|
|
91
|
-
|
|
95
|
+
append({ role: "user", content: results });
|
|
92
96
|
// History repair is the invariant. UI recovery is best-effort and must
|
|
93
97
|
// never replace the original exception or leave the conversation open.
|
|
94
98
|
for (const { call, run } of repairs) {
|
|
@@ -101,13 +105,13 @@ export async function runTurn(history, options, events, signal) {
|
|
|
101
105
|
// The surface is already failing; the next turn can still proceed.
|
|
102
106
|
}
|
|
103
107
|
}
|
|
104
|
-
await
|
|
108
|
+
await checkpoint("checkpointed");
|
|
105
109
|
if (interrupted)
|
|
106
110
|
throw abortReason(signal);
|
|
107
111
|
throw error;
|
|
108
112
|
}
|
|
109
|
-
|
|
110
|
-
await
|
|
113
|
+
append({ role: "user", content: results });
|
|
114
|
+
await checkpoint("checkpointed");
|
|
111
115
|
}
|
|
112
116
|
throw new Error(`gave up after ${options.maxSteps} steps without finishing (raise --max-steps)`);
|
|
113
117
|
}
|
|
@@ -190,3 +194,6 @@ function throwIfAborted(signal) {
|
|
|
190
194
|
function abortReason(signal) {
|
|
191
195
|
return signal.reason instanceof Error ? signal.reason : new Error("interrupted");
|
|
192
196
|
}
|
|
197
|
+
function clone(value) {
|
|
198
|
+
return structuredClone(value);
|
|
199
|
+
}
|
package/dist/conversation.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// Canonical settled conversation state.
|
|
2
2
|
//
|
|
3
3
|
// A node owns one complete user-turn delta. The selected root-to-node path is
|
|
4
|
-
// the
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// the durable source for the transcript and full history; context anchors can
|
|
5
|
+
// project an older prefix before it is sent to a provider. Provider traffic
|
|
6
|
+
// and live screen blocks remain prospective until a consistent checkpoint.
|
|
7
|
+
import { projectContext, validContextAnchor } from "./context/projection.js";
|
|
7
8
|
export const CONVERSATION_LIMITS = Object.freeze({
|
|
8
9
|
nodes: 1_024,
|
|
9
10
|
messageCodeUnits: 8_388_608,
|
|
10
11
|
transcriptCodeUnits: 8_388_608,
|
|
12
|
+
contextCodeUnits: 8_388_608,
|
|
11
13
|
});
|
|
12
14
|
/** Immutable tree with one selected model/transcript path. */
|
|
13
15
|
export class ConversationTree {
|
|
@@ -34,6 +36,7 @@ export class ConversationTree {
|
|
|
34
36
|
identity: node.identity,
|
|
35
37
|
messages: node.messages,
|
|
36
38
|
blocks: node.blocks,
|
|
39
|
+
...(node.context === undefined ? {} : { context: node.context }),
|
|
37
40
|
}, node.settlement);
|
|
38
41
|
const restored = tree.activeNode;
|
|
39
42
|
if (restored === undefined || restored.id !== node.id) {
|
|
@@ -92,6 +95,9 @@ export class ConversationTree {
|
|
|
92
95
|
get history() {
|
|
93
96
|
return this.#path().flatMap((node) => clone(node.messages));
|
|
94
97
|
}
|
|
98
|
+
get contextHistory() {
|
|
99
|
+
return projectContext(this.#path());
|
|
100
|
+
}
|
|
95
101
|
get transcript() {
|
|
96
102
|
return this.#path().flatMap((node) => clone(node.blocks));
|
|
97
103
|
}
|
|
@@ -108,6 +114,7 @@ export class ConversationTree {
|
|
|
108
114
|
identity: draft.identity,
|
|
109
115
|
messages: draft.messages,
|
|
110
116
|
blocks: settledBlocks(draft.blocks),
|
|
117
|
+
...(draft.context === undefined ? {} : { context: draft.context }),
|
|
111
118
|
});
|
|
112
119
|
assertTurn(node);
|
|
113
120
|
const nodes = [...this.#nodes, node];
|
|
@@ -127,6 +134,7 @@ export class ConversationTree {
|
|
|
127
134
|
identity: draft.identity,
|
|
128
135
|
messages: draft.messages,
|
|
129
136
|
blocks: settledBlocks(draft.blocks),
|
|
137
|
+
context: draft.context ?? current.context,
|
|
130
138
|
});
|
|
131
139
|
assertTurn(node);
|
|
132
140
|
const nodes = [...this.#nodes];
|
|
@@ -154,6 +162,7 @@ function ownedNode(node) {
|
|
|
154
162
|
identity: Object.freeze({ ...node.identity }),
|
|
155
163
|
messages: Object.freeze(clone(node.messages)),
|
|
156
164
|
blocks: Object.freeze(clone(node.blocks)),
|
|
165
|
+
...(node.context === undefined ? {} : { context: Object.freeze({ ...node.context }) }),
|
|
157
166
|
});
|
|
158
167
|
}
|
|
159
168
|
function settledBlocks(blocks) {
|
|
@@ -189,9 +198,13 @@ function assertTurn(node) {
|
|
|
189
198
|
function assertBounds(nodes) {
|
|
190
199
|
let messageCodeUnits = 0;
|
|
191
200
|
let transcriptCodeUnits = 0;
|
|
201
|
+
let contextCodeUnits = 0;
|
|
192
202
|
for (const node of nodes) {
|
|
193
203
|
messageCodeUnits += JSON.stringify(node.messages).length;
|
|
194
204
|
transcriptCodeUnits += JSON.stringify(node.blocks).length;
|
|
205
|
+
contextCodeUnits += node.context?.summary.length ?? 0;
|
|
206
|
+
if (node.context !== undefined)
|
|
207
|
+
assertContextPath(nodes, node);
|
|
195
208
|
}
|
|
196
209
|
if (messageCodeUnits > CONVERSATION_LIMITS.messageCodeUnits) {
|
|
197
210
|
throw new Error("conversation model history reached its session limit — start /new");
|
|
@@ -199,6 +212,21 @@ function assertBounds(nodes) {
|
|
|
199
212
|
if (transcriptCodeUnits > CONVERSATION_LIMITS.transcriptCodeUnits) {
|
|
200
213
|
throw new Error("conversation transcript reached its session limit — start /new");
|
|
201
214
|
}
|
|
215
|
+
if (contextCodeUnits > CONVERSATION_LIMITS.contextCodeUnits) {
|
|
216
|
+
throw new Error("conversation context summaries reached their session limit — start /new");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function assertContextPath(nodes, owner) {
|
|
220
|
+
const context = owner.context;
|
|
221
|
+
const boundary = nodes[context.throughNodeId - 1];
|
|
222
|
+
if (boundary === undefined || !validContextAnchor(context, boundary.messages.length)) {
|
|
223
|
+
throw new Error("turn context checkpoint is invalid");
|
|
224
|
+
}
|
|
225
|
+
let id = owner.id;
|
|
226
|
+
while (id !== 0 && id !== boundary.id)
|
|
227
|
+
id = nodes[id - 1]?.parentId ?? 0;
|
|
228
|
+
if (id !== boundary.id)
|
|
229
|
+
throw new Error("turn context checkpoint is outside its branch");
|
|
202
230
|
}
|
|
203
231
|
function validNodeId(value) {
|
|
204
232
|
return Number.isSafeInteger(value) && value >= 0;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// purpose: the default omits the reasoning text, which on a screen reads as a
|
|
8
8
|
// long silence before anything appears.
|
|
9
9
|
import { postSse } from "./http.js";
|
|
10
|
-
import {
|
|
10
|
+
import { modelCatalog } from "./catalog.js";
|
|
11
11
|
import { keyFor } from "../credentials.js";
|
|
12
12
|
import { EFFORTS, requireSupportedEffort } from "../effort.js";
|
|
13
13
|
import { assembleAnthropic } from "./anthropic-stream.js";
|
|
@@ -19,6 +19,7 @@ const KEY = "ANTHROPIC_API_KEY";
|
|
|
19
19
|
const ADAPTIVE = /^claude-(?:fable-5|mythos-(?:5|preview)|opus-(?:5|4-[678])|sonnet-(?:5|4-6))(?:-|$)/;
|
|
20
20
|
const MAX_WITHOUT_XHIGH = ["low", "medium", "high", "max"];
|
|
21
21
|
const ANTHROPIC_45_EFFORTS = ["low", "medium", "high"];
|
|
22
|
+
let contextByModel = new Map();
|
|
22
23
|
export function supportsAdaptiveThinking(model) {
|
|
23
24
|
return ADAPTIVE.test(model);
|
|
24
25
|
}
|
|
@@ -41,12 +42,24 @@ export const anthropic = {
|
|
|
41
42
|
},
|
|
42
43
|
// Newest first is how the endpoint already answers, so the order is left
|
|
43
44
|
// exactly as it arrives rather than re-sorted into something less useful.
|
|
44
|
-
models(signal, onStatus) {
|
|
45
|
-
|
|
45
|
+
async models(signal, onStatus) {
|
|
46
|
+
const catalog = await loadModels(signal, onStatus);
|
|
47
|
+
contextByModel = catalog.contexts;
|
|
48
|
+
return catalog.ids;
|
|
46
49
|
},
|
|
47
50
|
async efforts(model) {
|
|
48
51
|
return anthropicEfforts(model);
|
|
49
52
|
},
|
|
53
|
+
async contextWindow(model, signal, onStatus) {
|
|
54
|
+
if (contextByModel.has(model))
|
|
55
|
+
return contextByModel.get(model);
|
|
56
|
+
const catalog = await loadModels(signal, onStatus);
|
|
57
|
+
contextByModel = catalog.contexts;
|
|
58
|
+
const context = contextByModel.get(model);
|
|
59
|
+
if (!contextByModel.has(model))
|
|
60
|
+
contextByModel.set(model, undefined);
|
|
61
|
+
return context;
|
|
62
|
+
},
|
|
50
63
|
location: () => "cloud",
|
|
51
64
|
async send(req) {
|
|
52
65
|
const key = requireKey();
|
|
@@ -77,6 +90,23 @@ export const anthropic = {
|
|
|
77
90
|
return fromWireResponse(data);
|
|
78
91
|
},
|
|
79
92
|
};
|
|
93
|
+
async function loadModels(signal, onStatus) {
|
|
94
|
+
const entries = await modelCatalog(MODELS, headers(requireKey()), signal, onStatus);
|
|
95
|
+
return {
|
|
96
|
+
ids: entries.map((entry) => entry.id),
|
|
97
|
+
contexts: new Map(entries.map((entry) => [
|
|
98
|
+
entry.id,
|
|
99
|
+
contextWindow(entry.metadata["max_input_tokens"]),
|
|
100
|
+
])),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function contextWindow(value) {
|
|
104
|
+
return validTokenCount(value) ? Object.freeze({ tokens: value }) : undefined;
|
|
105
|
+
}
|
|
106
|
+
function validTokenCount(value) {
|
|
107
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
108
|
+
value >= 4_096 && value <= 10_000_000;
|
|
109
|
+
}
|
|
80
110
|
// Read at the moment it is used, never captured at import: a key typed into
|
|
81
111
|
// the running window has to count, and so does one exported after startup.
|
|
82
112
|
function apiKey() {
|
|
@@ -13,6 +13,9 @@ export const MAX_MODEL_CATALOG_ENTRIES = 1_000;
|
|
|
13
13
|
export const MAX_MODEL_CATALOG_ITEMS = 4_000;
|
|
14
14
|
export const MAX_MODEL_ID_CHARS = 256;
|
|
15
15
|
export async function listModels(url, headers, signal, onStatus) {
|
|
16
|
+
return (await modelCatalog(url, headers, signal, onStatus)).map((entry) => entry.id);
|
|
17
|
+
}
|
|
18
|
+
export async function modelCatalog(url, headers, signal, onStatus) {
|
|
16
19
|
const body = await getJson(url, headers, signal, onStatus);
|
|
17
20
|
const data = body.data;
|
|
18
21
|
if (!Array.isArray(data))
|
|
@@ -22,16 +25,20 @@ export async function listModels(url, headers, signal, onStatus) {
|
|
|
22
25
|
const inspected = Math.min(data.length, MAX_MODEL_CATALOG_ITEMS);
|
|
23
26
|
for (let index = 0; index < inspected && models.length < MAX_MODEL_CATALOG_ENTRIES; index++) {
|
|
24
27
|
const entry = data[index];
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const metadata = record(entry) ? entry : undefined;
|
|
29
|
+
if (metadata === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const id = metadata["id"];
|
|
28
32
|
if (typeof id !== "string" ||
|
|
29
33
|
id === "" ||
|
|
30
34
|
id.length > MAX_MODEL_ID_CHARS ||
|
|
31
35
|
unique.has(id))
|
|
32
36
|
continue;
|
|
33
37
|
unique.add(id);
|
|
34
|
-
models.push(id);
|
|
38
|
+
models.push({ id, metadata });
|
|
35
39
|
}
|
|
36
40
|
return models;
|
|
37
41
|
}
|
|
42
|
+
function record(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
package/dist/providers/ollama.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// pulled or the subscription grants — so the model has to be named with
|
|
7
7
|
// --model.
|
|
8
8
|
import { requireSupportedEffort } from "../effort.js";
|
|
9
|
-
import { postSse } from "./http.js";
|
|
9
|
+
import { getJson, postJson, postSse } from "./http.js";
|
|
10
10
|
import { listModels } from "./catalog.js";
|
|
11
11
|
import { keyFor } from "../credentials.js";
|
|
12
12
|
import { assembleOllama } from "./ollama-stream.js";
|
|
@@ -16,6 +16,8 @@ const KEY = "OLLAMA_API_KEY";
|
|
|
16
16
|
// Ollama also accepts `none`; Jecode's product-wide reasoning floor is `low`.
|
|
17
17
|
const OLLAMA_EFFORTS = ["low", "medium", "high"];
|
|
18
18
|
let configuredHost;
|
|
19
|
+
const CONTEXT_CACHE_MS = 30_000;
|
|
20
|
+
const contextByEndpoint = new Map();
|
|
19
21
|
/** Set the endpoint selected for this process. Undefined restores key-aware inference. */
|
|
20
22
|
export function configureOllama(host) {
|
|
21
23
|
configuredHost = host === undefined ? undefined : parseOllamaEndpoint(host).baseUrl;
|
|
@@ -51,6 +53,21 @@ export const ollama = {
|
|
|
51
53
|
async efforts() {
|
|
52
54
|
return OLLAMA_EFFORTS;
|
|
53
55
|
},
|
|
56
|
+
async contextWindow(model, signal, onStatus) {
|
|
57
|
+
const at = endpoint();
|
|
58
|
+
const cacheKey = `${at.baseUrl}\u0000${model}`;
|
|
59
|
+
const cached = contextByEndpoint.get(cacheKey);
|
|
60
|
+
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
61
|
+
return cached.value;
|
|
62
|
+
const observed = await nativeContextWindow(at, model, signal, onStatus);
|
|
63
|
+
if (observed?.runtime === true) {
|
|
64
|
+
contextByEndpoint.set(cacheKey, {
|
|
65
|
+
value: observed.value,
|
|
66
|
+
expiresAt: Date.now() + CONTEXT_CACHE_MS,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return observed?.value;
|
|
70
|
+
},
|
|
54
71
|
location: () => {
|
|
55
72
|
try {
|
|
56
73
|
return endpoint().loopback ? "local" : "cloud";
|
|
@@ -79,6 +96,63 @@ export const ollama = {
|
|
|
79
96
|
return fromWireReply(reply);
|
|
80
97
|
},
|
|
81
98
|
};
|
|
99
|
+
async function nativeContextWindow(at, model, signal, onStatus) {
|
|
100
|
+
try {
|
|
101
|
+
const running = await getJson(`${at.baseUrl}/api/ps`, headers(at), signal, onStatus);
|
|
102
|
+
const allocated = runningContext(running, model);
|
|
103
|
+
if (allocated !== undefined)
|
|
104
|
+
return { value: usableContext(allocated), runtime: true };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throwIfAborted(signal, error);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const details = await postJson(`${at.baseUrl}/api/show`, headers(at), { model }, signal, onStatus);
|
|
111
|
+
const capacity = modelCapacity(details);
|
|
112
|
+
return capacity === undefined
|
|
113
|
+
? undefined
|
|
114
|
+
: { value: usableContext(capacity), runtime: false };
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
throwIfAborted(signal, error);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function runningContext(value, model) {
|
|
122
|
+
if (!record(value) || !Array.isArray(value["models"]))
|
|
123
|
+
return undefined;
|
|
124
|
+
for (const entry of value["models"]) {
|
|
125
|
+
if (!record(entry))
|
|
126
|
+
continue;
|
|
127
|
+
if (entry["name"] !== model && entry["model"] !== model)
|
|
128
|
+
continue;
|
|
129
|
+
if (validTokenCount(entry["context_length"]))
|
|
130
|
+
return entry["context_length"];
|
|
131
|
+
}
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
function modelCapacity(value) {
|
|
135
|
+
if (!record(value) || !record(value["model_info"]))
|
|
136
|
+
return undefined;
|
|
137
|
+
const capacities = Object.entries(value["model_info"])
|
|
138
|
+
.filter(([name, count]) => name.endsWith(".context_length") && validTokenCount(count))
|
|
139
|
+
.map(([, count]) => count);
|
|
140
|
+
return capacities.length === 0 ? undefined : Math.max(...capacities);
|
|
141
|
+
}
|
|
142
|
+
function validTokenCount(value) {
|
|
143
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
144
|
+
value >= 4_096 && value <= 10_000_000;
|
|
145
|
+
}
|
|
146
|
+
function usableContext(tokens) {
|
|
147
|
+
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
148
|
+
}
|
|
149
|
+
function record(value) {
|
|
150
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
151
|
+
}
|
|
152
|
+
function throwIfAborted(signal, error) {
|
|
153
|
+
if (signal?.aborted === true)
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
82
156
|
function endpoint() {
|
|
83
157
|
return ollamaConnection();
|
|
84
158
|
}
|
|
@@ -19,6 +19,7 @@ const MAX_MODELS = 1_000;
|
|
|
19
19
|
const MAX_MODEL_CHARS = 256;
|
|
20
20
|
const XHIGH_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
21
21
|
let effortByModel = new Map();
|
|
22
|
+
let contextByModel = new Map();
|
|
22
23
|
export const openaiCodex = {
|
|
23
24
|
id: ID,
|
|
24
25
|
defaultModel: "",
|
|
@@ -29,7 +30,7 @@ export const openaiCodex = {
|
|
|
29
30
|
location: () => "cloud",
|
|
30
31
|
async models(signal, onStatus) {
|
|
31
32
|
const catalog = await loadCatalog(signal, onStatus);
|
|
32
|
-
|
|
33
|
+
rememberCatalog(catalog);
|
|
33
34
|
return catalog.ids;
|
|
34
35
|
},
|
|
35
36
|
async efforts(model, signal, onStatus) {
|
|
@@ -37,9 +38,19 @@ export const openaiCodex = {
|
|
|
37
38
|
if (cached !== undefined)
|
|
38
39
|
return cached;
|
|
39
40
|
const catalog = await loadCatalog(signal, onStatus);
|
|
40
|
-
|
|
41
|
+
rememberCatalog(catalog);
|
|
41
42
|
return effortByModel.get(model) ?? fallbackEfforts(model);
|
|
42
43
|
},
|
|
44
|
+
async contextWindow(model, signal, onStatus) {
|
|
45
|
+
if (contextByModel.has(model))
|
|
46
|
+
return contextByModel.get(model);
|
|
47
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
48
|
+
rememberCatalog(catalog);
|
|
49
|
+
const context = contextByModel.get(model);
|
|
50
|
+
if (!contextByModel.has(model))
|
|
51
|
+
contextByModel.set(model, undefined);
|
|
52
|
+
return context;
|
|
53
|
+
},
|
|
43
54
|
async send(req) {
|
|
44
55
|
const efforts = effortByModel.get(req.model) ?? fallbackEfforts(req.model);
|
|
45
56
|
const effort = requireSupportedEffort(req.model, req.effort, efforts);
|
|
@@ -75,6 +86,10 @@ async function loadCatalog(signal, onStatus) {
|
|
|
75
86
|
return modelCatalog(body);
|
|
76
87
|
}, signal, onStatus);
|
|
77
88
|
}
|
|
89
|
+
function rememberCatalog(catalog) {
|
|
90
|
+
effortByModel = catalog.efforts;
|
|
91
|
+
contextByModel = catalog.contexts;
|
|
92
|
+
}
|
|
78
93
|
async function withAuthorization(operation, signal, onStatus) {
|
|
79
94
|
let authorization = await openAIAuthorization(undefined, signal, onStatus);
|
|
80
95
|
try {
|
|
@@ -120,6 +135,7 @@ function modelCatalog(value) {
|
|
|
120
135
|
id,
|
|
121
136
|
priority: typeof entry["priority"] === "number" ? entry["priority"] : 0,
|
|
122
137
|
efforts: reasoningLevels(entry, id),
|
|
138
|
+
context: modelContextWindow(entry),
|
|
123
139
|
}];
|
|
124
140
|
})
|
|
125
141
|
.sort((left, right) => left.priority - right.priority)
|
|
@@ -127,8 +143,36 @@ function modelCatalog(value) {
|
|
|
127
143
|
return {
|
|
128
144
|
ids: models.map((entry) => entry.id),
|
|
129
145
|
efforts: new Map(models.map((entry) => [entry.id, entry.efforts])),
|
|
146
|
+
contexts: new Map(models.map((entry) => [entry.id, entry.context])),
|
|
130
147
|
};
|
|
131
148
|
}
|
|
149
|
+
function modelContextWindow(entry) {
|
|
150
|
+
const resolved = tokenCount(entry["context_window"]) ?? tokenCount(entry["max_context_window"]);
|
|
151
|
+
if (resolved === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
const percent = percentage(entry["effective_context_window_percent"]) ?? 95;
|
|
154
|
+
const tokens = Math.floor(resolved * percent / 100);
|
|
155
|
+
if (!validTokenCount(tokens))
|
|
156
|
+
return undefined;
|
|
157
|
+
const automatic = Math.floor(resolved * 9 / 10);
|
|
158
|
+
const advertised = tokenCount(entry["auto_compact_token_limit"]);
|
|
159
|
+
return Object.freeze({
|
|
160
|
+
tokens,
|
|
161
|
+
compactAtTokens: Math.min(advertised ?? automatic, automatic, tokens),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function tokenCount(value) {
|
|
165
|
+
return validTokenCount(value) ? value : undefined;
|
|
166
|
+
}
|
|
167
|
+
function validTokenCount(value) {
|
|
168
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
169
|
+
value >= 4_096 && value <= 10_000_000;
|
|
170
|
+
}
|
|
171
|
+
function percentage(value) {
|
|
172
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1 && value <= 100
|
|
173
|
+
? value
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
132
176
|
function reasoningLevels(entry, model) {
|
|
133
177
|
const source = entry["supported_reasoning_levels"];
|
|
134
178
|
if (!Array.isArray(source))
|
package/dist/providers/openai.js
CHANGED
|
@@ -37,6 +37,20 @@ export function openAIEfforts(model) {
|
|
|
37
37
|
return STANDARD_EFFORTS;
|
|
38
38
|
return STANDARD_EFFORTS;
|
|
39
39
|
}
|
|
40
|
+
/** Conservative capacities for the reasoning families accepted by this transport. */
|
|
41
|
+
export function openAIContextWindow(model) {
|
|
42
|
+
if (/^gpt-5\.6(?:[.-]|$)/.test(model))
|
|
43
|
+
return usableContext(1_050_000);
|
|
44
|
+
if (/^gpt-5(?:[.-]|$)/.test(model))
|
|
45
|
+
return usableContext(400_000);
|
|
46
|
+
if (/^(?:o(?:1|3|4)|codex-mini)(?:[.-]|$)/.test(model)) {
|
|
47
|
+
return usableContext(200_000);
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function usableContext(tokens) {
|
|
52
|
+
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
53
|
+
}
|
|
40
54
|
export const openai = {
|
|
41
55
|
id: "openai",
|
|
42
56
|
defaultModel: "gpt-5",
|
|
@@ -55,6 +69,9 @@ export const openai = {
|
|
|
55
69
|
async efforts(model) {
|
|
56
70
|
return openAIEfforts(model);
|
|
57
71
|
},
|
|
72
|
+
async contextWindow(model) {
|
|
73
|
+
return openAIContextWindow(model);
|
|
74
|
+
},
|
|
58
75
|
location: () => "cloud",
|
|
59
76
|
async send(req) {
|
|
60
77
|
const key = requireKey();
|
package/dist/sessions/codec.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Strict codecs for session files. Disk is an untrusted boundary even when
|
|
2
2
|
// the directory is owner-only: every value is bounded and re-owned before it
|
|
3
3
|
// can become conversation or provider input.
|
|
4
|
-
|
|
4
|
+
import { CONTEXT_LIMITS } from "../context/projection.js";
|
|
5
|
+
export const SESSION_SCHEMA = 2;
|
|
5
6
|
export const SESSION_FILE_LIMITS = Object.freeze({
|
|
6
7
|
text: 1_048_576,
|
|
7
8
|
jsonDepth: 24,
|
|
@@ -16,14 +17,14 @@ export function decodeMeta(value) {
|
|
|
16
17
|
if (!record(value) || !keys(value, "createdAt,id,version,workspaceDigest,workspaceRoot")) {
|
|
17
18
|
throw invalid();
|
|
18
19
|
}
|
|
19
|
-
if (value["version"]
|
|
20
|
+
if (!schema(value["version"]) ||
|
|
20
21
|
!identifier(value["id"]) ||
|
|
21
22
|
!bounded(value["workspaceRoot"], 32_768) ||
|
|
22
23
|
!digest(value["workspaceDigest"]) ||
|
|
23
24
|
!timestamp(value["createdAt"]))
|
|
24
25
|
throw invalid();
|
|
25
26
|
return Object.freeze({
|
|
26
|
-
version:
|
|
27
|
+
version: value["version"],
|
|
27
28
|
id: value["id"],
|
|
28
29
|
workspaceRoot: value["workspaceRoot"],
|
|
29
30
|
workspaceDigest: value["workspaceDigest"],
|
|
@@ -37,7 +38,7 @@ export function decodeHead(value) {
|
|
|
37
38
|
if (!record(value) || !keys(value, "nodeId,parentId,revision,sequence,updatedAt,version")) {
|
|
38
39
|
throw invalid();
|
|
39
40
|
}
|
|
40
|
-
if (value["version"]
|
|
41
|
+
if (!schema(value["version"]) ||
|
|
41
42
|
!integer(value["sequence"], 0) ||
|
|
42
43
|
!integer(value["nodeId"], 1) ||
|
|
43
44
|
!integer(value["parentId"], 0) || value["parentId"] >= value["nodeId"] ||
|
|
@@ -45,7 +46,7 @@ export function decodeHead(value) {
|
|
|
45
46
|
!timestamp(value["updatedAt"]))
|
|
46
47
|
throw invalid();
|
|
47
48
|
return Object.freeze({
|
|
48
|
-
version:
|
|
49
|
+
version: value["version"],
|
|
49
50
|
sequence: value["sequence"],
|
|
50
51
|
nodeId: value["nodeId"],
|
|
51
52
|
parentId: value["parentId"],
|
|
@@ -67,17 +68,22 @@ export function encodeNode(node, sequence, updatedAt) {
|
|
|
67
68
|
identity: node.identity,
|
|
68
69
|
messages: node.messages.map(messageRecord),
|
|
69
70
|
blocks: node.blocks.flatMap(blockRecord),
|
|
71
|
+
context: node.context ?? null,
|
|
70
72
|
},
|
|
71
73
|
});
|
|
72
74
|
}
|
|
73
75
|
export function decodeNode(value) {
|
|
74
76
|
if (!record(value) || !keys(value, "node,sequence,updatedAt,version"))
|
|
75
77
|
throw invalid();
|
|
76
|
-
|
|
78
|
+
const version = value["version"];
|
|
79
|
+
if (!schema(version) || !integer(value["sequence"], 1) ||
|
|
77
80
|
!timestamp(value["updatedAt"]))
|
|
78
81
|
throw invalid();
|
|
79
82
|
const raw = value["node"];
|
|
80
|
-
|
|
83
|
+
const nodeKeys = version === 1
|
|
84
|
+
? "blocks,createdAt,id,identity,messages,parentId,revision,settlement"
|
|
85
|
+
: "blocks,context,createdAt,id,identity,messages,parentId,revision,settlement";
|
|
86
|
+
if (!record(raw) || !keys(raw, nodeKeys)) {
|
|
81
87
|
throw invalid();
|
|
82
88
|
}
|
|
83
89
|
const identity = raw["identity"];
|
|
@@ -94,6 +100,9 @@ export function decodeNode(value) {
|
|
|
94
100
|
!Array.isArray(messages) || messages.length > SESSION_FILE_LIMITS.blocks ||
|
|
95
101
|
!Array.isArray(blocks) || blocks.length > SESSION_FILE_LIMITS.blocks)
|
|
96
102
|
throw invalid();
|
|
103
|
+
const context = version === 1
|
|
104
|
+
? undefined
|
|
105
|
+
: contextFromRecord(raw["context"], raw["id"], messages.length);
|
|
97
106
|
const node = Object.freeze({
|
|
98
107
|
id: raw["id"],
|
|
99
108
|
parentId: raw["parentId"],
|
|
@@ -107,9 +116,29 @@ export function decodeNode(value) {
|
|
|
107
116
|
}),
|
|
108
117
|
messages: Object.freeze(messages.map(messageFromRecord)),
|
|
109
118
|
blocks: Object.freeze(blocks.map(blockFromRecord)),
|
|
119
|
+
...(context === undefined ? {} : { context: Object.freeze(context) }),
|
|
110
120
|
});
|
|
111
121
|
return Object.freeze({ sequence: value["sequence"], updatedAt: value["updatedAt"], node });
|
|
112
122
|
}
|
|
123
|
+
function contextFromRecord(value, ownerId, ownerMessages) {
|
|
124
|
+
if (value === null)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (!record(value) || !keys(value, "createdAt,messageCount,summary,throughNodeId")) {
|
|
127
|
+
throw invalid();
|
|
128
|
+
}
|
|
129
|
+
if (!integer(value["throughNodeId"], 1) || value["throughNodeId"] > ownerId ||
|
|
130
|
+
!integer(value["messageCount"], 0) ||
|
|
131
|
+
(value["throughNodeId"] === ownerId && value["messageCount"] > ownerMessages) ||
|
|
132
|
+
!timestamp(value["createdAt"]) ||
|
|
133
|
+
!bounded(value["summary"], CONTEXT_LIMITS.summaryCodeUnits))
|
|
134
|
+
throw invalid();
|
|
135
|
+
return {
|
|
136
|
+
throughNodeId: value["throughNodeId"],
|
|
137
|
+
messageCount: value["messageCount"],
|
|
138
|
+
createdAt: value["createdAt"],
|
|
139
|
+
summary: value["summary"],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
113
142
|
function messageRecord(message) {
|
|
114
143
|
return {
|
|
115
144
|
role: message.role,
|
|
@@ -326,6 +355,9 @@ function boundedText(value, limit = SESSION_FILE_LIMITS.text) {
|
|
|
326
355
|
function integer(value, minimum) {
|
|
327
356
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
328
357
|
}
|
|
358
|
+
function schema(value) {
|
|
359
|
+
return value === 1 || value === SESSION_SCHEMA;
|
|
360
|
+
}
|
|
329
361
|
function nullableInteger(value, minimum) {
|
|
330
362
|
return value === null || integer(value, minimum);
|
|
331
363
|
}
|