@ykurilov/agent-chat-core 0.63.1 → 0.64.0

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,17 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.64.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 99cded9: Подробности сбоя провайдера для админа. Ответ, упавший на провайдере, хранит `error.details`: провайдер, модель, код, причина одной строкой (до 300 знаков; ключи окружения, `Bearer`, значения `key=`/`token`/`api-key`/`password`, маски ключей со `*`, параметры адресов, `org-…`/`proj_…` и e-mail вычищены; модерация может повторять текст человека) и `resetsAt` — когда провайдер снимет предел (OpenRouter «resets 2026-11-01»; только будущее до двух лет, без пояса — UTC). В браузер подробности уходят только по новому праву `chatCapabilities` → `errorDetails: true` (по умолчанию `false`, при `modelDetails: false` тоже `false`) — в истории, потоке и при переподключении; остальным — только `error.kind`. `afterTurn` получает их всегда. Экран: подпись-функция `labels.errorText(error, plain)`, без неё или если она бросила исключение — прежний текст `labels.errors[kind]`; типы `ChatError`, `ChatErrorDetails`, помощник `customErrorText`. Хранилища: тип колонки `error` — `ChatError`, миграций нет.
8
+
9
+ ## 0.63.2
10
+
11
+ ### Patch Changes
12
+
13
+ - c772243: Исчерпанный предел расходов ключа (OpenRouter 403 «Key limit exceeded», `limit_source: openrouter_key_limit`; бюджеты ключа у шлюзов — «ExceededBudget», «Budget has been exceeded») — теперь `insufficient_funds`, а не `invalid_key`: переход к запасному, пауза основного, человеку «кончились средства», громкая строка «исчерпан предел расходов ключа». Обычный 403 — как раньше.
14
+
3
15
  ## 0.63.1
4
16
 
5
17
  No changes in this release.
package/README.md CHANGED
@@ -82,8 +82,8 @@ import type { ChatStorage } from "@ykurilov/agent-chat-core";
82
82
  - Данные чата без записи: `service.getChatData(userId, chatId)` — копия, проверки владения как у `updateChatData`.
83
83
  - Проверка ответа виджета на сервере: `checkAnswer(answer, ctx)` у своего виджета и общая `agent.widgets.checkAnswer(call, answer, ctx)` (для всех виджетов без своей). Зовётся после схемы под замком чата. Исход: `undefined` — принять, `{ answer }` — принять с заменой, `{ refuse: "текст" }` — отказать (`WidgetAnswerRefusedError`, сервер — 409 `widget_answer_refused`, виджет остаётся открытым). `ctx` — `ownerId`, `chatId`, `messageId`, `toolName`, `toolCallId`, `input`, `subject?`, `chatData?`, `state` (записи ложатся в ответ вместе с результатом виджета, хук `onStateChanged` — с причиной `widgetAnswer`), `emitAppEvent` (событие приложения вместе с ответом виджета), `signal` (отмена по сроку или «Стопу»: приложение слушает его само). Срок — `agent.widgets.checkAnswerTimeoutMs` (5 000 мс, заметно меньше `requestTimeoutMs` страницы). Исключение — 500, ничего не записано, в журнале только имя ошибки; срок вышел — `WidgetAnswerCheckTimeoutError`, сервер — 504 `widget_check_timeout`. Чат закрыли во время проверки — `ChatClosedError`, ответ не пишется.
84
84
  - Набор виджетов по чату: `agent.widgets.enabledFor({ chat: { id, ownerId, data?, subject? } })` → имена включённых. Выключенных модель не видит (нет в инструментах запроса), их прошлые вызовы идут модели текстом. Синхронная, те же данные — тот же набор; смена набора сбрасывает кэш модели. Неизвестное имя — строка в журнал, исключение — ошибка хода. Подробно — [docs/widgets.md](../../docs/widgets.md).
85
- - Права человека в чате: `chatCapabilities({ userId, chat })` возвращает запреты (`edit`, `regenerate`, `retry`, `branches`, `modelChoice`, `answerEditing`, `chatList`, `compact`, `attachments`, `voice`, `reasoning`, `toolDetails`, `steer`, `modelDetails`; по умолчанию всё `true`; `modelDetails: false` выключает и `modelChoice`). Неизвестное право или не булево значение — `TypeError`. Запрещённое действие — `ActionForbiddenError(action)` до любой записи и до `beforeTurn`. За один вызов службы права спрашиваются не больше раза. История несёт `capabilities`. Вручную — `service.assertCapability(userId, chatId, capability)`.
86
- - Скрытие частей ответа: `reasoning: false` — мысли модели, `toolDetails: false` — служебные инструменты не уходят в историю, поток и повтор кусков после переподключения; хранилище, модель и `afterTurn` получают всё. Виджеты, браузерные инструменты, вызовы с запросом подтверждения и инструменты с `metadata: { showOnScreen: true }` в описании видны всегда. `modelDetails: false` убирает у чата истории `ownerId`, `modelId`, `providerId`, `reasoning`, у сообщений — `metadata.modelId`, `providerId`, `usage`, у частей и кусков — `providerMetadata`/`callProviderMetadata`/`resultProviderMetadata`, у `data-step-reset` — провайдера; отметки сжатия идут без текста сводки (`summary: ""`, `summaryTokens: 0`) в истории и `getCompactions`. Меню моделей — опция `modelMenu({ userId })`: `false` — `getModelMenu(userId)` бросает `ActionForbiddenError("modelDetails")`. История несёт `modelAcceptsImages` (видит ли модель чата картинки). Права — на того, кто открыл поток.
85
+ - Права человека в чате: `chatCapabilities({ userId, chat })` возвращает запреты (`edit`, `regenerate`, `retry`, `branches`, `modelChoice`, `answerEditing`, `chatList`, `compact`, `attachments`, `voice`, `reasoning`, `toolDetails`, `steer`, `modelDetails`, `errorDetails`; по умолчанию всё `true`, кроме `errorDetails`; `modelDetails: false` выключает и `modelChoice`, и `errorDetails`). Неизвестное право или не булево значение — `TypeError`. Запрещённое действие — `ActionForbiddenError(action)` до любой записи и до `beforeTurn`. За один вызов службы права спрашиваются не больше раза. История несёт `capabilities`. Вручную — `service.assertCapability(userId, chatId, capability)`.
86
+ - Скрытие частей ответа: `reasoning: false` — мысли модели, `toolDetails: false` — служебные инструменты не уходят в историю, поток и повтор кусков после переподключения; хранилище, модель и `afterTurn` получают всё. Виджеты, браузерные инструменты, вызовы с запросом подтверждения и инструменты с `metadata: { showOnScreen: true }` в описании видны всегда. `modelDetails: false` убирает у чата истории `ownerId`, `modelId`, `providerId`, `reasoning`, у сообщений — `metadata.modelId`, `providerId`, `usage`, у частей и кусков — `providerMetadata`/`callProviderMetadata`/`resultProviderMetadata`, у `data-step-reset` — провайдера; отметки сжатия идут без текста сводки (`summary: ""`, `summaryTokens: 0`) в истории и `getCompactions`. Подробности сбоя провайдера (`metadata.error.details`: `providerId`, `modelId`, `statusCode`, `reason` — одна строка до 300 знаков без секретов, параметров адресов, `org-…`/`proj_…` и e-mail, `resetsAt` — ISO, только будущее не дальше двух лет) хранятся всегда, а в историю и поток идут только при `errorDetails: true` (например, админу); без права — только `error.kind`. Ответ модерации в `reason` может повторять текст человека — отдавайте право только тому, кому этот текст можно видеть. Меню моделей — опция `modelMenu({ userId })`: `false` — `getModelMenu(userId)` бросает `ActionForbiddenError("modelDetails")`. История несёт `modelAcceptsImages` (видит ли модель чата картинки). Права — на того, кто открыл поток.
87
87
  - Закрытый чат: `service.closeChat({ ownerId, chatId, code, message })` и `reopenChat({ ownerId, chatId })` (события `chat-closed`, `chat-reopened`), из инструмента — `model.closeChat({ code, message })`. Закрытие — запись `$closed` на ветке: из инструмента — в ответе сразу при вызове, из кода — служебной записью в конце ветки (идёт ход — после него). Правка, «Переделать», выбор ветки и правка ответа до точки закрытия открывают чат; закрыть навсегда — правами (`chatCapabilities` видит `chat.closed`). Закрытие побеждает паузу: ждущие подтверждения и виджеты закрываются отказом, как при «Стопе». В закрытом чате отправка, «Повторить», подтверждения и виджеты — `ChatClosedError`; служебные сообщения пишутся без хода. `listChats` отдаёт `closed` — копию закрытия текущей ветки (`setChatClosed`); права и история берут закрытие с ветки, не записавшуюся копию дописывает следующее открытие истории. Закрытие чатов 0.27 переносится в запись лениво, при первой правке или смене ветки.
