@nemesis-oss/ollama-sdk 1.19.0 → 1.19.1

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/dist/index.cjs CHANGED
@@ -428,6 +428,43 @@ function isNetworkLevelError(error) {
428
428
  }
429
429
  return false;
430
430
  }
431
+ function lookupHeader(headers, name) {
432
+ if (headers === void 0) {
433
+ return void 0;
434
+ }
435
+ const needle = name.toLowerCase();
436
+ for (const [key, value] of Object.entries(headers)) {
437
+ if (key.toLowerCase() === needle) {
438
+ return value;
439
+ }
440
+ }
441
+ return void 0;
442
+ }
443
+ var HTTP_DATE_PATTERN = /^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun), \d{2} (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) \d{4} \d{2}:\d{2}:\d{2} GMT$/;
444
+ function parseRetryAfterMs(headerValue, now = Date.now()) {
445
+ if (headerValue === void 0) {
446
+ return void 0;
447
+ }
448
+ const trimmed = headerValue.trim();
449
+ if (trimmed === "") {
450
+ return void 0;
451
+ }
452
+ if (/^\d+$/.test(trimmed)) {
453
+ const seconds = Number(trimmed);
454
+ if (!Number.isSafeInteger(seconds)) {
455
+ return void 0;
456
+ }
457
+ return seconds * 1e3;
458
+ }
459
+ if (!HTTP_DATE_PATTERN.test(trimmed)) {
460
+ return void 0;
461
+ }
462
+ const instant = Date.parse(trimmed);
463
+ if (Number.isNaN(instant)) {
464
+ return void 0;
465
+ }
466
+ return Math.max(0, instant - now);
467
+ }
431
468
  function statusToError(status, message, options) {
432
469
  if (status === 401 || status === 403) {
433
470
  return new OllamaAuthError(message, { ...options, status });
@@ -439,7 +476,8 @@ function statusToError(status, message, options) {
439
476
  return new OllamaNotFoundError(message, { ...options, status });
440
477
  }
441
478
  if (status === 429) {
442
- return new OllamaRateLimitError(message, { ...options, status });
479
+ const retryAfterMs = parseRetryAfterMs(lookupHeader(options.response?.headers, "retry-after"));
480
+ return new OllamaRateLimitError(message, { ...options, status, retryAfterMs });
443
481
  }
444
482
  if (status === 502) {
445
483
  return new OllamaBadGatewayError(message, { ...options });
@@ -1142,6 +1180,21 @@ function sameHeaders(actual, expected) {
1142
1180
  const expectedKeys = Object.keys(expectedEntries);
1143
1181
  return actualKeys.length === expectedKeys.length && expectedKeys.every((key) => actualEntries[key] === expectedEntries[key]);
1144
1182
  }
1183
+ function retryAfterHeaderFrom(response) {
1184
+ try {
1185
+ const headers = response?.headers;
1186
+ if (headers === null || headers === void 0 || typeof headers.get !== "function") {
1187
+ return void 0;
1188
+ }
1189
+ const value = headers.get("retry-after");
1190
+ if (value === null || value === "") {
1191
+ return void 0;
1192
+ }
1193
+ return { "retry-after": value };
1194
+ } catch {
1195
+ return void 0;
1196
+ }
1197
+ }
1145
1198
  var HttpClient = class {
1146
1199
  baseUrl;
1147
1200
  apiKey;
@@ -1209,7 +1262,12 @@ var HttpClient = class {
1209
1262
  durationMs: Date.now() - startedAt,
1210
1263
  error: mapError(new Error(`HTTP ${response2.status}`), {
1211
1264
  request: { method: request.method, url: request.url },
1212
- response: { status: response2.status }
1265
+ // Mirrors the middleware path, which reports context.headers:
1266
+ // a 429 lifecycle event carries the server's Retry-After.
1267
+ response: {
1268
+ status: response2.status,
1269
+ headers: retryAfterHeaderFrom(response2)
1270
+ }
1213
1271
  }),
1214
1272
  timestamp: Date.now()
1215
1273
  });
@@ -1315,7 +1373,11 @@ var HttpClient = class {
1315
1373
  const message = typeof errorBody === "object" && errorBody !== null && "error" in errorBody ? String(errorBody.error) : `HTTP ${response.status} ${response.statusText}`;
1316
1374
  throw mapError(new Error(message), {
1317
1375
  request: { method, url },
1318
- response: { status: response.status, body: errorBody }
1376
+ response: {
1377
+ status: response.status,
1378
+ headers: retryAfterHeaderFrom(response),
1379
+ body: errorBody
1380
+ }
1319
1381
  });
1320
1382
  }
1321
1383
  if (options.method === "HEAD" || response.status === 204) {
@@ -1380,7 +1442,11 @@ var HttpClient = class {
1380
1442
  const errorText = await response.text();
1381
1443
  throw mapError(new Error(errorText || `HTTP ${response.status}`), {
1382
1444
  request: { method, url },
1383
- response: { status: response.status, body: errorText }
1445
+ response: {
1446
+ status: response.status,
1447
+ headers: retryAfterHeaderFrom(response),
1448
+ body: errorText
1449
+ }
1384
1450
  });
1385
1451
  }
1386
1452
  if (!response.body) {
@@ -1444,7 +1510,11 @@ var HttpClient = class {
1444
1510
  const errorText = await response.text();
1445
1511
  throw mapError(new Error(errorText || `HTTP ${response.status}`), {
1446
1512
  request: { method, url },
1447
- response: { status: response.status, body: errorText }
1513
+ response: {
1514
+ status: response.status,
1515
+ headers: retryAfterHeaderFrom(response),
1516
+ body: errorText
1517
+ }
1448
1518
  });
1449
1519
  }
1450
1520
  if (!response.body) {
@@ -1750,7 +1820,7 @@ function contextWarningMessage(check2, kind) {
1750
1820
  if (check2.windowSource === "client-default") {
1751
1821
  return `${where}: estimated prompt size ${estimate} is close to or beyond the client's \`defaultContextLength\` (${check2.effectiveContextLength}, injected as \`num_ctx\`). The server will likely silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Raise \`defaultContextLength\`, set \`options.num_ctx\` per request, or shorten the conversation.${suggestion} Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
1752
1822
  }
1753
- return `${where}: estimated prompt size ${estimate} outright exceeds Ollama's conservative server-default context window (${OLLAMA_FALLBACK_CONTEXT_LENGTH}\u20134096 tokens when \`num_ctx\` is unset \u2014 model-dependent). Unless the model's default is larger, the server will silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Set \`options.num_ctx\` (or the client's \`defaultContextLength\`) to an explicit window to make truncation visible and controllable.${suggestion} Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
1823
+ return `${where}: estimated prompt size ${estimate} outright exceeds the worst-case floor for the unset-\`num_ctx\` window (${OLLAMA_FALLBACK_CONTEXT_LENGTH} tokens \u2014 current daemons default higher, tiered by VRAM: 4096 / 32768 / 262144; \`OLLAMA_CONTEXT_LENGTH\` can override either way). If your daemon's effective window is that small, the server will silently truncate the top of the conversation \u2014 the model loses earlier turns with no error. Set \`options.num_ctx\` (or the client's \`defaultContextLength\`) to an explicit window \u2014 or discover the real one via \`client.models.getContextLength()\` \u2014 to make truncation visible and controllable.${suggestion} Estimates are heuristic (\xB120\u201330%); see \`estimateTokens\`.`;
1754
1824
  }
1755
1825
 
1756
1826
  // src/context-discovery.ts
@@ -2478,6 +2548,12 @@ function sleep(ms, signal) {
2478
2548
  signal?.addEventListener("abort", onAbort, { once: true });
2479
2549
  });
2480
2550
  }
2551
+ function retryDelayMs(error, attempt, backoff) {
2552
+ if (error instanceof OllamaRateLimitError && error.retryAfterMs !== void 0) {
2553
+ return error.retryAfterMs;
2554
+ }
2555
+ return calculateBackoff(attempt, backoff);
2556
+ }
2481
2557
  async function withRetry(operation, config = DEFAULT_RETRY_CONFIG, signal) {
2482
2558
  let attempt = 0;
2483
2559
  while (true) {
@@ -2489,7 +2565,7 @@ async function withRetry(operation, config = DEFAULT_RETRY_CONFIG, signal) {
2489
2565
  if (attempt >= config.maxRetries || !shouldRetry(error, attempt)) {
2490
2566
  throw error;
2491
2567
  }
2492
- const delayMs = calculateBackoff(attempt, config.backoff);
2568
+ const delayMs = retryDelayMs(error, attempt, config.backoff);
2493
2569
  config.onRetry?.(error, attempt, delayMs);
2494
2570
  await sleep(delayMs, signal);
2495
2571
  attempt += 1;
@@ -2658,7 +2734,8 @@ var ModelsClient = class {
2658
2734
  }
2659
2735
  /**
2660
2736
  * Discovers the model's real context window from the server — no more
2661
- * 2048/4096 guesswork. Consults, in precedence order:
2737
+ * default-window guesswork (current daemons tier the unset-`num_ctx`
2738
+ * default by VRAM: 4k / 32k / 256k). Consults, in precedence order:
2662
2739
  *
2663
2740
  * 1. `GET /api/ps` — the window the **running instance actually
2664
2741
  * allocated** (exact, reflects the Modelfile `num_ctx` default and
@@ -2670,7 +2747,7 @@ var ModelsClient = class {
2670
2747
  * 3. `POST /api/show` `model_info["<arch>.context_length"]` — the
2671
2748
  * model's **native GGUF maximum** (a ceiling to raise `num_ctx`
2672
2749
  * toward, not the allocated window).
2673
- * 4. Fallback — Ollama's conservative unset-`num_ctx` default (2048).
2750
+ * 4. Fallback — the 2048 worst-case floor for the unset-`num_ctx` window.
2674
2751
  *
2675
2752
  * The result carries every signal it found, so `nativeContextLength:
2676
2753
  * 131072` vs `contextLength: 4096` (a 32× gap seen in the official API
@@ -6371,7 +6448,9 @@ var OllamaClient = class _OllamaClient {
6371
6448
  * through a local server. Requires an Ollama account API key: pass `apiKey` to the
6372
6449
  * client constructor or set `OLLAMA_API_KEY`. Applies the same default `timeoutMs` and
6373
6450
  * retry policy as inference calls, but never cross-endpoint-fails-over — there is only
6374
- * ever the one cloud endpoint to call.
6451
+ * ever the one cloud endpoint to call. If the service answers 429 with a `Retry-After`
6452
+ * header, the transport parses it into `OllamaRateLimitError.retryAfterMs` and the
6453
+ * retry loop sleeps exactly that long instead of jittered backoff (see ADR 0035).
6375
6454
  */
6376
6455
  async webSearch(req) {
6377
6456
  const { count, max_results, ...rest } = req;
@@ -6402,8 +6481,11 @@ var OllamaClient = class _OllamaClient {
6402
6481
  * web search, and web fetch, bucketed by hour (`range: '24h'`) or day (`'7d'`/`'30d'`).
6403
6482
  *
6404
6483
  * See {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry
6405
- * behavior, which applies identically here. Query parameters mirror the official
6406
- * endpoint: `range` (`'24h' | '7d' | '30d'`, server default `'7d'`) and `scope`
6484
+ * behavior, which applies identically here. The pinned contract documents this
6485
+ * endpoint's 429 with a `Retry-After` header ("Seconds to wait before retrying"),
6486
+ * which the SDK parses into `OllamaRateLimitError.retryAfterMs` and honors verbatim
6487
+ * when retrying. Query parameters mirror the official endpoint: `range`
6488
+ * (`'24h' | '7d' | '30d'`, server default `'7d'`) and `scope`
6407
6489
  * (`'self' | 'team'`, server default `'self'`; team scope requires a team admin).
6408
6490
  * Omitted options are left unset so the server applies its own defaults.
6409
6491
  *
@@ -6436,7 +6518,9 @@ var OllamaClient = class _OllamaClient {
6436
6518
  * object (`balance_usd`/`allowance_usd`/`period`) or, on legacy plans, session/weekly
6437
6519
  * percentage limits; the shape is discriminated at runtime by the server response.
6438
6520
  * See {@link OllamaClient.webSearch} for the cloud-endpoint/auth/timeout/retry
6439
- * behavior, which applies identically here.
6521
+ * behavior, which applies identically here. Like `/api/usage`, the pinned contract
6522
+ * documents this endpoint's 429 with a `Retry-After` header, parsed into
6523
+ * `OllamaRateLimitError.retryAfterMs` and honored verbatim when retrying (ADR 0035).
6440
6524
  *
6441
6525
  * ```ts
6442
6526
  * const balance = await client.balance();
@@ -6722,6 +6806,21 @@ function sleep2(ms, signal) {
6722
6806
  signal?.addEventListener("abort", onAbort, { once: true });
6723
6807
  });
6724
6808
  }
6809
+ function retryAfterMsFromResponse(response) {
6810
+ try {
6811
+ const headers = response?.headers;
6812
+ if (headers === null || headers === void 0 || typeof headers.get !== "function") {
6813
+ return void 0;
6814
+ }
6815
+ const value = headers.get("retry-after");
6816
+ if (value === null || value === "") {
6817
+ return void 0;
6818
+ }
6819
+ return parseRetryAfterMs(value);
6820
+ } catch {
6821
+ return void 0;
6822
+ }
6823
+ }
6725
6824
  async function fetchWithBackoff(input, init, config = {}) {
6726
6825
  const fetchImpl = config.fetch ?? globalThis.fetch;
6727
6826
  if (typeof fetchImpl !== "function") {
@@ -6743,7 +6842,8 @@ async function fetchWithBackoff(input, init, config = {}) {
6743
6842
  ...signal !== void 0 ? { signal } : {}
6744
6843
  });
6745
6844
  if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
6746
- const delayMs = calculateBackoff(attempt, backoff);
6845
+ const serverDelayMs = response.status === 429 ? retryAfterMsFromResponse(response) : void 0;
6846
+ const delayMs = serverDelayMs ?? calculateBackoff(attempt, backoff);
6747
6847
  config.onRetry?.(attempt, response.status, delayMs);
6748
6848
  await sleep2(delayMs, signal);
6749
6849
  continue;
@@ -8448,6 +8548,7 @@ exports.observeStream = observeStream;
8448
8548
  exports.onProgress = onProgress;
8449
8549
  exports.parseFrontmatter = parseFrontmatter;
8450
8550
  exports.parseNdjsonStream = parseNdjsonStream;
8551
+ exports.parseRetryAfterMs = parseRetryAfterMs;
8451
8552
  exports.parseSseStream = parseSseStream;
8452
8553
  exports.parseStructuredOutput = parseStructuredOutput;
8453
8554
  exports.recommendContextWindow = recommendContextWindow;