fix(ci): repair the API Route Typecheck base-red blocking every PR (#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.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-09-18 08:13:43 -03:00
committed by GitHub
parent 1b484c57a0
commit 24fc202d9f
5 changed files with 72 additions and 21 deletions

View File

@@ -0,0 +1 @@
- **ci:** repair the `API Route Typecheck` base-red on `release/v3.8.51` — type the awaited-callback contract of `runWithConnectionFetch` (glmResetCards), annotate `handleSingleModelChat(): Promise<Response>`, and give the codex-responses-ws bridge helpers real `{ error } | payload` discriminants so `"error" in x` narrows again; baseline ratcheted 294 → 283 (no widening).

View File

@@ -48,8 +48,7 @@
"TS2322": 1
},
"src/app/api/combos/test/route.ts": {
"TS2345": 1,
"TS2339": 1
"TS2345": 1
},
"src/app/api/compression/compare/route.ts": {
"TS2345": 1
@@ -70,11 +69,7 @@
"TS2554": 1
},
"src/app/api/internal/codex-responses-ws/route.ts": {
"TS2740": 1,
"TS2339": 7
},
"src/app/api/keys/[id]/route.ts": {
"TS2339": 1
"TS2339": 4
},
"src/app/api/local/redis/start/route.ts": {
"TS2339": 1

View File

@@ -34,7 +34,10 @@ import {
validateCodexWsDecision,
} from "@/lib/reasoningRouting/policy";
import { resolveRequestRoutingTags } from "@/domain/tagRouter";
import { validateApiKeyRoutingTarget } from "@/shared/utils/apiKeyPolicy";
import {
validateApiKeyRoutingTarget,
type ApiKeyMetadata as PolicyApiKeyMetadata,
} from "@/shared/utils/apiKeyPolicy";
import { persistResponsesWsCallHistory } from "./history";
import { applyResponsesWsCompression } from "./compression";
import { getComboByName } from "@/lib/db/combos";
@@ -50,7 +53,52 @@ const executor = new CodexExecutor();
const log = logger("RESPONSES_WS");
type JsonRecord = Record<string, unknown>;
type ApiKeyMetadata = Awaited<ReturnType<typeof getApiKeyMetadata>>;
// Key metadata reaches this bridge from two sources that each declare their own
// shape: `getApiKeyMetadata()` (every field required) and `enforceApiKeyPolicy()`
// (every field optional). The policy shape is the wider of the two and the db
// shape is assignable to it, so it is the only one that can hold both — pinning
// the alias to the db shape is what produced the "Type 'ApiKeyMetadata' is
// missing … from type 'ApiKeyMetadata'" mismatch at the policy boundary.
type ApiKeyMetadata = PolicyApiKeyMetadata | null;
/**
* Bridge helpers below either fail with a ready-made HTTP response or return
* their success payload. `error` must exist on exactly ONE member of each union:
* for an unannotated object-literal union TypeScript synthesises `error?:
* undefined` on the success member, and `"error" in x` then keeps that member
* too — which is how every `if ("error" in context)` guard in this file silently
* stopped narrowing. Annotating the returns keeps the discriminant real.
*/
type CodexWsFailure = { error: Response };
type CodexWsReasoningRoute = {
decision: Awaited<ReturnType<typeof resolveReasoningRoutingRule>>;
intent: ReturnType<typeof extractReasoningIntent>;
sourceModels: Awaited<ReturnType<typeof resolveReasoningSourceModels>>;
routingTags: ReturnType<typeof resolveRequestRoutingTags>;
};
type CodexWsCredentials = {
credentials: NonNullable<Awaited<ReturnType<typeof checkAndRefreshToken>>>;
leaseId: string;
};
type CodexWsRequestContext = CodexWsReasoningRoute & {
authRequest: Request;
apiKey: string | null;
responseBody: JsonRecord;
requestedModel: string;
clientHeaders: Record<string, string>;
metadata: ApiKeyMetadata;
allowedConnections: string[] | null;
};
type CodexWsUpstreamContext = CodexWsRequestContext &
CodexWsCredentials & {
provider: string;
model: string;
reasoningDecision: Awaited<ReturnType<typeof resolveReasoningRoutingRule>>;
};
const bridgePayloadSchema = z
.object({
@@ -325,10 +373,10 @@ async function enforceCodexWsApiKeyPolicy(
async function prepareReasoningRoute(
authRequest: Request,
apiKey: string | null,
metadata: ApiKeyMetadata | null,
metadata: ApiKeyMetadata,
requestedModel: string,
responseBody: JsonRecord
) {
): Promise<CodexWsFailure | CodexWsReasoningRoute> {
const reasoningIntent = extractReasoningIntent(requestedModel, responseBody);
const sourceModels = await resolveReasoningSourceModels(reasoningIntent.model, (model) =>
resolveCodexWsModelInfo(model, getModelInfo)
@@ -373,7 +421,7 @@ async function resolveCodexCredentials(
provider: string,
model: string,
allowedConnections: string[] | null
) {
): Promise<CodexWsFailure | CodexWsCredentials> {
const excludedConnectionIds: string[] = [];
let credentials: Awaited<ReturnType<typeof getProviderCredentialsWithQuotaPreflight>> = null;
@@ -428,7 +476,9 @@ async function resolveCodexCredentials(
};
}
async function resolveCodexRequestContext(body: JsonRecord) {
async function resolveCodexRequestContext(
body: JsonRecord
): Promise<CodexWsFailure | CodexWsRequestContext> {
if (!isFeatureFlagEnabled("OMNIROUTE_CODEX_WS_ENABLED")) {
return {
error: jsonError(503, "codex_ws_disabled", "Codex Responses WebSocket transport is disabled"),
@@ -477,7 +527,7 @@ async function resolveCodexRequestContext(body: JsonRecord) {
requestedModel,
responseBody
);
if (reasoningRoute.error) return { error: reasoningRoute.error };
if ("error" in reasoningRoute) return reasoningRoute;
return {
authRequest,
apiKey,
@@ -491,8 +541,8 @@ async function resolveCodexRequestContext(body: JsonRecord) {
}
async function resolveCodexUpstreamContext(
context: Awaited<ReturnType<typeof resolveCodexRequestContext>>
) {
context: CodexWsFailure | CodexWsRequestContext
): Promise<CodexWsFailure | CodexWsUpstreamContext> {
if ("error" in context) return context;
const routedModel = context.decision?.targetModel ?? context.requestedModel;
const modelInfo = await resolveCodexWsModelInfo(routedModel, getModelInfo);
@@ -512,7 +562,7 @@ async function resolveCodexUpstreamContext(
model,
context.allowedConnections
);
if (credentialResult.error) return credentialResult;
if ("error" in credentialResult) return credentialResult;
let reasoningDecision = context.decision;
if (!reasoningDecision) {
try {
@@ -563,7 +613,7 @@ async function resolveCodexProxy(provider: string): Promise<string | undefined>
try {
return proxyConfigToUrl(await resolveProxy(provider)) || undefined;
} catch (err) {
logger.warn(`[codex-responses-ws] proxy resolution failed: ${sanitizeErrorMessage(err)}`);
log.warn(`[codex-responses-ws] proxy resolution failed: ${sanitizeErrorMessage(err)}`);
return undefined;
}
}

View File

@@ -42,8 +42,13 @@ const OPERATION_LEASE_OWNER_PREFIX = "vlo_";
type ProxyLike = Parameters<typeof runWithProxyContext>[0];
/** A connection with no proxy must go explicitly direct — passing null to
* runWithProxyContext would inherit an ambient context instead. */
function runWithConnectionFetch<T>(proxy: ProxyLike, fn: () => T): T {
* runWithProxyContext would inherit an ambient context instead.
*
* `runWithProxyContext` is an untyped `async` JS helper (`Promise<any>`), so the
* awaited-callback contract has to be stated here: every call site passes an
* async callback and awaits the result. Declaring `fn: () => Promise<T>` keeps
* `runWithDirectFetchContext<Promise<T>>` returning `Promise<T>` too. */
function runWithConnectionFetch<T>(proxy: ProxyLike, fn: () => Promise<T>): Promise<T> {
return proxy ? runWithProxyContext(proxy, fn) : runWithDirectFetchContext(fn);
}

View File

@@ -1430,7 +1430,7 @@ async function handleSingleModelChat(
} = {},
comboStrategy: string | null = null,
isCombo: boolean = false
) {
): Promise<Response> {
// 1. Resolve model → provider/model
const resolved = await resolveModelOrError(
modelStr,