@giovannijecha/jecode 0.8.5 → 0.8.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/README.md +36 -9
- package/assets/tokenizers/LICENSE +21 -0
- package/assets/tokenizers/o200k-base.tiktoken.gz +0 -0
- package/dist/cli-info.js +8 -14
- package/dist/commands.js +7 -3
- package/dist/config.js +47 -53
- package/dist/context/automatic.js +16 -1
- package/dist/context/budget.js +5 -2
- package/dist/context/compactor.js +91 -37
- package/dist/context/diagnostics.js +108 -0
- package/dist/context/lifetime.js +18 -0
- package/dist/context/manager.js +67 -0
- package/dist/context/manual.js +11 -10
- package/dist/context/measurement.js +75 -0
- package/dist/context/policy.js +13 -10
- package/dist/context/request-observation.js +46 -0
- package/dist/context/request-projection.js +8 -34
- package/dist/context/request.js +22 -0
- package/dist/context/tokenizer/bpe.js +68 -0
- package/dist/context/tokenizer/o200k.js +54 -0
- package/dist/context/tokenizer/vocabulary.js +34 -0
- package/dist/controller-request.js +47 -45
- package/dist/controller.js +13 -2
- package/dist/credential-commands.js +3 -3
- package/dist/input-boundary.js +0 -62
- package/dist/launch.js +13 -9
- package/dist/model-command.js +7 -17
- package/dist/oauth-result-page.js +68 -0
- package/dist/openai-account-command.js +14 -12
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +15 -54
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +9 -24
- package/dist/permissions.js +10 -8
- package/dist/provider-commands.js +1 -33
- package/dist/provider-errors.js +1 -10
- package/dist/provider-label.js +3 -10
- package/dist/providers/anthropic-wire.js +1 -1
- package/dist/providers/anthropic.js +3 -2
- package/dist/providers/index.js +1 -5
- package/dist/providers/input-measurement.js +85 -0
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-wire.js +1 -1
- package/dist/providers/ollama.js +15 -147
- package/dist/providers/openai-codex.js +7 -4
- package/dist/providers/openai-stream.js +20 -8
- package/dist/providers/openai-summary.js +37 -0
- package/dist/providers/openai-wire.js +1 -1
- package/dist/providers/openai.js +3 -1
- package/dist/sessions/bucket.js +55 -0
- package/dist/sessions/catalog-io.js +162 -0
- package/dist/sessions/catalog.js +3 -1
- package/dist/sessions/codec-messages.js +122 -0
- package/dist/sessions/codec-transcript.js +93 -0
- package/dist/sessions/codec-values.js +52 -0
- package/dist/sessions/codec.js +4 -257
- package/dist/sessions/files.js +158 -0
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +42 -461
- package/dist/settings.js +17 -16
- package/dist/start.js +36 -53
- package/dist/timeline.js +2 -0
- package/dist/tools/file-read.js +192 -0
- package/dist/tools/file-summary.js +9 -0
- package/dist/tools/{fs.js → file-write.js} +5 -193
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +1 -105
- package/dist/tui/activity.js +1 -1
- package/dist/tui/app-input.js +34 -17
- package/dist/tui/app-workflows.js +13 -361
- package/dist/tui/app.js +55 -28
- package/dist/tui/approve.js +3 -2
- package/dist/tui/blocks.js +1 -2
- package/dist/tui/command-workflow.js +112 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/menu.js +82 -43
- package/dist/tui/components/messages.js +16 -9
- package/dist/tui/components/status.js +1 -1
- package/dist/tui/components/tool-evidence.js +107 -0
- package/dist/tui/components/tool-motion.js +32 -0
- package/dist/tui/components/tool.js +48 -202
- package/dist/tui/frame.js +8 -3
- package/dist/tui/help.js +3 -2
- package/dist/tui/picker-layout.js +44 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/screen.js +7 -0
- package/dist/tui/session-view.js +0 -1
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +1 -1
- package/dist/tui/transcript-view.js +30 -112
- package/dist/tui/turn-workflow.js +235 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/view.js +4 -4
- package/dist/tui/workflow-types.js +2 -0
- package/dist/ui/render.js +0 -4
- package/package.json +16 -12
- package/dist/batch-view.js +0 -17
- package/dist/batch.js +0 -267
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tui/motion.js +0 -32
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Cloud model capacity, with bounded metadata and conservative safety headroom.
|
|
2
|
+
import { postJson } from "./http.js";
|
|
3
|
+
import { OLLAMA_CLOUD_HOST } from "./ollama-endpoint.js";
|
|
4
|
+
const CACHE_MS = 30_000;
|
|
5
|
+
const capacities = new Map();
|
|
6
|
+
export async function ollamaContextWindow(model, headers, signal, onStatus) {
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
const cached = capacities.get(model);
|
|
9
|
+
if (cached !== undefined && cached.expiresAt > Date.now())
|
|
10
|
+
return cached.value;
|
|
11
|
+
capacities.delete(model);
|
|
12
|
+
try {
|
|
13
|
+
const details = await postJson(`${OLLAMA_CLOUD_HOST}/api/show`, headers, { model }, signal, onStatus);
|
|
14
|
+
const tokens = modelCapacity(details);
|
|
15
|
+
if (tokens === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
18
|
+
capacities.set(model, { value, expiresAt: Date.now() + CACHE_MS });
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (signal?.aborted)
|
|
23
|
+
throw error;
|
|
24
|
+
// Missing metadata leaves budgeting to the controller's safe fallback.
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function modelCapacity(value) {
|
|
29
|
+
if (!record(value) || !record(value["model_info"]))
|
|
30
|
+
return undefined;
|
|
31
|
+
const counts = Object.entries(value["model_info"])
|
|
32
|
+
.filter(([name, count]) => name.endsWith(".context_length") && validTokenCount(count))
|
|
33
|
+
.map(([, count]) => count);
|
|
34
|
+
return counts.length === 0 ? undefined : Math.max(...counts);
|
|
35
|
+
}
|
|
36
|
+
function validTokenCount(value) {
|
|
37
|
+
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
38
|
+
value >= 4_096 && value <= 10_000_000;
|
|
39
|
+
}
|
|
40
|
+
function record(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
@@ -1,40 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
export const OLLAMA_LOCAL_HOST = "http://127.0.0.1:11434";
|
|
1
|
+
// The fixed API endpoint and recognition of its retired saved setting.
|
|
3
2
|
export const OLLAMA_CLOUD_HOST = "https://ollama.com";
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
/** Recognize old official-cloud values without making endpoints configurable. */
|
|
4
|
+
export function isLegacyOllamaCloudHost(value) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return false;
|
|
6
7
|
try {
|
|
7
|
-
|
|
8
|
+
return new URL(value.trim()).href.replace(/\/+$/, "") === OLLAMA_CLOUD_HOST;
|
|
8
9
|
}
|
|
9
10
|
catch {
|
|
10
|
-
|
|
11
|
+
return false;
|
|
11
12
|
}
|
|
12
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
13
|
-
throw new Error("Ollama endpoint must use HTTP or HTTPS");
|
|
14
|
-
}
|
|
15
|
-
if (url.username !== "" || url.password !== "") {
|
|
16
|
-
throw new Error("Ollama endpoint must not contain credentials");
|
|
17
|
-
}
|
|
18
|
-
if (url.search !== "" || url.hash !== "") {
|
|
19
|
-
throw new Error("Ollama endpoint must not contain a query or fragment");
|
|
20
|
-
}
|
|
21
|
-
const loopback = isExactLoopback(url.hostname);
|
|
22
|
-
if (url.protocol === "http:" && !loopback) {
|
|
23
|
-
throw new Error("Ollama endpoint must use HTTPS unless it is an exact loopback address");
|
|
24
|
-
}
|
|
25
|
-
const pathname = url.pathname.replace(/\/+$/, "");
|
|
26
|
-
return {
|
|
27
|
-
baseUrl: `${url.origin}${pathname === "" ? "" : pathname}`,
|
|
28
|
-
loopback,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
export function ollamaConnectionKind(endpoint) {
|
|
32
|
-
if (endpoint.baseUrl === OLLAMA_CLOUD_HOST)
|
|
33
|
-
return "cloud";
|
|
34
|
-
if (endpoint.baseUrl === OLLAMA_LOCAL_HOST)
|
|
35
|
-
return "local";
|
|
36
|
-
return "custom";
|
|
37
|
-
}
|
|
38
|
-
function isExactLoopback(hostname) {
|
|
39
|
-
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
40
13
|
}
|
|
@@ -107,6 +107,6 @@ function normalizeUsage(reply) {
|
|
|
107
107
|
}
|
|
108
108
|
export function stopNotice(reply) {
|
|
109
109
|
return reply.finishReason === "length"
|
|
110
|
-
? "[truncated: hit the output limit — raise
|
|
110
|
+
? "[truncated: hit the output limit — raise max output tokens in /settings]"
|
|
111
111
|
: undefined;
|
|
112
112
|
}
|
package/dist/providers/ollama.js
CHANGED
|
@@ -1,58 +1,28 @@
|
|
|
1
|
-
// Ollama
|
|
2
|
-
//
|
|
3
|
-
// One provider covers both deployments. An explicit session endpoint wins;
|
|
4
|
-
// otherwise a configured key selects Ollama Cloud and no key selects the local
|
|
5
|
-
// daemon. There is no default model — the catalogue is whatever the host has
|
|
6
|
-
// pulled or the subscription grants — so the model has to be named with
|
|
7
|
-
// --model.
|
|
1
|
+
// Ollama Cloud through its OpenAI-compatible streaming Chat Completions API.
|
|
8
2
|
import { requireSupportedEffort } from "../effort.js";
|
|
9
|
-
import {
|
|
3
|
+
import { postSse } from "./http.js";
|
|
10
4
|
import { listModels } from "./catalog.js";
|
|
11
5
|
import { keyFor } from "../credentials.js";
|
|
12
6
|
import { assembleOllama } from "./ollama-stream.js";
|
|
7
|
+
import { ollamaContextWindow } from "./ollama-context.js";
|
|
13
8
|
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
14
|
-
import { OLLAMA_CLOUD_HOST
|
|
9
|
+
import { OLLAMA_CLOUD_HOST } from "./ollama-endpoint.js";
|
|
15
10
|
import { fromWireReply, stopNotice, toWireMessages, toWireTool } from "./ollama-wire.js";
|
|
11
|
+
import { measureOllamaInput } from "./input-measurement.js";
|
|
16
12
|
const KEY = "OLLAMA_API_KEY";
|
|
17
13
|
const ID = "ollama";
|
|
18
14
|
// Ollama also accepts `none`; Jecode's product-wide reasoning floor is `low`.
|
|
19
15
|
const OLLAMA_EFFORTS = ["low", "medium", "high"];
|
|
20
|
-
let configuredHost;
|
|
21
|
-
const CONTEXT_CACHE_MS = 30_000;
|
|
22
|
-
const runtimeContextByEndpoint = new Map();
|
|
23
|
-
const modelContextByEndpoint = new Map();
|
|
24
|
-
/** Set the endpoint selected for this process. Undefined restores key-aware inference. */
|
|
25
|
-
export function configureOllama(host) {
|
|
26
|
-
configuredHost = host === undefined ? undefined : parseOllamaEndpoint(host).baseUrl;
|
|
27
|
-
}
|
|
28
|
-
export function ollamaConnection() {
|
|
29
|
-
const inferred = configuredHost === undefined;
|
|
30
|
-
const endpoint = parseOllamaEndpoint(configuredHost ?? (apiKey() === undefined ? OLLAMA_LOCAL_HOST : OLLAMA_CLOUD_HOST));
|
|
31
|
-
return { ...endpoint, kind: ollamaConnectionKind(endpoint), inferred };
|
|
32
|
-
}
|
|
33
16
|
export const ollama = {
|
|
34
17
|
id: ID,
|
|
35
18
|
defaultModel: "",
|
|
36
19
|
auth: { kind: "api-key", keyVar: KEY },
|
|
37
|
-
// The only provider whose key is conditional: a daemon on this machine is
|
|
38
|
-
// reached over loopback and asks for nothing, so demanding a key there
|
|
39
|
-
// would be an invented requirement.
|
|
40
20
|
blocked() {
|
|
41
|
-
|
|
42
|
-
const at = endpoint();
|
|
43
|
-
if (apiKey() !== undefined || at.loopback)
|
|
44
|
-
return undefined;
|
|
45
|
-
return `${KEY} is not set (required by ${at.baseUrl})`;
|
|
46
|
-
}
|
|
47
|
-
catch (error) {
|
|
48
|
-
return error.message;
|
|
49
|
-
}
|
|
21
|
+
return keyFor(KEY) === undefined ? `${KEY} is not set` : undefined;
|
|
50
22
|
},
|
|
51
|
-
// Whatever the daemon has pulled, or whatever the subscription grants.
|
|
52
23
|
async models(signal, onStatus) {
|
|
53
24
|
try {
|
|
54
|
-
|
|
55
|
-
return await listModels(`${at.baseUrl}/v1/models`, headers(at), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
|
|
25
|
+
return await listModels(`${OLLAMA_CLOUD_HOST}/v1/models`, headers(), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
|
|
56
26
|
}
|
|
57
27
|
catch (error) {
|
|
58
28
|
throwProviderError(ID, signal, error);
|
|
@@ -62,36 +32,13 @@ export const ollama = {
|
|
|
62
32
|
return OLLAMA_EFFORTS;
|
|
63
33
|
},
|
|
64
34
|
async contextWindow(model, signal, onStatus) {
|
|
65
|
-
|
|
66
|
-
const cacheKey = `${at.baseUrl}\u0000${model}`;
|
|
67
|
-
const runtime = cachedContext(runtimeContextByEndpoint, cacheKey);
|
|
68
|
-
if (runtime !== undefined)
|
|
69
|
-
return runtime;
|
|
70
|
-
const modelCapacity = cachedContext(modelContextByEndpoint, cacheKey);
|
|
71
|
-
const observed = await nativeContextWindow(at, model, modelCapacity, signal, onStatus);
|
|
72
|
-
if (observed?.runtime === true) {
|
|
73
|
-
rememberContext(runtimeContextByEndpoint, cacheKey, observed.value);
|
|
74
|
-
}
|
|
75
|
-
else if (observed !== undefined && modelCapacity === undefined) {
|
|
76
|
-
rememberContext(modelContextByEndpoint, cacheKey, observed.value);
|
|
77
|
-
}
|
|
78
|
-
return observed?.value;
|
|
79
|
-
},
|
|
80
|
-
location: () => {
|
|
81
|
-
try {
|
|
82
|
-
return endpoint().loopback ? "local" : "cloud";
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
return "cloud";
|
|
86
|
-
}
|
|
35
|
+
return ollamaContextWindow(model, headers(), signal, onStatus);
|
|
87
36
|
},
|
|
37
|
+
measureInput: measureOllamaInput,
|
|
88
38
|
async send(req) {
|
|
89
|
-
const at = endpoint();
|
|
90
39
|
const effort = requireSupportedEffort(req.model, req.effort, OLLAMA_EFFORTS);
|
|
91
|
-
// The OpenAI-compatible endpoint accepts this vocabulary for thinking
|
|
92
|
-
// models. Invalid levels are rejected locally instead of being rewritten.
|
|
93
40
|
try {
|
|
94
|
-
const events = await postSse(`${
|
|
41
|
+
const events = await postSse(`${OLLAMA_CLOUD_HOST}/v1/chat/completions`, headers(), {
|
|
95
42
|
model: req.model,
|
|
96
43
|
messages: toWireMessages(req.system, req.messages),
|
|
97
44
|
tools: req.tools.map(toWireTool),
|
|
@@ -111,88 +58,9 @@ export const ollama = {
|
|
|
111
58
|
}
|
|
112
59
|
},
|
|
113
60
|
};
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return { value: usableContext(allocated), runtime: true };
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
throwIfAborted(signal, error);
|
|
123
|
-
}
|
|
124
|
-
if (fallback !== undefined)
|
|
125
|
-
return { value: fallback, runtime: false };
|
|
126
|
-
try {
|
|
127
|
-
const details = await postJson(`${at.baseUrl}/api/show`, headers(at), { model }, signal, onStatus);
|
|
128
|
-
const capacity = modelCapacity(details);
|
|
129
|
-
return capacity === undefined
|
|
130
|
-
? undefined
|
|
131
|
-
: { value: usableContext(capacity), runtime: false };
|
|
132
|
-
}
|
|
133
|
-
catch (error) {
|
|
134
|
-
throwIfAborted(signal, error);
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function cachedContext(cache, key) {
|
|
139
|
-
const cached = cache.get(key);
|
|
140
|
-
if (cached === undefined)
|
|
141
|
-
return undefined;
|
|
142
|
-
if (cached.expiresAt > Date.now())
|
|
143
|
-
return cached.value;
|
|
144
|
-
cache.delete(key);
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
function rememberContext(cache, key, value) {
|
|
148
|
-
cache.set(key, { value, expiresAt: Date.now() + CONTEXT_CACHE_MS });
|
|
149
|
-
}
|
|
150
|
-
function runningContext(value, model) {
|
|
151
|
-
if (!record(value) || !Array.isArray(value["models"]))
|
|
152
|
-
return undefined;
|
|
153
|
-
for (const entry of value["models"]) {
|
|
154
|
-
if (!record(entry))
|
|
155
|
-
continue;
|
|
156
|
-
if (entry["name"] !== model && entry["model"] !== model)
|
|
157
|
-
continue;
|
|
158
|
-
if (validTokenCount(entry["context_length"]))
|
|
159
|
-
return entry["context_length"];
|
|
160
|
-
}
|
|
161
|
-
return undefined;
|
|
162
|
-
}
|
|
163
|
-
function modelCapacity(value) {
|
|
164
|
-
if (!record(value) || !record(value["model_info"]))
|
|
165
|
-
return undefined;
|
|
166
|
-
const capacities = Object.entries(value["model_info"])
|
|
167
|
-
.filter(([name, count]) => name.endsWith(".context_length") && validTokenCount(count))
|
|
168
|
-
.map(([, count]) => count);
|
|
169
|
-
return capacities.length === 0 ? undefined : Math.max(...capacities);
|
|
170
|
-
}
|
|
171
|
-
function validTokenCount(value) {
|
|
172
|
-
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
173
|
-
value >= 4_096 && value <= 10_000_000;
|
|
174
|
-
}
|
|
175
|
-
function usableContext(tokens) {
|
|
176
|
-
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
177
|
-
}
|
|
178
|
-
function record(value) {
|
|
179
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
180
|
-
}
|
|
181
|
-
function throwIfAborted(signal, error) {
|
|
182
|
-
if (signal?.aborted === true)
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
function endpoint() {
|
|
186
|
-
return ollamaConnection();
|
|
187
|
-
}
|
|
188
|
-
function apiKey() {
|
|
189
|
-
return keyFor(KEY);
|
|
190
|
-
}
|
|
191
|
-
function headers(at) {
|
|
192
|
-
if (at.loopback)
|
|
193
|
-
return {};
|
|
194
|
-
const key = apiKey();
|
|
195
|
-
if (key !== undefined)
|
|
196
|
-
return { authorization: `Bearer ${key}` };
|
|
197
|
-
throw new Error(`${KEY} is not set (required by ${at.baseUrl})`);
|
|
61
|
+
function headers() {
|
|
62
|
+
const key = keyFor(KEY);
|
|
63
|
+
if (key === undefined)
|
|
64
|
+
throw new Error(`${KEY} is not set`);
|
|
65
|
+
return { authorization: `Bearer ${key}` };
|
|
198
66
|
}
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { openAICodexAccount } from "../accounts.js";
|
|
4
4
|
import { openAIAuthorization } from "../openai-account.js";
|
|
5
|
+
import { providerLabel } from "../provider-label.js";
|
|
5
6
|
import { applicationVersion } from "../version.js";
|
|
6
7
|
import { EFFORTS, isEffort, requireSupportedEffort } from "../effort.js";
|
|
7
8
|
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
8
9
|
import { getJson, postSse } from "./http.js";
|
|
9
10
|
import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
|
|
10
11
|
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
12
|
+
import { measureResponsesInput, responsesTokenization } from "./input-measurement.js";
|
|
11
13
|
const ID = "openai-codex";
|
|
12
14
|
const BASE = "https://chatgpt.com/backend-api/codex";
|
|
13
15
|
// Jecode's product version is unrelated to the Codex protocol gate. OpenAI's
|
|
@@ -23,11 +25,10 @@ let contextByModel = new Map();
|
|
|
23
25
|
export const openaiCodex = {
|
|
24
26
|
id: ID,
|
|
25
27
|
defaultModel: "",
|
|
26
|
-
auth: { kind: "oauth", account: ID, label:
|
|
28
|
+
auth: { kind: "oauth", account: ID, label: providerLabel(ID) },
|
|
27
29
|
blocked() {
|
|
28
|
-
return openAICodexAccount() === undefined ?
|
|
30
|
+
return openAICodexAccount() === undefined ? `${providerLabel(ID)} is not connected` : undefined;
|
|
29
31
|
},
|
|
30
|
-
location: () => "cloud",
|
|
31
32
|
async models(signal, onStatus) {
|
|
32
33
|
try {
|
|
33
34
|
const catalog = await loadCatalog(signal, onStatus);
|
|
@@ -66,6 +67,8 @@ export const openaiCodex = {
|
|
|
66
67
|
throwProviderError(ID, signal, error);
|
|
67
68
|
}
|
|
68
69
|
},
|
|
70
|
+
measureInput: (request, signal) => measureResponsesInput(request, ID, signal),
|
|
71
|
+
inputTokenization: (model) => responsesTokenization(model, ID),
|
|
69
72
|
async send(req) {
|
|
70
73
|
const efforts = effortByModel.get(req.model) ?? fallbackEfforts(req.model);
|
|
71
74
|
const effort = requireSupportedEffort(req.model, req.effort, efforts);
|
|
@@ -140,7 +143,7 @@ function headers(authorization, sessionId, requestId) {
|
|
|
140
143
|
function modelCatalog(value) {
|
|
141
144
|
const source = record(value) && Array.isArray(value["models"]) ? value["models"] : undefined;
|
|
142
145
|
if (source === undefined)
|
|
143
|
-
throw new Error(
|
|
146
|
+
throw new Error(`${providerLabel(ID)} did not return a model list`);
|
|
144
147
|
const seen = new Set();
|
|
145
148
|
const models = source
|
|
146
149
|
.slice(0, MAX_CATALOG_ITEMS)
|
|
@@ -5,9 +5,16 @@
|
|
|
5
5
|
// an empty final `output` after complete `response.output_item.done` events, so
|
|
6
6
|
// those streamed items remain the fallback when the final envelope is empty.
|
|
7
7
|
import { providerWireError } from "./failure.js";
|
|
8
|
+
import { OpenAISummary } from "./openai-summary.js";
|
|
8
9
|
export async function assembleOpenAI(events, onStream, onStatus) {
|
|
9
10
|
const items = [];
|
|
10
11
|
const announcedTools = { identities: new Set(), anonymous: false };
|
|
12
|
+
const summary = new OpenAISummary();
|
|
13
|
+
const display = (event) => {
|
|
14
|
+
if (event.kind !== "thinking")
|
|
15
|
+
summary.reset();
|
|
16
|
+
onStream?.(event);
|
|
17
|
+
};
|
|
11
18
|
let refusal = false;
|
|
12
19
|
let activity;
|
|
13
20
|
const status = (next) => {
|
|
@@ -27,32 +34,36 @@ export async function assembleOpenAI(events, onStream, onStatus) {
|
|
|
27
34
|
case "response.output_text.delta":
|
|
28
35
|
if (typeof event.delta === "string") {
|
|
29
36
|
status("Responding");
|
|
30
|
-
|
|
37
|
+
display({ kind: "text", text: event.delta });
|
|
31
38
|
}
|
|
32
39
|
break;
|
|
33
40
|
case "response.refusal.delta":
|
|
34
41
|
if (typeof event.delta === "string") {
|
|
35
42
|
status("Responding");
|
|
36
|
-
|
|
43
|
+
display({ kind: "text", text: `${refusal ? "" : "[refused] "}${event.delta}` });
|
|
37
44
|
refusal = true;
|
|
38
45
|
}
|
|
39
46
|
break;
|
|
40
|
-
case "response.reasoning_summary_text.delta":
|
|
41
|
-
|
|
47
|
+
case "response.reasoning_summary_text.delta": {
|
|
48
|
+
const text = summary.delta(event);
|
|
49
|
+
if (text !== undefined) {
|
|
42
50
|
status("Thinking");
|
|
43
|
-
|
|
51
|
+
display({ kind: "thinking", text });
|
|
44
52
|
}
|
|
45
53
|
break;
|
|
54
|
+
}
|
|
46
55
|
case "response.reasoning_summary_part.added":
|
|
56
|
+
summary.end();
|
|
47
57
|
status("Thinking");
|
|
48
58
|
break;
|
|
49
59
|
case "response.reasoning_summary_text.done":
|
|
50
60
|
case "response.reasoning_summary_part.done":
|
|
61
|
+
summary.end();
|
|
51
62
|
status("Working");
|
|
52
63
|
break;
|
|
53
64
|
case "response.output_item.added":
|
|
54
65
|
if (isFunctionCall(event.item)) {
|
|
55
|
-
announceTool(event, event.item, announcedTools,
|
|
66
|
+
announceTool(event, event.item, announcedTools, display, status);
|
|
56
67
|
}
|
|
57
68
|
else if (itemType(event.item) === "reasoning") {
|
|
58
69
|
status("Thinking");
|
|
@@ -63,14 +74,15 @@ export async function assembleOpenAI(events, onStream, onStatus) {
|
|
|
63
74
|
break;
|
|
64
75
|
case "response.function_call_arguments.delta":
|
|
65
76
|
case "response.function_call_arguments.done":
|
|
66
|
-
announceTool(event, undefined, announcedTools,
|
|
77
|
+
announceTool(event, undefined, announcedTools, display, status);
|
|
67
78
|
break;
|
|
68
79
|
case "response.output_item.done":
|
|
69
80
|
if (event.item !== undefined) {
|
|
70
81
|
if (isFunctionCall(event.item)) {
|
|
71
|
-
announceTool(event, event.item, announcedTools,
|
|
82
|
+
announceTool(event, event.item, announcedTools, display, status);
|
|
72
83
|
}
|
|
73
84
|
else if (itemType(event.item) === "reasoning") {
|
|
85
|
+
summary.end();
|
|
74
86
|
status("Working");
|
|
75
87
|
}
|
|
76
88
|
items.push(event.item);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Display boundaries between Responses summary parts; provider replay stays untouched.
|
|
2
|
+
export class OpenAISummary {
|
|
3
|
+
#part;
|
|
4
|
+
#ended = false;
|
|
5
|
+
#tail = "";
|
|
6
|
+
reset() {
|
|
7
|
+
this.#part = undefined;
|
|
8
|
+
this.#ended = false;
|
|
9
|
+
this.#tail = "";
|
|
10
|
+
}
|
|
11
|
+
end() { this.#ended = true; }
|
|
12
|
+
delta(event) {
|
|
13
|
+
if (typeof event.delta !== "string" || event.delta === "")
|
|
14
|
+
return undefined;
|
|
15
|
+
const part = {
|
|
16
|
+
item: typeof event.item_id === "string" ? event.item_id : undefined,
|
|
17
|
+
output: index(event.output_index),
|
|
18
|
+
summary: index(event.summary_index),
|
|
19
|
+
};
|
|
20
|
+
const previous = this.#part;
|
|
21
|
+
const changed = previous !== undefined && ["item", "output", "summary"].some(key => (part[key] !== undefined && previous[key] !== undefined && part[key] !== previous[key]));
|
|
22
|
+
// Completion events cover older/idless streams. Identity changes also cover
|
|
23
|
+
// streams that omit those events. Wait for text so empty parts add no rows.
|
|
24
|
+
const boundary = previous !== undefined && (this.#ended || changed);
|
|
25
|
+
const trailing = this.#tail.match(/\n{0,2}$/u)?.[0].length ?? 0;
|
|
26
|
+
const leading = event.delta.match(/^\n{0,2}/u)?.[0].length ?? 0;
|
|
27
|
+
const prefix = boundary ? "\n".repeat(Math.max(0, 2 - trailing - leading)) : "";
|
|
28
|
+
const text = prefix + event.delta;
|
|
29
|
+
this.#tail = (this.#tail + text).slice(-2);
|
|
30
|
+
this.#part = part;
|
|
31
|
+
this.#ended = false;
|
|
32
|
+
return text;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function index(value) {
|
|
36
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
|
|
37
|
+
}
|
|
@@ -45,7 +45,7 @@ export function stopNotice(data) {
|
|
|
45
45
|
return data.status === "incomplete" ? "[incomplete response]" : undefined;
|
|
46
46
|
}
|
|
47
47
|
return reason === "max_output_tokens"
|
|
48
|
-
? "[truncated: hit max_output_tokens — raise
|
|
48
|
+
? "[truncated: hit max_output_tokens — raise max output tokens in /settings]"
|
|
49
49
|
: `[incomplete: ${reason}]`;
|
|
50
50
|
}
|
|
51
51
|
export function fromWireResponse(data, providerId = "openai") {
|
package/dist/providers/openai.js
CHANGED
|
@@ -11,6 +11,7 @@ import { EFFORTS, requireSupportedEffort } from "../effort.js";
|
|
|
11
11
|
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
12
12
|
import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
|
|
13
13
|
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
14
|
+
import { measureResponsesInput, responsesTokenization } from "./input-measurement.js";
|
|
14
15
|
const ENDPOINT = "https://api.openai.com/v1/responses";
|
|
15
16
|
const MODELS = "https://api.openai.com/v1/models";
|
|
16
17
|
const KEY = "OPENAI_API_KEY";
|
|
@@ -87,7 +88,8 @@ export const openai = {
|
|
|
87
88
|
async contextWindow(model) {
|
|
88
89
|
return openAIContextWindow(model);
|
|
89
90
|
},
|
|
90
|
-
|
|
91
|
+
measureInput: (request, signal) => measureResponsesInput(request, ID, signal),
|
|
92
|
+
inputTokenization: (model) => responsesTokenization(model, ID),
|
|
91
93
|
async send(req) {
|
|
92
94
|
const key = requireKey();
|
|
93
95
|
const effort = requireSupportedEffort(req.model, req.effort, openAIEfforts(req.model));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Stable workspace identity and directory anchors shared by session operations.
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { assertDirectoryAnchor, captureDirectDirectory, preparePrivateDirectory, } from "../directory-anchor.js";
|
|
6
|
+
import { DIRECTORY_MODE, SESSION_NAME } from "./files.js";
|
|
7
|
+
export class SessionBucket {
|
|
8
|
+
workspaceRoot;
|
|
9
|
+
workspaceDigest;
|
|
10
|
+
anchor;
|
|
11
|
+
#root;
|
|
12
|
+
constructor(workspaceRoot, root, anchor) {
|
|
13
|
+
this.workspaceRoot = workspaceRoot;
|
|
14
|
+
this.workspaceDigest = digestWorkspace(workspaceRoot);
|
|
15
|
+
this.#root = root;
|
|
16
|
+
this.anchor = anchor;
|
|
17
|
+
Object.freeze(this);
|
|
18
|
+
}
|
|
19
|
+
static async open(workspaceRoot, sessionsRoot) {
|
|
20
|
+
const canonical = await realpath(path.resolve(workspaceRoot));
|
|
21
|
+
const digest = digestWorkspace(canonical);
|
|
22
|
+
const root = await preparePrivateDirectory(path.resolve(sessionsRoot), "session storage root", DIRECTORY_MODE);
|
|
23
|
+
const anchor = await preparePrivateDirectory(path.join(root.path, digest), "workspace session directory", DIRECTORY_MODE);
|
|
24
|
+
return new SessionBucket(canonical, root, anchor);
|
|
25
|
+
}
|
|
26
|
+
directory(id) {
|
|
27
|
+
return path.join(this.anchor.path, id);
|
|
28
|
+
}
|
|
29
|
+
async captureSession(id) {
|
|
30
|
+
await this.assert();
|
|
31
|
+
return captureDirectDirectory(this.directory(id), "session directory");
|
|
32
|
+
}
|
|
33
|
+
async assertSession(anchor) {
|
|
34
|
+
await Promise.all([this.assert(), assertDirectoryAnchor(anchor)]);
|
|
35
|
+
}
|
|
36
|
+
async assert() {
|
|
37
|
+
await Promise.all([assertDirectoryAnchor(this.#root), assertDirectoryAnchor(this.anchor)]);
|
|
38
|
+
}
|
|
39
|
+
assertWorkspace(meta, id) {
|
|
40
|
+
if (meta.id !== id || meta.workspaceDigest !== this.workspaceDigest ||
|
|
41
|
+
workspaceKey(meta.workspaceRoot) !== workspaceKey(this.workspaceRoot))
|
|
42
|
+
throw new Error("session belongs to a different workspace");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function digestWorkspace(workspaceRoot) {
|
|
46
|
+
return createHash("sha256").update(workspaceKey(workspaceRoot)).digest("hex");
|
|
47
|
+
}
|
|
48
|
+
export function workspaceKey(workspaceRoot) {
|
|
49
|
+
const normalized = path.normalize(workspaceRoot);
|
|
50
|
+
return process.platform === "win32" ? normalized.toLocaleLowerCase("en-US") : normalized;
|
|
51
|
+
}
|
|
52
|
+
export function assertSessionId(id) {
|
|
53
|
+
if (!SESSION_NAME.test(id))
|
|
54
|
+
throw new Error("session id is invalid");
|
|
55
|
+
}
|