@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Provider failures normalized at adapter boundaries. The controller and TUI
|
|
2
|
+
// never need to understand a vendor response shape in order to make a safe
|
|
3
|
+
// retry decision or show an actionable error.
|
|
4
|
+
import { leadingText } from "../text-boundary.js";
|
|
5
|
+
const MAX_WIRE_ERROR_CHARS = 2_000;
|
|
6
|
+
/** One stable error contract for every provider adapter. */
|
|
7
|
+
export class ProviderRequestError extends Error {
|
|
8
|
+
providerId;
|
|
9
|
+
kind;
|
|
10
|
+
status;
|
|
11
|
+
code;
|
|
12
|
+
retryAfterMs;
|
|
13
|
+
requestId;
|
|
14
|
+
body;
|
|
15
|
+
constructor(providerId, source, details) {
|
|
16
|
+
super(source.message, { cause: source });
|
|
17
|
+
this.name = "ProviderRequestError";
|
|
18
|
+
this.providerId = providerId;
|
|
19
|
+
this.kind = details.kind;
|
|
20
|
+
this.status = details.status;
|
|
21
|
+
this.code = details.code;
|
|
22
|
+
this.retryAfterMs = details.retryAfterMs;
|
|
23
|
+
this.requestId = details.requestId;
|
|
24
|
+
this.body = source.body;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Preserve structured fields carried by an error event inside a live stream. */
|
|
28
|
+
export function providerWireError(prefix, message, metadata = {}) {
|
|
29
|
+
const detail = message === undefined ? "unspecified" : leadingText(message, MAX_WIRE_ERROR_CHARS);
|
|
30
|
+
const error = new Error(`${prefix}: ${detail}`);
|
|
31
|
+
const code = safeIdentifier(metadata.code);
|
|
32
|
+
const type = safeIdentifier(metadata.type);
|
|
33
|
+
if (code !== undefined)
|
|
34
|
+
error.code = code;
|
|
35
|
+
if (type !== undefined)
|
|
36
|
+
error.type = type;
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
export function normalizeProviderError(providerId, error) {
|
|
40
|
+
if (error instanceof ProviderRequestError)
|
|
41
|
+
return error;
|
|
42
|
+
const source = error instanceof Error ? error : new Error(String(error));
|
|
43
|
+
return new ProviderRequestError(providerId, source, providerFailureDetails(providerId, source));
|
|
44
|
+
}
|
|
45
|
+
/** Keep caller cancellation intact; normalize every other adapter failure. */
|
|
46
|
+
export function throwProviderError(providerId, signal, error) {
|
|
47
|
+
if (signal?.aborted === true)
|
|
48
|
+
throw error;
|
|
49
|
+
throw normalizeProviderError(providerId, error);
|
|
50
|
+
}
|
|
51
|
+
export function providerFailureDetails(providerId, error) {
|
|
52
|
+
if (error instanceof ProviderRequestError) {
|
|
53
|
+
return compact({
|
|
54
|
+
kind: error.kind,
|
|
55
|
+
status: error.status,
|
|
56
|
+
code: error.code,
|
|
57
|
+
retryAfterMs: error.retryAfterMs,
|
|
58
|
+
requestId: error.requestId,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const http = error;
|
|
62
|
+
const wire = error;
|
|
63
|
+
const body = bodyError(http.body);
|
|
64
|
+
const code = firstIdentifier(wire.code, body.code);
|
|
65
|
+
const type = firstIdentifier(wire.type, body.type);
|
|
66
|
+
const evidence = [code, type, leadingText(error.message, 4_000), body.message]
|
|
67
|
+
.filter((value) => value !== undefined)
|
|
68
|
+
.join(" ")
|
|
69
|
+
.toLocaleLowerCase();
|
|
70
|
+
const status = number(http.status);
|
|
71
|
+
const kind = classify(providerId, status, evidence);
|
|
72
|
+
return compact({
|
|
73
|
+
kind,
|
|
74
|
+
status,
|
|
75
|
+
code,
|
|
76
|
+
retryAfterMs: number(http.retryAfterMs),
|
|
77
|
+
requestId: safeRequestId(http.requestId),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Generation replay is allowed only for a definite transient rate rejection. */
|
|
81
|
+
export function isRetryableGenerationFailure(providerId, error) {
|
|
82
|
+
return providerFailureDetails(providerId, error).kind === "rate-limit";
|
|
83
|
+
}
|
|
84
|
+
/** Idempotent reads may retry transient pressure, never account hard stops. */
|
|
85
|
+
export function isRetryableReadFailure(providerId, error) {
|
|
86
|
+
const kind = providerFailureDetails(providerId, error).kind;
|
|
87
|
+
return kind === "rate-limit" || kind === "overload" || kind === "network" || kind === "unknown";
|
|
88
|
+
}
|
|
89
|
+
function classify(providerId, status, evidence) {
|
|
90
|
+
if (providerId === "anthropic" &&
|
|
91
|
+
/enforced[_ -]?spend[_ -]?limit[_ -]?reached/u.test(evidence))
|
|
92
|
+
return "billing";
|
|
93
|
+
if ((providerId === "openai" || providerId === "openai-codex") &&
|
|
94
|
+
/billing[_ -]?hard[_ -]?limit[_ -]?reached/u.test(evidence))
|
|
95
|
+
return "billing";
|
|
96
|
+
if (providerId === "openai-codex" &&
|
|
97
|
+
/usage[_ -]?limit[_ -]?reached|plan limit/u.test(evidence))
|
|
98
|
+
return "quota";
|
|
99
|
+
if (/insufficient[_ -]?quota|usage quota|quota (?:exceeded|exhausted)/u.test(evidence)) {
|
|
100
|
+
return "quota";
|
|
101
|
+
}
|
|
102
|
+
if (status === 402 ||
|
|
103
|
+
/\b(?:no|zero) credits? remaining\b|\bcredits? exhausted\b|billing|payment required|(?:hard|spend)[_ -]?limit/u
|
|
104
|
+
.test(evidence))
|
|
105
|
+
return "billing";
|
|
106
|
+
if (status === 401 ||
|
|
107
|
+
/invalid[_ -]?(?:api[_ -]?)?key|authentication|unauthorized|invalid[_ -]?token|oauth/u
|
|
108
|
+
.test(evidence))
|
|
109
|
+
return "authentication";
|
|
110
|
+
if (/context[_ -]?(?:length|window)|maximum context|too many input tokens|prompt (?:is )?too long/u
|
|
111
|
+
.test(evidence))
|
|
112
|
+
return "context";
|
|
113
|
+
if (status === 429 ||
|
|
114
|
+
/rate[_ -]?limit|too many requests|tokens per min|requests per min|\btpm\b|\brpm\b/u
|
|
115
|
+
.test(evidence))
|
|
116
|
+
return "rate-limit";
|
|
117
|
+
if (status === 408 ||
|
|
118
|
+
status === 409 ||
|
|
119
|
+
status === 500 ||
|
|
120
|
+
status === 502 ||
|
|
121
|
+
status === 503 ||
|
|
122
|
+
status === 504 ||
|
|
123
|
+
status === 529 ||
|
|
124
|
+
/overload|temporarily unavailable|server is busy/u.test(evidence))
|
|
125
|
+
return "overload";
|
|
126
|
+
if (status === undefined &&
|
|
127
|
+
/network error calling|timed out waiting|response body was idle|stream was idle|fetch failed|socket/u
|
|
128
|
+
.test(evidence))
|
|
129
|
+
return "network";
|
|
130
|
+
return "unknown";
|
|
131
|
+
}
|
|
132
|
+
function bodyError(body) {
|
|
133
|
+
if (body === undefined || body.trim() === "")
|
|
134
|
+
return {};
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = JSON.parse(body);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return { message: body };
|
|
141
|
+
}
|
|
142
|
+
if (!record(parsed))
|
|
143
|
+
return {};
|
|
144
|
+
const nested = record(parsed["error"]) ? parsed["error"] : undefined;
|
|
145
|
+
const detail = record(parsed["detail"]) ? parsed["detail"] : undefined;
|
|
146
|
+
return compact({
|
|
147
|
+
code: firstText(nested?.["code"], detail?.["code"], parsed["code"]),
|
|
148
|
+
type: firstText(nested?.["type"], detail?.["type"], parsed["type"]),
|
|
149
|
+
message: firstText(nested?.["message"], typeof parsed["error"] === "string" ? parsed["error"] : undefined, detail?.["message"], typeof parsed["detail"] === "string" ? parsed["detail"] : undefined, parsed["message"]),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function firstText(...values) {
|
|
153
|
+
return values.find((value) => typeof value === "string" && value !== "");
|
|
154
|
+
}
|
|
155
|
+
function firstIdentifier(...values) {
|
|
156
|
+
for (const value of values) {
|
|
157
|
+
const identifier = safeIdentifier(value);
|
|
158
|
+
if (identifier !== undefined)
|
|
159
|
+
return identifier;
|
|
160
|
+
}
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
function safeIdentifier(value) {
|
|
164
|
+
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/u.test(value)
|
|
165
|
+
? value
|
|
166
|
+
: undefined;
|
|
167
|
+
}
|
|
168
|
+
function number(value) {
|
|
169
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
170
|
+
}
|
|
171
|
+
function safeRequestId(value) {
|
|
172
|
+
return typeof value === "string" && /^[A-Za-z0-9._:-]{1,256}$/u.test(value)
|
|
173
|
+
? value
|
|
174
|
+
: undefined;
|
|
175
|
+
}
|
|
176
|
+
function compact(value) {
|
|
177
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
|
|
178
|
+
}
|
|
179
|
+
function record(value) {
|
|
180
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
181
|
+
}
|
package/dist/providers/http.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// The entire HTTP layer: one bounded request, then either a JSON body or an
|
|
2
|
-
// event stream.
|
|
3
|
-
//
|
|
2
|
+
// event stream. Idempotent reads retry transient failures. A generation POST
|
|
3
|
+
// gets one retry only when its provider adapter classifies an explicit,
|
|
4
|
+
// delayed rejection as transient, before a stream exists. Ambiguous POST
|
|
5
|
+
// failures and failures after response bytes flow are surfaced.
|
|
4
6
|
import { leadingText } from "../text-boundary.js";
|
|
5
7
|
import { readSseJson } from "./sse.js";
|
|
6
8
|
import { sseStreamCharacterLimit } from "./stream-limits.js";
|
|
@@ -9,19 +11,26 @@ const MAX_JSON_CHARS = 5_000_000;
|
|
|
9
11
|
const MAX_ERROR_CHARS = 2_000;
|
|
10
12
|
const HANDSHAKE_TIMEOUT_MS = 60_000;
|
|
11
13
|
const BODY_IDLE_TIMEOUT_MS = 120_000;
|
|
14
|
+
const MODEL_PROGRESS_TIMEOUT_MS = 300_000;
|
|
12
15
|
const GET_RETRIES = 3;
|
|
13
|
-
|
|
16
|
+
const GENERATION_RATE_LIMIT_RETRIES = 1;
|
|
17
|
+
const MAX_RETRY_DELAY_MS = 60_000;
|
|
18
|
+
function httpError(message, status, body, metadata = {}) {
|
|
14
19
|
const error = new Error(message);
|
|
15
20
|
error.status = status;
|
|
16
21
|
error.body = body;
|
|
22
|
+
if (metadata.retryAfterMs !== undefined)
|
|
23
|
+
error.retryAfterMs = metadata.retryAfterMs;
|
|
24
|
+
if (metadata.requestId !== undefined)
|
|
25
|
+
error.requestId = metadata.requestId;
|
|
17
26
|
return error;
|
|
18
27
|
}
|
|
19
28
|
export async function postJson(url, headers, body, signal, onStatus) {
|
|
20
29
|
return asJson(url, await request(url, headers, body, signal, onStatus));
|
|
21
30
|
}
|
|
22
31
|
/** A plain read. The only thing jecode asks for without sending anything. */
|
|
23
|
-
export async function getJson(url, headers, signal, onStatus) {
|
|
24
|
-
return asJson(url, await request(url, headers, undefined, signal, onStatus));
|
|
32
|
+
export async function getJson(url, headers, signal, onStatus, retry) {
|
|
33
|
+
return asJson(url, await request(url, headers, undefined, signal, onStatus, 0, undefined, retry));
|
|
25
34
|
}
|
|
26
35
|
async function asJson(url, res) {
|
|
27
36
|
const { text, truncated } = await boundedText(url, res, MAX_JSON_CHARS);
|
|
@@ -35,23 +44,39 @@ async function asJson(url, res) {
|
|
|
35
44
|
throw httpError(`${url} returned non-JSON`, res.status, leadingText(text, 500));
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
|
-
export async function postSse(url, headers, body, maxOutputTokens, signal, onStatus) {
|
|
47
|
+
export async function postSse(url, headers, body, maxOutputTokens, signal, onStatus, progress, retry) {
|
|
39
48
|
const maximumChars = sseStreamCharacterLimit(maxOutputTokens);
|
|
40
|
-
|
|
49
|
+
onStatus?.("Connecting");
|
|
50
|
+
const res = await request(url, { accept: "text/event-stream", ...headers }, body, signal, onStatus, GENERATION_RATE_LIMIT_RETRIES, retry);
|
|
41
51
|
if (res.body === null)
|
|
42
52
|
throw httpError(`${url} returned no body`, res.status);
|
|
53
|
+
onStatus?.("Waiting for model");
|
|
43
54
|
return readSseJson(res.body, maximumChars, {
|
|
44
55
|
milliseconds: BODY_IDLE_TIMEOUT_MS,
|
|
45
56
|
error: () => httpError(`${url} SSE stream was idle for ${BODY_IDLE_TIMEOUT_MS}ms without an event`, res.status),
|
|
57
|
+
...(progress === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: {
|
|
60
|
+
progress: {
|
|
61
|
+
milliseconds: MODEL_PROGRESS_TIMEOUT_MS,
|
|
62
|
+
observed: progress,
|
|
63
|
+
error: () => httpError(`${url} SSE stream made no model progress for ${MODEL_PROGRESS_TIMEOUT_MS}ms`, res.status),
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
46
66
|
});
|
|
47
67
|
}
|
|
48
|
-
async function request(url, headers, body, signal, onStatus) {
|
|
49
|
-
const
|
|
68
|
+
async function request(url, headers, body, signal, onStatus, generationRetries = 0, generationRetry, readRetry) {
|
|
69
|
+
const read = body === undefined;
|
|
70
|
+
const maxRetries = read ? GET_RETRIES : generationRetries;
|
|
50
71
|
let lastError;
|
|
51
72
|
let waitMs = 0;
|
|
52
73
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
53
|
-
if (
|
|
54
|
-
|
|
74
|
+
if (attempt > 0) {
|
|
75
|
+
if (waitMs > 0)
|
|
76
|
+
await sleep(waitMs, signal);
|
|
77
|
+
if (!read)
|
|
78
|
+
onStatus?.("Connecting");
|
|
79
|
+
}
|
|
55
80
|
let res;
|
|
56
81
|
const handshake = handshakeSignal(url, signal);
|
|
57
82
|
try {
|
|
@@ -73,9 +98,10 @@ async function request(url, headers, body, signal, onStatus) {
|
|
|
73
98
|
throw cause;
|
|
74
99
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
75
100
|
lastError = httpError(`network error calling ${url}: ${detail}`);
|
|
101
|
+
if (!read || attempt >= maxRetries)
|
|
102
|
+
throw lastError;
|
|
76
103
|
waitMs = backoff(attempt);
|
|
77
|
-
|
|
78
|
-
onStatus?.(`Network error · retrying in ${waitLabel(waitMs)}`);
|
|
104
|
+
onStatus?.(`Network error · retrying in ${waitLabel(waitMs)}`);
|
|
79
105
|
continue;
|
|
80
106
|
}
|
|
81
107
|
finally {
|
|
@@ -88,14 +114,22 @@ async function request(url, headers, body, signal, onStatus) {
|
|
|
88
114
|
if (res.ok)
|
|
89
115
|
return res;
|
|
90
116
|
const { text } = await boundedText(url, res, MAX_ERROR_CHARS);
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
const providerDelay = retryAfter(res) ?? retryAfterMessage(text);
|
|
118
|
+
const requestId = responseRequestId(res);
|
|
119
|
+
lastError = httpError(`${url} -> ${res.status} ${res.statusText}`, res.status, text, {
|
|
120
|
+
...(providerDelay === undefined ? {} : { retryAfterMs: providerDelay }),
|
|
121
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
122
|
+
});
|
|
123
|
+
const retryable = read
|
|
124
|
+
? RETRYABLE.has(res.status) && (readRetry?.(lastError) ?? true)
|
|
125
|
+
: generationRetries > 0 &&
|
|
126
|
+
providerDelay !== undefined &&
|
|
127
|
+
generationRetry?.(lastError) === true;
|
|
128
|
+
if (!retryable || attempt >= maxRetries)
|
|
93
129
|
throw lastError;
|
|
94
|
-
waitMs =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
onStatus?.(`${reason} · retrying in ${waitLabel(waitMs)}`);
|
|
98
|
-
}
|
|
130
|
+
waitMs = providerDelay ?? backoff(attempt);
|
|
131
|
+
const reason = res.status === 429 ? "Rate limited" : `HTTP ${res.status}`;
|
|
132
|
+
onStatus?.(`${reason} · retrying in ${waitLabel(waitMs)}`);
|
|
99
133
|
}
|
|
100
134
|
throw lastError ?? httpError(`${url} failed`);
|
|
101
135
|
}
|
|
@@ -168,10 +202,35 @@ function retryAfter(res) {
|
|
|
168
202
|
if (header === null)
|
|
169
203
|
return undefined;
|
|
170
204
|
const seconds = Number(header);
|
|
171
|
-
if (Number.isFinite(seconds) && seconds >= 0)
|
|
172
|
-
return Math.min(
|
|
205
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
206
|
+
return Math.min(MAX_RETRY_DELAY_MS, seconds * 1_000);
|
|
207
|
+
}
|
|
173
208
|
const at = Date.parse(header);
|
|
174
|
-
return Number.isNaN(at)
|
|
209
|
+
return Number.isNaN(at)
|
|
210
|
+
? undefined
|
|
211
|
+
: Math.max(0, Math.min(MAX_RETRY_DELAY_MS, at - Date.now()));
|
|
212
|
+
}
|
|
213
|
+
function retryAfterMessage(body) {
|
|
214
|
+
const match = /\btry again in\s+(\d+(?:\.\d+)?)\s*(milliseconds?|ms|seconds?|secs?|s)\b/iu
|
|
215
|
+
.exec(body);
|
|
216
|
+
if (match === null)
|
|
217
|
+
return undefined;
|
|
218
|
+
const amount = Number(match[1]);
|
|
219
|
+
if (!Number.isFinite(amount) || amount < 0)
|
|
220
|
+
return undefined;
|
|
221
|
+
const unit = match[2]?.toLowerCase();
|
|
222
|
+
const milliseconds = unit === "ms" || unit?.startsWith("millisecond") === true
|
|
223
|
+
? amount
|
|
224
|
+
: amount * 1_000;
|
|
225
|
+
return Math.min(MAX_RETRY_DELAY_MS, Math.ceil(milliseconds));
|
|
226
|
+
}
|
|
227
|
+
function responseRequestId(res) {
|
|
228
|
+
for (const name of ["x-request-id", "request-id"]) {
|
|
229
|
+
const value = res.headers.get(name)?.trim();
|
|
230
|
+
if (value !== undefined && /^[A-Za-z0-9._:-]{1,256}$/u.test(value))
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
175
234
|
}
|
|
176
235
|
function sleep(ms, signal) {
|
|
177
236
|
return new Promise((resolve, reject) => {
|
package/dist/providers/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { anthropic } from "./anthropic.js";
|
|
2
2
|
import { openai } from "./openai.js";
|
|
3
3
|
import { openaiCodex } from "./openai-codex.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ollama } from "./ollama.js";
|
|
5
5
|
export const PROVIDERS = [anthropic, openai, openaiCodex, ollama];
|
|
6
6
|
export function providerNames() {
|
|
7
7
|
return PROVIDERS.map((provider) => provider.id);
|
|
@@ -13,7 +13,3 @@ export function selectProvider(id) {
|
|
|
13
13
|
}
|
|
14
14
|
return found;
|
|
15
15
|
}
|
|
16
|
-
/** Apply provider-specific process state after startup precedence is resolved. */
|
|
17
|
-
export function configureProviders(config) {
|
|
18
|
-
configureOllama(config.ollamaHost);
|
|
19
|
-
}
|
|
@@ -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
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Nothing arrives finished here: text accumulates, and each tool call is
|
|
4
4
|
// spread across chunks keyed by `index` — id and name usually in the first,
|
|
5
5
|
// arguments as JSON fragments after it.
|
|
6
|
+
import { providerWireError } from "./failure.js";
|
|
6
7
|
import { addBounded, MAX_TOOL_ARGUMENT_CHARS } from "./stream-limits.js";
|
|
7
8
|
export async function assembleOllama(events, onStream) {
|
|
8
9
|
const calls = new Map();
|
|
@@ -16,7 +17,10 @@ export async function assembleOllama(events, onStream) {
|
|
|
16
17
|
const event = raw;
|
|
17
18
|
if (event.error !== undefined) {
|
|
18
19
|
const message = typeof event.error === "string" ? event.error : event.error.message;
|
|
19
|
-
throw
|
|
20
|
+
throw providerWireError("ollama stream error", message, {
|
|
21
|
+
code: typeof event.error === "string" ? undefined : event.error.code,
|
|
22
|
+
type: typeof event.error === "string" ? undefined : event.error.type,
|
|
23
|
+
});
|
|
20
24
|
}
|
|
21
25
|
if (event.usage !== undefined)
|
|
22
26
|
usage = event.usage;
|