Skip to content
Merged
8 changes: 5 additions & 3 deletions config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@
"src/lib/providers/validation.ts": 4523,
"src/lib/resilience/settings.ts": 841,
"src/lib/tailscaleTunnel.ts": 1202,
"src/lib/usage/callLogs.ts": 975,
"src/lib/usage/callLogs.ts": 985,
"src/lib/usage/providerLimits.ts": 955,
"src/lib/usage/usageHistory.ts": 983,
"src/lib/usage/usageHistory.ts": 988,
"_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
"src/shared/components/OAuthModal.tsx": 969,
"src/shared/components/RequestLoggerV2.tsx": 1316,
Expand All @@ -240,7 +240,9 @@
"src/shared/constants/sidebarVisibility.ts": 1198,
"src/shared/services/cliRuntime.ts": 1090,
"src/shared/validation/schemas.ts": 2523,
"src/sse/handlers/chat.ts": 1575,
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
"src/sse/handlers/chat.ts": 1632,
"src/sse/handlers/chatHelpers.ts": 811,
"src/sse/services/auth.ts": 2401,
"open-sse/executors/default.ts": 876,
"open-sse/translator/request/openai-responses.ts": 902,
Expand Down
3 changes: 3 additions & 0 deletions open-sse/handlers/chatCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ export async function handleChatCore({
cachedSettings = null,
skipUpstreamRetry = false,
createPiiTransform = null,
correlationId = null,
}) {
let { provider, model, extendedContext } = modelInfo;
// ── Memory pressure guard ────────────────────────────────────────────
Expand Down Expand Up @@ -660,6 +661,7 @@ export async function handleChatCore({
clientRequest: clientRawRequest?.body ?? body,
providerRequest: initialProviderRequest,
stage: "registered",
correlationId,
}) || generateRequestId();

// Initialize rate limit settings from persisted DB (once, lazy)
Expand Down Expand Up @@ -745,6 +747,7 @@ export async function handleChatCore({
tokensCompressed,
apiKeyInfo,
noLogEnabled,
correlationId,
});

// Primary path: merge client model id + alias target so config on either key applies; resolved
Expand Down
3 changes: 3 additions & 0 deletions open-sse/handlers/chatCore/attemptLogging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type PersistAttemptLogsContext = {
tokensCompressed: unknown;
apiKeyInfo: { id?: string | null; name?: string | null } | null | undefined;
noLogEnabled: unknown;
correlationId?: string | null;
};

function toConnectionId(value: unknown): string | null {
Expand Down Expand Up @@ -106,6 +107,7 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
tokensCompressed,
apiKeyInfo,
noLogEnabled,
correlationId,
} = ctx;
const initialConnectionId = toConnectionId(connectionId);
const finalConnectionId = toConnectionId(credentials?.connectionId) || initialConnectionId;
Expand Down Expand Up @@ -200,5 +202,6 @@ export function persistAttemptLogs(args: PersistAttemptLogsArgs, ctx: PersistAtt
apiKeyName: apiKeyInfo?.name || null,
noLog: noLogEnabled,
pipelinePayloads,
correlationId,
}).catch(() => {});
}
2 changes: 1 addition & 1 deletion src/app/api/v1/messages/count_tokens/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function POST(request) {
return estimated;
}

const executor = getExecutor(modelInfo.provider);
const executor = await getExecutor(modelInfo.provider);
// The provider-side count is a real upstream call — it must honor the
// connection's proxy assignment exactly like chat execution does.
const proxyInfo = await safeResolveProxy(credentials.connectionId);
Expand Down
3 changes: 2 additions & 1 deletion src/lib/db/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
* All domain modules import `getDbInstance` and helpers from here.
*/

import type { SqliteAdapter } from "./adapters/types";

Check failure on line 7 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './adapters/types.js'?
import {
tryOpenSync,
getSqlJsAdapter,
preInitSqlJs,
openDatabaseAsync,
} from "./adapters/driverFactory";

Check failure on line 13 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './adapters/driverFactory.js'?
import path from "path";
import fs from "fs";
import { resolveWritableDataDir, getLegacyDotDataDir } from "../dataPaths";

Check failure on line 16 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../dataPaths.js'?
import { runMigrations } from "./migrationRunner";

Check failure on line 17 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './migrationRunner.js'?
import { runDbHealthCheck } from "./healthCheck";

Check failure on line 18 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './healthCheck.js'?
import { resetAllDbModuleState } from "./stateReset";

