@giovannijecha/jecode 0.8.4 → 0.8.6
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 +28 -9
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch-view.js +27 -2
- package/dist/batch.js +39 -4
- package/dist/bounded-file.js +212 -0
- package/dist/cli-info.js +0 -1
- package/dist/commands.js +2 -0
- package/dist/config.js +16 -7
- package/dist/context/automatic.js +35 -0
- package/dist/context/compactor.js +32 -2
- package/dist/context/manual.js +20 -3
- package/dist/context/request-projection.js +130 -0
- package/dist/controller-request.js +33 -11
- package/dist/credential-commands.js +25 -9
- package/dist/credentials.js +56 -18
- package/dist/directory-anchor.js +91 -0
- package/dist/file-identity.js +12 -0
- package/dist/model-command.js +5 -4
- package/dist/openai-account-command.js +23 -10
- package/dist/openai-account.js +7 -5
- package/dist/openai-oauth-callback.js +13 -4
- package/dist/openai-oauth-tokens.js +9 -7
- package/dist/openai-oauth.js +8 -6
- package/dist/permission-command.js +1 -1
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +11 -37
- package/dist/provider-errors.js +31 -7
- package/dist/provider-label.js +9 -3
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -20
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +82 -23
- package/dist/providers/index.js +1 -5
- package/dist/providers/ollama-context.js +42 -0
- package/dist/providers/ollama-endpoint.js +7 -34
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +36 -158
- package/dist/providers/openai-codex.js +71 -45
- package/dist/providers/openai-stream.js +26 -2
- package/dist/providers/openai.js +51 -25
- package/dist/providers/sse.js +52 -8
- package/dist/request-identity.js +32 -0
- 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/lease.js +132 -49
- package/dist/sessions/load.js +90 -0
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/snapshot.js +33 -0
- package/dist/sessions/store.js +183 -378
- package/dist/settings-command.js +10 -5
- package/dist/settings.js +75 -22
- package/dist/stable-directory.js +148 -0
- package/dist/start.js +1 -2
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- 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 -171
- package/dist/tools/glob.js +107 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/search.js +66 -205
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +9 -334
- package/dist/tui/approve.js +5 -3
- package/dist/tui/blocks.js +8 -7
- package/dist/tui/command-workflow.js +106 -0
- package/dist/tui/components/command-menu.js +7 -10
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/menu.js +74 -43
- package/dist/tui/components/messages.js +16 -9
- 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/feedback.js +4 -0
- package/dist/tui/help.js +1 -1
- package/dist/tui/picker-layout.js +40 -0
- package/dist/tui/picker.js +7 -71
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/tool-details.js +135 -0
- package/dist/tui/transcript-grammar.js +8 -1
- package/dist/tui/transcript-view.js +26 -112
- package/dist/tui/turn-workflow.js +264 -0
- package/dist/tui/turn.js +7 -140
- package/dist/tui/workflow-types.js +2 -0
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +14 -15
- package/dist/ollama-settings-command.js +0 -74
- package/dist/tools/ripgrep.js +0 -230
- package/dist/tui/motion.js +0 -32
package/dist/providers/ollama.js
CHANGED
|
@@ -1,186 +1,64 @@
|
|
|
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";
|
|
13
|
-
import {
|
|
7
|
+
import { ollamaContextWindow } from "./ollama-context.js";
|
|
8
|
+
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
9
|
+
import { OLLAMA_CLOUD_HOST } from "./ollama-endpoint.js";
|
|
14
10
|
import { fromWireReply, stopNotice, toWireMessages, toWireTool } from "./ollama-wire.js";
|
|
15
11
|
const KEY = "OLLAMA_API_KEY";
|
|
12
|
+
const ID = "ollama";
|
|
16
13
|
// Ollama also accepts `none`; Jecode's product-wide reasoning floor is `low`.
|
|
17
14
|
const OLLAMA_EFFORTS = ["low", "medium", "high"];
|
|
18
|
-
let configuredHost;
|
|
19
|
-
const CONTEXT_CACHE_MS = 30_000;
|
|
20
|
-
const runtimeContextByEndpoint = new Map();
|
|
21
|
-
const modelContextByEndpoint = new Map();
|
|
22
|
-
/** Set the endpoint selected for this process. Undefined restores key-aware inference. */
|
|
23
|
-
export function configureOllama(host) {
|
|
24
|
-
configuredHost = host === undefined ? undefined : parseOllamaEndpoint(host).baseUrl;
|
|
25
|
-
}
|
|
26
|
-
export function ollamaConnection() {
|
|
27
|
-
const inferred = configuredHost === undefined;
|
|
28
|
-
const endpoint = parseOllamaEndpoint(configuredHost ?? (apiKey() === undefined ? OLLAMA_LOCAL_HOST : OLLAMA_CLOUD_HOST));
|
|
29
|
-
return { ...endpoint, kind: ollamaConnectionKind(endpoint), inferred };
|
|
30
|
-
}
|
|
31
15
|
export const ollama = {
|
|
32
|
-
id:
|
|
16
|
+
id: ID,
|
|
33
17
|
defaultModel: "",
|
|
34
18
|
auth: { kind: "api-key", keyVar: KEY },
|
|
35
|
-
// The only provider whose key is conditional: a daemon on this machine is
|
|
36
|
-
// reached over loopback and asks for nothing, so demanding a key there
|
|
37
|
-
// would be an invented requirement.
|
|
38
19
|
blocked() {
|
|
20
|
+
return keyFor(KEY) === undefined ? `${KEY} is not set` : undefined;
|
|
21
|
+
},
|
|
22
|
+
async models(signal, onStatus) {
|
|
39
23
|
try {
|
|
40
|
-
|
|
41
|
-
if (apiKey() !== undefined || at.loopback)
|
|
42
|
-
return undefined;
|
|
43
|
-
return `${KEY} is not set (required by ${at.baseUrl})`;
|
|
24
|
+
return await listModels(`${OLLAMA_CLOUD_HOST}/v1/models`, headers(), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
|
|
44
25
|
}
|
|
45
26
|
catch (error) {
|
|
46
|
-
|
|
27
|
+
throwProviderError(ID, signal, error);
|
|
47
28
|
}
|
|
48
29
|
},
|
|
49
|
-
// Whatever the daemon has pulled, or whatever the subscription grants.
|
|
50
|
-
models(signal, onStatus) {
|
|
51
|
-
const at = endpoint();
|
|
52
|
-
return listModels(`${at.baseUrl}/v1/models`, headers(at), signal, onStatus);
|
|
53
|
-
},
|
|
54
30
|
async efforts() {
|
|
55
31
|
return OLLAMA_EFFORTS;
|
|
56
32
|
},
|
|
57
33
|
async contextWindow(model, signal, onStatus) {
|
|
58
|
-
|
|
59
|
-
const cacheKey = `${at.baseUrl}\u0000${model}`;
|
|
60
|
-
const runtime = cachedContext(runtimeContextByEndpoint, cacheKey);
|
|
61
|
-
if (runtime !== undefined)
|
|
62
|
-
return runtime;
|
|
63
|
-
const modelCapacity = cachedContext(modelContextByEndpoint, cacheKey);
|
|
64
|
-
const observed = await nativeContextWindow(at, model, modelCapacity, signal, onStatus);
|
|
65
|
-
if (observed?.runtime === true) {
|
|
66
|
-
rememberContext(runtimeContextByEndpoint, cacheKey, observed.value);
|
|
67
|
-
}
|
|
68
|
-
else if (observed !== undefined && modelCapacity === undefined) {
|
|
69
|
-
rememberContext(modelContextByEndpoint, cacheKey, observed.value);
|
|
70
|
-
}
|
|
71
|
-
return observed?.value;
|
|
34
|
+
return ollamaContextWindow(model, headers(), signal, onStatus);
|
|
72
35
|
},
|
|
73
|
-
|
|
36
|
+
async send(req) {
|
|
37
|
+
const effort = requireSupportedEffort(req.model, req.effort, OLLAMA_EFFORTS);
|
|
74
38
|
try {
|
|
75
|
-
|
|
39
|
+
const events = await postSse(`${OLLAMA_CLOUD_HOST}/v1/chat/completions`, headers(), {
|
|
40
|
+
model: req.model,
|
|
41
|
+
messages: toWireMessages(req.system, req.messages),
|
|
42
|
+
tools: req.tools.map(toWireTool),
|
|
43
|
+
max_tokens: req.maxTokens,
|
|
44
|
+
reasoning_effort: effort,
|
|
45
|
+
stream: true,
|
|
46
|
+
stream_options: { include_usage: true },
|
|
47
|
+
}, req.maxTokens, req.signal, req.onStatus, undefined, (error) => isRetryableGenerationFailure(ID, error));
|
|
48
|
+
const reply = await assembleOllama(events, req.onStream);
|
|
49
|
+
const notice = stopNotice(reply);
|
|
50
|
+
if (notice !== undefined)
|
|
51
|
+
req.onStream?.({ kind: "text", text: `\n${notice}` });
|
|
52
|
+
return fromWireReply(reply);
|
|
76
53
|
}
|
|
77
|
-
catch {
|
|
78
|
-
|
|
54
|
+
catch (error) {
|
|
55
|
+
throwProviderError(ID, req.signal, error);
|
|
79
56
|
}
|
|
80
57
|
},
|
|
81
|
-
async send(req) {
|
|
82
|
-
const at = endpoint();
|
|
83
|
-
const effort = requireSupportedEffort(req.model, req.effort, OLLAMA_EFFORTS);
|
|
84
|
-
// The OpenAI-compatible endpoint accepts this vocabulary for thinking
|
|
85
|
-
// models. Invalid levels are rejected locally instead of being rewritten.
|
|
86
|
-
const events = await postSse(`${at.baseUrl}/v1/chat/completions`, headers(at), {
|
|
87
|
-
model: req.model,
|
|
88
|
-
messages: toWireMessages(req.system, req.messages),
|
|
89
|
-
tools: req.tools.map(toWireTool),
|
|
90
|
-
max_tokens: req.maxTokens,
|
|
91
|
-
reasoning_effort: effort,
|
|
92
|
-
stream: true,
|
|
93
|
-
stream_options: { include_usage: true },
|
|
94
|
-
}, req.maxTokens, req.signal, req.onStatus);
|
|
95
|
-
const reply = await assembleOllama(events, req.onStream);
|
|
96
|
-
const notice = stopNotice(reply);
|
|
97
|
-
if (notice !== undefined)
|
|
98
|
-
req.onStream?.({ kind: "text", text: `\n${notice}` });
|
|
99
|
-
return fromWireReply(reply);
|
|
100
|
-
},
|
|
101
58
|
};
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
return { value: usableContext(allocated), runtime: true };
|
|
108
|
-
}
|
|
109
|
-
catch (error) {
|
|
110
|
-
throwIfAborted(signal, error);
|
|
111
|
-
}
|
|
112
|
-
if (fallback !== undefined)
|
|
113
|
-
return { value: fallback, runtime: false };
|
|
114
|
-
try {
|
|
115
|
-
const details = await postJson(`${at.baseUrl}/api/show`, headers(at), { model }, signal, onStatus);
|
|
116
|
-
const capacity = modelCapacity(details);
|
|
117
|
-
return capacity === undefined
|
|
118
|
-
? undefined
|
|
119
|
-
: { value: usableContext(capacity), runtime: false };
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
throwIfAborted(signal, error);
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function cachedContext(cache, key) {
|
|
127
|
-
const cached = cache.get(key);
|
|
128
|
-
if (cached === undefined)
|
|
129
|
-
return undefined;
|
|
130
|
-
if (cached.expiresAt > Date.now())
|
|
131
|
-
return cached.value;
|
|
132
|
-
cache.delete(key);
|
|
133
|
-
return undefined;
|
|
134
|
-
}
|
|
135
|
-
function rememberContext(cache, key, value) {
|
|
136
|
-
cache.set(key, { value, expiresAt: Date.now() + CONTEXT_CACHE_MS });
|
|
137
|
-
}
|
|
138
|
-
function runningContext(value, model) {
|
|
139
|
-
if (!record(value) || !Array.isArray(value["models"]))
|
|
140
|
-
return undefined;
|
|
141
|
-
for (const entry of value["models"]) {
|
|
142
|
-
if (!record(entry))
|
|
143
|
-
continue;
|
|
144
|
-
if (entry["name"] !== model && entry["model"] !== model)
|
|
145
|
-
continue;
|
|
146
|
-
if (validTokenCount(entry["context_length"]))
|
|
147
|
-
return entry["context_length"];
|
|
148
|
-
}
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
function modelCapacity(value) {
|
|
152
|
-
if (!record(value) || !record(value["model_info"]))
|
|
153
|
-
return undefined;
|
|
154
|
-
const capacities = Object.entries(value["model_info"])
|
|
155
|
-
.filter(([name, count]) => name.endsWith(".context_length") && validTokenCount(count))
|
|
156
|
-
.map(([, count]) => count);
|
|
157
|
-
return capacities.length === 0 ? undefined : Math.max(...capacities);
|
|
158
|
-
}
|
|
159
|
-
function validTokenCount(value) {
|
|
160
|
-
return typeof value === "number" && Number.isSafeInteger(value) &&
|
|
161
|
-
value >= 4_096 && value <= 10_000_000;
|
|
162
|
-
}
|
|
163
|
-
function usableContext(tokens) {
|
|
164
|
-
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
165
|
-
}
|
|
166
|
-
function record(value) {
|
|
167
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
168
|
-
}
|
|
169
|
-
function throwIfAborted(signal, error) {
|
|
170
|
-
if (signal?.aborted === true)
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
function endpoint() {
|
|
174
|
-
return ollamaConnection();
|
|
175
|
-
}
|
|
176
|
-
function apiKey() {
|
|
177
|
-
return keyFor(KEY);
|
|
178
|
-
}
|
|
179
|
-
function headers(at) {
|
|
180
|
-
if (at.loopback)
|
|
181
|
-
return {};
|
|
182
|
-
const key = apiKey();
|
|
183
|
-
if (key !== undefined)
|
|
184
|
-
return { authorization: `Bearer ${key}` };
|
|
185
|
-
throw new Error(`${KEY} is not set (required by ${at.baseUrl})`);
|
|
59
|
+
function headers() {
|
|
60
|
+
const key = keyFor(KEY);
|
|
61
|
+
if (key === undefined)
|
|
62
|
+
throw new Error(`${KEY} is not set`);
|
|
63
|
+
return { authorization: `Bearer ${key}` };
|
|
186
64
|
}
|
|
@@ -2,10 +2,12 @@
|
|
|
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";
|
|
8
|
+
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
7
9
|
import { getJson, postSse } from "./http.js";
|
|
8
|
-
import { assembleOpenAI } from "./openai-stream.js";
|
|
10
|
+
import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
|
|
9
11
|
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
10
12
|
const ID = "openai-codex";
|
|
11
13
|
const BASE = "https://chatgpt.com/backend-api/codex";
|
|
@@ -13,7 +15,6 @@ const BASE = "https://chatgpt.com/backend-api/codex";
|
|
|
13
15
|
// own catalogue updater uses this sentinel to request the complete current
|
|
14
16
|
// manifest; Jecode then keeps only entries explicitly visible in that manifest.
|
|
15
17
|
const CATALOG_COMPATIBILITY_VERSION = "99.99.99";
|
|
16
|
-
const SESSION_ID = randomUUID();
|
|
17
18
|
const MAX_CATALOG_ITEMS = 4_000;
|
|
18
19
|
const MAX_MODELS = 1_000;
|
|
19
20
|
const MAX_MODEL_CHARS = 256;
|
|
@@ -23,66 +24,89 @@ let contextByModel = new Map();
|
|
|
23
24
|
export const openaiCodex = {
|
|
24
25
|
id: ID,
|
|
25
26
|
defaultModel: "",
|
|
26
|
-
auth: { kind: "oauth", account: ID, label:
|
|
27
|
+
auth: { kind: "oauth", account: ID, label: providerLabel(ID) },
|
|
27
28
|
blocked() {
|
|
28
|
-
return openAICodexAccount() === undefined ?
|
|
29
|
+
return openAICodexAccount() === undefined ? `${providerLabel(ID)} is not connected` : undefined;
|
|
29
30
|
},
|
|
30
|
-
location: () => "cloud",
|
|
31
31
|
async models(signal, onStatus) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
try {
|
|
33
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
34
|
+
rememberCatalog(catalog);
|
|
35
|
+
return catalog.ids;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throwProviderError(ID, signal, error);
|
|
39
|
+
}
|
|
35
40
|
},
|
|
36
41
|
async efforts(model, signal, onStatus) {
|
|
37
42
|
const cached = effortByModel.get(model);
|
|
38
43
|
if (cached !== undefined)
|
|
39
44
|
return cached;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
try {
|
|
46
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
47
|
+
rememberCatalog(catalog);
|
|
48
|
+
return effortByModel.get(model) ?? fallbackEfforts(model);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throwProviderError(ID, signal, error);
|
|
52
|
+
}
|
|
43
53
|
},
|
|
44
54
|
async contextWindow(model, signal, onStatus) {
|
|
45
55
|
if (contextByModel.has(model))
|
|
46
56
|
return contextByModel.get(model);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
contextByModel.
|
|
52
|
-
|
|
57
|
+
try {
|
|
58
|
+
const catalog = await loadCatalog(signal, onStatus);
|
|
59
|
+
rememberCatalog(catalog);
|
|
60
|
+
const context = contextByModel.get(model);
|
|
61
|
+
if (!contextByModel.has(model))
|
|
62
|
+
contextByModel.set(model, undefined);
|
|
63
|
+
return context;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throwProviderError(ID, signal, error);
|
|
67
|
+
}
|
|
53
68
|
},
|
|
54
69
|
async send(req) {
|
|
55
70
|
const efforts = effortByModel.get(req.model) ?? fallbackEfforts(req.model);
|
|
56
71
|
const effort = requireSupportedEffort(req.model, req.effort, efforts);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
const sessionId = req.identity?.conversationId ?? randomUUID();
|
|
73
|
+
const cacheKey = req.identity?.cacheKey ?? sessionId;
|
|
74
|
+
try {
|
|
75
|
+
return await withAuthorization(async (authorization) => {
|
|
76
|
+
const events = await postSse(`${BASE}/responses`, {
|
|
77
|
+
...headers(authorization, sessionId, randomUUID()),
|
|
78
|
+
"openai-beta": "responses=experimental",
|
|
79
|
+
}, {
|
|
80
|
+
model: req.model,
|
|
81
|
+
store: false,
|
|
82
|
+
stream: true,
|
|
83
|
+
instructions: req.system,
|
|
84
|
+
input: req.messages.flatMap((message) => toWireItems(message, ID)),
|
|
85
|
+
tools: req.tools.map(toWireTool),
|
|
86
|
+
tool_choice: "auto",
|
|
87
|
+
parallel_tool_calls: true,
|
|
88
|
+
reasoning: { effort, summary: "auto" },
|
|
89
|
+
text: { verbosity: "low" },
|
|
90
|
+
include: ["reasoning.encrypted_content"],
|
|
91
|
+
...(req.identity?.purpose === "compaction"
|
|
92
|
+
? {}
|
|
93
|
+
: { prompt_cache_key: cacheKey }),
|
|
94
|
+
}, req.maxTokens, req.signal, req.onStatus, openAIStreamProgress, (error) => isRetryableGenerationFailure(ID, error));
|
|
95
|
+
const data = await assembleOpenAI(events, req.onStream, req.onStatus);
|
|
96
|
+
const notice = stopNotice(data);
|
|
97
|
+
if (notice !== undefined)
|
|
98
|
+
req.onStream?.({ kind: "text", text: `\n${notice}` });
|
|
99
|
+
return fromWireResponse(data, ID);
|
|
100
|
+
}, req.signal, req.onStatus);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throwProviderError(ID, req.signal, error);
|
|
104
|
+
}
|
|
81
105
|
},
|
|
82
106
|
};
|
|
83
107
|
async function loadCatalog(signal, onStatus) {
|
|
84
108
|
return withAuthorization(async (authorization) => {
|
|
85
|
-
const body = await getJson(`${BASE}/models?client_version=${CATALOG_COMPATIBILITY_VERSION}`, headers(authorization, randomUUID()), signal, onStatus);
|
|
109
|
+
const body = await getJson(`${BASE}/models?client_version=${CATALOG_COMPATIBILITY_VERSION}`, headers(authorization, randomUUID(), randomUUID()), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
|
|
86
110
|
return modelCatalog(body);
|
|
87
111
|
}, signal, onStatus);
|
|
88
112
|
}
|
|
@@ -102,21 +126,21 @@ async function withAuthorization(operation, signal, onStatus) {
|
|
|
102
126
|
return operation(authorization);
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
|
-
function headers(authorization, requestId) {
|
|
129
|
+
function headers(authorization, sessionId, requestId) {
|
|
106
130
|
const version = applicationVersion();
|
|
107
131
|
return {
|
|
108
132
|
authorization: `Bearer ${authorization.accessToken}`,
|
|
109
133
|
"chatgpt-account-id": authorization.accountId,
|
|
110
134
|
originator: "jecode",
|
|
111
135
|
"user-agent": `jecode/${version} (${process.platform}; ${process.arch})`,
|
|
112
|
-
"session-id":
|
|
136
|
+
"session-id": sessionId,
|
|
113
137
|
"x-client-request-id": requestId,
|
|
114
138
|
};
|
|
115
139
|
}
|
|
116
140
|
function modelCatalog(value) {
|
|
117
141
|
const source = record(value) && Array.isArray(value["models"]) ? value["models"] : undefined;
|
|
118
142
|
if (source === undefined)
|
|
119
|
-
throw new Error(
|
|
143
|
+
throw new Error(`${providerLabel(ID)} did not return a model list`);
|
|
120
144
|
const seen = new Set();
|
|
121
145
|
const models = source
|
|
122
146
|
.slice(0, MAX_CATALOG_ITEMS)
|
|
@@ -185,6 +209,8 @@ function reasoningLevels(entry, model) {
|
|
|
185
209
|
return efforts.length === 0 ? fallbackEfforts(model) : efforts;
|
|
186
210
|
}
|
|
187
211
|
function fallbackEfforts(model) {
|
|
212
|
+
if (/^gpt-6-astra(?:-|$)/.test(model))
|
|
213
|
+
return EFFORTS;
|
|
188
214
|
if (/^gpt-5\.6-(?:sol|terra|luna)(?:-|$)/.test(model))
|
|
189
215
|
return EFFORTS;
|
|
190
216
|
return XHIGH_EFFORTS;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// response in `response.completed`. The ChatGPT Codex backend can instead send
|
|
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
|
+
import { providerWireError } from "./failure.js";
|
|
7
8
|
export async function assembleOpenAI(events, onStream, onStatus) {
|
|
8
9
|
const items = [];
|
|
9
10
|
const announcedTools = { identities: new Set(), anonymous: false };
|
|
@@ -69,6 +70,9 @@ export async function assembleOpenAI(events, onStream, onStatus) {
|
|
|
69
70
|
if (isFunctionCall(event.item)) {
|
|
70
71
|
announceTool(event, event.item, announcedTools, onStream, status);
|
|
71
72
|
}
|
|
73
|
+
else if (itemType(event.item) === "reasoning") {
|
|
74
|
+
status("Working");
|
|
75
|
+
}
|
|
72
76
|
items.push(event.item);
|
|
73
77
|
}
|
|
74
78
|
break;
|
|
@@ -82,16 +86,36 @@ export async function assembleOpenAI(events, onStream, onStatus) {
|
|
|
82
86
|
};
|
|
83
87
|
case "response.failed": {
|
|
84
88
|
const response = event.response;
|
|
85
|
-
throw
|
|
89
|
+
throw providerWireError("openai stream error", response?.error?.message, {
|
|
90
|
+
code: response?.error?.code,
|
|
91
|
+
type: response?.error?.type,
|
|
92
|
+
});
|
|
86
93
|
}
|
|
87
94
|
case "error":
|
|
88
|
-
throw
|
|
95
|
+
throw providerWireError("openai stream error", event.error?.message ?? event.message, { code: event.error?.code, type: event.error?.type });
|
|
89
96
|
default:
|
|
90
97
|
break;
|
|
91
98
|
}
|
|
92
99
|
}
|
|
93
100
|
throw new Error("openai stream ended before a terminal response event");
|
|
94
101
|
}
|
|
102
|
+
/** State-only keepalives prove transport liveness, not forward model progress. */
|
|
103
|
+
export function openAIStreamProgress(raw) {
|
|
104
|
+
if (typeof raw !== "object" || raw === null)
|
|
105
|
+
return false;
|
|
106
|
+
const type = raw["type"];
|
|
107
|
+
if (typeof type !== "string")
|
|
108
|
+
return false;
|
|
109
|
+
if (type === "response.created")
|
|
110
|
+
return true;
|
|
111
|
+
if (type === "response.done" ||
|
|
112
|
+
type === "response.completed" ||
|
|
113
|
+
type === "response.incomplete" ||
|
|
114
|
+
type === "response.failed" ||
|
|
115
|
+
type === "error")
|
|
116
|
+
return true;
|
|
117
|
+
return /\.(?:added|delta|done)$/u.test(type);
|
|
118
|
+
}
|
|
95
119
|
function isFunctionCall(item) {
|
|
96
120
|
return typeof item === "object" && item !== null &&
|
|
97
121
|
item["type"] === "function_call";
|
package/dist/providers/openai.js
CHANGED
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Responses wire contract verified against the official API reference on
|
|
4
4
|
// 2026-08-29. Keep final response events authoritative over display deltas.
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { applicationVersion } from "../version.js";
|
|
5
7
|
import { postSse } from "./http.js";
|
|
6
8
|
import { listModels } from "./catalog.js";
|
|
7
9
|
import { keyFor } from "../credentials.js";
|
|
8
10
|
import { EFFORTS, requireSupportedEffort } from "../effort.js";
|
|
9
|
-
import {
|
|
11
|
+
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
12
|
+
import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
|
|
10
13
|
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
11
14
|
const ENDPOINT = "https://api.openai.com/v1/responses";
|
|
12
15
|
const MODELS = "https://api.openai.com/v1/models";
|
|
13
16
|
const KEY = "OPENAI_API_KEY";
|
|
17
|
+
const ID = "openai";
|
|
18
|
+
const ASTRA_MODEL = /^gpt-6-astra(?:-|$)/;
|
|
14
19
|
const RESPONSES_REASONING_MODEL = /^(?:gpt-5(?:[.-]|$)|o(?:1|3|4)(?:[.-]|$)|codex-mini(?:[.-]|$))/;
|
|
15
20
|
// Jecode's transport always streams and always declares local tools. Hide
|
|
16
21
|
// catalog entries that cannot satisfy either half of that contract.
|
|
@@ -20,11 +25,14 @@ const XHIGH_EFFORTS = ["low", "medium", "high", "xhigh"];
|
|
|
20
25
|
const PRO_EFFORTS = ["medium", "high", "xhigh"];
|
|
21
26
|
const HIGH_ONLY_EFFORT = ["high"];
|
|
22
27
|
export function supportsOpenAIModel(model) {
|
|
23
|
-
return
|
|
28
|
+
return ASTRA_MODEL.test(model) ||
|
|
29
|
+
(RESPONSES_REASONING_MODEL.test(model) && !INCOMPATIBLE_MODEL.test(model));
|
|
24
30
|
}
|
|
25
31
|
export function openAIEfforts(model) {
|
|
26
32
|
if (!supportsOpenAIModel(model))
|
|
27
33
|
return [];
|
|
34
|
+
if (ASTRA_MODEL.test(model))
|
|
35
|
+
return EFFORTS;
|
|
28
36
|
if (/^gpt-5-pro(?:-|$)/.test(model))
|
|
29
37
|
return HIGH_ONLY_EFFORT;
|
|
30
38
|
if (/^gpt-5\.[2-5]-pro(?:-|$)/.test(model))
|
|
@@ -39,6 +47,8 @@ export function openAIEfforts(model) {
|
|
|
39
47
|
}
|
|
40
48
|
/** Conservative capacities for the reasoning families accepted by this transport. */
|
|
41
49
|
export function openAIContextWindow(model) {
|
|
50
|
+
if (ASTRA_MODEL.test(model))
|
|
51
|
+
return usableContext(1_050_000);
|
|
42
52
|
if (/^gpt-5\.6(?:[.-]|$)/.test(model))
|
|
43
53
|
return usableContext(1_050_000);
|
|
44
54
|
if (/^gpt-5(?:[.-]|$)/.test(model))
|
|
@@ -52,7 +62,7 @@ function usableContext(tokens) {
|
|
|
52
62
|
return Object.freeze({ tokens: Math.floor(tokens * 95 / 100) });
|
|
53
63
|
}
|
|
54
64
|
export const openai = {
|
|
55
|
-
id:
|
|
65
|
+
id: ID,
|
|
56
66
|
defaultModel: "gpt-5",
|
|
57
67
|
auth: { kind: "api-key", keyVar: KEY },
|
|
58
68
|
blocked() {
|
|
@@ -61,10 +71,15 @@ export const openai = {
|
|
|
61
71
|
// The endpoint answers in no order worth keeping, so descending puts the
|
|
62
72
|
// highest-numbered family — usually the newest — at the top of the menu.
|
|
63
73
|
async models(signal, onStatus) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
try {
|
|
75
|
+
const ids = await listModels(MODELS, headers(requireKey()), signal, onStatus, (error) => isRetryableReadFailure(ID, error));
|
|
76
|
+
return ids
|
|
77
|
+
.filter(supportsOpenAIModel)
|
|
78
|
+
.sort((a, b) => b.localeCompare(a));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throwProviderError(ID, signal, error);
|
|
82
|
+
}
|
|
68
83
|
},
|
|
69
84
|
async efforts(model) {
|
|
70
85
|
return openAIEfforts(model);
|
|
@@ -72,26 +87,33 @@ export const openai = {
|
|
|
72
87
|
async contextWindow(model) {
|
|
73
88
|
return openAIContextWindow(model);
|
|
74
89
|
},
|
|
75
|
-
location: () => "cloud",
|
|
76
90
|
async send(req) {
|
|
77
91
|
const key = requireKey();
|
|
78
92
|
const effort = requireSupportedEffort(req.model, req.effort, openAIEfforts(req.model));
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
req.
|
|
94
|
-
|
|
93
|
+
try {
|
|
94
|
+
const events = await postSse(ENDPOINT, headers(key), {
|
|
95
|
+
model: req.model,
|
|
96
|
+
instructions: req.system,
|
|
97
|
+
input: req.messages.flatMap((message) => toWireItems(message)),
|
|
98
|
+
tools: req.tools.map(toWireTool),
|
|
99
|
+
max_output_tokens: req.maxTokens,
|
|
100
|
+
reasoning: { effort, summary: "auto" },
|
|
101
|
+
store: false,
|
|
102
|
+
include: ["reasoning.encrypted_content"],
|
|
103
|
+
stream: true,
|
|
104
|
+
...(req.identity?.purpose === "turn"
|
|
105
|
+
? { prompt_cache_key: req.identity.cacheKey }
|
|
106
|
+
: {}),
|
|
107
|
+
}, req.maxTokens, req.signal, req.onStatus, openAIStreamProgress, (error) => isRetryableGenerationFailure(ID, error));
|
|
108
|
+
const data = await assembleOpenAI(events, req.onStream, req.onStatus);
|
|
109
|
+
const notice = stopNotice(data);
|
|
110
|
+
if (notice !== undefined)
|
|
111
|
+
req.onStream?.({ kind: "text", text: `\n${notice}` });
|
|
112
|
+
return fromWireResponse(data);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throwProviderError(ID, req.signal, error);
|
|
116
|
+
}
|
|
95
117
|
},
|
|
96
118
|
};
|
|
97
119
|
function apiKey() {
|
|
@@ -104,5 +126,9 @@ function requireKey() {
|
|
|
104
126
|
return key;
|
|
105
127
|
}
|
|
106
128
|
function headers(key) {
|
|
107
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
authorization: `Bearer ${key}`,
|
|
131
|
+
"user-agent": `jecode/${applicationVersion()} (${process.platform}; ${process.arch})`,
|
|
132
|
+
"x-client-request-id": randomUUID(),
|
|
133
|
+
};
|
|
108
134
|
}
|