mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-15 19:21:08 +00:00
openai-codex: gate scope failures to codex
This commit is contained in:
@@ -180,14 +180,14 @@ describe("buildApiErrorObservationFields", () => {
|
||||
expect(observed.rawErrorPreview).toContain("custom");
|
||||
});
|
||||
|
||||
it("records runtime failure kind for missing-scope auth payloads", () => {
|
||||
it("keeps provider-less missing-scope auth payloads out of the codex-specific scope lane", () => {
|
||||
const observed = buildApiErrorObservationFields(
|
||||
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
|
||||
);
|
||||
|
||||
expect(observed).toMatchObject({
|
||||
httpCode: "401",
|
||||
providerRuntimeFailureKind: "auth_scope",
|
||||
providerRuntimeFailureKind: "unknown",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,7 +104,10 @@ function buildObservationFingerprint(params: {
|
||||
return getApiErrorPayloadFingerprint(params.raw);
|
||||
}
|
||||
|
||||
export function buildApiErrorObservationFields(rawError?: string): {
|
||||
export function buildApiErrorObservationFields(
|
||||
rawError?: string,
|
||||
opts?: { provider?: string },
|
||||
): {
|
||||
rawErrorPreview?: string;
|
||||
rawErrorHash?: string;
|
||||
rawErrorFingerprint?: string;
|
||||
@@ -146,6 +149,7 @@ export function buildApiErrorObservationFields(rawError?: string): {
|
||||
providerRuntimeFailureKind: classifyProviderRuntimeFailureKind({
|
||||
status: parsed?.httpCode ? Number(parsed.httpCode) : undefined,
|
||||
message: trimmed,
|
||||
provider: opts?.provider,
|
||||
}),
|
||||
providerErrorType: parsed?.type,
|
||||
providerErrorMessagePreview: truncateForObservation(
|
||||
@@ -159,7 +163,10 @@ export function buildApiErrorObservationFields(rawError?: string): {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildTextObservationFields(text?: string): {
|
||||
export function buildTextObservationFields(
|
||||
text?: string,
|
||||
opts?: { provider?: string },
|
||||
): {
|
||||
textPreview?: string;
|
||||
textHash?: string;
|
||||
textFingerprint?: string;
|
||||
@@ -169,7 +176,7 @@ export function buildTextObservationFields(text?: string): {
|
||||
providerErrorMessagePreview?: string;
|
||||
requestIdHash?: string;
|
||||
} {
|
||||
const observed = buildApiErrorObservationFields(text);
|
||||
const observed = buildApiErrorObservationFields(text, opts);
|
||||
return {
|
||||
textPreview: observed.rawErrorPreview,
|
||||
textHash: observed.rawErrorHash,
|
||||
|
||||
@@ -228,11 +228,20 @@ describe("formatAssistantErrorText", () => {
|
||||
const msg = makeAssistantError(
|
||||
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write model.request"}}',
|
||||
);
|
||||
expect(formatAssistantErrorText(msg)).toBe(
|
||||
expect(formatAssistantErrorText(msg, { provider: "openai-codex" })).toBe(
|
||||
"Authentication is missing the required OpenAI Codex scopes. Re-run OpenAI/Codex login and try again.",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not misdiagnose non-Codex permission errors as missing-scope failures", () => {
|
||||
const msg = makeAssistantError(
|
||||
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write model.request"}}',
|
||||
);
|
||||
expect(formatAssistantErrorText(msg, { provider: "openai" })).not.toContain(
|
||||
"required OpenAI Codex scopes",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an HTML-403 auth message for HTML provider auth failures", () => {
|
||||
const msg = makeAssistantError("403 <!DOCTYPE html><html><body>Access denied</body></html>");
|
||||
expect(formatAssistantErrorText(msg)).toBe(
|
||||
|
||||
@@ -1106,12 +1106,24 @@ describe("classifyFailoverReason", () => {
|
||||
describe("classifyProviderRuntimeFailureKind", () => {
|
||||
it("classifies missing scope failures", () => {
|
||||
expect(
|
||||
classifyProviderRuntimeFailureKind(
|
||||
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
|
||||
),
|
||||
classifyProviderRuntimeFailureKind({
|
||||
provider: "openai-codex",
|
||||
message:
|
||||
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
|
||||
}),
|
||||
).toBe("auth_scope");
|
||||
});
|
||||
|
||||
it("does not classify non-Codex permission errors as missing scope failures", () => {
|
||||
expect(
|
||||
classifyProviderRuntimeFailureKind({
|
||||
provider: "openai",
|
||||
message:
|
||||
'401 {"type":"error","error":{"type":"permission_error","message":"Missing scopes: api.responses.write"}}',
|
||||
}),
|
||||
).not.toBe("auth_scope");
|
||||
});
|
||||
|
||||
it("classifies OAuth refresh failures", () => {
|
||||
expect(
|
||||
classifyProviderRuntimeFailureKind(
|
||||
|
||||
@@ -502,10 +502,22 @@ function isHtmlErrorResponse(raw: string, status?: number): boolean {
|
||||
return HTML_BODY_RE.test(rest) && HTML_CLOSE_RE.test(rest);
|
||||
}
|
||||
|
||||
function isAuthScopeErrorMessage(raw: string, status?: number): boolean {
|
||||
function isOpenAICodexScopeContext(raw: string, provider?: string): boolean {
|
||||
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
|
||||
return (
|
||||
normalizedProvider === "openai-codex" ||
|
||||
/\bopenai\s+codex\b/i.test(raw) ||
|
||||
/\bcodex\b.*\bscopes?\b/i.test(raw)
|
||||
);
|
||||
}
|
||||
|
||||
function isAuthScopeErrorMessage(raw: string, status?: number, provider?: string): boolean {
|
||||
if (!raw) {
|
||||
return false;
|
||||
}
|
||||
if (!isOpenAICodexScopeContext(raw, provider)) {
|
||||
return false;
|
||||
}
|
||||
const inferred =
|
||||
typeof status === "number" && Number.isFinite(status)
|
||||
? status
|
||||
@@ -916,7 +928,7 @@ export function classifyProviderRuntimeFailureKind(
|
||||
if (message && classifyOAuthRefreshFailure(message)) {
|
||||
return "auth_refresh";
|
||||
}
|
||||
if (message && isAuthScopeErrorMessage(message, status)) {
|
||||
if (message && isAuthScopeErrorMessage(message, status, normalizedSignal.provider)) {
|
||||
return "auth_scope";
|
||||
}
|
||||
if (message && status === 403 && isHtmlErrorResponse(message, status)) {
|
||||
|
||||
@@ -60,9 +60,13 @@ export function handleAgentEnd(ctx: EmbeddedPiSubscribeContext): void | Promise<
|
||||
provider: lastAssistant.provider,
|
||||
});
|
||||
const errorText = (friendlyError || lastAssistant.errorMessage || "LLM request failed.").trim();
|
||||
const observedError = buildApiErrorObservationFields(rawError);
|
||||
const observedError = buildApiErrorObservationFields(rawError, {
|
||||
provider: lastAssistant.provider,
|
||||
});
|
||||
const safeErrorText =
|
||||
buildTextObservationFields(errorText).textPreview ?? "LLM request failed.";
|
||||
buildTextObservationFields(errorText, {
|
||||
provider: lastAssistant.provider,
|
||||
}).textPreview ?? "LLM request failed.";
|
||||
lifecycleErrorText = safeErrorText;
|
||||
const safeRunId = sanitizeForConsole(ctx.params.runId) ?? "-";
|
||||
const safeModel = sanitizeForConsole(lastAssistant.model) ?? "unknown";
|
||||
|
||||
Reference in New Issue
Block a user