Check failure on line 19 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './stateReset.js'?
import { parseStoredPayload } from "../logPayloads";

Check failure on line 20 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean '../logPayloads.js'?
import { DEFAULT_DATABASE_SETTINGS, type DatabaseSettings } from "@/types/databaseSettings";

Check failure on line 21 in src/lib/db/core.ts

View workflow job for this annotation

GitHub Actions / dast-smoke

Cannot find module '@/types/databaseSettings' or its corresponding type declarations.
import {
applyDatabaseOptimizationSettingsForDb,
applyStoredDatabaseOptimizationSettings,
Expand Down Expand Up @@ -341,7 +341,8 @@
has_request_body INTEGER DEFAULT 0,
has_response_body INTEGER DEFAULT 0,
has_pipeline_details INTEGER DEFAULT 0,
request_summary TEXT
request_summary TEXT,
correlation_id TEXT
);
CREATE INDEX IF NOT EXISTS idx_cl_timestamp ON call_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_cl_status ON call_logs(status);
Expand Down
2 changes: 2 additions & 0 deletions src/lib/db/migrations/109_call_logs_correlation_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE call_logs ADD COLUMN correlation_id TEXT;
CREATE INDEX IF NOT EXISTS idx_cl_correlation_id ON call_logs(correlation_id);
5 changes: 5 additions & 0 deletions src/lib/db/schemaColumns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ export function ensureCallLogsColumns(db: SqliteDatabase) {
db.exec("ALTER TABLE call_logs ADD COLUMN request_summary TEXT DEFAULT NULL");
console.log("[DB] Added call_logs.request_summary column");
}
if (!columnNames.has("correlation_id")) {
db.exec("ALTER TABLE call_logs ADD COLUMN correlation_id TEXT DEFAULT NULL");
console.log("[DB] Added call_logs.correlation_id column");
}

