Merge remote-tracking branch 'origin/release/v3.8.51' into feat/radar-d32-optin-explainer

This commit is contained in:
diegosouzapw
2026-09-01 17:00:31 -03:00
12 changed files with 274 additions and 113 deletions

View File

@@ -58,6 +58,22 @@ updates:
# on the VPS — so keep auto-bumps frozen (no update-types = ignore every version).
# Migrate it intentionally, not via dependabot (#4050).
- dependency-name: "@huggingface/transformers"
# onnxruntime-node is the OTHER HALF of the @huggingface/transformers pair frozen
# above: the hoisted copy must equal the exact version transformers pins, or npm
# nests a second ABI-incompatible native copy (contract test
# tests/unit/onnxruntime-single-copy.test.ts, pair established in #9962). A solo
# bump can never be correct — it only ever moves together with transformers, in
# the same deliberate migration PR. Freezing it keeps the production group PRs
# (e.g. #12219) from being born red on the pair contract.
- dependency-name: "onnxruntime-node"
# eslint-plugin-react-hooks is pinned to 7.0.1 by a contract test
# (tests/unit/eslint-react-hooks-version-pinned.test.ts) until the 7.1.1 rule set
# is adopted deliberately — that adoption needs a full cold lint run and its own
# PR (the #12146 react-hooks migration finished on 2026-09-01, so the path is
# open; the bump still must not ride a dependabot group, where it reds the
# development group PRs, e.g. #12220). Remove this ignore in the adoption PR
# together with the pin test.
- dependency-name: "eslint-plugin-react-hooks"
- package-ecosystem: "github-actions"
directory: "/"

View File

@@ -0,0 +1 @@
- **feat(providers):** the provider plugin manifest now also advertises a `usage-supported` capability for the 46 providers whose usage API is accepted by the server and Dashboard routes, so integrators can distinguish "the server will serve quota for this provider" from "a fetcher is wired" without reading TypeScript. Discovery only — no fetcher or quota change. `usage-fetch` resolves on id or alias (the usage dispatcher accepts both); `usage-supported` resolves on id alone, matching the runtime guard `USAGE_SUPPORTED_PROVIDERS.includes(providerId)`. `USAGE_SUPPORTED_PROVIDERS` moved to a zero-dependency leaf (`open-sse/services/usage/supportedProviders.ts`) and is re-exported from `providers.ts`, mirroring the `fetcherProviders` leaf from #11903 and keeping the manifest a light module. ([#12214](https://github.com/diegosouzapw/OmniRoute/pull/12214)) — thanks @maxmad64bis

View File

@@ -48,7 +48,7 @@ The manifest contains:
- JSON-safe model metadata such as context length, vision/reasoning flags, and
unsupported params
- capability tags including `apikey`, `oauth`, `custom-executor`,
`passthrough-models`, `responses`, `sidecar-candidate`, and `usage-fetch`
`passthrough-models`, `responses`, `sidecar-candidate`, `usage-fetch`, and `usage-supported`
The manifest intentionally excludes:
@@ -74,6 +74,7 @@ re-reading the TypeScript sources.
| `custom-executor` | Runs a non-default executor, so it stays on the TypeScript path. |
| `sidecar-candidate` | Mirrors `sidecar.eligible` — safe to consider for sidecar import. |
| `usage-fetch` | Has a wired usage or quota fetcher (`getUsageForProvider`). |
| `usage-supported` | The usage API accepts this provider (`isSupportedUsageConnection`). |
`usage-fetch` is discovery only. It reports that OmniRoute knows how to read usage for the
provider; it does not activate fetching, change quota semantics, or imply that the
@@ -86,6 +87,16 @@ with aliases and is slightly longer than the number of tagged providers: entries
not chat providers in the manifest registry (for example the `firecrawl` search provider
and the `amazon-q` ACP provider) have no manifest entry to tag.
`usage-supported` answers whether the server and Dashboard usage routes accept a connection
for the provider. It mirrors `isSupportedUsageConnection()` (`src/lib/usage/providerLimits.ts`)
and `supportsProviderQuota()` (`src/shared/utils/providerQuotaVisibility.ts`), both gated by
`USAGE_SUPPORTED_PROVIDERS` (`open-sse/services/usage/supportedProviders.ts`). Unlike
`usage-fetch`, it is emitted on the provider id alone — the runtime guard does
`USAGE_SUPPORTED_PROVIDERS.includes(providerId)` with no alias resolution, so the manifest
keeps the same rule. The two tags have different perimeters: 4 providers carry only
`usage-fetch` (`opencode`, `opencode-zen`, `openrouter`, `xai`) and 1 carries only
`usage-supported` (`xiaomi-mimo-token-plan`), so one does not imply the other.
## Sidecar Use
Sidecars should treat `sidecar.eligible` as a conservative candidate signal, not

