Skip to content

Commit 59311ce

Browse files
fix(daemon): classify byok opencode failures (#5309)
Co-authored-by: Siri-Ray <2667192167@qq.com>
1 parent d41cdd9 commit 59311ce

2 files changed

Lines changed: 87 additions & 5 deletions

File tree

apps/daemon/src/run-failure-classification.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,10 @@ function isAgentConfigInvalidText(text: string): boolean {
198198
/\bunknown variant [`'"][^`'"]+[`'"], expected\b[\s\S]*\bin `service_tier`/i.test(text) ||
199199
/\bdefault_permissions requires a `?\[permissions\]`? table\b/i.test(text) ||
200200
/\bdefault_permissions refers to undefined profile\b/i.test(text) ||
201-
/\bError loading config\.toml:[\s\S]*\bduplicate key\b/i.test(text);
201+
/\bError loading config\.toml:[\s\S]*\bduplicate key\b/i.test(text) ||
202+
/\bBYOK OpenCode requires a provider, API key, and model for this run\b/i.test(text) ||
203+
/\bBYOK_PROVIDER_REQUIRED\b/i.test(text) ||
204+
/\bEACCES: permission denied, mkdir\b[\s\S]*\.config[\\/]+opencode\b/i.test(text);
202205
}
203206

204207
function isCliNotInstalledText(text: string): boolean {
@@ -262,12 +265,13 @@ function isPromptTooLargeText(text: string): boolean {
262265
}
263266

264267
function isUpstreamDetailText(text: string): boolean {
265-
return /\b(stream disconnected before completion|response\.completed|Transport error: network error|Upstream request failed|websocket closed|socket connection was closed unexpectedly|tls handshake eof|Connection reset by (?:peer|server)|TLS close_notify|Broken pipe|remote host||No route to host|Connection refused|ConnectionRefused|error sending request|Provider returned error|high demand|model is at capacity|selected model is at capacity|temporarily unavailable|upstream_error|http2: response body closed|peer closed connection|incomplete chunked read|Client network socket disconnected before secure TLS connection|Connection failed repeatedly|lost its connection to the Anthropic API|Server error mid-response|empty or malformed response|Unexpected server error|Streaming response failed|Failed to process error response|gateway or proxy|Country, region, or territory not supported|AMR model catalog is (?:temporarily )?unavailable|statusCode[\"']?\s*:\s*(?:400|403|404)|400 Bad Request|403 Forbidden|404 Not Found|NotFoundError|OpenAIException - \{\"detail\":\"Not Found\"\}|API Error:\s*(?:400|403)\b)\b/i
266-
.test(text);
268+
return isUpstreamClientErrorText(text) ||
269+
/\b(stream disconnected before completion|response\.completed|Transport error: network error|Upstream request failed|websocket closed|socket connection was closed unexpectedly|tls handshake eof|Connection reset by (?:peer|server)|TLS close_notify|Broken pipe|remote host||No route to host|Connection refused|ConnectionRefused|error sending request|Provider returned error|high demand|model is at capacity|selected model is at capacity|temporarily unavailable|upstream_error|http2: response body closed|peer closed connection|incomplete chunked read|Client network socket disconnected before secure TLS connection|Connection failed repeatedly|lost its connection to (?:the Anthropic API|the configured custom Anthropic endpoint)|Server error mid-response|empty or malformed response|Unexpected server error|Streaming response failed|Failed to process error response|AMR model catalog is (?:temporarily )?unavailable)\b/i
270+
.test(text);
267271
}
268272

269273
function isUpstreamClientErrorText(text: string): boolean {
270-
return /\b(statusCode[\"']?\s*:\s*(?:400|403|404)|400 Bad Request|403 Forbidden|404 Not Found|NotFoundError|OpenAIException - \{\"detail\":\"Not Found\"\}|API Error:\s*(?:400|403)\b|Country, region, or territory not supported|gateway or proxy|validation error|literal_error|Invalid input|Type validation failed)\b/i
274+
return /\b(statusCode[\"']?\s*:\s*(?:400|403|404)|400 Bad Request|403 Forbidden|404 Not Found|404 page not found|Not Found:\s*(?:404 page not found|Not Found)|NotFoundError|OpenAIException - \{\"detail\":\"Not Found\"\}|API Error:\s*(?:400|403)\b|Country, region, or territory not supported|gateway or proxy|validation error|literal_error|Invalid input|Type validation failed|data did not match any variant of untagged enum InputParam)\b/i
271275
.test(text);
272276
}
273277

@@ -321,7 +325,7 @@ function upstreamDetail(text: string): TrackingRunFailureDetail {
321325
return 'provider_routing_error';
322326
}
323327
if (/\bhigh demand|temporary errors|model is at capacity|selected model is at capacity\b/i.test(text)) return 'provider_high_demand';
324-
if (/\b(stream disconnected before completion|response\.completed|websocket closed|socket connection was closed unexpectedly|connection reset|ConnectionRefused|tls handshake eof|tls close_notify|broken pipe|peer closed connection|remote host||http2: response body closed|incomplete chunked read|Client network socket disconnected before secure TLS connection|Connection failed repeatedly|lost its connection to the Anthropic API|Server error mid-response|empty or malformed response|Streaming response failed)\b/i
328+
if (/\b(stream disconnected before completion|response\.completed|websocket closed|socket connection was closed unexpectedly|connection reset|ConnectionRefused|tls handshake eof|tls close_notify|broken pipe|peer closed connection|remote host||http2: response body closed|incomplete chunked read|Client network socket disconnected before secure TLS connection|Connection failed repeatedly|lost its connection to (?:the Anthropic API|the configured custom Anthropic endpoint)|Server error mid-response|empty or malformed response|Streaming response failed)\b/i
325329
.test(text)) {
326330
return 'stream_disconnected';
327331
}

apps/daemon/tests/run-failure-classification.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1311,3 +1311,81 @@ describe('classifyRunFailure — batch A reclassification out of execution_faile
13111311
expect(result?.failure_detail).toBe('session_resume_expired');
13121312
});
13131313
});
1314+
1315+
describe('classifyRunFailure — BYOK OpenCode reclassification out of stream_error', () => {
1316+
it('classifies missing BYOK OpenCode run config as fixable agent config', () => {
1317+
const result = classify(
1318+
'BYOK_PROVIDER_REQUIRED',
1319+
'BYOK OpenCode requires a provider, API key, and model for this run.',
1320+
);
1321+
expect(result).toMatchObject({
1322+
failure_category: 'process_exit',
1323+
failure_detail: 'agent_config_invalid',
1324+
failure_stage: 'session_init',
1325+
retryable: false,
1326+
user_action: 'fix_config',
1327+
});
1328+
});
1329+
1330+
it('classifies BYOK OpenCode 404 provider responses as non-retryable upstream client errors', () => {
1331+
const result = classify(
1332+
'AGENT_EXECUTION_FAILED',
1333+
'json-rpc id 4: opencode event stream: opencode session error: Not Found: 404 page not found',
1334+
);
1335+
expect(result).toMatchObject({
1336+
failure_category: 'upstream_unavailable',
1337+
failure_detail: 'upstream_client_error',
1338+
failure_stage: 'first_token_wait',
1339+
retryable: false,
1340+
user_action: 'none',
1341+
});
1342+
});
1343+
1344+
it('classifies BYOK OpenCode provider request-shape rejections as non-retryable upstream client errors', () => {
1345+
const result = classify(
1346+
'AGENT_EXECUTION_FAILED',
1347+
'json-rpc id 4: opencode event stream: data did not match any variant of untagged enum InputParam',
1348+
);
1349+
expect(result).toMatchObject({
1350+
failure_category: 'upstream_unavailable',
1351+
failure_detail: 'upstream_client_error',
1352+
retryable: false,
1353+
user_action: 'none',
1354+
});
1355+
});
1356+
1357+
it('classifies BYOK OpenCode config directory permission errors as fixable agent config', () => {
1358+
const result = classify(
1359+
'AGENT_EXECUTION_FAILED',
1360+
[
1361+
"EACCES: permission denied, mkdir '/Users/11140200/.config/opencode'",
1362+
' path: "/Users/11140200/.config/opencode",',
1363+
' syscall: "mkdir",',
1364+
' errno: -13,',
1365+
' code: "EACCES"',
1366+
].join('\n'),
1367+
);
1368+
expect(result).toMatchObject({
1369+
failure_category: 'process_exit',
1370+
failure_detail: 'agent_config_invalid',
1371+
failure_stage: 'session_init',
1372+
retryable: false,
1373+
user_action: 'fix_config',
1374+
});
1375+
});
1376+
});
1377+
1378+
describe('classifyRunFailure — custom Anthropic endpoint disconnects', () => {
1379+
it('classifies configured custom Anthropic endpoint drops as stream_disconnected', () => {
1380+
const result = classify(
1381+
'AGENT_CONNECTION_DROPPED',
1382+
'Claude Code lost its connection to the configured custom Anthropic endpoint before the response finished.',
1383+
);
1384+
expect(result).toMatchObject({
1385+
failure_category: 'upstream_unavailable',
1386+
failure_detail: 'stream_disconnected',
1387+
retryable: true,
1388+
user_action: 'retry',
1389+
});
1390+
});
1391+
});

0 commit comments

Comments
 (0)