fix: show clean error for OpenAI encrypted content org mismatch (#884)

This commit is contained in:
jnjpng
2026-02-09 23:44:12 -08:00
committed by GitHub
parent 078a7d41ff
commit fe99bfe4fd
3 changed files with 210 additions and 6 deletions

View File

@@ -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<string, unknown>;
// 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<string, unknown>;
if (errObj.error && typeof errObj.error === "object") {
const inner = errObj.error as Record<string, unknown>;
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);