@ykurilov/agent-chat-core 0.69.1 → 0.70.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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.70.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ba86a4a: Стандартный DeepSeek 4.1 Flash обновлён до 1017 у OpenRouter, Together и Fireworks. У OpenRouter обновлены цены и расписание пикового тарифа; id профиля сохранён.
8
+
9
+ ## 0.70.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 7e54665: Сетевые отказы (закрытый порт, DNS, сброс или таймаут соединения, Cannot connect to API) распознаются по коду и
14
+ вложенной причине как временный сбой. До начала ответа ход переходит к запасному, как при 5xx.
15
+ При доступном запасном по умолчанию один повтор через 250 мс. `providerFallback.retryDelaysMs` задаёт паузы и
16
+ число повторов; `[]` - переход без повтора. Без доступного запасного сохраняются три повтора с паузами 1/3/6 с.
17
+ Сетевые сбои и 5xx не ставят основной на пятиминутную паузу: следующий ход снова пробует его.
18
+ Начавшийся ответ не переводится на другого провайдера, сроки молчания и onSilence остаются прежними.
19
+
20
+ Как обновить приложение: обновить пакеты SDK одной версией; миграций нет. Быстрый режим включён автоматически.
21
+ Для немедленного перехода добавить `retryDelaysMs: []` в существующий `providerFallback`; для прежних повторов -
22
+ `retryDelaysMs: [1000, 3000, 6000]`. Настройка ограничивает паузы повторов, а не время соединения или ответа.
23
+
3
24
  ## 0.69.1
4
25
 
5
26
  ### Patch Changes
@@ -3513,10 +3513,47 @@ function looksTransient(text) {
3513
3513
  return RETRYABLE_SIGNS.test(text);
3514
3514
  }
3515
3515
 
3516
- // src/provider-errors/transient-retry-middleware.ts
3516
+ // src/provider-errors/network-failure.ts
3517
3517
  import { APICallError as APICallError2 } from "ai";
3518
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
3519
+ "ECONNREFUSED",
3520
+ "ENOTFOUND",
3521
+ "EAI_AGAIN",
3522
+ "EHOSTUNREACH",
3523
+ "ENETUNREACH",
3524
+ "ETIMEDOUT",
3525
+ "ECONNRESET",
3526
+ "EPIPE",
3527
+ "ECONNABORTED",
3528
+ "UND_ERR_CONNECT_TIMEOUT",
3529
+ "UND_ERR_SOCKET",
3530
+ "UND_ERR_HEADERS_TIMEOUT",
3531
+ "UND_ERR_BODY_TIMEOUT",
3532
+ "ConnectionRefused",
3533
+ "FailedToOpenSocket",
3534
+ "ConnectionClosed"
3535
+ ]);
3536
+ function isNetworkFailure(error) {
3537
+ const pending = [underlyingError(error)];
3538
+ const seen = /* @__PURE__ */ new Set();
3539
+ while (pending.length > 0) {
3540
+ const current = pending.pop();
3541
+ if (typeof current !== "object" || current === null || seen.has(current)) continue;
3542
+ seen.add(current);
3543
+ if (errorStatusCode(current) !== void 0) continue;
3544
+ const fields = current;
3545
+ if (typeof fields.code === "string" && NETWORK_CODES.has(fields.code)) return true;
3546
+ if (APICallError2.isInstance(current) && current.isRetryable) return true;
3547
+ if (typeof fields.message === "string" && /^Cannot connect to API\b/i.test(fields.message)) return true;
3548
+ pending.push(fields.cause);
3549
+ if (Array.isArray(fields.errors)) pending.push(...fields.errors);
3550
+ }
3551
+ return false;
3552
+ }
3553
+
3554
+ // src/provider-errors/transient-retry-middleware.ts
3555
+ import { APICallError as APICallError3 } from "ai";
3518
3556
  var RETRY_DELAYS_MS = [1e3, 3e3, 6e3];
3519
- var RETRY_BUDGET_MS = RETRY_DELAYS_MS.reduce((sum, delay) => sum + delay, 0);
3520
3557
  var RETRY_JITTER = 0.25;
3521
3558
  var MAX_SILENCE_ATTEMPTS = 2;