88
88
  - `beforeCreateChat({ userId, origin? })` — можно ли человеку завести новый чат (`createChat`): `{ code, message }` → `ChatCreationRefusedError`. Чаты объектов и импорт хук не спрашивают. У чата `closed?: { code, message }`, хранилище — необязательный `setChatClosed(chatId, closed | null)`.
89
89
  - Чат объекта: `service.openSubjectChat({ ownerId, key, data?, title?, greeting? })` → `{ chat, created }`, один чат на ключ у человека. Инструменты получают `model.subject`, итог хода — `turn.subject`. `listChats(userId, { subjects })` — по умолчанию без чатов объектов.
package/dist/access.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  accessRefusedResponse,
3
3
  allowedUser
4
- } from "./chunk-FBULM4V6.js";
5
- import "./chunk-FOAIG4DY.js";
4
+ } from "./chunk-5UUWSY6V.js";
5
+ import "./chunk-CVRDB3NM.js";
6
6
  export {
7
7
  accessRefusedResponse,
8
8
  allowedUser
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  normalizeTurnRefusal
3
- } from "./chunk-FOAIG4DY.js";
3
+ } from "./chunk-CVRDB3NM.js";
4
4
 
5
5
  // src/access/resolved-user.ts
6
6
  function allowedUser(resolved) {
@@ -17,4 +17,4 @@ export {
17
17
  allowedUser,
18
18
  accessRefusedResponse
19
19
  };
20
- //# sourceMappingURL=chunk-FBULM4V6.js.map
20
+ //# sourceMappingURL=chunk-5UUWSY6V.js.map
@@ -53,4 +53,4 @@ export {
53
53
  TurnRefusedError,
54
54
  validateAppEvent
55
55
  };
56
- //# sourceMappingURL=chunk-FOAIG4DY.js.map
56
+ //# sourceMappingURL=chunk-CVRDB3NM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/turn-hooks/turn-hooks.ts"],"sourcesContent":["import type { AgentChannel, Chat, ChatError, ChatSubject, FinalAnswerStatus, StoredMessage, TokenUsage } from \"../conversation/types\";\n\n/** Что человек делает с чатом, когда приложение решает, пускать ли ход. */\nexport type TurnAction = \"send\" | \"edit\" | \"retry\" | \"regenerate\" | \"approval\" | \"widget\" | \"notice\";\n\n/** Отказ приложения в ходе: код для программы, текст для человека. */\nexport interface TurnRefusal {\n /** Латиница в нижнем регистре, цифры и `.`, `_`, `-`; от 1 до 64 знаков. */\n code: string;\n /** До 300 знаков; длиннее — обрезается. */\n message: string;\n}\n\n/**\n * Сотрудник, который действует в чужом чате (копия чата человека в песочнице). Пока он есть у хода (`model.staff`),\n * инструменты приложения данные `ownerId` только читают: любая запись в них запрещена — это обязанность инструментов.\n */\nexport interface StaffActor {\n id: string;\n}\n\n/** Событие приложения в ответе агента: `type` — 1–100 знаков, `data` — JSON до 16 КБ. */\nexport interface AppEvent {\n type: string;\n data?: unknown;\n}\n\n/** Записать сообщение человека без хода: агент промолчит (рамки дня, пауза сценария). */\nexport interface TurnHold {\n hold: true;\n}\n\n/** Ход с готовым текстом, без модели: путь `reply` из `deliverNotices`. */\nexport interface TurnReply {\n reply: { text: string };\n}\n\n/** Что `beforeTurn` может решить, кроме «пускать»: отказ, «записать без хода» или «ответить готовым текстом». */\nexport type BeforeTurnDecision = TurnRefusal | TurnHold | TurnReply;\n\n/**\n * Проверка приложения перед ходом: вернуть отказ — ход не начнётся и ничего не запишется. `{ hold: true }` и\n * `{ reply }` — с 0.49 (сценарии): сообщение записывается, хода модели нет.\n */\nexport type BeforeTurnHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n /** Текст сообщения человека (`send`, `edit`). */\n text?: string;\n /** Id сообщения человека (`send`, `edit`): по нему приложение свяжет свой резерв с ходом. */\n messageId?: string;\n /**\n * Ход на пачку сообщений человека (`sendBurstMessage`): id всех её сообщений по порядку, последнее — `messageId`, а\n * `text` — их тексты через пустую строку. Одно решение на всю пачку; счёт сообщений — по длине списка.\n */\n burstMessageIds?: string[];\n /**\n * Ход на пачку начал сам сервер после перезапуска или за упавшую копию (`answerAfterRestart`): вызова, который ждёт\n * ответ или отказ, нет. Отказ дойдёт только до журнала — сообщить человеку может само приложение.\n */\n unattended?: true;\n} & AbandonedAnswers) => Promise<BeforeTurnDecision | void> | BeforeTurnDecision | void;\n\n/**\n * Ответы агента, которые уйдут с текущей ветки, если ход начнётся (`regenerate`, `edit`, `retry`): по ним приложение откатывает\n * побочные дела брошенных ответов (правки формы и т. п.). Ход не начался (`turnNotStarted`) — ветка прежняя, ничего не\n * ушло: тот хук получает те же поля. Посчитаны по ветке на миг проверки; сообщения нет на ветке — полей нет.\n */\nexport interface AbandonedAnswers {\n /** Ответы агента, которые уходят с ветки, сверху вниз; у `edit` без ответов ниже — пустой список. */\n abandonedAnswerIds?: string[];\n /**\n * Ответ, который заменит новый: переделываемый (`regenerate`), ответ на правленый вопрос (`edit`), если был, или\n * оборванный ответ, который «Повторить» (`retry`) заменяет соседом. У «Повторить» вопроса без ответа полей нет.\n */\n previousAnswerId?: string;\n}\n\n/** Почему ход не начался после того, как `beforeTurn` его пропустил. */\nexport type TurnNotStartedReason =\n /** В чате уже идёт ответ, или служба останавливается (`flush`). */\n | \"busy\"\n /** У человека уже идёт предел одновременных ходов. */\n | \"too_many_turns\"\n /** Лимит расходов исчерпан. */\n | \"spending_limit\"\n /** Сообщение с таким id уже есть (повтор запроса). */\n | \"duplicate_message\"\n /** Запрос неверен: сообщение, файл, решение, ответ на виджет. */\n | \"invalid\"\n /** Чат закрыли, пока ход запускался (`closeChat` между проверкой и началом хода). */\n | \"chat_closed\"\n /** Хода и не будет: проверка без хода (`assertTurnAllowed`), ответ ждёт ещё решений, служебное сообщение без ответа. */\n | \"no_turn\"\n /** Сбой (база, сеть): ход не начат. */\n | \"error\";\n\n/**\n * Ход не начался, хотя `beforeTurn` его пропустил: приложение снимает свой резерв. Каждый пропуск `beforeTurn`\n * закрывается ровно одним: ходом (`afterTurn`) или этим хуком. Ошибка хода наружу прежняя; бросок хука — в журнал.\n */\nexport type TurnNotStartedHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n messageId?: string;\n text?: string;\n /** Пачка сообщений, как в `beforeTurn`. */\n burstMessageIds?: string[];\n /** Ход без вызова, как в `beforeTurn`: об отказе (например, `spending_limit`) человеку может сказать только приложение. */\n unattended?: true;\n reason: TurnNotStartedReason;\n} & AbandonedAnswers) => Promise<void> | void;\n\n/** Кто остановил ход: человек («Стоп»), общий срок хода (`agent.turnTimeoutMs`) или остановка службы (`flush`). */\nexport type TurnStopReason = \"user\" | \"timeout\" | \"shutdown\";\n\n/** Итог законченного хода для приложения. */\nexport interface TurnResult {\n ownerId: string;\n /** Кто сделал ход: владелец чата, а в копии у сотрудника — сотрудник. */\n actorId: string;\n chatId: string;\n messageId: string;\n status: FinalAnswerStatus;\n /** Кто остановил ход; только у `status: \"stopped\"` живого хода (у доделанного после перезапуска нет). */\n stopReason?: TurnStopReason;\n /** Ответ упёрся в предел токенов (`finishReason: \"length\"`, см. `agent.maxOutputTokens`) и оборван. */\n lengthCut?: true;\n /**\n * Ход дошёл до предела шагов (`agent.maxSteps`): последний шаг был итоговым, без инструментов. Это «дошли до потолка»,\n * а не обязательно «недоделали»: модель могла закончить дело как раз на последнем шаге. Статус обычный (`complete`).\n * В журнале SDK — строка `[предел шагов]`.\n */\n stepLimit?: true;\n /** Текстовые части ответа подряд. */\n text: string;\n /** Части ответа, как они записаны, кроме служебной части с блоками контекста (`data-frozen-context`). */\n parts: StoredMessage[\"parts\"];\n usage: TokenUsage | null;\n /** Цена хода в долларах; `null` — у модели нет цены. */\n costUsd: number | null;\n /** Вид сбоя и подробности провайдера (`details`): хук на сервере получает их всегда. */\n error?: ChatError;\n /**\n * Каким действием начат ход (см. `TurnAction`). `null` — итог доделан после перезапуска (`recovered`), а ядро не\n * помнит действие; ход по служебному сообщению и тогда — `notice`.\n */\n trigger: TurnAction | null;\n channel: AgentChannel;\n /**\n * Ход сделал сотрудник в копии чата человека (песочница): `ownerId` и `subject` — человека и его исходного чата.\n * При `staff` в данные человека не писать.\n */\n staff?: StaffActor;\n /** Объект чата (урок, заказ…): ключ и данные чата объекта. Нет — обычный чат. */\n subject?: ChatSubject;\n /** Данные чата (`Chat.data`) на момент итога; нет данных — поля нет. */\n chatData?: Record<string, unknown>;\n /** Итог доделан после перезапуска сервера, а не сразу после хода. */\n recovered: boolean;\n}\n\n/**\n * Хук итога хода: зовётся после записи итога и расхода, до `finish` клиенту; пока он идёт, чат занят. Получает любой\n * итог: законченный, остановленный, с ошибкой, прерванный (в том числе восстановлением после перезапуска) и ждущий\n * человека. Ждущий ответ (`awaiting_approval`) после решения человека продолжается тем же сообщением — хук позовут\n * второй раз, с новым статусом. Хук может быть вызван повторно (повтор после сбоя, доделка после перезапуска):\n * ключ повтора — `messageId` + `status`. Возвращённые `events` ложатся в конец ответа частями `data-app-event`.\n */\nexport type AfterTurnHandler = (\n result: TurnResult,\n) => Promise<{ events?: AppEvent[] } | void> | { events?: AppEvent[] } | void;\n\nexport const TURN_REFUSAL_MESSAGE_MAX_LENGTH = 300;\nexport const APP_EVENT_TYPE_MAX_LENGTH = 100;\nexport const APP_EVENT_DATA_MAX_BYTES = 16 * 1024;\n\nconst TURN_REFUSAL_CODE = /^[a-z0-9_.-]{1,64}$/;\n\nexport function isTurnRefusalCode(code: unknown): code is string {\n return typeof code === \"string\" && TURN_REFUSAL_CODE.test(code);\n}\n\n/** Отказ по правилам: код проверен (иначе `TypeError`), текст обрезан до 300 знаков. */\nexport function normalizeTurnRefusal(refusal: TurnRefusal): TurnRefusal {\n if (!isTurnRefusalCode(refusal?.code)) throw new TypeError(\"Код отказа — [a-z0-9_.-], от 1 до 64 знаков\");\n if (typeof refusal.message !== \"string\") throw new TypeError(\"Текст отказа — строка\");\n // По знакам, а не по половинкам: эмодзи на границе не рвётся.\n const message = Array.from(refusal.message).slice(0, TURN_REFUSAL_MESSAGE_MAX_LENGTH).join(\"\");\n return { code: refusal.code, message };\n}\n\n/** Приложение отказало в ходе: ход не начат, ничего не записано. */\nexport class TurnRefusedError extends Error {\n readonly code: string;\n\n constructor(refusal: TurnRefusal) {\n const normalized = normalizeTurnRefusal(refusal);\n super(normalized.message);\n this.name = \"TurnRefusedError\";\n this.code = normalized.code;\n }\n\n get refusal(): TurnRefusal {\n return { code: this.code, message: this.message };\n }\n}\n\n/** Событие по правилам или `TypeError`. Отдаёт копию: данные прошли через JSON. */\nexport function validateAppEvent(event: unknown): AppEvent {\n if (event === null || typeof event !== \"object\") throw new TypeError(\"Событие приложения — объект { type, data? }\");\n const { type, data } = event as { type?: unknown; data?: unknown };\n if (typeof type !== \"string\" || type.length < 1 || type.length > APP_EVENT_TYPE_MAX_LENGTH) {\n throw new TypeError(`Тип события приложения — строка от 1 до ${APP_EVENT_TYPE_MAX_LENGTH} знаков`);\n }\n if (data === undefined) return { type };\n let json: string | undefined;\n try {\n json = JSON.stringify(data);\n } catch {\n json = undefined;\n }\n if (json === undefined) throw new TypeError(\"Данные события приложения — значение JSON\");\n if (new TextEncoder().encode(json).length > APP_EVENT_DATA_MAX_BYTES) {\n throw new TypeError(`Данные события приложения больше ${APP_EVENT_DATA_MAX_BYTES} байт`);\n }\n return { type, data: JSON.parse(json) };\n}\n"],"mappings":";AA+KO,IAAM,kCAAkC;AACxC,IAAM,4BAA4B;AAClC,IAAM,2BAA2B,KAAK;AAE7C,IAAM,oBAAoB;AAEnB,SAAS,kBAAkB,MAA+B;AAC/D,SAAO,OAAO,SAAS,YAAY,kBAAkB,KAAK,IAAI;AAChE;AAGO,SAAS,qBAAqB,SAAmC;AACtE,MAAI,CAAC,kBAAkB,SAAS,IAAI,EAAG,OAAM,IAAI,UAAU,iJAA6C;AACxG,MAAI,OAAO,QAAQ,YAAY,SAAU,OAAM,IAAI,UAAU,iHAAuB;AAEpF,QAAM,UAAU,MAAM,KAAK,QAAQ,OAAO,EAAE,MAAM,GAAG,+BAA+B,EAAE,KAAK,EAAE;AAC7F,SAAO,EAAE,MAAM,QAAQ,MAAM,QAAQ;AACvC;AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EAET,YAAY,SAAsB;AAChC,UAAM,aAAa,qBAAqB,OAAO;AAC/C,UAAM,WAAW,OAAO;AACxB,SAAK,OAAO;AACZ,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAEA,IAAI,UAAuB;AACzB,WAAO,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,QAAQ;AAAA,EAClD;AACF;AAGO,SAAS,iBAAiB,OAA0B;AACzD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,OAAM,IAAI,UAAU,qKAA6C;AAClH,QAAM,EAAE,MAAM,KAAK,IAAI;AACvB,MAAI,OAAO,SAAS,YAAY,KAAK,SAAS,KAAK,KAAK,SAAS,2BAA2B;AAC1F,UAAM,IAAI,UAAU,sMAA2C,yBAAyB,uCAAS;AAAA,EACnG;AACA,MAAI,SAAS,OAAW,QAAO,EAAE,KAAK;AACtC,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAW,OAAM,IAAI,UAAU,2MAA2C;AACvF,MAAI,IAAI,YAAY,EAAE,OAAO,IAAI,EAAE,SAAS,0BAA0B;AACpE,UAAM,IAAI,UAAU,qLAAoC,wBAAwB,2BAAO;AAAA,EACzF;AACA,SAAO,EAAE,MAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AACxC;","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  TurnRefusedError,
4
4
  normalizeTurnRefusal,
5
5
  validateAppEvent
6
- } from "./chunk-FOAIG4DY.js";
6
+ } from "./chunk-CVRDB3NM.js";
7
7
 
