From dcc697c33db86287f1d41b42180d36fd0c5578f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 02:01:56 +0200 Subject: [PATCH] fix(ai): codex-client mirrors openai-client structured-error pattern P14. CodexClient previously threw a bare `Error("Codex request failed (500)")` on upstream failure, which made it impossible to triage from logs. Mirror the openai-client.ts:42-58 pattern: pull the body (truncated 500 chars), mask any leaked `sk-` / Bearer token, and tag the Error with `httpStatus`, `upstream`, `model`, and `bodyExcerpt` fields. Glitchtip and Wide Events now have the same shape for both providers. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/ai/codex-client.ts | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lib/ai/codex-client.ts b/src/lib/ai/codex-client.ts index 016698e26..107671adc 100644 --- a/src/lib/ai/codex-client.ts +++ b/src/lib/ai/codex-client.ts @@ -25,18 +25,47 @@ export class CodexClient implements AIProvider { this.accessToken = await this.onTokenRefresh(); const retryAttempt = await this.doRequest(params); if (!retryAttempt.ok) { - throw new Error(`Codex request failed after token refresh (${retryAttempt.status})`); + throw await this.buildUpstreamError( + retryAttempt, + "Codex request failed after token refresh", + ); } return this.parseResponse(retryAttempt); } if (!firstAttempt.ok) { - throw new Error(`Codex request failed (${firstAttempt.status})`); + throw await this.buildUpstreamError(firstAttempt, "Codex request failed"); } return this.parseResponse(firstAttempt); } + /** + * Build a structured error from a non-OK upstream response. Mirrors the + * pattern used in `OpenAIClient.generateCompletion` so logs and Glitchtip + * issues for both providers carry the same fields (httpStatus, upstream, + * model, bodyExcerpt). The body excerpt is truncated to 500 chars and any + * `sk-…` / `Bearer …` token is masked before it reaches log shipping. + */ + private async buildUpstreamError( + res: Response, + message: string, + ): Promise { + const rawBody = await res.text().catch(() => ""); + const bodyExcerpt = rawBody + .slice(0, 500) + .replace(/sk-(?:ant-)?[A-Za-z0-9_-]{8,}/g, "sk-***redacted***") + .replace(/Bearer\s+[A-Za-z0-9_.-]+/gi, "Bearer ***redacted***"); + const err = new Error(`${message} (${res.status})`); + Object.assign(err, { + httpStatus: res.status, + upstream: "codex", + model: CODEX_MODEL, + bodyExcerpt, + }); + return err; + } + private async doRequest(params: CompletionParams): Promise { return fetch(CODEX_ENDPOINT, { method: "POST",