Repository navigation
feat(usage): redeem GLM Coding Plan Reset Cards from Provider Limits - #12754
Conversation
z.ai sells Reset Cards that clear an exhausted GLM coding-plan window (5-hour or weekly) ahead of its natural rollover, but OmniRoute only ever read the passive nextResetTime, so redeeming one meant leaving the dashboard. Add the wire layer for z.ai's two reset endpoints (/api/biz/customer-package-reset/list and /use), which authenticate with the same Bearer API key as /api/monitor/usage/quota/limit and report failures inside an HTTP-200 envelope, so callers must inspect success/code rather than the status line. The banked count rides along with the quota poll - only for keys that actually report a resettable window, and strictly best-effort so a card-less account or a transient failure still renders its quotas. The existing reset-credit card, picker and confirmation flow, until now gated to Codex, now also drive glm/glm-cn/glmt/zai through the new /api/usage/glm-reset-card route, reusing z.ai's requestId as the idempotency key so a retry cannot burn two cards.
… duplicates - Acquire a synthetic 60s exclusive-connection lease around each list/use wire operation; release in finally so a competing lease can acquire immediately after success or failure. - Coalesce same-key duplicates that arrive after lease acquisition by checking the in-flight attempt before loading the connection. - Run the post-commit quota refresh outside the lease (redemption is already committed; the refresh is auxiliary and failure-tolerant). - Do not discard a retained ambiguous attempt on a lease-conflict 409. - Harden transport error mapping: static messages for proxy transport failures, keep explicit direct routing for unproxied connections through list, use, and refresh.
|
This is a thorough, well-engineered PR — the fail-closed envelope validation, the |
# Conflicts: # src/app/(dashboard)/dashboard/usage/components/ProviderLimits/quotaParsing.ts # src/app/(dashboard)/dashboard/usage/components/ProviderLimits/useCodexResetCreditRedemption.ts # src/app/(dashboard)/dashboard/usage/components/ProviderLimits/utils.tsx # tests/unit/hard-session-lease-bypass-inventory.test.ts
241e63b
into
cheaper-inference:release/v3.8.51
…14079) The gate fails on the pure release/v3.8.51 tip (3 files above the frozen baseline), so every PR against the release is born red on it. None of the three is a PR defect — they are drift from merged work: - src/lib/usage/glmResetCards.ts: entered the gate's scope when #12754 added the route that imports it. runWithProxyContext is an untyped async helper (Promise<any>), so runWithConnectionFetch<T> could not return T. Every call site passes an async callback and awaits it — declare that contract: fn: () => Promise<T> → Promise<T>. - src/sse/handlers/chat.ts: handleSingleModelChat had no return annotation, so runWithTransientBackendRetry<T extends ResponseLike> fell back to the constraint and the value could no longer feed withSessionHeader(Response). Annotated Promise<Response> (every return path builds a Response). - src/app/api/internal/codex-responses-ws/route.ts: the bridge helpers return either { error: Response } or a payload, but as unannotated object-literal unions TypeScript synthesised error?: undefined on the success member, and every "error" in x guard stopped narrowing (TS2339 x5 on the destructure). Explicit return types keep the discriminant real; the ApiKeyMetadata alias now points at the policy shape (the wider one both sources are assignable to), which clears the TS2740 self-mismatch; logger.warn → log.warn (the module logger factory has no .warn). check-api-typecheck.mjs: OK — 283 errors, baseline ratcheted DOWN from 294 (codex-responses-ws 7→4 TS2339, TS2740 1→0; combos/test and keys/[id] 1→0). typecheck:core clean; 43/43 unit tests in the touched areas green.
…the write, drain the 09-18 base-reds (#14101) * fix(quality): drain the 09-18 base-reds, part 1 — thinking gate parity, inventory, webpack externals Reproduced on the clean tip 7cc454d before touching anything. Five of the failures trace to one commit, #12905 (b7192b7): it gated thinking-block emission on `requestedThinking === true` in the streaming translator, while its own non-streaming path documents `undefined` as the legacy caller shape that keeps "always a thinking block". The two paths disagreed on the same input, and the streaming side also synthesized the reasoning into a TEXT block for that legacy shape. chatCore always resolves a boolean, so production never sends `undefined` — but every direct caller and the older #5786 suites do. Aligned the streaming gate to the documented tri-state: `false` suppresses, `true` and `undefined` relay, and the fix-B text synthesis fires only on an explicit opt-out. The #12905 test that asserted suppression used a bare createState() (`undefined`) to mean "client did not request thinking"; it now passes `requestedThinking: false`, which is what that sentence resolves to in production. The whole thinking family — dsml, adapter, translator, non-stream parity, #13620, #5786, markdown boundary — is 77/77. #12864 added requestRejectedFailure.ts with a getProviderConnectionById read that seeds the refusal streak across restarts; inventoried as a connection state read next to the family-cooldown site it resembles. #13909 made machineToken.ts import ./dataPaths; the isolated webpack compile has no repo tree, so it joins the sibling externals. The free-tier budget card SVG was one wave behind again (482 -> 491 models). Refs #13866 * fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the rest Part 2 of the 09-18 base-red drain. Two of the remaining failures were not stale tests but production defects the tests had caught. #12911 taught accountSemaphore to read `maxQueueSize: 0` as "reject when the slot is busy", which is what its Codex WS lease wants. But chatCore forwards `resilienceSettings.requestQueue.maxQueueDepth` into that option, and that setting's documented default since #6593 is `0 = disabled`. Under default settings every request that found its account slot occupied was answered 429 "Semaphore queue full (0)" instead of waiting — the managed-lease routing test saw exactly that. `0` (and any non-positive value) is unbounded again; the lease gets an explicit `failFast` option and its four tests stay green, so the #12911 behaviour is preserved where it was meant to apply. A contract test pins the #6593 semantics on the semaphore itself. #12864 moved two providerFailure persistence branches out of chatCore into requestRejectedFailure.ts and the sanitization did not travel with them: three `lastError` writes stored the message as received. The only caller already hands in the projected persistentMessage, so nothing leaks today, but a persistence branch must be safe at its own write (docs/security/ ERROR_SANITIZATION.md) rather than trust whoever calls it. The module now sanitizes on entry, and the public-boundary guard — which caught this by counting sanitized writes in chatCore and coming up two short — covers the extracted module too, verified by mutating one write back to raw. The rest are tests that had fallen behind legitimate changes: - #12905 inserted `requestedThinking` as the 14th positional argument of createSSETransformStreamWithLogger; two tests passed customToolNames or the buffer budget at their old positions. Both production callers were already correct. - #12754 added a per-connection reset-card fetch after the quota fetch; the spacing test now marks a chunk at the quota request only. - #13910 renamed `error` to `errorMetadata` in the timeout classification; the probe matches the identifier with a backreference and still fails when BodyTimeoutError is removed from both sites. Refs #13866 * fix(test): pin the opt-out thinking cases to requestedThinking=false; keep acquireMany under the complexity ceiling The #12905 gate-restore suite encoded 'requestedThinking absent' as opt-out, the same undefined-means-false shape its non-streaming twin documents the other way and that the two-month-old #5786 suites contradict. The three opt-out cases now set the flag explicitly, which is what chatCore resolves for an opted-out client; the two opt-in cases already did. Both suites pass together (27/27). The failFast branch pushed acquireMany over the complexity ceiling it already sat on; the admission policy (fail-fast / bounded / unbounded queue) moves to findQueueRejection() and the new-code ratchet is back at its base. Refs #13866 * fix(test): suspend the #14110 redaction assertion inline; refresh the budget card The 57 commits merged since the previous validation moved two things. #13295 changed how an unknown-root path with an ambiguous tail is answered: where `Provider failed at /custom/internal secret directory` used to become `Provider failed at <path>` it now ships verbatim. The #12506 boundary guard caught it. Two candidate fixes were tried and each breaks one of the two live contracts — #12506's fail-closed swallow, or #13144's rule that a route in prose must survive — so the choice is the owner's (#14110). The one contested assertion is suspended inline with the exact line and the issue; the other nine stay active. The isolated-child harness requires tests == pass, which is why it is a comment and not a todo. The free-tier budget card was one wave behind again (491 -> 489 models). Refs #13866, #14110 * fix(providers): type the TinyCMS DOM stub global as a loose record #13957 typed the mock global as `typeof globalThis & Record<string, unknown>`. The api-route typecheck loads lib.dom, so that intersection carries the real Window / HTMLCanvasElement / document signatures — every stub assignment fails against a DOM constructor, and `delete g.window` narrows the object to `never` (13 diagnostics, the API Route Typecheck base-red on the tip). The function exists to overwrite those globals with stubs; it is now typed as the plain record it manipulates. 29/29 tinycms tests unchanged. Refs #13866 * fix(test): pin the last opt-out thinking sibling to requestedThinking=false translator-reasoning-gate-502-repro is the third #12905 test that encoded a bare state as opt-out; the previous sweep matched files by glob and missed it. The family is now enumerated by grep on requestedThinking (7 files) plus the two pre-#12905 suites: 83/83 together. Refs #13866
…heaper-inference#12754) * feat(usage): redeem GLM Coding Plan Reset Cards from Provider Limits z.ai sells Reset Cards that clear an exhausted GLM coding-plan window (5-hour or weekly) ahead of its natural rollover, but OmniRoute only ever read the passive nextResetTime, so redeeming one meant leaving the dashboard. Add the wire layer for z.ai's two reset endpoints (/api/biz/customer-package-reset/list and /use), which authenticate with the same Bearer API key as /api/monitor/usage/quota/limit and report failures inside an HTTP-200 envelope, so callers must inspect success/code rather than the status line. The banked count rides along with the quota poll - only for keys that actually report a resettable window, and strictly best-effort so a card-less account or a transient failure still renders its quotas. The existing reset-credit card, picker and confirmation flow, until now gated to Codex, now also drive glm/glm-cn/glmt/zai through the new /api/usage/glm-reset-card route, reusing z.ai's requestId as the idempotency key so a retry cannot burn two cards. * test(usage): cover GLM reset-card edge cases * test(dashboard): require GLM reset-card copy * fix(usage): harden GLM reset-card redemption * fix(usage): treat missing GLM key as empty * fix(usage): fence GLM reset-card operations and coalesce lease-window duplicates - Acquire a synthetic 60s exclusive-connection lease around each list/use wire operation; release in finally so a competing lease can acquire immediately after success or failure. - Coalesce same-key duplicates that arrive after lease acquisition by checking the in-flight attempt before loading the connection. - Run the post-commit quota refresh outside the lease (redemption is already committed; the refresh is auxiliary and failure-tolerant). - Do not discard a retained ambiguous attempt on a lease-conflict 409. - Harden transport error mapping: static messages for proxy transport failures, keep explicit direct routing for unproxied connections through list, use, and refresh. * fix(i18n): sync GLM reset-card keys to pt-BR and vi locales --------- Co-authored-by: insoln <is@careerum.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
…heaper-inference#14079) The gate fails on the pure release/v3.8.51 tip (3 files above the frozen baseline), so every PR against the release is born red on it. None of the three is a PR defect — they are drift from merged work: - src/lib/usage/glmResetCards.ts: entered the gate's scope when cheaper-inference#12754 added the route that imports it. runWithProxyContext is an untyped async helper (Promise<any>), so runWithConnectionFetch<T> could not return T. Every call site passes an async callback and awaits it — declare that contract: fn: () => Promise<T> → Promise<T>. - src/sse/handlers/chat.ts: handleSingleModelChat had no return annotation, so runWithTransientBackendRetry<T extends ResponseLike> fell back to the constraint and the value could no longer feed withSessionHeader(Response). Annotated Promise<Response> (every return path builds a Response). - src/app/api/internal/codex-responses-ws/route.ts: the bridge helpers return either { error: Response } or a payload, but as unannotated object-literal unions TypeScript synthesised error?: undefined on the success member, and every "error" in x guard stopped narrowing (TS2339 x5 on the destructure). Explicit return types keep the discriminant real; the ApiKeyMetadata alias now points at the policy shape (the wider one both sources are assignable to), which clears the TS2740 self-mismatch; logger.warn → log.warn (the module logger factory has no .warn). check-api-typecheck.mjs: OK — 283 errors, baseline ratcheted DOWN from 294 (codex-responses-ws 7→4 TS2339, TS2740 1→0; combos/test and keys/[id] 1→0). typecheck:core clean; 43/43 unit tests in the touched areas green.
…the write, drain the 09-18 base-reds (cheaper-inference#14101) * fix(quality): drain the 09-18 base-reds, part 1 — thinking gate parity, inventory, webpack externals Reproduced on the clean tip 7784784 before touching anything. Five of the failures trace to one commit, cheaper-inference#12905 (8b5f4dd): it gated thinking-block emission on `requestedThinking === true` in the streaming translator, while its own non-streaming path documents `undefined` as the legacy caller shape that keeps "always a thinking block". The two paths disagreed on the same input, and the streaming side also synthesized the reasoning into a TEXT block for that legacy shape. chatCore always resolves a boolean, so production never sends `undefined` — but every direct caller and the older cheaper-inference#5786 suites do. Aligned the streaming gate to the documented tri-state: `false` suppresses, `true` and `undefined` relay, and the fix-B text synthesis fires only on an explicit opt-out. The cheaper-inference#12905 test that asserted suppression used a bare createState() (`undefined`) to mean "client did not request thinking"; it now passes `requestedThinking: false`, which is what that sentence resolves to in production. The whole thinking family — dsml, adapter, translator, non-stream parity, cheaper-inference#13620, cheaper-inference#5786, markdown boundary — is 77/77. cheaper-inference#12864 added requestRejectedFailure.ts with a getProviderConnectionById read that seeds the refusal streak across restarts; inventoried as a connection state read next to the family-cooldown site it resembles. cheaper-inference#13909 made machineToken.ts import ./dataPaths; the isolated webpack compile has no repo tree, so it joins the sibling externals. The free-tier budget card SVG was one wave behind again (482 -> 491 models). Refs cheaper-inference#13866 * fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the rest Part 2 of the 09-18 base-red drain. Two of the remaining failures were not stale tests but production defects the tests had caught. cheaper-inference#12911 taught accountSemaphore to read `maxQueueSize: 0` as "reject when the slot is busy", which is what its Codex WS lease wants. But chatCore forwards `resilienceSettings.requestQueue.maxQueueDepth` into that option, and that setting's documented default since cheaper-inference#6593 is `0 = disabled`. Under default settings every request that found its account slot occupied was answered 429 "Semaphore queue full (0)" instead of waiting — the managed-lease routing test saw exactly that. `0` (and any non-positive value) is unbounded again; the lease gets an explicit `failFast` option and its four tests stay green, so the cheaper-inference#12911 behaviour is preserved where it was meant to apply. A contract test pins the cheaper-inference#6593 semantics on the semaphore itself. cheaper-inference#12864 moved two providerFailure persistence branches out of chatCore into requestRejectedFailure.ts and the sanitization did not travel with them: three `lastError` writes stored the message as received. The only caller already hands in the projected persistentMessage, so nothing leaks today, but a persistence branch must be safe at its own write (docs/security/ ERROR_SANITIZATION.md) rather than trust whoever calls it. The module now sanitizes on entry, and the public-boundary guard — which caught this by counting sanitized writes in chatCore and coming up two short — covers the extracted module too, verified by mutating one write back to raw. The rest are tests that had fallen behind legitimate changes: - cheaper-inference#12905 inserted `requestedThinking` as the 14th positional argument of createSSETransformStreamWithLogger; two tests passed customToolNames or the buffer budget at their old positions. Both production callers were already correct. - cheaper-inference#12754 added a per-connection reset-card fetch after the quota fetch; the spacing test now marks a chunk at the quota request only. - cheaper-inference#13910 renamed `error` to `errorMetadata` in the timeout classification; the probe matches the identifier with a backreference and still fails when BodyTimeoutError is removed from both sites. Refs cheaper-inference#13866 * fix(test): pin the opt-out thinking cases to requestedThinking=false; keep acquireMany under the complexity ceiling The cheaper-inference#12905 gate-restore suite encoded 'requestedThinking absent' as opt-out, the same undefined-means-false shape its non-streaming twin documents the other way and that the two-month-old cheaper-inference#5786 suites contradict. The three opt-out cases now set the flag explicitly, which is what chatCore resolves for an opted-out client; the two opt-in cases already did. Both suites pass together (27/27). The failFast branch pushed acquireMany over the complexity ceiling it already sat on; the admission policy (fail-fast / bounded / unbounded queue) moves to findQueueRejection() and the new-code ratchet is back at its base. Refs cheaper-inference#13866 * fix(test): suspend the cheaper-inference#14110 redaction assertion inline; refresh the budget card The 57 commits merged since the previous validation moved two things. cheaper-inference#13295 changed how an unknown-root path with an ambiguous tail is answered: where `Provider failed at /custom/internal secret directory` used to become `Provider failed at <path>` it now ships verbatim. The cheaper-inference#12506 boundary guard caught it. Two candidate fixes were tried and each breaks one of the two live contracts — cheaper-inference#12506's fail-closed swallow, or cheaper-inference#13144's rule that a route in prose must survive — so the choice is the owner's (cheaper-inference#14110). The one contested assertion is suspended inline with the exact line and the issue; the other nine stay active. The isolated-child harness requires tests == pass, which is why it is a comment and not a todo. The free-tier budget card was one wave behind again (491 -> 489 models). Refs cheaper-inference#13866, cheaper-inference#14110 * fix(providers): type the TinyCMS DOM stub global as a loose record cheaper-inference#13957 typed the mock global as `typeof globalThis & Record<string, unknown>`. The api-route typecheck loads lib.dom, so that intersection carries the real Window / HTMLCanvasElement / document signatures — every stub assignment fails against a DOM constructor, and `delete g.window` narrows the object to `never` (13 diagnostics, the API Route Typecheck base-red on the tip). The function exists to overwrite those globals with stubs; it is now typed as the plain record it manipulates. 29/29 tinycms tests unchanged. Refs cheaper-inference#13866 * fix(test): pin the last opt-out thinking sibling to requestedThinking=false translator-reasoning-gate-502-repro is the third cheaper-inference#12905 test that encoded a bare state as opt-out; the previous sweep matched files by glob and missed it. The family is now enumerated by grep on requestedThinking (7 files) plus the two pre-cheaper-inference#12905 suites: 83/83 together. Refs cheaper-inference#13866
Summary
glm,glm-cn,glmt, andzai.open-sse/services/usage/glmResetCards.ts(listGET /api/biz/customer-package-reset/list?targetType=PERSONAL, redeemPOST .../use) with Bearer auth,bigmodel-organization/bigmodel-projectteam headers, and 15 sAbortSignal.timeoutper request. International hostapi.z.ai, China hostopen.bigmodel.cn.success === trueplus numericcode∈ {0, 200}; list responses additionally require bothfiveHourResetsandweekResetsto be arrays, so a truncateddata: {}fails closed instead of parsing as an authoritative zero. Known code1001maps to 401. Public error messages are static or sanitized — no payload or credential text ever reaches a response body.consumed|redeeming|redeemed|used|expired|unavailablestatuses,available:false, and boolean consumed/redeemed flags. Timestamps parse both ISO and z.ai's timezone-lessYYYY-MM-DD HH:mm:ss(as UTC, with component validation). Parseably expired cards are dropped, unknown expiries are kept, and usable cards sort earliest-expiry-first with unknown last./usesuccess is the irreversible commit boundary. A later quota-refresh failure returns{ outcome: "reset", refreshPending: true }instead of HTTP 500 — committed success is never reported as a server error.connectionId + normalized idempotency keyand bound to the selected card: concurrent identical requests coalesce (including duplicates that arrive while the wire operation is already in flight), committed results replay for a short TTL, and a transport failure after card selection retains the selected card so a retry reissues the exact same/usebody andrequestIdwithout relisting. Reusing one key for a different card returns 409idempotency_key_conflict. Bounded to 500 entries / 10-minute TTL. This is process-local — it does not survive a restart and does not claim exactly-once semantics.proxy_unreachable503,proxy_family_unavailable/proxy_request_failed502,relay_timeout504) and static messages, never falling back to direct. Each list/use wire operation is fenced by a short synthetic exclusive-connection lease (60 s, released infinally); while it is active the connection cannot simultaneously hold a real exclusive lease. A lease-conflict 409 does not discard a retained ambiguous-retry attempt.getGlmUsage()requests the auxiliary card list only when the account has a resettable window (sessionorweeklyquota), and emitsbankedResetCreditson success. The count distinguishes authoritative zero (empty API key or confirmed empty list) from unknown (failed/truncated response →null); an unknown preserves the previously cached count, an authoritative zero clears it.zaiis dispatched through the GLM-family parsing/order path;getResetCreditEndpoint()is exhaustive (Codex → its route, the four GLM aliases → the GLM route, anything else → no mutation endpoint). GLM cards are labeled by window ("5-hour window" / "weekly window"), never "Full reset". GLM-specific i18n keys with safe English fallbacks (onlyen.jsonedited; legacy Codex-specifices/plstrings remain valid on the Codex path). The seven new GLM keys are also mirrored intopt-BR.jsonandvi.json(3acf3d01a) — the locale-parity gates (i18n-pt-brfix: i18n drift — pt-BR (and other locales) missing/untranslated keys after update (194 missing, ~1792 untranslated) #6695,i18n-vi-completeness) require full key parity and were failing when onlyen.jsonwas edited. The browser idempotency key persists across picker reopen after an ambiguous failure. Codex behavior and copy are unchanged.opencode-gokeeps GLM-family parsing but has no redemption endpoint.Related Issues
Validation
npm run lintCommands run locally (worktree at
307ea7c97, baseupstream/release/v3.8.51):node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test tests/unit/glm-reset-cards.test.ts— 23/23 passglm-reset-card-route,glm-reset-card-cache,hard-session-lease-bypass-inventory,provider-limits-ui,glm-team-quota— 63/63 passnpm run test:unit— pass except documented inherited base-red failures (🔴 Release branch not green: release/v3.8.51 #12732) and unrelated sanitizer/stream tests; no GLM reset-card failuresnpm run test:vitest— 463/465; the 2 failures areautoCombo/provider-family-combosandautoCombo/tieredRotation, which this PR does not touch (no files underopen-sse/services/combo*or MCP changed) — inheritednpm run typecheck:core— cleannpm run typecheck:noimplicit:core— cleannpm run lint— 0 errors (stale-suppressions note is inherited)npm run lint:json— passnpm run quality:scan:fast— passnpx prettier --checkon touched files,git diff --check— clean3acf3d01a):i18n-pt-br3/3,i18n-vi-completeness8/8,i18n-translation-ratio-gate4/4,lint:json, Prettier — all pass. CI on3acf3d01aconfirms: the four prior PR-introduced locale-parity failures are fixed, zero new test failures appeared; the remaining red checks are byte-identical inherited base-red failures (🔴 Release branch not green: release/v3.8.51 #12732) present on both heads.Tests Added Or Updated
tests/unit/glm-reset-cards.test.ts— full backend behavior: proxied/direct/ambient-context routing for list and redemption, dead-proxy fail-closed with typed 503 and no topology leakage, synthetic lease fencing + post-operation release, coalescing of concurrent and lease-window duplicates, ambiguous-transport retry with identical body/requestId, idempotency-key conflict, committed-success-with-failed-refresh, authoritative-zero vs unknown count, provider validation, availability/expiry/order parsingtests/unit/glm-reset-card-route.test.ts— API route: auth-before-parsing, schema rejection, typed status/code propagation, sanitized 500, envelope-401 mapping, malformed upstream → 502tests/unit/glm-reset-card-cache.test.ts— count merge semantics (unknown preserves cache, authoritative zero clears)tests/unit/glm-team-quota.test.ts—getGlmUsage()integration (no request without resettable window, one with, empty-key, zero, transient unknown)tests/unit/provider-limits-ui.test.ts— GLM-family ordering, modal copy/window labels, committed-refresh fallback, i18n keys with fallbacks, preserved Codex behaviortests/unit/hard-session-lease-bypass-inventory.test.ts— new class-B entry forsrc/lib/usage/glmResetCards.ts(one connection lookup, classified besidecodexResetCredits.ts)Coverage Notes
glm-reset-cards.test.ts(23 tests),glm-reset-card-route.test.ts,glm-reset-card-cache.test.ts,glm-team-quota.test.ts.provider-limits-ui.test.ts(ordering, copy, fallback behavior, i18n).Reviewer Notes
open-sse/executors/glm.tsTS2554 typecheck failure and the malformed unrelatedchangelog.d/fixes/reset-aware-model-family.mdare present on the base branch and are not addressed here (base-red fixes belong in their own freeze-gated PR).GET/POST /api/usage/glm-reset-card(management-authenticated, Zod-validated, sanitized errors).