8
8
  // src/conversation/types.ts
9
9
  var CURRENT_FORMAT_VERSION = 1;
@@ -525,7 +525,10 @@ var MIN_SECRET_LENGTH = 8;
525
525
  var HIDDEN = "[\u0441\u043A\u0440\u044B\u0442\u043E]";
526
526
  var SECRET_FIELD = String.raw`[\w-]*(?:password|passwd|secret|token|api[_-]?key|(?:secret|private|access)[_-]?key)(?![\w-])`;
527
527
  var FIELD_VALUE = String.raw`(?!\d+(?![^&\s"',;]))(?:"[^"]*"|'[^']*'|[^&\s"',;]+)`;
528
+ var SECRET_LIKE = String.raw`(?=[^\s"',;]*\d)(?=[^\s"',;]*[A-Za-z])[^\s"',;]{8,}`;
528
529
  var KNOWN_KEY_PATTERNS = [
530
+ // Маска ключа целиком: провайдер показывает начало и последние знаки ключа.
531
+ /[\w-]*\*{4,}[\w-]*/g,
529
532
  /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
530
533
  /\bsk-[A-Za-z0-9_-]{8,}/g,
531
534
  /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}/g,
@@ -547,7 +550,7 @@ function scrubSecrets(text, knownSecrets = []) {
547
550
  if (secret && secret.length >= MIN_SECRET_LENGTH) scrubbed = scrubbed.split(secret).join(HIDDEN);
548
551
  }
