@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.
Files changed (99) hide show
  1. package/README.md +28 -9
  2. package/dist/accounts.js +47 -10
  3. package/dist/atomic.js +24 -14
  4. package/dist/batch-view.js +27 -2
  5. package/dist/batch.js +39 -4
  6. package/dist/bounded-file.js +212 -0
  7. package/dist/cli-info.js +0 -1
  8. package/dist/commands.js +2 -0
  9. package/dist/config.js +16 -7
  10. package/dist/context/automatic.js +35 -0
  11. package/dist/context/compactor.js +32 -2
  12. package/dist/context/manual.js +20 -3
  13. package/dist/context/request-projection.js +130 -0
  14. package/dist/controller-request.js +33 -11
  15. package/dist/credential-commands.js +25 -9
  16. package/dist/credentials.js +56 -18
  17. package/dist/directory-anchor.js +91 -0
  18. package/dist/file-identity.js +12 -0
  19. package/dist/model-command.js +5 -4
  20. package/dist/openai-account-command.js +23 -10
  21. package/dist/openai-account.js +7 -5
  22. package/dist/openai-oauth-callback.js +13 -4
  23. package/dist/openai-oauth-tokens.js +9 -7
  24. package/dist/openai-oauth.js +8 -6
  25. package/dist/permission-command.js +1 -1
  26. package/dist/process-lease.js +329 -0
  27. package/dist/provider-commands.js +11 -37
  28. package/dist/provider-errors.js +31 -7
  29. package/dist/provider-label.js +9 -3
  30. package/dist/providers/anthropic-stream.js +4 -1
  31. package/dist/providers/anthropic-wire.js +8 -3
  32. package/dist/providers/anthropic.js +39 -20
  33. package/dist/providers/catalog.js +4 -4
  34. package/dist/providers/failure.js +181 -0
  35. package/dist/providers/http.js +82 -23
  36. package/dist/providers/index.js +1 -5
  37. package/dist/providers/ollama-context.js +42 -0
  38. package/dist/providers/ollama-endpoint.js +7 -34
  39. package/dist/providers/ollama-stream.js +5 -1
  40. package/dist/providers/ollama.js +36 -158
  41. package/dist/providers/openai-codex.js +71 -45
  42. package/dist/providers/openai-stream.js +26 -2
  43. package/dist/providers/openai.js +51 -25
  44. package/dist/providers/sse.js +52 -8
  45. package/dist/request-identity.js +32 -0
  46. package/dist/sessions/bucket.js +55 -0
  47. package/dist/sessions/catalog-io.js +162 -0
  48. package/dist/sessions/catalog.js +3 -1
  49. package/dist/sessions/codec-messages.js +122 -0
  50. package/dist/sessions/codec-transcript.js +93 -0
  51. package/dist/sessions/codec-values.js +52 -0
  52. package/dist/sessions/codec.js +4 -257
  53. package/dist/sessions/files.js +158 -0
  54. package/dist/sessions/lease.js +132 -49
  55. package/dist/sessions/load.js +90 -0
  56. package/dist/sessions/runtime.js +15 -8
  57. package/dist/sessions/snapshot.js +33 -0
  58. package/dist/sessions/store.js +183 -378
  59. package/dist/settings-command.js +10 -5
  60. package/dist/settings.js +75 -22
  61. package/dist/stable-directory.js +148 -0
  62. package/dist/start.js +1 -2
  63. package/dist/store-lock.js +68 -84
  64. package/dist/tools/args.js +2 -2
  65. package/dist/tools/file-read.js +192 -0
  66. package/dist/tools/file-summary.js +9 -0
  67. package/dist/tools/{fs.js → file-write.js} +5 -171
  68. package/dist/tools/glob.js +107 -0
  69. package/dist/tools/index.js +2 -1
  70. package/dist/tools/search.js +66 -205
  71. package/dist/tools/text-boundary.js +7 -33
  72. package/dist/tui/app-workflows.js +9 -334
  73. package/dist/tui/approve.js +5 -3
  74. package/dist/tui/blocks.js +8 -7
  75. package/dist/tui/command-workflow.js +106 -0
  76. package/dist/tui/components/command-menu.js +7 -10
  77. package/dist/tui/components/footer.js +1 -1
  78. package/dist/tui/components/menu.js +74 -43
  79. package/dist/tui/components/messages.js +16 -9
  80. package/dist/tui/components/tool-evidence.js +107 -0
  81. package/dist/tui/components/tool-motion.js +32 -0
  82. package/dist/tui/components/tool.js +48 -202
  83. package/dist/tui/feedback.js +4 -0
  84. package/dist/tui/help.js +1 -1
  85. package/dist/tui/picker-layout.js +40 -0
  86. package/dist/tui/picker.js +7 -71
  87. package/dist/tui/session-view.js +7 -2
  88. package/dist/tui/tool-details.js +135 -0
  89. package/dist/tui/transcript-grammar.js +8 -1
  90. package/dist/tui/transcript-view.js +26 -112
  91. package/dist/tui/turn-workflow.js +264 -0
  92. package/dist/tui/turn.js +7 -140
  93. package/dist/tui/workflow-types.js +2 -0
  94. package/dist/tui/workspace.js +21 -7
  95. package/dist/user-store.js +23 -31
  96. package/package.json +14 -15
  97. package/dist/ollama-settings-command.js +0 -74
  98. package/dist/tools/ripgrep.js +0 -230
  99. 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
+ }
@@ -1,6 +1,8 @@
1
1
  // The entire HTTP layer: one bounded request, then either a JSON body or an
2
- // event stream. Only idempotent reads retry. Once a POST starts or response
3
- // bytes flow, a failure is surfaced rather than silently replayed.
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
- function httpError(message, status, body) {
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
- const res = await request(url, { accept: "text/event-stream", ...headers }, body, signal, onStatus);
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 maxRetries = body === undefined ? GET_RETRIES : 0;
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 (waitMs > 0)
54
- await sleep(waitMs, signal);
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
- if (attempt < maxRetries)
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
- lastError = httpError(`${url} -> ${res.status} ${res.statusText}`, res.status, text);
92
- if (!RETRYABLE.has(res.status))
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 = retryAfter(res) ?? backoff(attempt);
95
- if (attempt < maxRetries) {
96
- const reason = res.status === 429 ? "Rate limited" : `HTTP ${res.status}`;
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(60_000, seconds * 1_000);
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) ? undefined : Math.max(0, Math.min(60_000, at - Date.now()));
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) => {
@@ -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 { configureOllama, ollama } from "./ollama.js";
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
- // Turning an Ollama host into one safe, normalized endpoint.
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
- export function parseOllamaEndpoint(value) {
5
- let url;
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
- url = new URL(value.trim());
8
+ return new URL(value.trim()).href.replace(/\/+$/, "") === OLLAMA_CLOUD_HOST;
8
9
  }
9
10
  catch {
10
- throw new Error("Ollama endpoint must be an absolute HTTP(S) URL");
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 new Error(`ollama stream error: ${message ?? "unspecified"}`);
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;