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

@@ -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(

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);

View File

@@ -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" });