549
552
  for (const pattern of KNOWN_KEY_PATTERNS) scrubbed = scrubbed.replace(pattern, HIDDEN);
550
- return scrubbed.replace(URL_PASSWORD, `$1:${HIDDEN}@`).replace(/\b((?:Proxy-)?Authorization\s*[:=]\s*)(?:(Basic|Bearer|Token|Digest)\s+)?[^\s"',;]+/gi, (_, head, scheme) => `${head}${scheme ? `${scheme} ` : ""}${HIDDEN}`).replace(/\bBearer\s+(?!\[скрыто\])[^\s"',;]+/g, `Bearer ${HIDDEN}`).replace(/([?&;]key=)[^&\s"]+/g, `$1${HIDDEN}`).replace(new RegExp(String.raw`("${SECRET_FIELD}"\s*:\s*)"[^"]*"`, "gi"), `$1"${HIDDEN}"`).replace(
553
+ return scrubbed.replace(URL_PASSWORD, `$1:${HIDDEN}@`).replace(/\b((?:Proxy-)?Authorization\s*[:=]\s*)(?:(Basic|Bearer|Token|Digest)\s+)?[^\s"',;]+/gi, (_, head, scheme) => `${head}${scheme ? `${scheme} ` : ""}${HIDDEN}`).replace(/\b(Bearer\s+)(?!\[скрыто\])[^\s"',;]+/gi, `$1${HIDDEN}`).replace(/([?&;]key=)[^&\s"]+/g, `$1${HIDDEN}`).replace(new RegExp(String.raw`(?<![\w-])(key\s*[:=]\s*)${SECRET_LIKE}`, "gi"), `$1${HIDDEN}`).replace(new RegExp(String.raw`(?<![\w-])((?:token|api[_-]?key|secret|password|passwd|key)\s+)${SECRET_LIKE}`, "gi"), `$1${HIDDEN}`).replace(new RegExp(String.raw`("${SECRET_FIELD}"\s*:\s*)"[^"]*"`, "gi"), `$1"${HIDDEN}"`).replace(
551
554
  new RegExp(String.raw`(?<![\w-])(${SECRET_FIELD}\s*[:=]\s*)(?!"\[скрыто\]")(${FIELD_VALUE})`, "gi"),
552
555
  (_, head, value) => /^["']/.test(value) ? `${head}${value[0]}${HIDDEN}${value[0]}` : `${head}${HIDDEN}`
553
556
  );
@@ -3331,7 +3334,8 @@ function sleep(ms, signal) {
3331
3334
 
3332
3335
  // src/provider-errors/classify-provider-error.ts
3333
3336
  var KEY_SIGNS = /api[ _-]?key|authenticat|unauthori[sz]ed|credential|access[ _-]?token|key (?:is )?(?:invalid|revoked|expired|disabled)/i;
3334
- var FUNDS_SIGNS = /key limit exceeded|insufficient (?:credits|funds|balance)/i;
3337
+ var KEY_LIMIT_SIGNS = /key limit exceeded|openrouter_key_limit|ExceededBudget|budget has been exceeded|(?:key|token) has (?:reached|exceeded) its (?:spend(?:ing)?|budget|usage) limit/i;
3338
+ var FUNDS_SIGNS = /insufficient (?:credits|funds|balance)/i;
3335
3339
  var MODERATION_SIGNS = /flagged|moderation|content[ _-]?policy|safety/i;
3336
3340
  var RATE_LIMIT_SIGNS = /rate.?limit|too many requests|throttl|please wait|ResourceExhausted/i;
3337
3341
  function classifyProviderError(error, provider) {
@@ -3342,6 +3346,7 @@ function classifyProviderError(error, provider) {
3342
3346
  for (const hint of provider?.errorHints ?? []) {
3343
3347
  if (hint.match.test(text)) return hint.kind;
3344
3348
  }
3349
+ if (KEY_LIMIT_SIGNS.test(text)) return "insufficient_funds";
3345
3350
  if (status === 401) return "invalid_key";
3346
3351
  if (status === 403) {
3347
3352
  if (KEY_SIGNS.test(text)) return "invalid_key";
@@ -3359,6 +3364,9 @@ function isAccessRefusal(error) {
3359
3364
  const cause = underlyingError(error);
3360
3365
  return errorStatusCode(cause) === 403 && !MODERATION_SIGNS.test(errorText(cause));
3361
3366
  }
3367
+ function isKeyLimit(error) {
3368
+ return KEY_LIMIT_SIGNS.test(errorText(underlyingError(error)));
3369
+ }
3362
3370
 
3363
3371
  // src/provider-errors/provider-failure-log.ts
3364
3372
  import { APICallError as APICallError3 } from "ai";
@@ -3405,14 +3413,15 @@ function createProviderOutages(options2) {
3405
3413
  return time < outage.pausedUntil || time < (outage.probeUntil ?? 0) ? outage.kind : void 0;
3406
3414
  };
3407
3415
  return {
3408
- report({ providerId, kind, fallbackProviderId, fallbacksPaused }) {
3416
+ report({ providerId, kind, fallbackProviderId, fallbacksPaused, keyLimit }) {
3409
3417
  const time = options2.now();
3410
3418
  outages.set(providerId, { kind, pausedUntil: time + PROVIDER_OUTAGE_PAUSE_MS });
3411
3419
  const last = alertedAt.get(providerId);
3412
3420
  if (last !== void 0 && time - last < PROVIDER_OUTAGE_ALERT_EVERY_MS) return;
3413
3421
  alertedAt.set(providerId, time);
3414
3422
  const route = fallbackProviderId !== void 0 ? `\u0445\u043E\u0434\u044B \u0438\u0434\u0443\u0442 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0439 ${fallbackProviderId}` : `${fallbacksPaused ? "\u0437\u0430\u043F\u0430\u0441\u043D\u044B\u0435 \u0442\u043E\u0436\u0435 \u043D\u0430 \u043F\u0430\u0443\u0437\u0435" : "\u0437\u0430\u043F\u0430\u0441\u043D\u043E\u0433\u043E \u043D\u0435\u0442"}, \u0445\u043E\u0434\u044B \u043F\u0430\u0434\u0430\u044E\u0442 \u0441 \u043E\u0448\u0438\u0431\u043A\u043E\u0439`;
3415
- options2.logError(`[\u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] ${providerId}: ${SAID[kind]} \u2014 ${route}`);
3423
+ const said = keyLimit && kind === "insufficient_funds" ? "\u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D \u043F\u0440\u0435\u0434\u0435\u043B \u0440\u0430\u0441\u0445\u043E\u0434\u043E\u0432 \u043A\u043B\u044E\u0447\u0430" : SAID[kind];
3424
+ options2.logError(`[\u0412\u041D\u0418\u041C\u0410\u041D\u0418\u0415 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440] ${providerId}: ${said} \u2014 ${route}`);
3416
3425
  },
3417
3426
  pausedKind,
3418
3427
  admit(providerId) {
@@ -5320,6 +5329,100 @@ ${summary}
5320
5329
  </answer_progress>`;
5321
5330
  }
5322
5331
 
5332
+ // src/provider-errors/provider-error-details.ts
5333
+ import { APICallError as APICallError4 } from "ai";
5334
+ var MAX_REASON_LENGTH = 300;
5335
+ var MAX_RESET_AHEAD_MS = 2 * 366 * 24 * 60 * 60 * 1e3;
5336
+ function providerErrorDetails(error, context) {
5337
+ const cause = underlyingError(error);
5338
+ const statusCode = errorStatusCode(cause);
5339
+ const { message, metadata } = providerMessageOf(cause);
5340
+ const reason = message === void 0 ? void 0 : shownReason(message, environmentSecrets(context.env, context.catalog));
5341
+ const nowMs = (context.now ?? Date.now)();
5342
+ const found = resetFromMetadata(metadata, nowMs) ?? (message === void 0 ? void 0 : resetFromText(message));
5343
+ const resetsAt = found !== void 0 && found > nowMs && found - nowMs <= MAX_RESET_AHEAD_MS ? new Date(found).toISOString() : void 0;
5344
+ return {
5345
+ providerId: context.providerId,
5346
+ modelId: context.modelId,
5347
+ ...statusCode !== void 0 && { statusCode },
5348
+ ...reason !== void 0 && reason !== "" && { reason },
5349
+ ...resetsAt !== void 0 && { resetsAt }
5350
+ };
5351
+ }
5352
+ function providerMessageOf(error) {
5353
+ if (APICallError4.isInstance(error)) {
5354
+ const fromBody = typeof error.responseBody === "string" ? fromJson(error.responseBody) : void 0;
5355
+ if (fromBody?.message !== void 0) return fromBody;
5356
+ const raw = [error.responseBody, error.message].find(isText);
5357
+ return { ...raw !== void 0 && { message: raw }, metadata: fromBody?.metadata };
5358
+ }
5359
+ if (typeof error === "string") return isText(error) ? { message: error } : {};
5360
+ return fromObject(error) ?? {};
5361
+ }
5362
+ function fromJson(body) {
5363
+ try {
5364
+ return fromObject(JSON.parse(body));
5365
+ } catch {
5366
+ return void 0;
5367
+ }
5368
+ }
5369
+ function fromObject(value) {
5370
+ if (typeof value !== "object" || value === null) return void 0;
5371
+ const fields = value;
5372
+ if (typeof fields.error === "object" && fields.error !== null) {
5373
+ const nested = fields.error;
5374
+ if (isText(nested.message)) return { message: nested.message, metadata: nested.metadata };
5375
+ }
5376
+ if (isText(fields.error)) return { message: fields.error, metadata: fields.metadata };
5377
+ if (isText(fields.message)) return { message: fields.message, metadata: fields.metadata };
5378
+ return void 0;
5379
+ }
5380
+ var isText = (value) => typeof value === "string" && value.trim() !== "";
5381
+ var URL_WITH_QUERY = /(https?:\/\/[^\s?#"'<>]+)[?#][^\s"'<>]*/g;
5382
+ var ACCOUNT_IDS = [/\borg-[A-Za-z0-9]{6,}/g, /\bproj_[A-Za-z0-9]{6,}/g, /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g];
5383
+ function shownReason(message, knownSecrets) {
5384
+ const oneLine2 = message.replace(/\s*[\r\n]+\s*/g, " ").trim();
5385
+ let shown = scrubSecrets(oneLine2.replace(URL_WITH_QUERY, "$1"), knownSecrets);
5386
+ for (const pattern of ACCOUNT_IDS) shown = shown.replace(pattern, "[\u0441\u043A\u0440\u044B\u0442\u043E]");
5387
+ return cutAtCharBoundary(shown, MAX_REASON_LENGTH);
5388
+ }
5389
+ var RESET_FIELD = /reset/i;
5390
+ var RELATIVE_FIELD = /sec|after|in$|_in_|ms$|_ms|Ms$/i;
5391
+ var MS_FIELD = /ms$|_ms|Ms$/;
5392
+ var MIN_UNIX_SECONDS = 1e9;
5393
+ function resetFromMetadata(metadata, nowMs) {
5394
+ if (typeof metadata !== "object" || metadata === null) return void 0;
5395
+ for (const [key, value] of Object.entries(metadata)) {
5396
+ if (!RESET_FIELD.test(key)) continue;
5397
+ const parsed = typeof value === "number" ? fromNumber(key, value, nowMs) : typeof value === "string" ? fromDateText(value) : void 0;
5398
+ if (parsed !== void 0) return parsed;
5399
+ }
5400
+ return void 0;
5401
+ }
5402
+ function fromNumber(key, value, nowMs) {
5403
+ if (!Number.isFinite(value) || value <= 0) return void 0;
5404
+ if (RELATIVE_FIELD.test(key) || value < MIN_UNIX_SECONDS) return nowMs + (MS_FIELD.test(key) ? value : value * 1e3);
5405
+ return value < 1e12 ? value * 1e3 : value;
5406
+ }
5407
+ var LIMIT_MESSAGE_START = /^[^'"«“]{0,80}?\b(?:limit|budget|quota)\b/i;
5408
+ var QUOTED = /'[^']*'|"[^"]*"|«[^»]*»|“[^”]*”/g;
5409
+ var RESET_IN_TEXT = /\bresets?\s+(?:at\s+|on\s+)?(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?)/i;
5410
+ function resetFromText(message) {
5411
+ if (!LIMIT_MESSAGE_START.test(message)) return void 0;
5412
+ const found = RESET_IN_TEXT.exec(message.replace(QUOTED, " "))?.[1];
5413
+ return found === void 0 ? void 0 : fromDateText(found);
5414
+ }
5415
+ function fromDateText(text) {
5416
+ const trimmed = text.trim();
5417
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(Z|[+-]\d{2}:?\d{2})?)?$/i.exec(trimmed);
5418
+ if (!match) return void 0;
5419
+ const [, year, month, day, time, zone] = match;
5420
+ const calendarDay = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
5421
+ if (calendarDay.getUTCMonth() !== Number(month) - 1 || calendarDay.getUTCDate() !== Number(day)) return void 0;
5422
+ const parsed = Date.parse(`${year}-${month}-${day}${time === void 0 ? "" : `T${time}${zone ?? "Z"}`}`);
5423
+ return Number.isNaN(parsed) ? void 0 : parsed;
5424
+ }
5425
+
5323
5426
  // src/agent-run/answer-saver.ts
5324
5427
  import { readUIMessageStream } from "ai";
5325
5428
 
@@ -6602,6 +6705,14 @@ async function runTurn(turnInput, emitRaw) {
6602
6705
  let errors = [];
6603
6706
  let terminal = [];
6604
6707
  let errorKind;
6708
+ let errorDetails;
6709
+ const detailsOf2 = (error2, failedProviderId) => providerErrorDetails(error2, {
6710
+ providerId: failedProviderId,
6711
+ modelId,
6712
+ env: input.env,
6713
+ catalog: input.catalog,
6714
+ now: () => (input.now ?? (() => /* @__PURE__ */ new Date()))().getTime()
6715
+ });
6605
6716
  let providerId = input.providerId;
6606
6717
  let answeredProviderId = input.providerId;
6607
6718
  let viewer;
@@ -6644,6 +6755,7 @@ async function runTurn(turnInput, emitRaw) {
6644
6755
  });
6645
6756
  const fallbackProviderIds = allFallbackIds.filter((candidate) => outages.pausedKind(candidate) === void 0);
6646
6757
  let firstOutage;
6758
+ let firstOutageDetails;
6647
6759
  const primaryPaused = outages.admit(input.providerId);
6648
6760
  if (primaryPaused !== void 0 && fallbackProviderIds[0] !== void 0) {
6649
6761
  providerId = fallbackProviderIds[0];
@@ -6740,7 +6852,9 @@ async function runTurn(turnInput, emitRaw) {
6740
6852
  } catch (error2) {
6741
6853
  if (error2 instanceof MissingProviderKeyError) logError(error2.message);
6742
6854
  if (!input.abortSignal.aborted) {
6743
- errorKind = errorKindForPerson(classify(error2), firstOutage);
6855
+ const kind = classify(error2);
6856
+ errorKind = errorKindForPerson(kind, firstOutage);
6857
+ errorDetails = errorKind === firstOutage ? firstOutageDetails : detailsOf2(error2, providerId);
6744
6858
  errors = [{ type: "error", errorText: errorKind }];
6745
6859
  }
6746
6860
  break;
@@ -6784,10 +6898,14 @@ async function runTurn(turnInput, emitRaw) {
6784
6898
  let next = stepsDone < maxSteps ? decideNextAttempt(result, nextState()) : { action: "fail" };
6785
6899
  const outage = isProviderOutage(result.kind) ? result.kind : void 0;
6786
6900
  if (outage) {
6787
- if (providerId === input.providerId) firstOutage ??= outage;
6901
+ if (providerId === input.providerId && firstOutage === void 0) {
6902
+ firstOutage = outage;
6903
+ firstOutageDetails = detailsOf2(result.error, providerId);
6904
+ }
6788
6905
  outages.report({
6789
6906
  providerId,
6790
6907
  kind: outage,
6908
+ ...isKeyLimit(result.error) && { keyLimit: true },
6791
6909
  ...next.action === "fallback" ? { fallbackProviderId: next.providerId } : { fallbacksPaused: allFallbackIds.some((candidate) => candidate !== providerId && outages.pausedKind(candidate) !== void 0) }
6792
6910
  });
6793
6911
  }
@@ -6872,6 +6990,7 @@ async function runTurn(turnInput, emitRaw) {
6872
6990
  continue;
6873
6991
  }
6874
6992
  errorKind = errorKindForPerson(result.kind, firstOutage);
6993
+ errorDetails = errorKind === firstOutage ? firstOutageDetails : detailsOf2(result.error, providerId);
6875
6994
  errors = errorKind === result.kind ? attempt.errors : [{ type: "error", errorText: errorKind }];
6876
6995
  terminal = attempt.terminal;
6877
6996
  logError(
@@ -6909,6 +7028,7 @@ async function runTurn(turnInput, emitRaw) {
6909
7028
  if (!input.abortSignal.aborted) {
6910
7029
  if (error2 instanceof AgentViewerError) logError(`[agent.viewer] \u0447\u0430\u0442=${input.chatId} \u0445\u043E\u0434 \u043D\u0435 \u043D\u0430\u0447\u0430\u0442: ${error2.message}`);
6911
7030
  errorKind = error2 instanceof AgentViewerError ? "other" : classifyProviderError(error2, void 0);
7031
+ errorDetails = void 0;
6912
7032
  errors = [{ type: "error", errorText: errorKind }];
6913
7033
  }
6914
7034
  }
@@ -6979,7 +7099,7 @@ async function runTurn(turnInput, emitRaw) {
6979
7099
  for (const chunk of heldText?.release() ?? []) emitShown(chunk);
6980
7100
  turnUsage.closeAttempt();
6981
7101
  const usage = messageUsage();
6982
- const error = status === "error" ? { kind: errorKind ?? "other" } : null;
7102
+ const error = status === "error" ? { kind: errorKind ?? "other", ...errorDetails && { details: errorDetails } } : null;
6983
7103
  const answeredByBackup = answeredProviderId !== input.providerId && { providerId: answeredProviderId };
6984
7104
  let finished = false;
6985
7105
  if (!takeover.signal.aborted) {
@@ -7780,6 +7900,7 @@ async function* helperSteps(input) {
7780
7900
  outages.report({
7781
7901
  providerId,
7782
7902
  kind: outage,
7903
+ ...isKeyLimit(error) && { keyLimit: true },
7783
7904
  ...next.action === "fallback" ? { fallbackProviderId: next.providerId } : { fallbacksPaused: allFallbackIds.some((candidate) => candidate !== providerId && outages.pausedKind(candidate) !== void 0) }
7784
7905
  });
7785
7906
  }
@@ -9411,10 +9532,12 @@ var CHAT_CAPABILITIES = [
9411
9532
  "reasoning",
9412
9533
  "toolDetails",
9413
9534
  "steer",
9414
- "modelDetails"
9535
+ "modelDetails",
9536
+ "errorDetails"
9415
9537
  ];
9538
+ var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails"]);
9416
9539
  function resolveChatCapabilities(given) {
9417
- const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, true]));
9540
+ const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, !OFF_BY_DEFAULT.has(capability)]));
9418
9541
  if (given === void 0 || given === null) return resolved;
9419
9542
  if (typeof given !== "object") throw new TypeError("\u041F\u0440\u0430\u0432\u0430 \u0447\u0430\u0442\u0430 \u2014 \u043E\u0431\u044A\u0435\u043A\u0442 { \u043F\u0440\u0430\u0432\u043E: boolean }");
9420
9543
  const unknown = Object.keys(given).find((key) => !CHAT_CAPABILITIES.includes(key));
@@ -9425,7 +9548,10 @@ function resolveChatCapabilities(given) {
9425
9548
  if (typeof value !== "boolean") throw new TypeError(`\u041F\u0440\u0430\u0432\u043E \xAB${capability}\xBB \u2014 true \u0438\u043B\u0438 false`);
9426
9549
  resolved[capability] = value;
9427
9550
  }
9428
- if (!resolved.modelDetails) resolved.modelChoice = false;
9551
+ if (!resolved.modelDetails) {
9552
+ resolved.modelChoice = false;
9553
+ resolved.errorDetails = false;
9554
+ }
9429
9555
  return resolved;
9430
9556
  }
9431
9557
  var ActionForbiddenError = class extends Error {
@@ -11457,6 +11583,12 @@ var SHOWS_EVERYTHING = {
11457
11583
  };
11458
11584
  var MODEL_METADATA_KEYS = ["modelId", "providerId", "usage"];
11459
11585
  var MODEL_CHAT_KEYS = ["ownerId", "modelId", "providerId", "reasoning"];
11586
+ var withoutErrorDetails = (metadata) => {
11587
+ const error = metadata.error;
11588
+ if (typeof error !== "object" || error === null || !("details" in error)) return metadata;
11589
+ const { details: _details, ...rest } = error;
11590
+ return { ...metadata, error: rest };
11591
+ };
11460
11592
  var without = (carrier, keys) => {
11461
11593
  if (!keys.some((key) => key in carrier)) return carrier;
11462
11594
  const rest = { ...carrier };
@@ -11494,10 +11626,15 @@ function clientVisibilityOf(capabilities, sdkScreenTool) {
11494
11626
  const hidesReasoning = !capabilities.reasoning;
11495
11627
  const hidesTools = !capabilities.toolDetails;
11496
11628
  const hidesModel = capabilities.modelDetails === false;
11497
- if (!hidesReasoning && !hidesTools && !hidesModel) return SHOWS_EVERYTHING;
11629
+ const hidesErrorDetails = hidesModel || capabilities.errorDetails !== true;
11630
+ if (!hidesReasoning && !hidesTools && !hidesModel && !hidesErrorDetails) return SHOWS_EVERYTHING;
11498
11631
  const hidesCall = (toolName, call) => hidesTools && !sdkScreenTool(toolName) && !markedForScreen(call);
11499
11632
  const hiddenMetadataKeys = [...hidesReasoning ? ["reasoningDurationsMs"] : [], ...hidesModel ? MODEL_METADATA_KEYS : []];
11500
- const visibleMetadata = (metadata) => typeof metadata === "object" && metadata !== null ? without(metadata, hiddenMetadataKeys) : metadata;
11633
+ const visibleMetadata = (metadata) => {
11634
+ if (typeof metadata !== "object" || metadata === null) return metadata;
11635
+ const shown = without(metadata, hiddenMetadataKeys);
11636
+ return hidesErrorDetails ? withoutErrorDetails(shown) : shown;
11637
+ };
11501
11638
  const withoutProviderData2 = (carrier) => hidesModel ? without(carrier, PROVIDER_METADATA_KEYS) : hidesReasoning ? withoutReasoningDetails(carrier) : carrier;
11502
11639
  return {
11503
11640
  showsEverything: false,
@@ -12208,10 +12345,18 @@ function createChatService(input) {
12208
12345
  scope.capabilities ??= askCapabilities(userId, chat);
12209
12346
  return scope.capabilities;
12210
12347
  }
12348
+ let warnedErrorDetailsWithoutModel = false;
12349
+ function warnErrorDetailsWithoutModel(given) {
12350
+ if (warnedErrorDetailsWithoutModel || given?.errorDetails !== true || given.modelDetails !== false) return;
12351
+ warnedErrorDetailsWithoutModel = true;
12352
+ logError("chatCapabilities: errorDetails: true \u043F\u0440\u0438 modelDetails: false \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u2014 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0441\u0431\u043E\u044F \u043D\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u043C\u043E\u0434\u0435\u043B\u044C \u0438 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430.");
12353
+ }
12211
12354
  async function askCapabilities(userId, chat) {
12212
12355
  if (!options2.chatCapabilities) return resolveChatCapabilities(void 0);
12213
12356
  try {
12214
- return resolveChatCapabilities(await options2.chatCapabilities({ userId, chat }));
12357
+ const given = await options2.chatCapabilities({ userId, chat });
12358
+ warnErrorDetailsWithoutModel(given);
12359
+ return resolveChatCapabilities(given);
12215
12360
  } catch (error) {
12216
12361
  logError(`chatCapabilities \u0443\u043F\u0430\u043B \u0438\u043B\u0438 \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u0430 (\u0447\u0430\u0442 ${chat.id}): ${error instanceof Error ? error.message : String(error)}`);
12217
12362
  throw error;
@@ -14276,4 +14421,4 @@ export {
14276
14421
  InvalidWidgetAnswerError,
14277
14422
  createChatService
14278
14423
  };
14279
- //# sourceMappingURL=chunk-XQ5FJ2VT.js.map
14424
+ //# sourceMappingURL=chunk-EWEEYX6U.js.map