db.exec(
"CREATE INDEX IF NOT EXISTS idx_call_logs_requested_model ON call_logs(requested_model)"
Expand All @@ -187,6 +191,7 @@ export function ensureCallLogsColumns(db: SqliteDatabase) {
db.exec(
"CREATE INDEX IF NOT EXISTS idx_cl_combo_target ON call_logs(combo_name, combo_execution_key, timestamp)"
);
db.exec("CREATE INDEX IF NOT EXISTS idx_cl_correlation_id ON call_logs(correlation_id)");
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
console.warn("[DB] Failed to verify call_logs schema:", message);
Expand Down
14 changes: 12 additions & 2 deletions src/lib/usage/callLogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type CallLogSummaryRow = {
request_summary: string | null;
provider_node_prefix?: string | null;
resolved_account?: string | null;
correlation_id?: string | null;
};

const RESOLVED_ACCOUNT_SQL = "COALESCE(NULLIF(pc.name, ''), NULLIF(pc.email, ''), cl.account)";
Expand Down Expand Up @@ -578,6 +579,7 @@ function mapSummaryRow(row: CallLogSummaryRow) {
hasRequestBody: toNumber(row.has_request_body) === 1,
hasResponseBody: toNumber(row.has_response_body) === 1,
hasPipelineDetails: toNumber(row.has_pipeline_details) === 1,
correlationId: row.correlation_id || null,
};
}

Expand Down Expand Up @@ -659,6 +661,7 @@ export async function saveCallLog(entry: any) {
comboStepId: toStringOrNull(entry.comboStepId),
comboExecutionKey:
toStringOrNull(entry.comboExecutionKey) || toStringOrNull(entry.comboStepId),
correlationId: entry.correlationId || null,
};

const requestSummary = noLogEnabled
Expand Down Expand Up @@ -705,7 +708,8 @@ export async function saveCallLog(entry: any) {
cache_source, request_type, source_format, target_format, api_key_id, api_key_name,
combo_name, combo_step_id, combo_execution_key, error_summary, detail_state,
artifact_relpath, artifact_size_bytes, artifact_sha256,
has_request_body, has_response_body, has_pipeline_details, request_summary
has_request_body, has_response_body, has_pipeline_details, request_summary,
correlation_id
)
VALUES (
@id, @timestamp, @method, @path, @status, @model, @requestedModel, @provider,
Expand All @@ -714,7 +718,8 @@ export async function saveCallLog(entry: any) {
@cacheSource, @requestType, @sourceFormat, @targetFormat, @apiKeyId, @apiKeyName,
@comboName, @comboStepId, @comboExecutionKey, @errorSummary, @detailState,
@artifactRelPath, @artifactSizeBytes, @artifactSha256,
@hasRequestBody, @hasResponseBody, @hasPipelineDetails, @requestSummary
@hasRequestBody, @hasResponseBody, @hasPipelineDetails, @requestSummary,
@correlationId
)
`
).run({
Expand Down Expand Up @@ -831,6 +836,10 @@ export async function getCallLogs(filter: any = {}) {
conditions.push("(cl.api_key_name LIKE @apiKeyQ OR cl.api_key_id LIKE @apiKeyQ)");
params.apiKeyQ = `%${filter.apiKey}%`;
}
if (filter.correlationId) {
conditions.push("cl.correlation_id = @correlationId");
params.correlationId = filter.correlationId;
}
if (filter.combo) {
conditions.push("cl.combo_name IS NOT NULL");
}
Expand All @@ -851,6 +860,7 @@ export async function getCallLogs(filter: any = {}) {
cl.combo_name LIKE @searchQ OR CAST(cl.status AS TEXT) LIKE @searchQ
OR cl.combo_step_id LIKE @searchQ OR cl.combo_execution_key LIKE @searchQ
OR cl.error_summary LIKE @searchQ
OR cl.correlation_id LIKE @searchQ
)`);
params.searchQ = `%${filter.search}%`;
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/usage/providerLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ export async function refreshAndUpdateCredentials(
if (!shouldAttemptRotatingRefresh(connection.provider, opts.allowRotatingRefresh)) {
return { connection, refreshed: false };
}
const executor = getExecutor(connection.provider);
const executor = await getExecutor(connection.provider);
const credentials = {
connectionId: connection.id,
accessToken: connection.accessToken,
Expand Down
5 changes: 5 additions & 0 deletions src/lib/usage/usageHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export type PendingRequestMetadata = {
errorCode?: string | null;
stage?: string | null;
stageUpdatedAt?: number | null;
correlationId?: string | null;
};
export type PendingRequestDetail = {
id: string;
Expand All @@ -58,6 +59,7 @@ export type PendingRequestDetail = {
durationMs?: number | null;
stage?: string | null;
stageUpdatedAt?: number | null;
correlationId?: string | null;
streamChunks?: {
provider?: string[];
openai?: string[];
Expand Down Expand Up @@ -193,6 +195,9 @@ function normalizePendingMetadata(metadata?: PendingRequestMetadata): PendingReq
if (metadata.errorCode !== undefined) {
normalized.errorCode = toStringOrNull(metadata.errorCode) || null;
}
if (metadata.correlationId !== undefined) {
normalized.correlationId = toStringOrNull(metadata.correlationId) || null;
}

return normalized;
}
Expand Down
75 changes: 66 additions & 9 deletions src/sse/handlers/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
shouldRetryStreamEarlyEof,
withSessionHeader,
withSelectedConnectionHeader,
withCorrelationId,
} from "./chatHelpers";
import { connectionHasExtraKeys } from "@omniroute/open-sse/services/apiKeyRotator.ts";

Expand Down Expand Up @@ -229,13 +230,12 @@ export async function handleChat(
// a clear OmniRoute-level error before any routing or upstream call (#5110).
// Responses-API requests use `input` (not `messages`) so they are unaffected,
// and an absent `messages` field is left to downstream validation.
if (Array.isArray((body as { messages?: unknown }).messages) &&
(body as { messages: unknown[] }).messages.length === 0) {
if (
Array.isArray((body as { messages?: unknown }).messages) &&
(body as { messages: unknown[] }).messages.length === 0
) {
log.warn("CHAT", "Rejecting request with empty messages array");
return errorResponse(
HTTP_STATUS.BAD_REQUEST,
"messages: at least one message is required"
);
return errorResponse(HTTP_STATUS.BAD_REQUEST, "messages: at least one message is required");
}

// Build clientRawRequest for logging (if not provided)
Expand Down Expand Up @@ -689,6 +689,7 @@ export async function handleChat(
),
cachedSettings: settings,
providerId: target?.providerId ?? null,
correlationId: reqId,
},
target?.effectiveComboStrategy ?? combo.strategy,
true
Expand Down Expand Up @@ -716,6 +717,7 @@ export async function handleChat(
}
: undefined,
signal: request?.signal ?? null,
correlationId: reqId,
});

// ── Global Fallback Provider (#689) ────────────────────────────────────
Expand Down Expand Up @@ -766,7 +768,30 @@ export async function handleChat(

// Record telemetry
recordTelemetry(telemetry);
return withSessionHeader(response, sessionId);
// Log combo failures that bypassed handleChatCore (e.g. all targets skipped by circuit breaker)
if (!response.ok) {
try {
const { saveCallLog } = await import("@/lib/usageDb");
saveCallLog({
id: undefined,
method: "POST",
path: clientRawRequest?.endpoint || "/v1/chat/completions",
status: response.status,
model: body?.model || resolvedModelStr,
requestedModel: body?.model || resolvedModelStr,
provider: "-",
connectionId: undefined,
duration: Date.now() - (telemetry?.startTime || Date.now()),
tokens: {},
error: `[${response.status}] Combo "${combo.name}" failed — all targets exhausted`,
comboName: combo.name,
comboStepId: null,
comboExecutionKey: null,
correlationId: reqId,
}).catch(() => {});
} catch {}
}
return withCorrelationId(withSessionHeader(response, sessionId), reqId);
}
telemetry.endPhase();

Expand All @@ -784,12 +809,13 @@ export async function handleChat(
sessionAffinityKey,
forceLiveComboTest: isComboLiveTest,
forcedConnectionId: requestedConnectionId,
correlationId: reqId,
},
null,
false
);
recordTelemetry(telemetry);
return withSessionHeader(response, sessionId);
return withCorrelationId(withSessionHeader(response, sessionId), reqId);
}

export function buildClientRawRequest(request: Request, body: unknown) {
Expand Down Expand Up @@ -831,6 +857,7 @@ async function handleSingleModelChat(
preselectedCredentials?: any;
cachedSettings?: any;
providerId?: string | null;
correlationId?: string | null;
} = {},
comboStrategy: string | null = null,
isCombo: boolean = false
Expand All @@ -849,6 +876,10 @@ async function handleSingleModelChat(
// resolveModelOrError found a combo but the main handler's combo lookup missed it.
if ((resolved as any).combo) {
const redirectCombo = (resolved as any).combo;
log.info(
"ROUTING",
`Safety-net combo redirect for "${modelStr}" → combo="${redirectCombo.name}"`
);
log.info("ROUTING", `Auto-combo redirect from handleSingleModelChat for "${modelStr}"`);
log.info("ROUTING", `Auto-combo redirect to combo flow for "${modelStr}"`);
return handleComboChat({
Expand Down Expand Up @@ -885,6 +916,7 @@ async function handleSingleModelChat(
skipUpstreamRetry: target?.failoverBeforeRetry ?? false,
allowRateLimitedConnection: target?.allowRateLimitedConnection === true,
providerId: target?.providerId ?? null,
correlationId: runtimeOptions?.correlationId ?? null,
},
target?.effectiveComboStrategy ?? redirectCombo.strategy ?? "priority",
false
Expand All @@ -895,6 +927,7 @@ async function handleSingleModelChat(
allCombos: [],
relayOptions: undefined,
signal: request?.signal ?? null,
correlationId: reqId,
});
}

Expand Down Expand Up @@ -957,7 +990,30 @@ async function handleSingleModelChat(
providerProfile,
...(bypassReason ? { bypassReason } : {}),
});
if (gate) return gate;
if (gate) {
// Log the rejected request so it appears in /dashboard/logs
try {
const { saveCallLog } = await import("@/lib/usageDb");
saveCallLog({
id: undefined,
method: "POST",
path: clientRawRequest?.endpoint || "/v1/chat/completions",
status: gate.status,
model,
requestedModel: body?.model || modelStr,
provider,
connectionId: undefined,
duration: Date.now() - (telemetry?.startTime || Date.now()),
tokens: {},
error: `[${gate.status}] Pipeline gate rejected`,
comboName: isCombo ? comboName : null,
comboStepId: isCombo ? (runtimeOptions?.comboStepId ?? null) : null,
comboExecutionKey: isCombo ? (runtimeOptions?.comboExecutionKey ?? null) : null,
correlationId: runtimeOptions?.correlationId ?? null,
}).catch(() => {});
} catch {}
return gate;
}

// Issue #2100 follow-up: opt-in upstream 429 hint trust per provider.
const useHints429 = resolveUseUpstream429BreakerHints(
Expand Down Expand Up @@ -1201,6 +1257,7 @@ async function handleSingleModelChat(
providerProfile,
cachedSettings: runtimeOptions.cachedSettings,
skipUpstreamRetry: runtimeOptions.skipUpstreamRetry ?? false,
correlationId: runtimeOptions?.correlationId ?? null,
});
if (telemetry) telemetry.endPhase();

Expand Down
Loading
Loading