View File

@@ -1,5 +1,6 @@
import type { RegistryEntry, RegistryModel } from "./providers/shared.ts";
import { USAGE_FETCHER_PROVIDERS } from "../services/usage/fetcherProviders.ts";
import { USAGE_SUPPORTED_PROVIDERS } from "../services/usage/supportedProviders.ts";
export type ProviderPluginCapability =
| "apikey"
@@ -8,7 +9,8 @@ export type ProviderPluginCapability =
| "passthrough-models"
| "responses"
| "sidecar-candidate"
| "usage-fetch";
| "usage-fetch"
| "usage-supported";
export interface ProviderPluginModel {
id: string;
@@ -66,6 +68,15 @@ const SIDECAR_COMPATIBLE_EXECUTORS = new Set(["default"]);
*/
const USAGE_FETCHER_PROVIDER_SET = new Set<string>(USAGE_FETCHER_PROVIDERS);
/**
* Providers whose usage API is accepted by dashboard/server routes (#10078).
* Unlike USAGE_FETCHER_PROVIDERS this gate is checked with a plain
* `USAGE_SUPPORTED_PROVIDERS.includes(providerId)` — no alias resolution —
* so the manifest must emit on the identifier alone to stay faithful to the
* runtime guard.
*/
const USAGE_SUPPORTED_PROVIDER_SET = new Set<string>(USAGE_SUPPORTED_PROVIDERS);
function compactObject<T extends Record<string, unknown>>(value: T): Partial<T> {
return Object.fromEntries(
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined)
@@ -142,6 +153,9 @@ function capabilitiesFor(entry: RegistryEntry, eligible: boolean): ProviderPlugi
) {
capabilities.add("usage-fetch");
}
if (USAGE_SUPPORTED_PROVIDER_SET.has(entry.id)) {
capabilities.add("usage-supported");
}
return [...capabilities].sort();
}

View File