3522
3559
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 409, 429]);
@@ -3547,7 +3584,8 @@ function isTransient(error) {
3547
3584
  const text = errorText(error);
3548
3585
  if (isNotRetryable(text, status) || isContextOverflow(text, status)) return false;
3549
3586
  if (status !== void 0) return RETRYABLE_STATUS_CODES.has(status) || status >= 500;
3550
- if (APICallError2.isInstance(error) && error.isRetryable) return true;
3587
+ if (APICallError3.isInstance(error) && error.isRetryable) return true;
3588
+ if (isNetworkFailure(error)) return true;
3551
3589
  return looksTransient(text);
3552
3590
  }
3553
3591
  function createTransientRetryMiddleware(context = {}) {
@@ -3557,6 +3595,7 @@ function createTransientRetryMiddleware(context = {}) {
3557
3595
  const silence = context.silence ?? DEFAULT_MODEL_SILENCE;
3558
3596
  const repeatSilence = context.repeatSilence ?? true;
3559
3597
  const silenceAttempts = () => (typeof repeatSilence === "function" ? repeatSilence() : repeatSilence) ? MAX_SILENCE_ATTEMPTS : 1;
3598
+ let answerStarted = false;
3560
3599
  const onSilence = (waited, timeoutMs) => context.logError?.(
3561
3600
  `[\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u0435 \u043C\u043E\u0434\u0435\u043B\u0438] \u0447\u0430\u0442=${context.chatId ?? "\u043D\u0435\u0442"} \u043C\u043E\u0434\u0435\u043B\u044C=${context.modelId ?? "\u043D\u0435\u0442"} \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440=${context.providerId ?? "\u043D\u0435\u0442"} \u0436\u0434\u0430\u043B\u0438=${waited} \u0441\u0440\u043E\u043A=${timeoutMs}\u043C\u0441`
3562
3601
  );
@@ -3567,10 +3606,11 @@ function createTransientRetryMiddleware(context = {}) {
3567
3606
  try {
3568
3607
  return await attemptOnce();
3569
3608
  } catch (error) {
3570
- const delay = RETRY_DELAYS_MS[attempt];
3609
+ const delays = !answerStarted && !isTimeoutError(error) ? context.retryDelaysMs?.() ?? RETRY_DELAYS_MS : RETRY_DELAYS_MS;
3610
+ const delay = delays[attempt];
3571
3611
  if (isTimeoutError(error)) silences++;
3572
3612
  if (abortSignal?.aborted || delay === void 0 || !isTransient(error) || silences >= silenceAttempts()) throw error;
3573
- const remainingMs = RETRY_BUDGET_MS - waitedMs;
3613
+ const remainingMs = Math.max(0, delays.reduce((sum, value) => sum + value, 0) - waitedMs);
3574
3614
  const pauseMs = requestedPauseMs(error, remainingMs, now()) ?? Math.round(Math.min(delay * (1 + random() * RETRY_JITTER), remainingMs));
3575
3615
  context.logError?.(retryLine(context, attempt + 2, error, pauseMs));
3576
3616
  await pause2(pauseMs, abortSignal);
@@ -3580,7 +3620,11 @@ function createTransientRetryMiddleware(context = {}) {
3580
3620
  }
3581
3621
  return {
3582
3622
  specificationVersion: "v4",
3583
- wrapStream: ({ model, params }) => withRetries2(() => streamUntilFirstChunk(model, params, silence, onSilence), params.abortSignal),
3623
+ wrapStream: async ({ model, params }) => {
3624
+ const result = await withRetries2(() => streamUntilFirstChunk(model, params, silence, onSilence), params.abortSignal);
3625
+ answerStarted = true;
3626
+ return result;
3627
+ },
3584
3628
  wrapGenerate: ({ doGenerate, params }) => withRetries2(doGenerate, params.abortSignal)
3585
3629
  };
3586
3630
  }
@@ -3744,6 +3788,7 @@ function classifyProviderError(error, provider) {
3744
3788
  if (isProviderSilence(cause)) return "overloaded";
3745
3789
  if (status !== void 0) return status >= 500 ? "overloaded" : "other";
3746
3790
  if (RATE_LIMIT_SIGNS.test(text)) return "rate_limited";
3791
+ if (isNetworkFailure(cause)) return "overloaded";
3747
3792
  return looksTransient(text) ? "overloaded" : "other";
3748
3793
  }
3749
3794
  function isAccessRefusal(error) {
@@ -3755,7 +3800,7 @@ function isKeyLimit(error) {
3755
3800
  }
3756
3801
 
3757
3802
  // src/provider-errors/provider-failure-log.ts
3758
- import { APICallError as APICallError3 } from "ai";
3803
+ import { APICallError as APICallError4 } from "ai";
3759
3804
  var MAX_BODY_LENGTH = 4e3;
3760
3805
  function describeProviderFailure(error, context) {
3761
3806
  const cause = underlyingError(error);
@@ -3766,7 +3811,7 @@ function describeProviderFailure(error, context) {
3766
3811
  }
3767
3812
  function bodyOf(error) {
3768
3813
  const candidates = [];
3769
- if (APICallError3.isInstance(error)) candidates.push(error.responseBody);
3814
+ if (APICallError4.isInstance(error)) candidates.push(error.responseBody);
3770
3815
  if (typeof error === "object" && error !== null) {
3771
3816
  const fields = error;
3772
3817
  candidates.push(fields.message);
@@ -5031,7 +5076,8 @@ async function buildModelRequest(input) {
5031
5076
  providerId: provider.id,
5032
5077
  logError: input.logError,
5033
5078
  ...silenceOfProvider(provider, input.modelSilence),
5034
- ...input.repeatSilence && { repeatSilence: input.repeatSilence }
5079
+ ...input.repeatSilence && { repeatSilence: input.repeatSilence },
5080
+ ...input.retryDelaysMs && { retryDelaysMs: input.retryDelaysMs }
5035
5081
  });
5036
5082
  const tools = input.tools ? sortToolsByName(input.tools) : void 0;
5037
5083
  const untitled = withoutThoughtTitles(input.history);
@@ -5348,6 +5394,7 @@ function nextSilenceProvider(input) {
5348
5394
  return index === -1 ? input.fallbackProviderIds[0] : input.fallbackProviderIds[index + 1];
5349
5395
  }
5350
5396
  var PROVIDER_FALLBACK_BY_DEFAULT = {};
5397
+ var FALLBACK_RETRY_DELAYS_MS = [250];
5351
5398
  var TRANSIENT_KINDS = /* @__PURE__ */ new Set(["overloaded", "rate_limited"]);
5352
5399
  function fallbackProviders(input) {
5353
5400
  const setting = input.setting ?? PROVIDER_FALLBACK_BY_DEFAULT;
@@ -5397,7 +5444,10 @@ function checkProviderFallback(setting, catalog) {
5397
5444
  if (typeof setting !== "object" || setting === null || Array.isArray(setting)) {
5398
5445
  throw new TypeError("providerFallback: \u0436\u0434\u0451\u043C false \u0438\u043B\u0438 \u043E\u0431\u044A\u0435\u043A\u0442 { byModel }");
5399
5446
  }
5400
- const { byModel, onSilence, scope, maxSwitches } = setting;
5447
+ const { byModel, onSilence, scope, maxSwitches, retryDelaysMs } = setting;
5448
+ if (retryDelaysMs !== void 0 && (!Array.isArray(retryDelaysMs) || retryDelaysMs.some((delay) => typeof delay !== "number" || !Number.isFinite(delay) || delay < 0))) {
5449
+ throw new TypeError("providerFallback.retryDelaysMs: \u0436\u0434\u0451\u043C \u043C\u0430\u0441\u0441\u0438\u0432 \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0445 \u0447\u0438\u0441\u0435\u043B \u043E\u0442 \u043D\u0443\u043B\u044F (\u043C\u0441), [] \u2014 \u0431\u0435\u0437 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u0432");
5450
+ }
5401
5451
  if (onSilence !== void 0 && onSilence !== "continue" && onSilence !== "restart-step-on-next") {
5402
5452
  throw new TypeError(`providerFallback.onSilence: \u0436\u0434\u0451\u043C "continue" \u0438\u043B\u0438 "restart-step-on-next", \u043F\u0440\u0438\u0448\u043B\u043E ${JSON.stringify(onSilence)}`);
5403
5453
  }
@@ -5607,7 +5657,7 @@ ${summary}
5607
5657
  }
5608
5658
 
5609
5659
  // src/provider-errors/provider-error-details.ts
5610
- import { APICallError as APICallError4 } from "ai";
5660
+ import { APICallError as APICallError5 } from "ai";
5611
5661
  var MAX_REASON_LENGTH = 300;
5612
5662
  var MAX_RESET_AHEAD_MS = 2 * 366 * 24 * 60 * 60 * 1e3;
5613
5663
  function providerErrorDetails(error, context) {
@@ -5627,7 +5677,7 @@ function providerErrorDetails(error, context) {
5627
5677
  };
5628
5678
  }
5629
5679
  function providerMessageOf(error) {
5630
- if (APICallError4.isInstance(error)) {
5680
+ if (APICallError5.isInstance(error)) {
5631
5681
  const fromBody = typeof error.responseBody === "string" ? fromJson(error.responseBody) : void 0;
5632
5682
  if (fromBody?.message !== void 0) return fromBody;
5633
5683
  const raw = [error.responseBody, error.message].find(isText);
@@ -7188,6 +7238,7 @@ async function runTurn(turnInput, emitRaw) {
7188
7238
  mainLoopWaits = true;
7189
7239
  }
7190
7240
  let fallbackUsed = false;
7241
+ const remainingFallbacks = () => providerId === input.providerId ? fallbackProviderIds : fallbackProviderIds.slice(fallbackProviderIds.indexOf(providerId) + 1);
7191
7242
  let historyCompacted = false;
7192
7243
  let answerStepsCompacted = false;
7193
7244
  let autoContinues = 0;
@@ -7235,6 +7286,7 @@ async function runTurn(turnInput, emitRaw) {
7235
7286
  forgedBlocks,
7236
7287
  // Есть куда перейти при молчании — у того же провайдера оно не повторяется.
7237
7288
  repeatSilence: () => silenceTarget() === void 0,
7289
+ retryDelaysMs: () => !contentSeen && !fallbackUsed && remainingFallbacks().length > 0 && stepsDone < maxSteps ? (input.providerFallback || {}).retryDelaysMs ?? FALLBACK_RETRY_DELAYS_MS : void 0,
7238
7290
  ...silenceSwitch.scope === "step" && switchedBySilence && {
7239
7291
  backToPrimary: () => {
7240
7292
  backToPrimary = true;
@@ -7289,7 +7341,7 @@ async function runTurn(turnInput, emitRaw) {
7289
7341
  const nextState = () => ({
7290
7342
  fallbackUsed,
7291
7343
  // С запасного — только провайдеры после него: перегрузка у запасного не ведёт обратно к нему же.
7292
- fallbackProviderIds: providerId === input.providerId ? fallbackProviderIds : fallbackProviderIds.slice(fallbackProviderIds.indexOf(providerId) + 1),
7344
+ fallbackProviderIds: remainingFallbacks(),
7293
7345
  overflowCompaction: overflowCompaction(),
7294
7346
  autoContinue: { done: autoContinues, approvalRequested: pendingHumanInput(parts2, input.waitingWidgets).length > 0 },
7295
7347
  silenceSwitch: { done: silenceSwitches, max: silenceSwitch.maxSwitches, nextProviderId: silenceTarget() }
@@ -7762,6 +7814,7 @@ async function openAttempt(input, attempt, attemptSignal) {
7762
7814
  ...attempt.tailMessages && { tailMessages: attempt.tailMessages },
7763
7815
  logError,
7764
7816
  repeatSilence: attempt.repeatSilence,
7817
+ retryDelaysMs: attempt.retryDelaysMs,
7765
7818
  ...turnObserver && { observe: { observer: turnObserver, ownerId: input.ownerId ?? "", messageId: input.messageId, purpose: "answer" } },
7766
7819
  ...tools && { tools: approval.tools },
7767
7820
  // Каждый запрос, на который провайдер начал отвечать, — в расход хода с номером генерации (FR-051, FR-058).
@@ -9393,10 +9446,10 @@ var deepseek41FlashProfile = {
9393
9446
  displayName: "DeepSeek 4.1 Flash",
9394
9447
  contextWindowTokens: 1e6,
9395
9448
  acceptsImages: true,
9396
- // Картинку в результате инструмента видит у самого DeepSeek (проверено вживую 2026-10-05), у Together и Fireworks
9397
- // (вживую 2026-10-15: картинка в ответе инструмента и в сообщении человека — цвет назван верно; запись каталога
9398
- // Together «только текст» устарела) и через OpenRouter у всех хозяев, кроме запрещённых ниже: одни её не видят,
9399
- // другие отвергают запрос (замер всех хозяев модели в OpenRouter 2026-10-06, specs/research-openrouter-tool-images.md).
9449
+ // 1017: картинка в результате инструмента проверена у DeepSeek, OpenRouter и Fireworks 2026-10-22.
9450
+ // Together: новый id в каталоге; живой вызов вернул 402, отметка сохранена по замеру предыдущей версии.
9451
+ // Запрет хозяев ниже сохранён по ранним замерам (specs/research-openrouter-tool-images.md);
9452
+ // проверка обычного маршрута 1017 не заменяет отдельного замера каждого хозяина.
9400
9453
  toolResultImageProviders: ["deepseek", "together", "fireworks", "openrouter"],
9401
9454
  reasoning: { replayPastTurns: true, levels: ["off", "on"], defaultLevel: "on" },
9402
9455
  // Запрет стоит всегда, а не только при картинке: запрос одинаков в каждом ходе, кэш и выбранный хозяин не теряются.
@@ -9407,13 +9460,14 @@ var deepseek41FlashProfile = {
9407
9460
  // прогон 35 учеников «Мастера понятий» — каши 0 из 1036 ответов (было 2 случая). Уходит к Together и OpenRouter;
9408
9461
  // DeepSeek напрямую обрезки не принимает.
9409
9462
  sampling: { minP: 0.05 },
9463
+ // 1017: прямой DeepSeek обновляет alias сам; остальные провайдеры используют датированный id.
9410
9464
  providerModelIds: {
9411
9465
  deepseek: "deepseek-flash",
9412
- fireworks: "accounts/fireworks/models/deepseek-v4p1-flash",
9413
- together: "deepseek-ai/DeepSeek-V4.1-Flash",
9414
- openrouter: "deepseek/deepseek-v4.1-flash"
9466
+ fireworks: "accounts/fireworks/models/deepseek-v4p1-flash-1017",
9467
+ together: "deepseek-ai/DeepSeek-V4.1-Flash-1017",
9468
+ openrouter: "deepseek/deepseek-v4.1-flash-1017"
9415
9469
  },
9416
- // Цены: прайс-листы провайдеров на 2026-09-30, доллары за миллион токенов (specs/012-spending-limits/research.md).
9470
+ // Цены за миллион токенов: каталоги провайдеров 2026-10-22.
9417
9471
  pricing: {
9418
9472
  // DeepSeek: цена вне пика, в часы пик (будни 01–04 и 06–10 UTC, api-docs.deepseek.com, проверено 2026-10-05) всё
9419
9473
  // вдвое, включая попадания в кэш. Праздники Китая (тоже вне пика) не заданы: расход в них чуть завышается.
@@ -9432,9 +9486,23 @@ var deepseek41FlashProfile = {
9432
9486
  },
9433
9487
  fireworks: { inputPerMillion: 0.3, outputPerMillion: 1.2, cachedInputPerMillion: 6e-3 },
9434
9488
  together: { inputPerMillion: 0.3, outputPerMillion: 1.2, cachedInputPerMillion: 6e-3 },
9435
- // Платной записи в кэш у хозяев OpenRouter нет (ни `input_cache_write`, ни `input_cache_write_1h`, 2026-10-12): кэш
9489
+ // Платной записи в кэш у хозяев OpenRouter нет (ни `input_cache_write`, ни `input_cache_write_1h`, 2026-10-22): кэш
9436
9490
  // сам, записанное идёт по цене входа. Часовая цена — тоже цена входа: срок метки модель не читает.
9437
- openrouter: { inputPerMillion: 0.3, outputPerMillion: 1.2, cachedInputPerMillion: 6e-3, cacheWrite1hPerMillion: 0.3 }
9491
+ openrouter: {
9492
+ inputPerMillion: 0.15,
9493
+ outputPerMillion: 0.6,
9494
+ cachedInputPerMillion: 3e-3,
9495
+ cacheWrite1hPerMillion: 0.15,
9496
+ // Каталог OpenRouter pricing.overrides: то же расписание пика, что у прямого DeepSeek.
9497
+ peakHours: {
9498
+ timeZone: "UTC",
9499
+ multiplier: 2,
9500
+ windows: [
9501
+ { days: [1, 2, 3, 4, 5], from: "01:00", to: "04:00" },
9502
+ { days: [1, 2, 3, 4, 5], from: "06:00", to: "10:00" }
9503
+ ]
9504
+ }
9505
+ }
9438
9506
  }
9439
9507
  };
9440
9508
 
@@ -15075,4 +15143,4 @@ export {
15075
15143
  InvalidWidgetAnswerError,
15076
15144
  createChatService
15077
15145
  };
15078
- //# sourceMappingURL=chunk-7G63YBL5.js.map
15146
+ //# sourceMappingURL=chunk-EL2KSERW.js.map