From fe99bfe4fd19cab7f5137ec3ec8bf3180e48fabb Mon Sep 17 00:00:00 2001 From: jnjpng Date: Mon, 9 Feb 2026 23:44:12 -0800 Subject: [PATCH] fix: show clean error for OpenAI encrypted content org mismatch (#884) --- src/cli/App.tsx | 23 ++++-- src/cli/helpers/errorFormatter.ts | 115 +++++++++++++++++++++++++++ src/tests/cli/errorFormatter.test.ts | 78 ++++++++++++++++++ 3 files changed, 210 insertions(+), 6 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index a47f006..7f864ee 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -179,7 +179,10 @@ import { parsePatchToAdvancedDiff, } from "./helpers/diff"; import { setErrorContext } from "./helpers/errorContext"; -import { formatErrorDetails } from "./helpers/errorFormatter"; +import { + formatErrorDetails, + isEncryptedContentError, +} from "./helpers/errorFormatter"; import { formatCompact } from "./helpers/format"; import { parsePatchOperations } from "./helpers/formatArgsDisplay"; import { @@ -4527,13 +4530,21 @@ export default function App({ errorObject, agentIdRef.current, ); + + // Encrypted content errors are self-explanatory (include /clear advice) + // — skip the generic "Something went wrong?" hint appendError(errorDetails, true); // Skip telemetry - already tracked above - // Show appropriate error hint based on stop reason - appendError( - getErrorHintForStopReason(stopReasonToHandle, currentModelId), - true, - ); + if (!isEncryptedContentError(errorObject)) { + // Show appropriate error hint based on stop reason + appendError( + getErrorHintForStopReason( + stopReasonToHandle, + currentModelId, + ), + true, + ); + } } else { // No error metadata, show generic error with run info appendError( diff --git a/src/cli/helpers/errorFormatter.ts b/src/cli/helpers/errorFormatter.ts index 7eb40a4..d5c438d 100644 --- a/src/cli/helpers/errorFormatter.ts +++ b/src/cli/helpers/errorFormatter.ts @@ -166,6 +166,117 @@ function isCreditExhaustedError(e: APIError, reasons?: string[]): boolean { return hasErrorReason(e, "not-enough-credits", reasons); } +const ENCRYPTED_CONTENT_HINT = [ + "", + "This occurs when the conversation contains messages with encrypted", + "reasoning from a different OpenAI authentication scope (e.g. switching", + "between ChatGPT OAuth and an OpenAI API key).", + "Use /clear to start a new conversation.", +].join("\n"); + +/** + * Walk the error object to find the `detail` string containing the encrypted content error. + * Handles both direct (e.detail) and nested (e.error.error.detail) structures. + */ +function findEncryptedContentDetail( + e: unknown, +): string | undefined { + if (typeof e !== "object" || e === null) return undefined; + const obj = e as Record; + + // Check direct: e.detail + if ( + typeof obj.detail === "string" && + obj.detail.includes("invalid_encrypted_content") + ) { + return obj.detail; + } + + // Check nested: e.error.error.detail or e.error.detail + if (obj.error && typeof obj.error === "object") { + const errObj = obj.error as Record; + if (errObj.error && typeof errObj.error === "object") { + const inner = errObj.error as Record; + if ( + typeof inner.detail === "string" && + inner.detail.includes("invalid_encrypted_content") + ) { + return inner.detail; + } + } + if ( + typeof errObj.detail === "string" && + errObj.detail.includes("invalid_encrypted_content") + ) { + return errObj.detail; + } + } + + return undefined; +} + +/** + * Check if the error contains an encrypted content organization mismatch from OpenAI/ChatGPT. + * This occurs when switching between ChatGPT OAuth and OpenAI API key auth, + * leaving encrypted reasoning tokens from a different auth scope in the conversation. + */ +function checkEncryptedContentError(e: unknown): string | undefined { + // Walk the object structure first (cheap) before falling back to stringify + const detail = findEncryptedContentDetail(e); + if (!detail) { + // Fallback: stringify for edge cases (e.g. plain string errors) + try { + const errorStr = typeof e === "string" ? e : JSON.stringify(e); + if (!errorStr.includes("invalid_encrypted_content")) return undefined; + } catch { + return undefined; + } + // Detected via stringify but couldn't extract detail — return generic message + return ( + "OpenAI error: Encrypted content could not be verified — organization mismatch." + + ENCRYPTED_CONTENT_HINT + ); + } + + // Try to parse the embedded JSON from the detail string for pretty-printing + try { + const jsonStart = detail.indexOf("{"); + if (jsonStart >= 0) { + const parsed = JSON.parse(detail.slice(jsonStart)); + const innerError = parsed.error || parsed; + if (innerError.code === "invalid_encrypted_content") { + const msg = String( + innerError.message || "Encrypted content verification failed.", + ).replaceAll('"', '\\"'); + return [ + "OpenAI error:", + " {", + ` type: "${innerError.type || "invalid_request_error"}",`, + ` code: "${innerError.code}",`, + ` message: "${msg}"`, + " }", + ENCRYPTED_CONTENT_HINT, + ].join("\n"); + } + } + } catch { + // Fall through to generic message + } + + return ( + "OpenAI error: Encrypted content could not be verified — organization mismatch." + + ENCRYPTED_CONTENT_HINT + ); +} + +/** + * Returns true if the error is an OpenAI encrypted content org mismatch. + * Used by callers to skip generic error hints for this self-explanatory error. + */ +export function isEncryptedContentError(e: unknown): boolean { + return findEncryptedContentDetail(e) !== undefined; +} + /** * Extract comprehensive error details from any error object * Handles APIError, Error, and other error types consistently @@ -180,6 +291,10 @@ export function formatErrorDetails( ): string { let runId: string | undefined; + // Check for OpenAI encrypted content org mismatch before anything else + const encryptedContentMsg = checkEncryptedContentError(e); + if (encryptedContentMsg) return encryptedContentMsg; + // Handle APIError from streaming (event: error) if (e instanceof APIError) { const reasons = getErrorReasons(e); diff --git a/src/tests/cli/errorFormatter.test.ts b/src/tests/cli/errorFormatter.test.ts index 8cab50b..f73addf 100644 --- a/src/tests/cli/errorFormatter.test.ts +++ b/src/tests/cli/errorFormatter.test.ts @@ -11,6 +11,84 @@ describe("formatErrorDetails", () => { clearErrorContext(); }); + describe("encrypted content org mismatch", () => { + const chatGptDetail = + 'INTERNAL_SERVER_ERROR: ChatGPT request failed (400): {\n "error": {\n "message": "The encrypted content for item rs_0dd1c85f779f9f0301698a7e40a0508193ba9a669d32159bf0 could not be verified. Reason: Encrypted content organization_id did not match the target organization.",\n "type": "invalid_request_error",\n "param": null,\n "code": "invalid_encrypted_content"\n }\n}'; + + test("handles nested error object from run metadata", () => { + // This is the errorObject shape constructed in App.tsx from run.metadata.error + const errorObject = { + error: { + error: { + message_type: "error_message", + run_id: "run-cb408f59-f901-4bde-ad1f-ed58a1f13482", + error_type: "internal_error", + message: "An error occurred during agent execution.", + detail: chatGptDetail, + seq_id: null, + }, + run_id: "run-cb408f59-f901-4bde-ad1f-ed58a1f13482", + }, + }; + + const result = formatErrorDetails(errorObject); + + expect(result).toContain("OpenAI error:"); + expect(result).toContain("invalid_encrypted_content"); + expect(result).toContain("/clear to start a new conversation."); + expect(result).toContain("different OpenAI authentication scope"); + // Should NOT be raw JSON + expect(result).not.toContain('"message_type"'); + expect(result).not.toContain('"run_id"'); + }); + + test("formats inner error as JSON-like block", () => { + const errorObject = { + error: { + error: { + detail: chatGptDetail, + }, + }, + }; + + const result = formatErrorDetails(errorObject); + + // JSON-like structured format + expect(result).toContain('type: "invalid_request_error"'); + expect(result).toContain('code: "invalid_encrypted_content"'); + expect(result).toContain("organization_id did not match"); + expect(result).toContain(" {"); + expect(result).toContain(" }"); + }); + + test("handles error with direct detail field", () => { + const errorObject = { + detail: chatGptDetail, + }; + + const result = formatErrorDetails(errorObject); + + expect(result).toContain("OpenAI error:"); + expect(result).toContain("/clear to start a new conversation."); + }); + + test("falls back gracefully when detail JSON is malformed", () => { + const errorObject = { + error: { + error: { + detail: + "INTERNAL_SERVER_ERROR: ChatGPT request failed (400): invalid_encrypted_content garbled", + }, + }, + }; + + const result = formatErrorDetails(errorObject); + + expect(result).toContain("OpenAI error:"); + expect(result).toContain("/clear to start a new conversation."); + }); + }); + test("uses neutral credit exhaustion copy for free tier not-enough-credits", () => { setErrorContext({ billingTier: "free", modelDisplayName: "Kimi K2.5" });