@@ -0,0 +1,79 @@
/**
* usage/supportedProviders.ts — registration list of providers whose usage/quota
* API is accepted by the dashboard and server routes.
*
* Extracted from `src/shared/constants/providers.ts` so that light consumers —
* the provider-plugin manifest (`config/providerPluginManifest.ts`) above all —
* can read the list without pulling the ~12-module provider registry, and
* without an open-sse module reaching across the workspace boundary into
* `src/` (the open-sse typecheck gate forbids open-sse → src imports). Same
* pattern as `fetcherProviders.ts` (#11903): pure data — no imports, no module
* state — so it cannot introduce a cycle. `src/shared/constants/providers.ts`
* re-exports the value, so every existing `@/shared/constants/providers`
* import path keeps working unchanged.
*
* Typed `readonly string[]` (not `as const`): the dashboard/server gates call
* `USAGE_SUPPORTED_PROVIDERS.includes(providerId)` with a plain `string`, which
* a literal-tuple type would reject (TS2345).
*/
// Providers that support usage/quota API
export const USAGE_SUPPORTED_PROVIDERS: readonly string[] = [
"antigravity",
"agy",
"kiro",
"amazon-q",
"github",
"codex",
"claude",
"cursor",
"qoder",
"kimi-coding",
"kimi-coding-apikey",
"glm",
"glm-cn",
"zai",
"glmt",
"opencode-go",
"ollama-cloud",
"minimax",
"minimax-cn",
"crof",
"nanogpt",
"deepseek",
"xiaomi-mimo",
"xiaomi-mimo-token-plan",
"vertex",
"vertex-partner",
"codebuddy-cn",
// PromptQL playground credits (getCreditSummary → USD micros)
"promptql",
"pql",
// Adobe Firefly web (cookie/JWT as apikey) — GET firefly.adobe.io/v1/credits/balance
"adobe-firefly",
"firefly",
"hyperagent",
"ha",
// xAI OAuth (Grok) weekly quota (id + public alias, same pattern as ha/agy)
"xai-oauth",
"xao",
// Grok Build subscription, billing credits, and auto top-up status
"grok-cli",
// Firecrawl team credits (GET /v2/team/credit-usage)
"firecrawl",
// Volcano Ark Plan subscriptions (agent-plan / coding-plan)
"volcengine-agent-plan",
"volcengine-coding-plan",
// Command Code credits + 5h/weekly rolling windows
"command-code",
"conol-web",
"cnl",
// Alibaba Coding Plan triple-window quota (#9603 UI gap — fetcher existed, list entry missing)
"bailian-coding-plan",
// Qwen Cloud / Model Studio personal Token Plan (cookie-authenticated console gateway)
"qwen-cloud-token-plan",
// AgentRouter (New-API) console balance quota (consoleApiKey + newApiUserId)
"agentrouter",
// Kilo Code personal USD balance (GET /api/profile/balance, existing OAuth token)
"kilocode",
];

View File

