fix: show clean error for OpenAI encrypted content org mismatch (#884)
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user