@giovannijecha/jecode 0.8.3 → 0.8.5
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 +11 -8
- package/assets/wordmark-steel.svg +3 -0
- package/dist/accounts.js +47 -10
- package/dist/atomic.js +24 -14
- package/dist/batch.js +49 -4
- package/dist/bounded-file.js +212 -0
- package/dist/commands.js +2 -0
- package/dist/config.js +8 -4
- 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 +22 -6
- 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 +13 -2
- package/dist/process-lease.js +329 -0
- package/dist/provider-commands.js +53 -10
- package/dist/provider-errors.js +94 -3
- package/dist/provider-label.js +13 -0
- package/dist/providers/anthropic-stream.js +4 -1
- package/dist/providers/anthropic-wire.js +8 -3
- package/dist/providers/anthropic.js +39 -19
- package/dist/providers/catalog.js +4 -4
- package/dist/providers/failure.js +181 -0
- package/dist/providers/http.js +86 -57
- package/dist/providers/ollama-stream.js +5 -1
- package/dist/providers/ollama.js +31 -19
- package/dist/providers/openai-codex.js +67 -41
- package/dist/providers/openai-stream.js +69 -9
- package/dist/providers/openai.js +51 -24
- package/dist/providers/sse.js +89 -17
- package/dist/request-identity.js +32 -0
- package/dist/sessions/catalog.js +199 -0
- package/dist/sessions/lease.js +132 -49
- package/dist/sessions/runtime.js +15 -8
- package/dist/sessions/store.js +451 -183
- package/dist/settings.js +62 -10
- package/dist/stable-directory.js +148 -0
- package/dist/store-lock.js +68 -84
- package/dist/tools/args.js +2 -2
- package/dist/tools/fs.js +124 -102
- package/dist/tools/search.js +81 -107
- package/dist/tools/text-boundary.js +7 -33
- package/dist/tui/app-workflows.js +32 -4
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/feedback.js +4 -0
- package/dist/tui/session-view.js +7 -2
- package/dist/tui/workspace.js +21 -7
- package/dist/user-store.js +23 -31
- package/package.json +4 -4
- package/dist/tools/ripgrep.js +0 -230
|
@@ -4,8 +4,9 @@ import { openAICodexAccount } from "../accounts.js";
|
|
|
4
4
|
import { openAIAuthorization } from "../openai-account.js";
|
|
5
5
|
import { applicationVersion } from "../version.js";
|
|
6
6
|
import { EFFORTS, isEffort, requireSupportedEffort } from "../effort.js";
|
|
7
|
+
import { isRetryableGenerationFailure, isRetryableReadFailure, throwProviderError, } from "./failure.js";
|
|
7
8
|
import { getJson, postSse } from "./http.js";
|
|
8
|
-
import { assembleOpenAI } from "./openai-stream.js";
|
|
9
|
+
import { assembleOpenAI, openAIStreamProgress } from "./openai-stream.js";
|
|
9
10
|
import { fromWireResponse, stopNotice, toWireItems, toWireTool, } from "./openai-wire.js";
|
|
10
11
|
const ID = "openai-codex";
|
|
11
12
|
const BASE = "https://chatgpt.com/backend-api/codex";
|
|
@@ -13,7 +14,6 @@ const BASE = "https://chatgpt.com/backend-api/codex";
|
|
|
13
14
|
// own catalogue updater uses this sentinel to request the complete current
|
|
14
15
|
// manifest; Jecode then keeps only entries explicitly visible in that manifest.
|
|
15
16
|
const CATALOG_COMPATIBILITY_VERSION = "99.99.99";
|
|
16
|
-
const SESSION_ID = randomUUID();
|
|
17
17
|
const MAX_CATALOG_ITEMS = 4_000;
|
|
18
18
|
const MAX_MODELS = 1_000;
|
|
19
19
|
const MAX_MODEL_CHARS = 256;
|
|
@@ -29,60 +29,84 @@ export const openaiCodex = {
|
|
|
29
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,14 +126,14 @@ 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
|
}
|
|
@@ -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,40 +4,74 @@
|
|
|
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
|
-
|
|
7
|
+
import { providerWireError } from "./failure.js";
|
|
8
|
+
export async function assembleOpenAI(events, onStream, onStatus) {
|
|
8
9
|
const items = [];
|
|
9
10
|
const announcedTools = { identities: new Set(), anonymous: false };
|
|
10
11
|
let refusal = false;
|
|
12
|
+
let activity;
|
|
13
|
+
const status = (next) => {
|
|
14
|
+
if (activity === next)
|
|
15
|
+
return;
|
|
16
|
+
activity = next;
|
|
17
|
+
onStatus?.(next);
|
|
18
|
+
};
|
|
11
19
|
for await (const raw of events) {
|
|
12
20
|
const event = raw;
|
|
13
21
|
switch (event.type) {
|
|
22
|
+
case "response.created":
|
|
23
|
+
case "response.in_progress":
|
|
24
|
+
if (activity === undefined)
|
|
25
|
+
status("Working");
|
|
26
|
+
break;
|
|
14
27
|
case "response.output_text.delta":
|
|
15
|
-
if (typeof event.delta === "string")
|
|
28
|
+
if (typeof event.delta === "string") {
|
|
29
|
+
status("Responding");
|
|
16
30
|
onStream?.({ kind: "text", text: event.delta });
|
|
31
|
+
}
|
|
17
32
|
break;
|
|
18
33
|
case "response.refusal.delta":
|
|
19
34
|
if (typeof event.delta === "string") {
|
|
35
|
+
status("Responding");
|
|
20
36
|
onStream?.({ kind: "text", text: `${refusal ? "" : "[refused] "}${event.delta}` });
|
|
21
37
|
refusal = true;
|
|
22
38
|
}
|
|
23
39
|
break;
|
|
24
40
|
case "response.reasoning_summary_text.delta":
|
|
25
|
-
if (typeof event.delta === "string")
|
|
41
|
+
if (typeof event.delta === "string") {
|
|
42
|
+
status("Thinking");
|
|
26
43
|
onStream?.({ kind: "thinking", text: event.delta });
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case "response.reasoning_summary_part.added":
|
|
47
|
+
status("Thinking");
|
|
48
|
+
break;
|
|
49
|
+
case "response.reasoning_summary_text.done":
|
|
50
|
+
case "response.reasoning_summary_part.done":
|
|
51
|
+
status("Working");
|
|
27
52
|
break;
|
|
28
53
|
case "response.output_item.added":
|
|
29
54
|
if (isFunctionCall(event.item)) {
|
|
30
|
-
announceTool(event, event.item, announcedTools, onStream);
|
|
55
|
+
announceTool(event, event.item, announcedTools, onStream, status);
|
|
56
|
+
}
|
|
57
|
+
else if (itemType(event.item) === "reasoning") {
|
|
58
|
+
status("Thinking");
|
|
59
|
+
}
|
|
60
|
+
else if (itemType(event.item) === "message") {
|
|
61
|
+
status("Responding");
|
|
31
62
|
}
|
|
32
63
|
break;
|
|
33
64
|
case "response.function_call_arguments.delta":
|
|
34
65
|
case "response.function_call_arguments.done":
|
|
35
|
-
announceTool(event, undefined, announcedTools, onStream);
|
|
66
|
+
announceTool(event, undefined, announcedTools, onStream, status);
|
|
36
67
|
break;
|
|
37
68
|
case "response.output_item.done":
|
|
38
69
|
if (event.item !== undefined) {
|
|
39
70
|
if (isFunctionCall(event.item)) {
|
|
40
|
-
announceTool(event, event.item, announcedTools, onStream);
|
|
71
|
+
announceTool(event, event.item, announcedTools, onStream, status);
|
|
72
|
+
}
|
|
73
|
+
else if (itemType(event.item) === "reasoning") {
|
|
74
|
+
status("Working");
|
|
41
75
|
}
|
|
42
76
|
items.push(event.item);
|
|
43
77
|
}
|
|
@@ -52,21 +86,46 @@ export async function assembleOpenAI(events, onStream) {
|
|
|
52
86
|
};
|
|
53
87
|
case "response.failed": {
|
|
54
88
|
const response = event.response;
|
|
55
|
-
throw
|
|
89
|
+
throw providerWireError("openai stream error", response?.error?.message, {
|
|
90
|
+
code: response?.error?.code,
|
|
91
|
+
type: response?.error?.type,
|
|
92
|
+
});
|
|
56
93
|
}
|
|
57
94
|
case "error":
|
|
58
|
-
throw
|
|
95
|
+
throw providerWireError("openai stream error", event.error?.message ?? event.message, { code: event.error?.code, type: event.error?.type });
|
|
59
96
|
default:
|
|
60
97
|
break;
|
|
61
98
|
}
|
|
62
99
|
}
|
|
63
100
|
throw new Error("openai stream ended before a terminal response event");
|
|
64
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
|
+
}
|
|
65
119
|
function isFunctionCall(item) {
|
|
66
120
|
return typeof item === "object" && item !== null &&
|
|
67
121
|
item["type"] === "function_call";
|
|
68
122
|
}
|
|
69
|
-
function
|
|
123
|
+
function itemType(item) {
|
|
124
|
+
return typeof item === "object" && item !== null
|
|
125
|
+
? item["type"]
|
|
126
|
+
: undefined;
|
|
127
|
+
}
|
|
128
|
+
function announceTool(event, item, announced, onStream, onStatus) {
|
|
70
129
|
const identities = toolIdentities(event, item);
|
|
71
130
|
if (identities.length === 0) {
|
|
72
131
|
if (announced.anonymous)
|
|
@@ -82,6 +141,7 @@ function announceTool(event, item, announced, onStream) {
|
|
|
82
141
|
}
|
|
83
142
|
const rawName = item?.name ?? event.name;
|
|
84
143
|
const name = typeof rawName === "string" && rawName !== "" ? rawName : undefined;
|
|
144
|
+
onStatus?.(`Preparing ${name ?? "tool"}`);
|
|
85
145
|
onStream?.({ kind: "tool", ...(name === undefined ? {} : { name }) });
|
|
86
146
|
}
|
|
87
147
|
function toolIdentities(event, item) {
|
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);
|
|
@@ -76,22 +91,30 @@ export const openai = {
|
|
|
76
91
|
async send(req) {
|
|
77
92
|
const key = requireKey();
|
|
78
93
|
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
|
-
|
|
94
|
+
try {
|
|
95
|
+
const events = await postSse(ENDPOINT, headers(key), {
|
|
96
|
+
model: req.model,
|
|
97
|
+
instructions: req.system,
|
|
98
|
+
input: req.messages.flatMap((message) => toWireItems(message)),
|
|
99
|
+
tools: req.tools.map(toWireTool),
|
|
100
|
+
max_output_tokens: req.maxTokens,
|
|
101
|
+
reasoning: { effort, summary: "auto" },
|
|
102
|
+
store: false,
|
|
103
|
+
include: ["reasoning.encrypted_content"],
|
|
104
|
+
stream: true,
|
|
105
|
+
...(req.identity?.purpose === "turn"
|
|
106
|
+
? { prompt_cache_key: req.identity.cacheKey }
|
|
107
|
+
: {}),
|
|
108
|
+
}, req.maxTokens, req.signal, req.onStatus, openAIStreamProgress, (error) => isRetryableGenerationFailure(ID, error));
|
|
109
|
+
const data = await assembleOpenAI(events, req.onStream, req.onStatus);
|
|
110
|
+
const notice = stopNotice(data);
|
|
111
|
+
if (notice !== undefined)
|
|
112
|
+
req.onStream?.({ kind: "text", text: `\n${notice}` });
|
|
113
|
+
return fromWireResponse(data);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
throwProviderError(ID, req.signal, error);
|
|
117
|
+
}
|
|
95
118
|
},
|
|
96
119
|
};
|
|
97
120
|
function apiKey() {
|
|
@@ -104,5 +127,9 @@ function requireKey() {
|
|
|
104
127
|
return key;
|
|
105
128
|
}
|
|
106
129
|
function headers(key) {
|
|
107
|
-
return {
|
|
130
|
+
return {
|
|
131
|
+
authorization: `Bearer ${key}`,
|
|
132
|
+
"user-agent": `jecode/${applicationVersion()} (${process.platform}; ${process.arch})`,
|
|
133
|
+
"x-client-request-id": randomUUID(),
|
|
134
|
+
};
|
|
108
135
|
}
|
package/dist/providers/sse.js
CHANGED
|
@@ -4,39 +4,111 @@
|
|
|
4
4
|
// `data` matters here — both providers put the event discriminator inside the
|
|
5
5
|
// JSON payload, so the `event:` line is redundant and skipped.
|
|
6
6
|
import { addBounded, MAX_SSE_EVENT_CHARS, } from "./stream-limits.js";
|
|
7
|
-
export async function* readSseJson(body, maximumChars) {
|
|
7
|
+
export async function* readSseJson(body, maximumChars, idle) {
|
|
8
8
|
const reader = body.getReader();
|
|
9
9
|
const decoder = new TextDecoder();
|
|
10
10
|
const parser = new SseEventParser();
|
|
11
11
|
let finished = false;
|
|
12
12
|
let total = 0;
|
|
13
|
+
const progressDeadline = idle?.progress === undefined
|
|
14
|
+
? undefined
|
|
15
|
+
: eventDeadline(idle.progress);
|
|
13
16
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
let ended = false;
|
|
18
|
+
while (!ended) {
|
|
19
|
+
// One deadline spans every raw read until a complete JSON event lands.
|
|
20
|
+
// SSE comments and partial framing prove only that the socket is alive;
|
|
21
|
+
// they must not keep a model request pending forever.
|
|
22
|
+
const deadline = eventDeadline(idle);
|
|
23
|
+
const payloads = [];
|
|
24
|
+
try {
|
|
25
|
+
while (payloads.length === 0 && !ended) {
|
|
26
|
+
const read = reader.read();
|
|
27
|
+
const pending = progressDeadline === undefined
|
|
28
|
+
? read
|
|
29
|
+
: progressDeadline.wait(read);
|
|
30
|
+
const { done, value } = await deadline.wait(pending);
|
|
31
|
+
if (done) {
|
|
32
|
+
ended = true;
|
|
33
|
+
const text = decoder.decode();
|
|
34
|
+
total = addBounded(total, text.length, maximumChars, "SSE stream");
|
|
35
|
+
payloads.push(...parser.push(text));
|
|
36
|
+
const payload = parser.finish();
|
|
37
|
+
if (payload !== undefined)
|
|
38
|
+
payloads.push(payload);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const text = decoder.decode(value, { stream: true });
|
|
42
|
+
total = addBounded(total, text.length, maximumChars, "SSE stream");
|
|
43
|
+
payloads.push(...parser.push(text));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
deadline.clear();
|
|
48
|
+
}
|
|
49
|
+
for (const payload of payloads) {
|
|
50
|
+
if (idle?.progress?.observed(payload) === true)
|
|
51
|
+
progressDeadline?.reset();
|
|
21
52
|
yield payload;
|
|
53
|
+
}
|
|
22
54
|
}
|
|
23
|
-
// A stream that ends without a trailing blank line still owes us its last
|
|
24
|
-
// event.
|
|
25
|
-
const text = decoder.decode();
|
|
26
|
-
total = addBounded(total, text.length, maximumChars, "SSE stream");
|
|
27
|
-
for (const payload of parser.push(text))
|
|
28
|
-
yield payload;
|
|
29
|
-
const payload = parser.finish();
|
|
30
|
-
if (payload !== undefined)
|
|
31
|
-
yield payload;
|
|
32
55
|
finished = true;
|
|
33
56
|
}
|
|
34
57
|
finally {
|
|
58
|
+
progressDeadline?.clear();
|
|
35
59
|
if (!finished)
|
|
36
60
|
await reader.cancel().catch(() => undefined);
|
|
37
61
|
reader.releaseLock();
|
|
38
62
|
}
|
|
39
63
|
}
|
|
64
|
+
function eventDeadline(idle) {
|
|
65
|
+
if (idle === undefined) {
|
|
66
|
+
return {
|
|
67
|
+
wait: (pending) => pending,
|
|
68
|
+
reset: () => undefined,
|
|
69
|
+
clear: () => undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
let timer;
|
|
73
|
+
let expired;
|
|
74
|
+
let rejectWait;
|
|
75
|
+
const arm = () => {
|
|
76
|
+
if (timer !== undefined)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
expired = undefined;
|
|
79
|
+
timer = setTimeout(() => {
|
|
80
|
+
expired = idle.error();
|
|
81
|
+
const reject = rejectWait;
|
|
82
|
+
rejectWait = undefined;
|
|
83
|
+
reject?.(expired);
|
|
84
|
+
}, idle.milliseconds);
|
|
85
|
+
};
|
|
86
|
+
arm();
|
|
87
|
+
return {
|
|
88
|
+
wait: (pending) => {
|
|
89
|
+
if (expired !== undefined)
|
|
90
|
+
return Promise.reject(expired);
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
rejectWait = reject;
|
|
93
|
+
pending.then((value) => {
|
|
94
|
+
if (rejectWait === reject)
|
|
95
|
+
rejectWait = undefined;
|
|
96
|
+
resolve(value);
|
|
97
|
+
}, (error) => {
|
|
98
|
+
if (rejectWait === reject)
|
|
99
|
+
rejectWait = undefined;
|
|
100
|
+
reject(error);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
reset: arm,
|
|
105
|
+
clear: () => {
|
|
106
|
+
if (timer !== undefined)
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
rejectWait = undefined;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
40
112
|
// Keep fragments in bounded groups. A provider may split one SSE line into
|
|
41
113
|
// hundreds of thousands of tiny chunks; repeatedly flattening the growing line
|
|
42
114
|
// would make parsing quadratic even if boundary scanning itself were linear.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Stable provider-facing conversation identity. It is routing metadata only,
|
|
2
|
+
// never authorization, persistence authority, or a user-visible identifier.
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
const ephemeralSeeds = new WeakMap();
|
|
5
|
+
export function requestIdentityForSession(session) {
|
|
6
|
+
let conversation = session.persistence?.conversationId;
|
|
7
|
+
if (conversation === undefined) {
|
|
8
|
+
conversation = ephemeralSeeds.get(session);
|
|
9
|
+
if (conversation === undefined) {
|
|
10
|
+
conversation = randomUUID();
|
|
11
|
+
ephemeralSeeds.set(session, conversation);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return identityFromSeed(`${session.provider.id}\0${conversation}`);
|
|
15
|
+
}
|
|
16
|
+
export function resetRequestIdentity(session) {
|
|
17
|
+
ephemeralSeeds.delete(session);
|
|
18
|
+
}
|
|
19
|
+
function identityFromSeed(seed) {
|
|
20
|
+
const digest = createHash("sha256").update(seed).digest("hex");
|
|
21
|
+
const conversationId = [
|
|
22
|
+
digest.slice(0, 8),
|
|
23
|
+
digest.slice(8, 12),
|
|
24
|
+
`5${digest.slice(13, 16)}`,
|
|
25
|
+
`${variant(digest[16])}${digest.slice(17, 20)}`,
|
|
26
|
+
digest.slice(20, 32),
|
|
27
|
+
].join("-");
|
|
28
|
+
return Object.freeze({ conversationId, cacheKey: `jecode-${digest.slice(0, 32)}` });
|
|
29
|
+
}
|
|
30
|
+
function variant(value) {
|
|
31
|
+
return ((Number.parseInt(value, 16) & 0x3) | 0x8).toString(16);
|
|
32
|
+
}
|