@@ -771,6 +771,7 @@ export function createSSEStream(options: StreamOptions = {}) {
const passthroughResponsesOutputItems: unknown[] = [];
const passthroughResponsesPendingFunctionCalls = new Map<string, JsonRecord>();
let passthroughResponsesId: string | null = null;
let passthroughLastChatId: string | null = null;
let passthroughResponsesCurrentFunctionCallKey: string | null = null;
const passthroughResponsesReasoningSummarySeen = new Set<string>();
// #6199 — commentary-phase items announced via `response.output_item.added` are
@@ -1955,6 +1956,16 @@ export function createSSEStream(options: StreamOptions = {}) {
const isFinishChunk = parsed.choices?.[0]?.finish_reason;
// Remember the upstream's chat-completion id so synthetic chunks
// emitted at flush (e.g. the estimated usage-only chunk) carry the
// stream's real string id instead of null on the chat path
// (passthroughResponsesId is only ever set on the Responses path).
if (typeof parsed.id === "string" && parsed.id) {
passthroughLastChatId = parsed.id;
} else if (typeof parsed.id === "number") {
passthroughLastChatId = String(parsed.id);
}
if (isFinishChunk) {
passthroughSawFinishReason = true;
}
@@ -1973,28 +1984,21 @@ export function createSSEStream(options: StreamOptions = {}) {
parsed.choices[0].finish_reason !== "tool_calls"
) {
parsed.choices[0].finish_reason = "tool_calls";
// If we modify it, we must output the modified object
if (!injectedUsage && hasValidUsage(parsed.usage)) {
output = `data: ${JSON.stringify(parsed)}\n\n`;
injectedUsage = true;
}
// If we modify it, we must output the modified object. This used to
// piggyback on the estimated-usage rewrite below; with the estimate
// moved to flush() (#12151 follow-up) the rewrite must happen here.
// injectedUsage doubles as the "output already rewritten" latch —
// without it the raw line overwrites this rewrite further down.
output = `data: ${JSON.stringify(parsed)}\n\n`;
injectedUsage = true;
}
if (
isFinishChunk &&
!passthroughForwardedUsage &&
!hasValidUsage(parsed.usage) &&
!hasValidUsage(usage) &&
totalContentLength > 0
) {
const estimated = estimateUsage(body, totalContentLength, sourceFormat || FORMATS.OPENAI);
if (hasValidUsage(estimated)) {
parsed.usage = filterUsageForFormat(estimated, sourceFormat || FORMATS.OPENAI);
output = `data: ${JSON.stringify(parsed)}\n\n`;
usage = estimated;
passthroughForwardedUsage = true;
injectedUsage = true;
}
} else if (isFinishChunk && hasValidUsage(usage) && !passthroughForwardedUsage) {
// #12151 follow-up: do NOT inject estimated usage into the finish chunk.
// A genuine OpenAI upstream sends its usage in a trailing empty-choices
// chunk AFTER the finish; estimating here marked passthroughForwardedUsage
// and made the real trailing block get dropped in favor of the estimate
// (billing regression pinned by tests/unit/stream-utils.test.ts). The
// estimate is now emitted in flush(), only when the upstream stayed silent.
if (isFinishChunk && hasValidUsage(usage) && !passthroughForwardedUsage) {
const buffered = addBufferToUsage(usage);
parsed.usage = filterUsageForFormat(buffered, sourceFormat || FORMATS.OPENAI);
output = `data: ${JSON.stringify(parsed)}\n\n`;
@@ -2510,6 +2514,30 @@ export function createSSEStream(options: StreamOptions = {}) {
forward(controller, encoder.encode(finishOutput));
clientPayloadCollector.push(syntheticFinishChunk);
}
// #12151: upstream never reported usage — emit the estimate as a
// canonical OpenAI trailing usage-only chunk (empty choices) before
// [DONE], so metered clients still see token counts. When the
// upstream DID send usage (trailing or in-band), it was forwarded
// already and passthroughForwardedUsage guards this off.
if (
shouldEmitDoneTerminator &&
!passthroughForwardedUsage &&
hasValidUsage(usage)
) {
const usageOnlyChunk = {
id: passthroughLastChatId ?? passthroughResponsesId ?? `chatcmpl-${Date.now()}`,
object: "chat.completion.chunk",
created: Math.floor(Date.now() / 1000),
model,
choices: [],
usage: filterUsageForFormat(usage, sourceFormat || FORMATS.OPENAI),
};
const usageOutput = `data: ${JSON.stringify(usageOnlyChunk)}\n\n`;
reqLogger?.appendConvertedChunk?.(usageOutput);
forward(controller, encoder.encode(usageOutput));
clientPayloadCollector.push(usageOnlyChunk);
passthroughForwardedUsage = true;
}
await emitFinalSseMetadata(controller, usage);
doneSent = true;
if (shouldEmitDoneTerminator) {

View File

@@ -13392,6 +13392,9 @@
"colProvider": "Provider",
"colModel": "Model",
"colQuota": "Quota",
"colLimits": "Limits",
"trainsOnPrompts": "Trains on prompts",
"trainsOnPromptsHelp": "This provider discloses that it may use your prompts to train models",
"colContext": "Context",
"colCapabilities": "Capabilities",
"colTos": "ToS Risk",

View File

@@ -13393,12 +13393,12 @@
"colProvider": "Provedor",
"colModel": "Modelo",
"colQuota": "Cota",
"colLimits": "Limites",
"trainsOnPrompts": "Treina com prompts",
"trainsOnPromptsHelp": "Este provedor declara que pode usar seus prompts para treinar modelos",
"colContext": "Contexto",
"colCapabilities": "Capacidades",
"colTos": "Risco ToS",
"colLimits": "__MISSING__:Rate limits",
"trainsOnPrompts": "__MISSING__:Trains on prompts",
"trainsOnPromptsHelp": "__MISSING__:This provider's terms state it may train on the prompts you send. Models without this badge either state they do not, or do not document it — an absent statement is not a guarantee.",
"newBadge": "novo",
"setupGuide": "Guia de configuração",
"disabledByFeed": "Desativado pelo feed Radar",

View File

@@ -13379,12 +13379,12 @@
"colProvider": "Nhà cung cấp",
"colModel": "Mô hình",
"colQuota": "Hạn ngạch",
"colLimits": "Giới hạn",
"trainsOnPrompts": "Huấn luyện bằng prompt",
"trainsOnPromptsHelp": "Nhà cung cấp này công bố có thể dùng prompt của bạn để huấn luyện mô hình",
"colContext": "Ngữ cảnh",
"colCapabilities": "Khả năng",
"colTos": "Rủi ro ToS",
"colLimits": "__MISSING__:Rate limits",
"trainsOnPrompts": "__MISSING__:Trains on prompts",
"trainsOnPromptsHelp": "__MISSING__:This provider's terms state it may train on the prompts you send. Models without this badge either state they do not, or do not document it — an absent statement is not a guarantee.",
"newBadge": "mới",
"setupGuide": "Hướng dẫn thiết lập",
"disabledByFeed": "Bị vô hiệu hóa bởi nguồn cấp dữ liệu Radar",

View File

@@ -482,66 +482,7 @@ export const ID_TO_ALIAS = new Proxy({} as Record<string, string>, {
},
});
// Providers that support usage/quota API
export const USAGE_SUPPORTED_PROVIDERS = [
"antigravity",
"agy",
"kiro",
"amazon-q",
"github",
"codex",
"claude",
"cursor",
"qoder",
"kimi-coding",
"kimi-coding-apikey",
"glm",
"glm-cn",
"zai",
"glmt",
"opencode-go",
"ollama-cloud",
"minimax",
"minimax-cn",
"crof",
"nanogpt",
"deepseek",
"xiaomi-mimo",
"xiaomi-mimo-token-plan",
"vertex",
"vertex-partner",
"codebuddy-cn",
// PromptQL playground credits (getCreditSummary → USD micros)
"promptql",
"pql",
// Adobe Firefly web (cookie/JWT as apikey) — GET firefly.adobe.io/v1/credits/balance
"adobe-firefly",
"firefly",
"hyperagent",
"ha",
// xAI OAuth (Grok) weekly quota (id + public alias, same pattern as ha/agy)
"xai-oauth",
"xao",
// Grok Build subscription, billing credits, and auto top-up status
"grok-cli",
// Firecrawl team credits (GET /v2/team/credit-usage)
"firecrawl",
// Volcano Ark Plan subscriptions (agent-plan / coding-plan)
"volcengine-agent-plan",
"volcengine-coding-plan",
// Command Code credits + 5h/weekly rolling windows
"command-code",
"conol-web",
"cnl",
// Alibaba Coding Plan triple-window quota (#9603 UI gap — fetcher existed, list entry missing)
"bailian-coding-plan",
// Qwen Cloud / Model Studio personal Token Plan (cookie-authenticated console gateway)
"qwen-cloud-token-plan",
// AgentRouter (New-API) console balance quota (consoleApiKey + newApiUserId)
"agentrouter",
// Kilo Code personal USD balance (GET /api/profile/balance, existing OAuth token)
"kilocode",
];
export { USAGE_SUPPORTED_PROVIDERS } from "@omniroute/open-sse/services/usage/supportedProviders.ts";
// ── Zod validation, lazily on first AI_PROVIDERS access (perf: skips the walk
// for processes that never touch AI_PROVIDERS, e.g. short-lived CLI commands) ──

View File

@@ -7,6 +7,7 @@ import {
} from "../../open-sse/config/providerPluginManifest.ts";
import type { RegistryEntry } from "../../open-sse/config/providers/shared.ts";
import { USAGE_FETCHER_PROVIDERS } from "../../open-sse/services/usage/fetcherProviders.ts";
import { USAGE_SUPPORTED_PROVIDERS } from "../../open-sse/services/usage/supportedProviders.ts";
const registryFixture: Record<string, RegistryEntry> = {
openai: {
@@ -182,3 +183,81 @@ test("usage-fetch matches the fetcher list by alias too (#11722)", () => {
assert.ok(entry);
assert.ok(entry.capabilities.includes("usage-fetch"));
});
test("manifest advertises usage-supported for providers whose usage API is accepted (#10078)", () => {
// claude is in USAGE_SUPPORTED_PROVIDERS, openai is not — assert against the real
// list so the test cannot drift silently if the list moves.
const claude = getProviderPluginManifestEntryFromRegistry(registryFixture, "claude");
assert.ok(claude);
assert.ok(
(USAGE_SUPPORTED_PROVIDERS as readonly string[]).includes("claude"),
"fixture guard: claude must stay in USAGE_SUPPORTED_PROVIDERS for this test to mean anything"
);
assert.ok(
claude.capabilities.includes("usage-supported"),
"claude is in USAGE_SUPPORTED_PROVIDERS, so the manifest must advertise usage-supported"
);
});
test("manifest omits usage-supported for providers outside USAGE_SUPPORTED_PROVIDERS (#10078)", () => {
for (const providerId of ["openai", "anthropic", "claude-web"]) {
const entry = getProviderPluginManifestEntryFromRegistry(registryFixture, providerId);
assert.ok(entry, `fixture guard: ${providerId} must resolve`);
assert.equal(
(USAGE_SUPPORTED_PROVIDERS as readonly string[]).includes(entry.id),
false,
`fixture guard: ${entry.id} must stay out of USAGE_SUPPORTED_PROVIDERS`
);
assert.equal(
entry.capabilities.includes("usage-supported"),
false,
`${entry.id} is not in USAGE_SUPPORTED_PROVIDERS, so usage-supported must not be advertised`
);
}
});
test("usage-supported matches only on id, not alias (#10078)", () => {
// USAGE_SUPPORTED_PROVIDERS is checked with a plain .includes(providerId) — no alias
// resolution (providerQuotaVisibility.ts:12, providerLimits.ts:178). The manifest must
// keep the same rule: an alias-only hit must NOT emit the tag.
const aliasOnlyFixture: Record<string, RegistryEntry> = {
"some-provider": {
id: "some-provider",
alias: "claude",
format: "openai",
executor: "default",
baseUrl: "https://some.example/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
models: [{ id: "m1", name: "M1" }],
},
};
assert.equal(
(USAGE_SUPPORTED_PROVIDERS as readonly string[]).includes("some-provider"),
false,
"fixture guard: the id must NOT be in the list"
);
assert.ok(
(USAGE_SUPPORTED_PROVIDERS as readonly string[]).includes("claude"),
"fixture guard: the alias must be in the list, otherwise this test proves nothing"
);
const entry = getProviderPluginManifestEntryFromRegistry(aliasOnlyFixture, "some-provider");
assert.ok(entry);
assert.equal(
entry.capabilities.includes("usage-supported"),
false,
"usage-supported is id-only — an alias hit must not advertise it"
);
// Sanity: the same entry MUST still carry usage-fetch via its alias, proving the
// two tags deliberately diverge on alias handling.
assert.ok(
(USAGE_FETCHER_PROVIDERS as readonly string[]).includes("claude"),
"fixture guard: claude must also be in USAGE_FETCHER_PROVIDERS for the divergence check"
);
assert.ok(entry.capabilities.includes("usage-fetch"));
});

View File

@@ -34,23 +34,6 @@ test("passthrough no fake: tool_only contentLength==0 -> no estimate (tool_calls
import { createSSEStream } from "../../open-sse/utils/stream.ts";
function collectSSE(stream: TransformStream<Uint8Array, Uint8Array>) {
return async (writable: WritableStream<Uint8Array>, readable: ReadableStream<Uint8Array>) => {
const chunks: string[] = [];
const decoder = new TextDecoder();
const reader = readable.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(decoder.decode(value, { stream: true }));
}
} finally {
reader.releaseLock();
}
return chunks.join("");
};
}
function parseSSEUsage(sseText: string): unknown[] {
return sseText
@@ -107,7 +90,7 @@ test("passthrough SSE: finish stop without usage + include_usage:true -> emits u
assert.ok(typeof usage.completion_tokens === "number" && usage.completion_tokens > 0);
});
test("passthrough SSE: trailing choices:[] valid after estimated finish -> trailing is dropped (estimated wins)", async () => {
test("passthrough SSE: real trailing choices:[] usage is forwarded; no estimate is emitted (real wins)", async () => {
const body = { model: "m", messages: [{ role: "user", content: "hi" }], stream: true, stream_options: { include_usage: true } };
const stream = createSSEStream({
mode: "passthrough" as const,
@@ -129,17 +112,23 @@ test("passthrough SSE: trailing choices:[] valid after estimated finish -> trail
})();
const enc = new TextEncoder();
await writer.write(enc.encode(`data: ${JSON.stringify({ id: "chatcmpl-1", object: "chat.completion.chunk", choices: [{ index: 0, delta: { content: "hello world" }, finish_reason: null }] })}\n\n`));
// finish without usage -> should estimate (injectedUsage=false at that point)
// finish without usage -> passes through untouched (estimate only happens at flush, and only if no usage ever arrives)
await writer.write(enc.encode(`data: ${JSON.stringify({ id: "chatcmpl-1", object: "chat.completion.chunk", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n`));
// trailing choices:[] with valid usage 50ms after -> inside empty-choices block hasValid(emptyChoicesUsage)&&!injectedUsage is now false, so chunk is dropped (warn path)
// trailing choices:[] with valid usage -> forwarded verbatim (marks passthroughForwardedUsage, so flush skips the estimate)
await writer.write(enc.encode(`data: ${JSON.stringify({ id: "chatcmpl-1", object: "chat.completion.chunk", choices: [], usage: { prompt_tokens: 8, completion_tokens: 6, total_tokens: 14 } })}\n\n`));
await writer.write(enc.encode("data: [DONE]\n\n"));
await writer.close();
const text = await readAll;
const parsed = parseSSEUsage(text);
const withUsage = parsed.filter((p: unknown) => (p as Record<string, unknown>).usage);
// With the guard, the trailing valid is dropped (estimated was already sent on finish). Without guard we would see 2 (double). We assert drop.
// If upstream ever sends real include_usage trailing, this documents the v1 tradeoff: estimated wins, valid is dropped.
assert.equal(withUsage.length, 1, `expected 1 usage (estimated, trailing dropped), got ${withUsage.length} — usages: ${JSON.stringify(withUsage.map((p) => (p as Record<string, unknown>).usage))}`);
assert.equal((withUsage[0] as Record<string, unknown> & { usage: Record<string, unknown> }).usage.estimated, true);
// v2 contract (#12151 follow-up): the upstream's REAL trailing usage block is forwarded
// and wins; the estimate exists only for upstreams that never report usage (emitted at
// flush). Exactly one usage block ever reaches the client — never two, never estimated
// when a real one arrived (the v1 "estimated wins" tradeoff was a billing regression).
assert.equal(withUsage.length, 1, `expected 1 usage (the real trailing block), got ${withUsage.length} — usages: ${JSON.stringify(withUsage.map((p) => (p as Record<string, unknown>).usage))}`);
const forwarded = (withUsage[0] as Record<string, unknown> & { usage: Record<string, unknown> }).usage;
assert.equal(forwarded.estimated, undefined);
assert.equal(forwarded.prompt_tokens, 8);
assert.equal(forwarded.completion_tokens, 6);
assert.equal(forwarded.total_tokens, 14);
});