diff --git a/devlog/_plan/261003_train_recovery_d/010_late_review.md b/devlog/_plan/261003_train_recovery_d/010_late_review.md
new file mode 100644
index 00000000000..eff3beb347a
--- /dev/null
+++ b/devlog/_plan/261003_train_recovery_d/010_late_review.md
@@ -0,0 +1,56 @@
+# Lane D late-review follow-up
+
+This follow-up starts from landed dev `3ada270f3a0d955ad02b6a4f8f74b0412906bcb1`.
+The earlier verification record describes its inspected scenarios, not a claim
+that late review could find no further defect. Existing Doctor fixes remain intact.
+
+## Comment dispositions
+
+| Comment | Disposition | Evidence and change |
+| --- | --- | --- |
+| 4172062340 | Accepted | `gui/src/pages/integrations/FileIntegrationPage.tsx` reuses the omission-aware review path. No draft sends no defaults map; an explicit empty map still clears defaults. The GUI regression exercises Update and Save / review through preview and commit. |
+| 4172062382 | Accepted | `src/clients/config-export/droid.ts` filters inherited defaults against the current normalized model's declared efforts. Refresh removes unsupported headers and preserves compatible peers. Regressions cover incompatible, empty and unknown ladders plus explicit clearing. Ownership checks are unchanged. |
+| 4172062352 | Accepted | `gui/src/pages/Usage.tsx` shares a finite-number guard across summary, tables, tooltips and assistive text. Malformed rates are unavailable. Regressions cover null, strings and nonfinite values. |
+| 4172062362 | Accepted | `gui/src/use-model-visibility.ts` treats JSON integration details as optional after a successful write. Empty/non-JSON success still reconciles against the catalog; transport failures retain their existing error path. Regressions cover 204 and text responses. |
+| 4172062394 | Rebutted | `structure/dashboard-and-usage.md`, the English dashboard guide, `usage.throughput.title` and `src/usage/summary.ts` explicitly define qualifying **attempts**, with one sample for a legacy row without attempts. Changing this to distinct requests would change the accepted contract. No aggregation semantics changed. |
+| 4172062396 | Accepted | `tests/usage/usage-throughput.test.ts` identifies the second request's model and asserts two model rows at 10 and 15 tok/s with one sample each, alongside the weighted provider/summary rate. |
+
+## Focused verification
+
+From `gui/`:
+
+```sh
+bun test --isolate ./tests/models-visibility-queue.test.tsx ./tests/usage-custom-range.test.tsx
+bun test --isolate ./tests/integrations-surfaces.test.tsx
+```
+
+The first command reproduced seven failures before the display/response fixes
+(39 passed); after repair it passed **46/0**. The second passed **58/0**.
+Droid regressions were added before its source edit but were not run red;
+no Droid red-green claim is made.
+
+From the repository root:
+
+```sh
+bun test ./tests/usage/usage-throughput.test.ts
+bun test ./tests/clients/droid-managed-reasoning-defaults.test.ts ./tests/server/management-droid-reasoning-defaults.test.ts ./tests/responses/droid-reasoning-defaults.test.ts
+```
+
+These passed **5/0** and **44/0**, respectively. All batches were sequential;
+none reported skips. Structure, file-size ratchet and privacy checks passed.
+
+Three fixture-browser regressions exercised malformed string throughput across
+all three displays and a 204 visibility write through successful reconciliation.
+The Droid no-draft review also opened confirmation with the defaults map omitted.
+All passed; the throughput/visibility run had no uncaught page errors. Screenshots were inspected and
+remain outside tracked source. An initial browser assertion also selected cache
+cells; narrowing its selector to throughput corrected the harness, not the product.
+
+Broad typechecks, builds, full suites, Windows/native and real-client acceptance
+remain unrun locally. Final PR/CI and production-readiness judgment belong to the
+coordinator. No push, PR, merge, installation or release is part of this lane.
+
+Independent inherited review of the final source/tests/docs returned **PASS**,
+with no blocking findings; it supported all five accepted fixes/test improvements
+and the attempt-count contract rebuttal. This is a technical review, not final
+production-readiness approval.
diff --git a/docs-site/src/content/docs/guides/integrations.md b/docs-site/src/content/docs/guides/integrations.md
index 03462e50a7a..6c009571f78 100644
--- a/docs-site/src/content/docs/guides/integrations.md
+++ b/docs-site/src/content/docs/guides/integrations.md
@@ -667,10 +667,12 @@ against its own effort list, so an incompatible first target does not remove it
from a compatible fallback.
A saved default that the routed model no longer supports is ignored for requests.
-Clear it in the panel, then review and confirm the change to remove it from Droid.
+If the connected model's declared effort list no longer includes the saved value,
+it is omitted from the panel's defaults and removed from the managed row on refresh.
+Reviewing without editing lets OpenCodex preserve the remaining supported defaults.
Refresh preserves defaults while the exact `provider/model` selector remains
-connected. Renaming a provider, model, or combo alias replaces that managed row
+connected and declares the saved effort. Renaming a provider, model, or combo alias replaces that managed row
and clears its default; choose a default for the renamed row again. Disable
removes the defaults with the managed model rows, and Undo restores the saved
rows and their defaults together.
diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md
index d94e830e2a5..3adf5b7a12d 100644
--- a/docs-site/src/content/docs/guides/web-dashboard.md
+++ b/docs-site/src/content/docs/guides/web-dashboard.md
@@ -181,7 +181,7 @@ For a custom usage interval, the server must confirm the exact requested start a
If an older running proxy does not support those bounds, the dashboard and CLI reject its report;
upgrade and restart that proxy before retrying. Resetting a manual model price affects only that
model, preserving other rates saved independently.
-The **Usage** summary and Models/Providers tables show end-to-end output throughput from usage history for the selected range and filters: summed output tokens divided by summed wall-clock seconds, not an average of individual rates. The sample count is measured attempts (or requests for legacy rows without attempts); samples lacking positive finite output tokens or duration are excluded. An em dash means no sample qualified. This includes pre-decode waiting and is not estimated decode speed.
+The **Usage** summary and Models/Providers tables show end-to-end output throughput from usage history for the selected range and filters: summed output tokens divided by summed wall-clock seconds, not an average of individual rates. The sample count is measured attempts (or requests for legacy rows without attempts); samples lacking positive finite output tokens or duration are excluded. An em dash means no sample qualified or the server returned an unusable rate. This includes pre-decode waiting and is not estimated decode speed.
The **Usage** Models and Providers tables show the estimated priced portion for each row. Requests
without a matching price or usable usage are counted as excluded beside that amount when the proxy
diff --git a/docs-site/src/content/docs/ko/guides/integrations.md b/docs-site/src/content/docs/ko/guides/integrations.md
index 5f27b57b392..aa3e9b8d4d4 100644
--- a/docs-site/src/content/docs/ko/guides/integrations.md
+++ b/docs-site/src/content/docs/ko/guides/integrations.md
@@ -273,10 +273,11 @@ GitHub Copilot 데스크톱 앱에서 opencodex를 OpenAI 호환 모델 프로
확인하므로, 첫 대상이 지원하지 않아도 지원하는 다음 대상으로 전달됩니다.
저장된 기본값을 라우팅된 모델이 더 이상 지원하지 않으면 요청에는 적용하지
-않습니다. Droid 설정에서도 제거하려면 패널에서 지운 뒤 변경 내용을 검토하고
-확인하세요.
+않습니다. 현재 모델의 effort 목록에서 빠진 값은 패널의 기본값에서도 제외되며,
+새로고침할 때 관리 행에서 제거됩니다. 편집 없이 변경 내용을 검토하면 여전히
+지원되는 기본값은 유지됩니다.
-새로고침은 정확히 같은 `provider/model` 선택자가 계속 연결된 동안 기본값을
+새로고침은 정확히 같은 `provider/model` 선택자가 계속 연결되고 저장된 effort를 지원하는 동안 기본값을
유지합니다. 프로바이더, 모델, 콤보 별칭의 이름을 바꾸면 관리 행이 교체되고
기본값이 지워지므로 새 행에서 다시 선택하세요. 연동을 해제하면 관리되는 모델
행과 기본값이 함께 제거되며, 되돌리기는 저장된 행과 기본값을 함께 복원합니다.
diff --git a/docs-site/src/content/docs/reference/cli/providers-accounts.md b/docs-site/src/content/docs/reference/cli/providers-accounts.md
index 947bf9080e5..a491b1c350f 100644
--- a/docs-site/src/content/docs/reference/cli/providers-accounts.md
+++ b/docs-site/src/content/docs/reference/cli/providers-accounts.md
@@ -121,7 +121,7 @@ reauthentication flow above. A second `401` does not start another refresh/retry
A `403` asking to verify the account quarantines that credential as `needs-reauth(verify)`;
when account failover is enabled, the request can retry on another eligible account. Complete
Google's account verification, then run `ocx login google-antigravity`. Silent token refresh
-does not clear this verification requirement. If OpenCodex cannot save the quarantine, this adapter exchange preserves the original `403` without another recovery send; the account has not been durably quarantined. An enclosing combo or policy route can still apply its existing fallback rules.
+does not clear this verification requirement. If OpenCodex cannot save the quarantine, this adapter exchange preserves the original `403` without another recovery send; the account has not been durably quarantined. If a sibling request cannot be built or admitted for sending, the exchange delivers the original `403` through normal error formatting. An enclosing combo or policy route can still apply its existing fallback rules.
A proxy that is already running picks up the new credential without a restart: the CLI asks it to
reload that one provider from disk, and the request carries no credential of its own. If the
diff --git a/gui/src/pages/Usage.tsx b/gui/src/pages/Usage.tsx
index e36ab5215b8..2cdf673c5ca 100644
--- a/gui/src/pages/Usage.tsx
+++ b/gui/src/pages/Usage.tsx
@@ -211,8 +211,12 @@ function cacheHitRateTitle(model: UsageModel, locale: Locale, t: TFn): string |
});
}
+function hasThroughput(value: unknown): value is number {
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
+}
+
function throughputTitle(row: { throughputTokensPerSec?: number; throughputSamples?: number }, t: TFn): string {
- return Number.isFinite(row.throughputTokensPerSec)
+ return hasThroughput(row.throughputTokensPerSec)
? t("usage.throughput.title", { samples: row.throughputSamples ?? 0 })
: t("usage.throughput.unmeasured");
}
@@ -498,7 +502,7 @@ function UsageSummaryCards({
{t("usage.col.tokPerSec")}
- {summary.throughputTokensPerSec?.toFixed(1) ?? t("usage.unavailable")}
+ {hasThroughput(summary.throughputTokensPerSec) ? summary.throughputTokensPerSec.toFixed(1) : t("usage.unavailable")}
{throughputTitle(summary, t)}
{summary.estimatedCostUsd !== undefined && (
@@ -811,9 +815,9 @@ function UsageModelsTable({
{formatTokens(model.outputTokens, locale)} |
- {typeof model.throughputTokensPerSec === "number" ? `${model.throughputTokensPerSec.toFixed(1)} tok/s` : unavailable}
+ {hasThroughput(model.throughputTokensPerSec) ? `${model.throughputTokensPerSec.toFixed(1)} tok/s` : unavailable}
- {typeof model.throughputTokensPerSec === "number" && {throughputNote}}
+ {hasThroughput(model.throughputTokensPerSec) && {throughputNote}}
|
{formatOptionalTokens(model.cacheReadInputTokens ?? model.cachedInputTokens, locale, unavailable)} |
{formatOptionalTokens(model.cacheCreationInputTokens, locale, unavailable)} |
@@ -896,9 +900,9 @@ function UsageProvidersTable({
{formatTokens(provider.totalTokens, locale)} |
- {typeof provider.throughputTokensPerSec === "number" ? `${provider.throughputTokensPerSec.toFixed(1)} tok/s` : unavailable}
+ {hasThroughput(provider.throughputTokensPerSec) ? `${provider.throughputTokensPerSec.toFixed(1)} tok/s` : unavailable}
- {typeof provider.throughputTokensPerSec === "number" && {throughputTitle(provider, t)}}
+ {hasThroughput(provider.throughputTokensPerSec) && {throughputTitle(provider, t)}}
|
|
|
diff --git a/gui/src/pages/integrations/FileIntegrationPage.tsx b/gui/src/pages/integrations/FileIntegrationPage.tsx
index 6172b5b1f6d..94923fb7bca 100644
--- a/gui/src/pages/integrations/FileIntegrationPage.tsx
+++ b/gui/src/pages/integrations/FileIntegrationPage.tsx
@@ -351,9 +351,7 @@ function FileIntegrationControls({
: status.droidReasoning.defaults}
disabled={pending || plannedMutation !== null}
onChange={values => setDroidReasoningDraft({ scopeKey, values })}
- onReview={() => void requestMutation("apply", (droidReasoningDraft?.scopeKey === scopeKey
- ? droidReasoningDraft.values
- : status.droidReasoning?.defaults) ?? {})}
+ onReview={() => void requestMutation("apply")}
/>
)}
diff --git a/gui/src/use-model-visibility.ts b/gui/src/use-model-visibility.ts
index 22325683c87..9653c3ec79a 100644
--- a/gui/src/use-model-visibility.ts
+++ b/gui/src/use-model-visibility.ts
@@ -57,7 +57,9 @@ export function useModelVisibility(apiBase: string, options: Options) {
if (current.active) {
if (!response.ok) error = "models.saveFailed";
else {
- const body: unknown = await response.json();
+ // Integration refresh details are optional; a successful empty/legacy response
+ // still reconciles visibility through the authoritative catalog read below.
+ const body: unknown = await response.json().catch(() => undefined);
if (current.active) callbacks.current.onResponse(body);
}
}
diff --git a/gui/tests/integrations-surfaces.test.tsx b/gui/tests/integrations-surfaces.test.tsx
index 72e3fbf9963..9a872b75d9a 100644
--- a/gui/tests/integrations-surfaces.test.tsx
+++ b/gui/tests/integrations-surfaces.test.tsx
@@ -350,7 +350,7 @@ test("Droid reasoning defaults use one frozen snapshot for review and commit", a
expect(container.querySelector('[role="combobox"]')?.textContent).toContain("low");
});
-test("Droid refresh omits unsupported saved defaults while an explicit edit stays frozen through confirm", async () => {
+test.each(["Update", "Save / review changes"])("Droid %s omits unsupported saved defaults while an explicit edit stays frozen through confirm", async (action) => {
stateResponse = () => json(status({
clientId: "droid",
state: "stale",
@@ -362,14 +362,15 @@ test("Droid refresh omits unsupported saved defaults while an explicit edit stay
}));
await mountClient(true, "droid");
- await act(async () => { buttonByText("Update")!.click(); });
+ await act(async () => { buttonByText(action)!.click(); });
await act(async () => { await new Promise(resolve => testWindow.setTimeout(resolve, 20)); });
const refreshPreview = requests.find(request => request.method === "POST" && request.url.endsWith("/api/client-integrations/preview"));
expect(refreshPreview?.body).not.toHaveProperty("droidReasoningDefaults");
- const dialog = container.querySelector("dialog[open]")!;
- const close = Array.from(dialog.querySelectorAll("button")).find(button => button.textContent?.trim() === "Close") as HTMLButtonElement;
- await act(async () => { close.click(); });
+ await confirmDialog("Apply");
+ const unchangedMutation = requests.find(request => request.method === "PUT" && request.url.endsWith("/api/client-integrations/droid"));
+ expect(unchangedMutation).toBeDefined();
+ expect(unchangedMutation?.body).not.toHaveProperty("droidReasoningDefaults");
const selector = container.querySelector('[role="combobox"]')!;
await act(async () => { selector.click(); });
@@ -382,10 +383,27 @@ test("Droid refresh omits unsupported saved defaults while an explicit edit stay
expect(previews[1]?.body).toMatchObject({ droidReasoningDefaults: { "openai/gpt-test": "low" } });
expect(container.querySelector('[role="combobox"]')?.disabled).toBe(true);
await confirmDialog("Apply");
- const mutation = requests.find(request => request.method === "PUT" && request.url.endsWith("/api/client-integrations/droid"));
+ const mutation = requests.filter(request => request.method === "PUT" && request.url.endsWith("/api/client-integrations/droid")).at(-1);
expect(mutation?.body).toMatchObject({ droidReasoningDefaults: { "openai/gpt-test": "low" } });
});
+test("Droid explicitly clearing its last default sends an empty map for review and commit", async () => {
+ stateResponse = () => json(status({ clientId: "droid", droidReasoning: {
+ models: [{ model: "openai/gpt-test", label: "Test model", efforts: ["low"] }],
+ defaults: { "openai/gpt-test": "low" },
+ } }));
+ await mountClient(true, "droid");
+ await act(async () => { container.querySelector('[role="combobox"]')!.click(); });
+ const clear = [...testWindow.document.querySelectorAll('[role="option"]')].find(option => option.textContent === "No default") as HTMLButtonElement;
+ await act(async () => { clear.click(); });
+ await act(async () => { buttonByText("Save / review changes")!.click(); });
+ const preview = requests.find(request => request.method === "POST" && request.url.endsWith("/api/client-integrations/preview"));
+ expect(preview?.body).toHaveProperty("droidReasoningDefaults", {});
+ await confirmDialog("Apply");
+ const mutation = requests.find(request => request.method === "PUT" && request.url.endsWith("/api/client-integrations/droid"));
+ expect(mutation?.body).toHaveProperty("droidReasoningDefaults", {});
+});
+
function buttons(): HTMLButtonElement[] {
return Array.from(container.querySelectorAll("button")) as unknown as HTMLButtonElement[];
}
diff --git a/gui/tests/models-visibility-queue.test.tsx b/gui/tests/models-visibility-queue.test.tsx
index 72e7c0ba771..9661f59f9c7 100644
--- a/gui/tests/models-visibility-queue.test.tsx
+++ b/gui/tests/models-visibility-queue.test.tsx
@@ -161,6 +161,17 @@ test("a saved write whose reconciliation fails never publishes success", async (
expect(container.querySelector(".action-toast")?.className).toContain("notice-err");
});
+for (const response of [() => new Response(null, { status: 204 }), () => new Response("saved")]) {
+ test("a successful visibility write without JSON reconciles without a network error", async () => {
+ await mount();
+ await click("a");
+ await settle(response());
+ expect(pressed("a")).toBe("false");
+ expect(container.querySelector(".action-toast")?.className).toContain("notice-ok");
+ expect(container.querySelector(".action-toast")?.textContent).not.toContain("Network");
+ });
+}
+
test("a click during reconciliation survives the old read and starts another ordered write", async () => {
await mount();
await click("a");
diff --git a/gui/tests/usage-custom-range.test.tsx b/gui/tests/usage-custom-range.test.tsx
index d41085cd430..cde907f81f8 100644
--- a/gui/tests/usage-custom-range.test.tsx
+++ b/gui/tests/usage-custom-range.test.tsx
@@ -570,3 +570,25 @@ test("throughput renders in summary, Models and Providers with honest missing-sa
expect(rows[1]?.querySelector(`[title="${en["usage.throughput.unmeasured"]}"]`)?.textContent).toContain("—");
}
});
+
+for (const invalid of [null, "20", "Infinity", NaN, Infinity, -Infinity, 0, -5]) {
+ test(`invalid throughput ${String(invalid)} is unavailable in every display`, async () => {
+ await mount();
+ const base = report(requests[0], "invalid-throughput-model");
+ const metric = { throughputTokensPerSec: invalid, throughputSamples: 2 };
+ const data = { ...base, summary: { ...base.summary, ...metric },
+ models: [{ ...base.models[0], ...metric }], providers: [{ ...base.models[0], ...metric }],
+ };
+ // A custom response keeps nonfinite values visible at the GUI boundary; JSON encodes them as null.
+ const response = Response.json(data);
+ response.json = async () => data;
+ await act(async () => { requests[0].resolve(response); });
+ const title = en["usage.throughput.unmeasured"];
+ expect(container.querySelector(`.usage-cost-row[title="${title}"] .stat-value`)?.textContent).toBe("—");
+ for (const section of ["models", "providers"]) {
+ const cell = container.querySelector(`#usage-section-${section} tbody td[title="${title}"]`);
+ expect(cell?.textContent).toBe("—");
+ expect(cell?.querySelector(".sr-only")).toBeNull();
+ }
+ });
+}
diff --git a/src/clients/config-export/droid.ts b/src/clients/config-export/droid.ts
index 909ea3347d0..ced05b838f4 100644
--- a/src/clients/config-export/droid.ts
+++ b/src/clients/config-export/droid.ts
@@ -68,7 +68,7 @@ export function droidDefaultsFromOwnedRows(
document: unknown,
fragmentPaths: readonly (readonly string[])[],
): DroidReasoningDefaults {
- const exportableModels = new Set(droidReasoningModels(ctx.models).map(model => model.model));
+ const exportableModels = new Map(droidReasoningModels(ctx.models).map(model => [model.model, model.efforts]));
const defaults: DroidReasoningDefaults = {};
for (const path of fragmentPaths) {
if (path.length !== 2 || path[0] !== "customModels") continue;
@@ -79,8 +79,9 @@ export function droidDefaultsFromOwnedRows(
const headers = current.extraHeaders;
if (!headers || typeof headers !== "object" || Array.isArray(headers)) continue;
const header = headers as Record;
- if (typeof header[DROID_DEFAULT_EFFORT_HEADER] !== "string") continue;
- defaults[current.model] = header[DROID_DEFAULT_EFFORT_HEADER] as string;
+ const effort = header[DROID_DEFAULT_EFFORT_HEADER];
+ if (typeof effort !== "string" || !exportableModels.get(current.model)?.includes(effort)) continue;
+ defaults[current.model] = effort;
}
return defaults;
}
diff --git a/src/server/responses/adapter-dispatch.ts b/src/server/responses/adapter-dispatch.ts
index 55c4de5e216..ffc991fb755 100644
--- a/src/server/responses/adapter-dispatch.ts
+++ b/src/server/responses/adapter-dispatch.ts
@@ -1329,7 +1329,9 @@ export async function prepareAdapterExchange(
}
if ("failed" in result) {
hop.permit?.release();
- return result.failed;
+ if (result.failed !== failedResponse) return result.failed;
+ // Preserve common redaction and combo consumption without another recovery send.
+ break recovery;
}
transportState.genericFailovers += 1;
try { void failedResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
diff --git a/structure/clients/integrations.md b/structure/clients/integrations.md
index 549059212f4..33698214310 100644
--- a/structure/clients/integrations.md
+++ b/structure/clients/integrations.md
@@ -73,13 +73,15 @@ same path and competing-settings checks as status, including pre-resolved paths;
an ambiguous legacy model ID or managed endpoint suppresses the projected map.
Preview and apply accept optional `droidReasoningDefaults`. Omission preserves
-owned defaults; an empty map clears them. A supplied map is validated against the
+compatible owned defaults; an empty map clears them. A supplied map is validated against the
same roster used to build the contribution, and the contribution participates in
the preview fingerprint and frozen mutation input. Refresh preserves defaults only
-while the exact namespaced selector remains in the roster. Provider, model, and
+while the exact namespaced selector remains in the roster and its current effort
+list includes the saved value. Unsupported defaults are omitted from status and
+removed from owned rows on refresh. Provider, model, and
combo alias names are mutable selectors rather than stable identities, so renaming
one removes the old managed row and its default; the replacement row starts without
-a default. Ordinary refresh omits the map unless the user has edited it, so an
+a default. Refresh and Save / review changes omit the map unless the user has edited it, so an
unsupported saved value does not block unrelated refresh. Removing models uses the
ordinary removal preview. Disable and restore remove or restore the rows and their
defaults as one owned value.
diff --git a/structure/dashboard-and-usage.md b/structure/dashboard-and-usage.md
index 8af2a623ba0..24aed6d89ed 100644
--- a/structure/dashboard-and-usage.md
+++ b/structure/dashboard-and-usage.md
@@ -283,6 +283,7 @@ the exact revision that was copied, and then discards the Logs ring, the retaine
request-history index so no surface serves rows the ledger no longer has.
`src/usage/summary.ts` turns that file into the `/api/usage` shape — totals, daily zero-filled
grid, model and provider breakdowns, and `measured / reported / unreported / unsupported / estimated` counts. Each scope also aggregates end-to-end output throughput (#6309): measured output tokens and wall-clock `durationMs` are summed over attempts reporting both, exposing `throughputTokensPerSec` — a token-sum-over-duration-sum, never a mean of per-request rates. `throughputSamples` counts qualifying attempts (legacy rows without attempts contribute one); nonpositive or nonfinite tokens/timing are excluded. The Usage summary and Models/Providers tables display the rate and sample basis, or unavailable when none qualify.
+All three throughput displays use the same finite-number guard as their tooltip; malformed or nonfinite rates are unavailable rather than formatted.
The management route scans the ledger from its beginning in fixed 1 MiB chunks on a
cold rebuild, then retains compact numeric aggregate state and resumes at the last verified LF for
ordinary appends. It does not retain the full input or a normalized object for every request, and
diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md
index a538527dc2d..d543d2a9ef3 100644
--- a/structure/gui-and-management-api.md
+++ b/structure/gui-and-management-api.md
@@ -407,7 +407,7 @@ Models visibility switches update immediately while writes execute in click orde
Only visibility controls remain editable during that queue; preset and other settings writes retain
their mutual exclusion. Once the queue drains, one authoritative catalog read reconciles the
switches before saved feedback appears. Refused writes or failed reconciliation show error feedback;
-client integration refresh failures retain their separate warning. Stale reads cannot override newer
+client integration refresh failures retain their separate warning. A successful empty or non-JSON write response omits optional integration details but still reconciles visibility; it is not a network failure. Stale reads cannot override newer
intent. Changing the API target or unmounting the page aborts observations and drops unsent queued
changes. The queue lives in `gui/src/use-model-visibility.ts`; the dashboard page contract is in
[Dashboard surfaces](dashboard-and-usage.md).
diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md
index 945e45fec33..8fb3e8e04de 100644
--- a/structure/providers/openai-tiers.md
+++ b/structure/providers/openai-tiers.md
@@ -311,9 +311,10 @@ warmup remains conservative and does not spend paid credits to validate an exhau
Credit parsing, expiry, partial updates and reset-ticket separation are covered in
`tests/codex-integration/codex-quota-parser-parity.test.ts`; selection and bulk-pause behavior
-are covered in `tests/codex-integration/codex-routing.test.ts` and
-`tests/codex-integration/codex-auth-api.test.ts` and
-`tests/codex-integration/codex-cooldown-recovery.test.ts`.
+are covered in `tests/codex-integration/codex-credits-after-limit.test.ts` and
+`tests/codex-integration/codex-credits-after-limit-main.test.ts`, alongside the general routing,
+bulk-pause and recovery suites `tests/codex-integration/codex-routing.test.ts`,
+`tests/codex-integration/codex-auth-api.test.ts` and `tests/codex-integration/codex-cooldown-recovery.test.ts`.
### Quota cache and short-window history
diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md
index 39283302a35..f0de552888a 100644
--- a/structure/transports/inventory.md
+++ b/structure/transports/inventory.md
@@ -43,7 +43,7 @@ surface is listed here so a maintainer can find the owner without grepping:
| Image/video generation loop | `src/images/loop.ts`, `src/images/plan.ts`, `src/images/fulfill.ts`, `src/images/xai-client.ts`, `src/images/xai-video-client.ts`, `src/images/artifacts.ts` | A provider-returned image URL is downloaded into a local artifact once, then served locally; warnings stay URL-free because provider CDN URLs may embed credentials. Artifact downloads go through the pinned-IP transport with a 10 s connect deadline (`DOWNLOAD_CONNECT_TIMEOUT_MS`) that bounds TCP/TLS setup on its own, in addition to the 60 s idle timer, and `pinnedHttpsGet` accepts a per-call `connectTimeoutMs`. |
| GitHub Copilot | `src/providers/xai-transport.ts` (`resolveProviderTransport`), `src/providers/github-copilot-transport.ts` | `resolveProviderTransport` selects the Copilot transport when the routed provider name is `github-copilot`; the Copilot module then resolves its headers and base URL, and the registry seeds the provider row and model fallback. |
| API-key pools | `src/providers/api-key-selection.ts`, `src/providers/key-failover.ts` | A configured `apiKeyPoolStrategy` plus a cooling committed key rotates before the first send (`selectProactiveApiKeyTransport`); a 429 still rotates after the send and records a cooldown. `provider.apiKey` keeps mirroring the active entry so routing stays single-key. The pick is inert without a strategy or while the committed key is healthy. |
-| OAuth account failover | `src/oauth/generic-account-failover.ts`, `src/oauth/anthropic-routing.ts`, `src/server/responses/antigravity-validation-refusal.ts`, `src/adapters/antigravity-refusal.ts` | Reactive pre-output 429 recovery is presence-driven with 2+ eligible accounts. Anthropic also recovers classified pre-output account-entitlement 403s under its [account-pool contract](../providers/anthropic-account-pool.md#account-entitlement-refusal-recovery), and records the refused account's cooldown even when the request has no retry sends left. Primary Antigravity inference through the Google adapter main dispatch may send once on a live sibling after a replayed 401, a terminal credential-refresh failure that marks the account for reauthentication, or a normalized structured `VALIDATION_REQUIRED` 403, within the existing budget. A bounded message-worded verification 403 persists a generation-fenced `verify_account` cause in `src/oauth/store.ts` and `src/oauth/health.ts`, including singleton and last-account refusals. It reads as `needs-reauth(verify)` in `ocx account list`; silent refresh preserves quarantine, while explicit re-login clears it and retires its failover cooldown. Quarantine does not require a retry slot; if persisting it fails, dispatch exits this adapter exchange’s recovery loop and preserves the original bounded 403 without another recovery send in that exchange; enclosing combo and policy fallback rules remain unchanged. Successful sibling replay uses the captured pool activation and the existing send budget. The 401 and structured 403 sibling paths share one request guard; unrelated 403s and transient refresh failures do not rotate. Continuations, native passthrough and sidecars keep their existing behavior. Generic OAuth accounts can be operator-paused; paused accounts are excluded from selection, failover, and proactive Token Guardian refresh. If every account is paused, the request fails with 403 rather than as a login failure. Pool and `oauthAccountFailover` flags govern proactive routing, not the reactive retry: a disabled Anthropic pool recovers through quota ordering rather than its dormant strategy, a per-provider `enabled` beats the global default in either direction, and a non-positive fill-first threshold disables proactive usage-based rotation. Kiro's `kiroAutoSelection` projects the same candidate eligibility into routing and account-list status; unknown evidence stays eligible, while pause, reauth, suspension, cooldown, and confirmed exhaustion have closed reasons. An active singleton or a pool excluded only from automatic Kiro selection may still attempt its active account; an explicitly paused active account is never dispatched, and an all-paused pool fails with 403. A configured Kiro capacity cap can also time out. Kiro's optional process-local account lease is released at response completion or cancellation; other providers retain their admission path. |
+| OAuth account failover | `src/oauth/generic-account-failover.ts`, `src/oauth/anthropic-routing.ts`, `src/server/responses/antigravity-validation-refusal.ts`, `src/adapters/antigravity-refusal.ts` | Reactive pre-output 429 recovery is presence-driven with 2+ eligible accounts. Anthropic also recovers classified pre-output account-entitlement 403s under its [account-pool contract](../providers/anthropic-account-pool.md#account-entitlement-refusal-recovery), and records the refused account's cooldown even when the request has no retry sends left. Primary Antigravity inference through the Google adapter main dispatch may send once on a live sibling after a replayed 401, a terminal credential-refresh failure that marks the account for reauthentication, or a normalized structured `VALIDATION_REQUIRED` 403, within the existing budget. A bounded message-worded verification 403 persists a generation-fenced `verify_account` cause in `src/oauth/store.ts` and `src/oauth/health.ts`, including singleton and last-account refusals. It reads as `needs-reauth(verify)` in `ocx account list`; silent refresh preserves quarantine, while explicit re-login clears it and retires its failover cooldown. Quarantine does not require a retry slot; if persisting it fails, dispatch exits this adapter exchange’s recovery loop and preserves the original bounded 403 without another recovery send in that exchange; enclosing combo and policy fallback rules remain unchanged. A preserved original 403 after sibling build or pre-send admission failure also exits recovery through common bounded error formatting and the combo failure callback; newly constructed failure responses keep their own delivery. Successful sibling replay uses the captured pool activation and the existing send budget. The 401 and structured 403 sibling paths share one request guard; unrelated 403s and transient refresh failures do not rotate. Continuations, native passthrough and sidecars keep their existing behavior. Generic OAuth accounts can be operator-paused; paused accounts are excluded from selection, failover, and proactive Token Guardian refresh. If every account is paused, the request fails with 403 rather than as a login failure. Pool and `oauthAccountFailover` flags govern proactive routing, not the reactive retry: a disabled Anthropic pool recovers through quota ordering rather than its dormant strategy, a per-provider `enabled` beats the global default in either direction, and a non-positive fill-first threshold disables proactive usage-based rotation. Kiro's `kiroAutoSelection` projects the same candidate eligibility into routing and account-list status; unknown evidence stays eligible, while pause, reauth, suspension, cooldown, and confirmed exhaustion have closed reasons. An active singleton or a pool excluded only from automatic Kiro selection may still attempt its active account; an explicitly paused active account is never dispatched, and an all-paused pool fails with 403. A configured Kiro capacity cap can also time out. Kiro's optional process-local account lease is released at response completion or cancellation; other providers retain their admission path. |
| OAuth login callback (inbound) | `src/oauth/callback-server.ts` | Every response, including non-callback 404s, closes its connection so a pooled socket cannot deliver a later login to a retired flow on the same callback port. |
| Alibaba regions | `src/providers/alibaba-region-backup.ts`, `src/providers/alibaba-region-migration.ts`, `src/providers/alibaba-region-startup.ts` | Region migration backs up before rewriting and is idempotent across restarts. |
| Discovery and quota | `src/providers/model-discovery.ts`, `src/providers/quota.ts`, `src/providers/registry.ts` | Discovery rejects a response over 4 MiB or past 2,000 raw rows before caching it. Provider-scoped hints fill capabilities omitted by live rosters; OpenCode Go's `deepseek-v4.1-flash` keeps its 1,048,576-token context window. The fixed-key Opper preset uses the shared OpenAI Chat adapter at `https://api.opper.ai/v3/compat`, discovers models through its conventional authenticated `/models` path, preserves an older same-named custom destination, and falls back to bare pool ids while passing vendor-prefixed ids through unchanged. Codex quota DTOs suppress retired Spark evidence under the [OpenAI scope contract](../providers/openai-tiers.md#public-provider-contract), retaining ordinary custom windows. |
diff --git a/tests/clients/droid-managed-reasoning-defaults.test.ts b/tests/clients/droid-managed-reasoning-defaults.test.ts
index 93eca412c5b..5a24cbeec4e 100644
--- a/tests/clients/droid-managed-reasoning-defaults.test.ts
+++ b/tests/clients/droid-managed-reasoning-defaults.test.ts
@@ -77,7 +77,7 @@ describe("Droid managed reasoning defaults", () => {
const path = install();
expect(applyIntegration({ ...input(), droidReasoningDefaults: { "mock/chat": "high" } }).ok).toBe(true);
const changedModel: ExportModel = {
- ...MODEL, displayName: "Renamed Chat", inputModalities: ["text", "image"], reasoningEfforts: ["low"],
+ ...MODEL, displayName: "Renamed Chat", inputModalities: ["text", "image"], reasoningEfforts: ["high"],
};
const config = { ...CONFIG, port: 10101 };
const results = await refreshOwnedCatalogIntegrations({
@@ -92,6 +92,31 @@ describe("Droid managed reasoning defaults", () => {
expect(readOwnedDroidReasoningDefaults(input([changedModel], { port: 10101, config }))).toEqual({ "mock/chat": "high" });
});
+ test.each([
+ ["incompatible", ["low"]],
+ ["empty", []],
+ ["unknown", undefined],
+ ] as const)("refresh drops inherited defaults for a %s effort ladder and preserves compatible peers", async (_kind, efforts) => {
+ const path = install();
+ const peer: ExportModel = { ...MODEL, namespaced: "mock/peer", id: "peer" };
+ expect(applyIntegration({ ...input([MODEL, peer]), droidReasoningDefaults: {
+ "mock/chat": "high", "mock/peer": "low",
+ } }).ok).toBe(true);
+ const changedModel: ExportModel = { ...MODEL, reasoningEfforts: efforts === undefined ? undefined : [...efforts] };
+ const models = [changedModel, peer];
+ expect(readOwnedDroidReasoningDefaults(input(models))).toEqual({ "mock/peer": "low" });
+ expect(readIntegrationState(input(models)).state).toBe("stale");
+
+ expect(await refreshOwnedCatalogIntegrations({
+ models, config: CONFIG, port: 10100, env: {}, home, store,
+ }, ["droid"])).toEqual([{ client: "droid", ok: true, changed: true }]);
+ const rows = settings(path).customModels;
+ expect(rows.find(row => row.model === "mock/chat")).not.toHaveProperty("extraHeaders");
+ expect(rows.find(row => row.model === "mock/peer")).toMatchObject({ extraHeaders: { [HEADER]: "low" } });
+ expect(readOwnedDroidReasoningDefaults(input(models))).toEqual({ "mock/peer": "low" });
+ expect(readIntegrationState(input(models)).state).toBe("current");
+ });
+
test.each([
["provider", { ...MODEL, namespaced: "renamed/chat", provider: "renamed" }],
["model", { ...MODEL, namespaced: "mock/renamed", id: "renamed" }],
diff --git a/tests/server/server-google-antigravity-oauth-401-replay.test.ts b/tests/server/server-google-antigravity-oauth-401-replay.test.ts
index 759cc0bd3b1..f361f326b0a 100644
--- a/tests/server/server-google-antigravity-oauth-401-replay.test.ts
+++ b/tests/server/server-google-antigravity-oauth-401-replay.test.ts
@@ -7,6 +7,11 @@ import * as configModule from "../../src/config";
import { forceRefreshOAuthAccessSnapshot, getValidAccessTokenSnapshot } from "../../src/oauth";
import { credentialGeneration, getAccountSet, getAuthStorePath, saveCredential, setAccountPaused, setActiveAccount } from "../../src/oauth/store";
import { startServer } from "../../src/server";
+import { handleResponses } from "../../src/server/responses";
+import type { ConsumedComboFailure } from "../../src/server/responses/core-options";
+import { ADAPTER_REGISTRY } from "../../src/adapters/registry";
+import * as requestPacing from "../../src/providers/request-pacing";
+import { createRequestExecutionBudget } from "../../src/lib/request-execution-budget";
import { ANTIGRAVITY_VALIDATION_REQUIRED_PREFIX } from "../../src/adapters/google-errors";
import { readUsageEntries } from "../../src/usage/log";
import { clearGenericFailoverHealth, eligibleFailoverAccounts, genericFailoverRetryAfterSeconds, rotateAntigravityAccountOnAuthRefusal } from "../../src/oauth/generic-account-failover";
@@ -333,6 +338,78 @@ function installOAuthFetch(
}
describe("Google Antigravity OAuth upstream 401 replay", () => {
+ for (const comboAttempt of [false, true]) {
+ test.each(["build", "admission"])(`preserved verify 403 uses common delivery after %s failure (combo=${comboAttempt})`, async failurePoint => {
+ await seedOAuth();
+ const failedId = getAccountSet("google-antigravity")!.activeAccountId;
+ const siblingId = await seedSibling();
+ const config = antigravityConfig();
+ config.providers["google-antigravity"]!.reasoningEfforts = ["low", "high"];
+ saveConfig(config);
+ const secret = ["sk", "antigravity", "refusal", "secret", "canary"].join("-");
+ const observed = installOAuthFetch([
+ { status: 403, message: `Please verify your account to continue. Unsupported reasoning effort high. ${secret}` },
+ 200,
+ ]);
+ const budget = createRequestExecutionBudget();
+ const consumed: ConsumedComboFailure[] = [];
+ let injectedFailures = 0;
+ const create = ADAPTER_REGISTRY.google.create;
+ const withSlot = requestPacing.withProviderRequestSlot;
+ ADAPTER_REGISTRY.google.create = (provider, context) => {
+ const adapter = create(provider, context);
+ if (failurePoint === "build" && provider.apiKey === "access-b") {
+ adapter.buildRequest = async () => {
+ injectedFailures += 1;
+ throw new Error("replacement-build-canary");
+ };
+ }
+ return adapter;
+ };
+ const rejectSiblingSlot: typeof requestPacing.withProviderRequestSlot = async (name, provider, model, signal, send) => {
+ if (failurePoint === "admission" && provider.apiKey === "access-b") {
+ injectedFailures += 1;
+ throw new Error("replacement-admission-canary");
+ }
+ return withSlot(name, provider, model, signal, send);
+ };
+ const slotSpy = spyOn(requestPacing, "withProviderRequestSlot").mockImplementation(rejectSiblingSlot);
+ try {
+ const request = new Request("http://127.0.0.1/v1/responses", {
+ method: "POST", headers: { "content-type": "application/json" },
+ body: JSON.stringify({ model: "google-antigravity/gemini-3.8-flash", input: "hello", reasoning: { effort: "high" } }),
+ });
+ const response = await handleResponses(request, config, { provider: "google-antigravity", model: "gemini-3.8-flash" }, {
+ comboAttempt, sendBudget: budget, onConsumedComboFailure: failure => consumed.push(failure),
+ });
+ const body = await response.text();
+ expect(injectedFailures).toBe(1);
+ expect(response.status).toBe(403);
+ expect(observed.chatAuth).toEqual(["Bearer rejected-access"]);
+ expect(observed.counts.refresh).toBe(0);
+ expect(budget.used).toBe(1);
+ expect(body).toContain("verify your account");
+ expect(body).not.toContain(secret);
+ expect(body).not.toContain("replacement-build-canary");
+ expect(body).not.toContain("replacement-admission-canary");
+ expect(consumed).toHaveLength(comboAttempt ? 1 : 0);
+ expect(JSON.parse(body).error).toMatchObject({ type: "permission_error", code: "permission_denied" });
+ if (comboAttempt) {
+ expect(consumed[0]!.response.status).toBe(403);
+ expect(consumed[0]!.response.headers.get("content-type")).toBe("application/json");
+ expect(consumed[0]!.classificationText).toContain("verify your account");
+ expect(consumed[0]!.classificationText).not.toContain(secret);
+ }
+ const rows = getAccountSet("google-antigravity")!.accounts;
+ expect(rows.find(row => row.id === failedId)).toMatchObject({ needsReauth: true, needsReauthReason: "verify_account" });
+ expect(rows.find(row => row.id === siblingId)?.needsReauth).toBeUndefined();
+ } finally {
+ slotSpy.mockRestore();
+ ADAPTER_REGISTRY.google.create = create;
+ }
+ });
+ }
+
test.each([1, 2])("verify 403 survives when quarantine persistence fails in a %i-account pool", async count => {
await seedOAuth();
if (count === 2) await seedSibling();
diff --git a/tests/update/update-stop-classification.test.ts b/tests/update/update-stop-classification.test.ts
index 95bd04a6c46..18057c50ba5 100644
--- a/tests/update/update-stop-classification.test.ts
+++ b/tests/update/update-stop-classification.test.ts
@@ -1,5 +1,6 @@
-import { describe, expect, test } from "bun:test";
+import { describe, expect, spyOn, test } from "bun:test";
import { spawn } from "node:child_process";
+import * as childProcess from "node:child_process";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { STOP_HISTORY_DEFERRED_EXIT_CODE, STOP_HISTORY_INCOMPLETE_EXIT_CODE } from "../../src/update/stop-contract.mjs";
@@ -209,33 +210,65 @@ describe("stop failure classification (#3008)", () => {
}
});
- test("a silent dial to a port nobody holds any more is dead, not unknown", async () => {
- // The Tailscale case in miniature: the dial is neither answered nor refused, so it can
- // only time out, and the port is free by the time the probe asks to bind it. The
- // fixture accepts the probe's connection, closes its listener, and keeps that one
- // socket open and silent.
- const listener = spawn(process.execPath, ["-e", [
- "const net = require('node:net');",
- "const server = net.createServer(socket => { server.close(); socket.on('error', () => {}); });",
- "server.listen(0, '127.0.0.1', () => process.stdout.write(String(server.address().port)));",
- ].join("\n")], { stdio: ["ignore", "pipe", "ignore"] });
- const port = await new Promise((resolve, reject) => {
- const timer = setTimeout(() => reject(new Error("listener did not report a port")), 10_000);
- listener.stdout.once("data", chunk => { clearTimeout(timer); resolve(Number(String(chunk))); });
- listener.once("error", error => { clearTimeout(timer); reject(error); });
- });
+ test.each([false, true])("a silent dial uses a real bind fallback (port held=%s)", held => {
+ // Inject only the HTTP timeout to avoid depending on platform-specific accepted-socket
+ // port reuse. The child reserves its own port,
+ // then runs the unchanged production script against real released/held listeners.
+ const realSpawn = childProcess.spawnSync;
+ let events: Array<{ event: string; host?: string; port?: number; exclusive?: boolean }> = [];
+ const run = spyOn(childProcess, "spawnSync").mockImplementation(((file: string, args: readonly string[], options: childProcess.SpawnSyncOptionsWithStringEncoding) => {
+ const bootstrap = [
+ "const net = require('node:net'), http = require('node:http');",
+ "const { EventEmitter } = require('node:events');",
+ "const emit = data => process.stderr.write(JSON.stringify(data) + '\\n');",
+ "const reservation = net.createServer();",
+ "reservation.listen(0, '127.0.0.1', () => {",
+ " const port = reservation.address().port; process.argv[2] = String(port);",
+ " const start = () => {",
+ " const createServer = net.createServer;",
+ " net.createServer = (...args) => {",
+ " const server = createServer(...args), listen = server.listen;",
+ " server.listen = function(options, callback) {",
+ " emit({ event: 'bind', ...options });",
+ " return listen.call(this, options, callback);",
+ " };",
+ " server.once('listening', () => emit({ event: 'bound' }));",
+ " server.once('error', () => { emit({ event: 'bind-error' }); reservation.close(); });",
+ " return server;",
+ " };",
+ " http.get = options => {",
+ " emit({ event: 'dial', host: options.host, port: options.port });",
+ " const request = new EventEmitter(); request.destroy = () => {};",
+ " queueMicrotask(() => { emit({ event: 'timeout' }); request.emit('timeout'); });",
+ " return request;",
+ " };",
+ ` eval(${JSON.stringify(args[1])});`,
+ " };",
+ held ? " start();" : " reservation.close(start);",
+ "});",
+ ].join("\n");
+ const result = realSpawn(file, [args[0]!, bootstrap, ...args.slice(2)], options);
+ expect(result.error).toBeUndefined();
+ expect(result.status).toBe(0);
+ events = String(result.stderr).trim().split("\n").map(line => JSON.parse(line));
+ return result;
+ }) as typeof childProcess.spawnSync);
try {
- expect(probeProxyLiveness(port, "127.0.0.1", 400)).toBe("dead");
+ expect(probeProxyLiveness(12345, "127.0.0.1", 400)).toBe(held ? "unknown" : "dead");
+ expect(run).toHaveBeenCalledTimes(1);
+ expect(events.map(item => item.event)).toEqual(["dial", "timeout", "bind", held ? "bind-error" : "bound"]);
+ expect(events[0]!.port).toBeGreaterThan(0);
+ expect(events[0]!.host).toBe("127.0.0.1");
+ expect(events[2]).toEqual({ event: "bind", host: "127.0.0.1", port: events[0]!.port, exclusive: true });
} finally {
- listener.kill();
- await new Promise(resolve => listener.once("exit", () => resolve()));
+ run.mockRestore();
}
});
test("a hostname never gets the bind fallback, because the dial and the bind may resolve differently", async () => {
- // Same fixture as the silent-dial case, but addressed by name: the dial times out and
- // the port is bindable, yet a name can resolve to other addresses for the bind, so a
- // free port proves nothing about the endpoint the dial reached. The fixture listens
+ // Keep a real connection silent, but address it by name: timeout must stay unknown
+ // without attempting a bind, regardless of whether the accepted socket permits reuse.
+ // A name can resolve differently for dial and bind. The fixture listens
// dual-stack (`::`, falling back to `0.0.0.0` where IPv6 is unavailable) so the dial
// is silent whichever address `localhost` resolves to first, instead of being refused
// on `::1` and returning `dead` before the hostname rule is reached.
diff --git a/tests/usage/usage-throughput.test.ts b/tests/usage/usage-throughput.test.ts
index 48f3267863b..97e3673abfb 100644
--- a/tests/usage/usage-throughput.test.ts
+++ b/tests/usage/usage-throughput.test.ts
@@ -108,6 +108,7 @@ describe("usage throughput aggregation (#6309)", () => {
}),
entry({
ts: FIXED_NOW - 1,
+ model: "gpt-5.5-mini",
usageStatus: "reported",
usage: { inputTokens: 10, outputTokens: 300 },
durationMs: 10,
@@ -121,6 +122,13 @@ describe("usage throughput aggregation (#6309)", () => {
expect(sum.providers[0]?.throughputSamples).toBe(2);
expect(sum.summary.throughputTokensPerSec).toBeCloseTo(400 / 30, 9);
expect(sum.summary.throughputSamples).toBe(2);
+ expect(sum.models).toHaveLength(2);
+ expect(sum.models.find(row => row.model === "gpt-5.5")).toMatchObject({
+ throughputTokensPerSec: 10, throughputSamples: 1,
+ });
+ expect(sum.models.find(row => row.model === "gpt-5.5-mini")).toMatchObject({
+ throughputTokensPerSec: 15, throughputSamples: 1,
+ });
});
});
diff --git a/tests/web-search/web-search-forced-declaration.test.ts b/tests/web-search/web-search-forced-declaration.test.ts
index 0b9c86ffd5d..b8de3603e3b 100644
--- a/tests/web-search/web-search-forced-declaration.test.ts
+++ b/tests/web-search/web-search-forced-declaration.test.ts
@@ -32,6 +32,7 @@ for (const transport of ["runTurn", "fetch"] as const) {
describe(`${transport} forced search declaration`, () => {
async function drive(passes: AdapterEvent[][], controller?: AbortController, failAfterSearch = false) {
const seen: OcxParsedRequest[] = [];
+ const providerSends: number[] = [];
const output: AdapterEvent[] = [];
let iteration = 0;
const next = (request: OcxParsedRequest) => {
@@ -46,7 +47,7 @@ for (const transport of ["runTurn", "fetch"] as const) {
parsed, plan, exaApiKey: "fixture", abortSignal: controller?.signal,
dispatch: request => stream(next(request)),
})) output.push(event);
- return { seen, failed: output.some(e => e.type === "error"), output, text: JSON.stringify(output) };
+ return { seen, providerSends, failed: output.some(e => e.type === "error"), output, text: JSON.stringify(output) };
}
let events: AdapterEvent[] = [];
const adapter: ProviderAdapter = {
@@ -55,8 +56,11 @@ for (const transport of ["runTurn", "fetch"] as const) {
events = next(request);
return { url: "https://routed.test/v1", method: "POST", headers: {}, body: "{}" };
},
- fetchResponse: async () => failAfterSearch && seen.length > 1
- ? new Response("refused", { status: 429 }) : new Response("wire"),
+ fetchResponse: async () => {
+ providerSends.push(seen.length);
+ return failAfterSearch && seen.length > 1
+ ? new Response("refused", { status: 429 }) : new Response("wire");
+ },
async *parseStream() { yield* events; },
};
const response = await runWithWebSearch({
@@ -66,7 +70,7 @@ for (const transport of ["runTurn", "fetch"] as const) {
incomingMeta: { headers: new Headers(), translatorBudget: createTestTranslatorBudget() },
});
const text = await response.text();
- return { seen, failed: text.includes('"type":"response.failed"'), output, text };
+ return { seen, providerSends, failed: text.includes('"type":"response.failed"'), output, text };
}
test("over-budget calls retain the declaration and receive paired limit results without another search", async () => {
@@ -134,11 +138,12 @@ for (const transport of ["runTurn", "fetch"] as const) {
expect(result.failed).toBe(true);
});
- if (transport === "fetch") test("post-search refusal does not resend committed work", async () => {
+ if (transport === "fetch") test("post-search refusal without retry opt-in does not resend committed work", async () => {
const fetchMock = spyOn(globalThis, "fetch").mockResolvedValue(Response.json({ results: [] }));
const result = await drive([search("first"), answer], undefined, true);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(result.seen).toHaveLength(2);
+ expect(result.providerSends).toEqual([1, 2]);
expect(result.failed).toBe(true);
});
});