diff --git a/docs/superpowers/specs/2026-10-15-session-cost-bar-design.md b/docs/superpowers/specs/2026-10-15-session-cost-bar-design.md index d7fad79..806271f 100644 --- a/docs/superpowers/specs/2026-10-15-session-cost-bar-design.md +++ b/docs/superpowers/specs/2026-10-15-session-cost-bar-design.md @@ -80,10 +80,24 @@ this.fetch = options.fetch ?? Shims.getDefaultFetch(); // openai/client.js:160 ```js const [дляНас, потребителю] = response.body.tee(); наблюдать(дляНас); // разбираем SSE/JSON в фоне -return new Response(потребителю, { status, statusText, headers: response.headers }); +const rebuilt = new Response(потребителю, { status, statusText, headers: response.headers }); +Object.defineProperty(rebuilt, 'url', { value: response.url, configurable: true }); +return rebuilt; ``` -`tee()` — штатный примитив разделения потока: обе ветви текут инкрементально, ничего не буферизуется целиком, харнесс получает байты без задержки. Из своей ветви обёртка достаёт `usage.cost` и `id`; для не-потоковых ответов разбирает JSON так же. +**Это проверено на живом потоке routerai** (`.recon/spike-tee.mjs`), а не выведено умозрительно: + +- потребитель получил 9041 байт за 19 чтений, первый чанк через **1 мс**, последний через 1643 мс — поток течёт инкрементально, задержки нет; +- наша ветвь увидела 30 SSE-чанков, достала `id` генерации и `cost = 0.01082233360065`; +- `content-type: text/event-stream`, `content-encoding` и `content-length` пусты, поэтому пересборка заголовков на потоке ничего не портит. + +**Дефект, который нашёлся и уже закрыт:** у пересобранного `Response` поле `url` пустое, а OpenAI SDK его читает (`response.url` в логах повторов, `openai/client.js:432,446,458`). Присваивание напрямую падает: + +``` +Cannot assign to read only property 'url' of object '#<_Response>' +``` + +Работает только `Object.defineProperty` — проверено в `.recon/spike-url.mjs`. Без этой строки SDK, скорее всего, не сломался бы (поле используется только в отладочном выводе), но полагаться на это не стоит. Результат кладётся в реестр **ключом «id генерации → стоимость»**. @@ -169,7 +183,7 @@ return new Response(потребителю, { status, statusText, headers: respo **3. `tee()` и backpressure.** Обе ветви надо читать. Свою ветвь мы читаем сразу и до конца; если этого не делать, буфер потребителя будет расти. Требование к реализации: свою ветвь никогда не оставлять невычитанной. -**4. Конструирование нового `Response`.** `new Response(potок, {status, headers})` теряет поле `url` исходного ответа. Надо проверить, не полагается ли на него SDK; если полагается — переносить вручную. +**4. Конструирование нового `Response`.** ~~`new Response(potок, {status, headers})` теряет поле `url` исходного ответа.~~ **Проверено и закрыто:** `url` действительно теряется, SDK его читает, поэтому поле восстанавливается через `Object.defineProperty`. Прямое присваивание не работает (read-only), подробности и команда проверки — в разделе про обёртку `fetch`. ## Технические решения @@ -187,7 +201,6 @@ return new Response(потребителю, { status, statusText, headers: respo - Порядок загрузки, гарантирующий, что обёртка встанет до первого вызова модели. - Точный отбор запросов по URL и разбор SSE без потери чанков на границах буфера. -- Сохранение `url` (или подтверждение, что оно не нужно) при конструировании нового `Response`. - Структура реестров и где их хранить; как переживать перезапуск. - Разрешение гонки «стоимость и пара пришли в разном порядке»; политика для так и не собранных пар. - Выбор между опросом и push для доставки числа. @@ -203,6 +216,8 @@ return new Response(потребителю, { status, statusText, headers: respo | `probe-routerai.mjs` | сырой ответ routerai: заголовки и тело с `usage.cost` | | `probe-stream.mjs` | то же для потокового ответа | | `probe-immediate.mjs` | доступность `generation?id=…` сразу после ответа | +| `spike-tee.mjs` | **спайк механизма:** `tee()` на живом потоке — инкрементальность, извлечение `cost`, потеря `url` | +| `spike-url.mjs` | восстановление `url` на пересобранном `Response` | | `probe-generation.mjs` | `generation?id=…` и цены endpoint'ов с `variable_pricings` | | `audit-usage.mjs` | что лежит в durable-логах сессий | | `asar.mjs`, `list.mjs`, `extract.mjs`, `grep2.mjs` | чтение `app.asar`: список, извлечение, поиск |