bopodev-agent-sdk 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/README.md +27 -0
  4. package/dist/adapters/codex/src/server/quota.d.ts +2 -0
  5. package/dist/agent-sdk/src/adapters.d.ts +12 -1
  6. package/dist/agent-sdk/src/provider-failures/anthropic-api.d.ts +5 -0
  7. package/dist/agent-sdk/src/provider-failures/claude-code.d.ts +5 -0
  8. package/dist/agent-sdk/src/provider-failures/codex.d.ts +5 -0
  9. package/dist/agent-sdk/src/provider-failures/common.d.ts +7 -0
  10. package/dist/agent-sdk/src/provider-failures/cursor.d.ts +5 -0
  11. package/dist/agent-sdk/src/provider-failures/gemini-cli.d.ts +5 -0
  12. package/dist/agent-sdk/src/provider-failures/http.d.ts +5 -0
  13. package/dist/agent-sdk/src/provider-failures/index.d.ts +5 -0
  14. package/dist/agent-sdk/src/provider-failures/openai-api.d.ts +5 -0
  15. package/dist/agent-sdk/src/provider-failures/opencode.d.ts +5 -0
  16. package/dist/agent-sdk/src/provider-failures/shell.d.ts +5 -0
  17. package/dist/agent-sdk/src/provider-failures/types.d.ts +20 -0
  18. package/dist/agent-sdk/src/quota.d.ts +4 -0
  19. package/dist/agent-sdk/src/runtime-core.d.ts +1 -1
  20. package/dist/agent-sdk/src/runtime-http.d.ts +4 -2
  21. package/dist/agent-sdk/src/runtime-parsers.d.ts +1 -1
  22. package/dist/agent-sdk/src/runtime.d.ts +13 -0
  23. package/dist/agent-sdk/src/types.d.ts +19 -1
  24. package/dist/contracts/src/index.d.ts +426 -11
  25. package/package.json +2 -2
  26. package/src/adapters.ts +477 -58
  27. package/src/provider-failures/anthropic-api.ts +20 -0
  28. package/src/provider-failures/claude-code.ts +20 -0
  29. package/src/provider-failures/codex.ts +23 -0
  30. package/src/provider-failures/common.ts +86 -0
  31. package/src/provider-failures/cursor.ts +20 -0
  32. package/src/provider-failures/gemini-cli.ts +20 -0
  33. package/src/provider-failures/http.ts +12 -0
  34. package/src/provider-failures/index.ts +54 -0
  35. package/src/provider-failures/openai-api.ts +20 -0
  36. package/src/provider-failures/opencode.ts +20 -0
  37. package/src/provider-failures/shell.ts +12 -0
  38. package/src/provider-failures/types.ts +28 -0
  39. package/src/runtime-core.ts +7 -1
  40. package/src/runtime-http.ts +51 -6
  41. package/src/runtime-parsers.ts +1 -0
  42. package/src/runtime.ts +299 -1
  43. package/src/types.ts +20 -1
@@ -0,0 +1,20 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeAnthropicFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
9
+ return "Selected Anthropic model is unavailable for this account. Choose a supported model.";
10
+ }
11
+ if (/(auth|unauthorized|api key|invalid_api_key|permission denied)/.test(lower)) {
12
+ return "Authentication failed for provider runtime. Verify credentials and account access.";
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ export const anthropicApiFailureResolver = {
18
+ normalize: normalizeAnthropicFailureDetail,
19
+ classify: createClassifier(normalizeAnthropicFailureDetail)
20
+ };
@@ -0,0 +1,20 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeClaudeFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
9
+ return "Selected Anthropic model is unavailable for this account. Choose a supported model.";
10
+ }
11
+ if (/(auth|unauthorized|api key|invalid_api_key|permission denied|not logged in|claude login)/.test(lower)) {
12
+ return "Authentication failed for provider runtime. Verify credentials and account access.";
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ export const claudeCodeFailureResolver = {
18
+ normalize: normalizeClaudeFailureDetail,
19
+ classify: createClassifier(normalizeClaudeFailureDetail)
20
+ };
@@ -0,0 +1,23 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeCodexFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/not supported/.test(lower) && lower.includes("chatgpt account") && lower.includes("model")) {
9
+ return "Codex model not supported for this ChatGPT account. Select a supported Codex model.";
10
+ }
11
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
12
+ return "Selected model is unavailable for this account. Choose a supported model.";
13
+ }
14
+ if (/(auth|unauthorized|api key|invalid_api_key|permission denied)/.test(lower)) {
15
+ return "Authentication failed for provider runtime. Verify credentials and account access.";
16
+ }
17
+ return normalized;
18
+ };
19
+
20
+ export const codexFailureResolver = {
21
+ normalize: normalizeCodexFailureDetail,
22
+ classify: createClassifier(normalizeCodexFailureDetail)
23
+ };
@@ -0,0 +1,86 @@
1
+ import { containsUsageLimitHardStopFailure } from "../runtime-core";
2
+ import type {
3
+ ProviderFailureClassification,
4
+ ProviderFailureClassifier,
5
+ ProviderFailureInput,
6
+ ProviderFailureNormalizer
7
+ } from "./types";
8
+
9
+ export function normalizeWhitespace(value: string) {
10
+ return value.replace(/\s+/g, " ").trim();
11
+ }
12
+
13
+ export function hasModelUnsupportedSignal(haystack: string) {
14
+ return (
15
+ /model/.test(haystack) &&
16
+ /(not supported|unsupported|does not exist|not found|unavailable|invalid model|model_not_found)/.test(haystack)
17
+ );
18
+ }
19
+
20
+ export function hasAuthFailureSignal(haystack: string) {
21
+ return /(not logged in|login required|requires login|authentication required|authentication failed|unauthorized|invalid[_\s-]?api[_\s-]?key|api key)/.test(
22
+ haystack
23
+ );
24
+ }
25
+
26
+ export function hasUnknownSessionSignal(haystack: string) {
27
+ return /unknown session|session .* not found|could not resume|missing rollout path for thread|conversation .* not found|thread .* not found/.test(
28
+ haystack
29
+ );
30
+ }
31
+
32
+ export function classifyFromSignals(
33
+ normalize: ProviderFailureNormalizer,
34
+ input: ProviderFailureInput
35
+ ): ProviderFailureClassification {
36
+ const detail = normalize(input.detail);
37
+ const combined = `${detail}\n${input.stderr ?? ""}\n${input.stdout ?? ""}`.toLowerCase();
38
+ if (containsUsageLimitHardStopFailure(combined)) {
39
+ const blockerCode =
40
+ combined.includes("insufficient_quota") ||
41
+ combined.includes("billing_hard_limit_reached") ||
42
+ combined.includes("out of funds")
43
+ ? "provider_out_of_funds"
44
+ : "provider_quota_exhausted";
45
+ return {
46
+ detail,
47
+ blockerCode,
48
+ retryable: false,
49
+ providerUsageLimited: true
50
+ };
51
+ }
52
+ if (hasModelUnsupportedSignal(combined)) {
53
+ return {
54
+ detail,
55
+ blockerCode: "model_not_supported",
56
+ retryable: false,
57
+ providerUsageLimited: false
58
+ };
59
+ }
60
+ if (hasAuthFailureSignal(combined)) {
61
+ return {
62
+ detail,
63
+ blockerCode: "auth_required",
64
+ retryable: false,
65
+ providerUsageLimited: false
66
+ };
67
+ }
68
+ if (hasUnknownSessionSignal(combined)) {
69
+ return {
70
+ detail: "Saved provider session is no longer available. Retry with a fresh session.",
71
+ blockerCode: "unknown_session",
72
+ retryable: true,
73
+ providerUsageLimited: false
74
+ };
75
+ }
76
+ return {
77
+ detail,
78
+ blockerCode: input.failureType?.trim() || "runtime_failed",
79
+ retryable: input.failureType !== "auth" && input.failureType !== "bad_response",
80
+ providerUsageLimited: false
81
+ };
82
+ }
83
+
84
+ export function createClassifier(normalize: ProviderFailureNormalizer): ProviderFailureClassifier {
85
+ return (input) => classifyFromSignals(normalize, input);
86
+ }
@@ -0,0 +1,20 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeCursorFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
9
+ return "Selected model is unavailable for this account. Choose a supported model.";
10
+ }
11
+ if (/(auth|unauthorized|api key|invalid_api_key|permission denied)/.test(lower)) {
12
+ return "Authentication failed for provider runtime. Verify credentials and account access.";
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ export const cursorFailureResolver = {
18
+ normalize: normalizeCursorFailureDetail,
19
+ classify: createClassifier(normalizeCursorFailureDetail)
20
+ };
@@ -0,0 +1,20 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeGeminiFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
9
+ return "Selected model is unavailable for this account. Choose a supported model.";
10
+ }
11
+ if (/(auth|unauthorized|api key|permission denied)/.test(lower)) {
12
+ return "Gemini authentication failed. Verify Gemini credentials and account permissions.";
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ export const geminiCliFailureResolver = {
18
+ normalize: normalizeGeminiFailureDetail,
19
+ classify: createClassifier(normalizeGeminiFailureDetail)
20
+ };
@@ -0,0 +1,12 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeHttpFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ return normalized || detail;
7
+ };
8
+
9
+ export const httpFailureResolver = {
10
+ normalize: normalizeHttpFailureDetail,
11
+ classify: createClassifier(normalizeHttpFailureDetail)
12
+ };
@@ -0,0 +1,54 @@
1
+ import type { AgentProviderType } from "../types";
2
+ import { anthropicApiFailureResolver } from "./anthropic-api";
3
+ import { claudeCodeFailureResolver } from "./claude-code";
4
+ import { codexFailureResolver } from "./codex";
5
+ import { cursorFailureResolver } from "./cursor";
6
+ import { geminiCliFailureResolver } from "./gemini-cli";
7
+ import { httpFailureResolver } from "./http";
8
+ import { openAiApiFailureResolver } from "./openai-api";
9
+ import { opencodeFailureResolver } from "./opencode";
10
+ import { shellFailureResolver } from "./shell";
11
+ import type {
12
+ ProviderFailureClassification,
13
+ ProviderFailureInput,
14
+ ProviderFailureResolverMap
15
+ } from "./types";
16
+
17
+ const providerFailureResolvers: ProviderFailureResolverMap = {
18
+ codex: codexFailureResolver,
19
+ claude_code: claudeCodeFailureResolver,
20
+ openai_api: openAiApiFailureResolver,
21
+ anthropic_api: anthropicApiFailureResolver,
22
+ cursor: cursorFailureResolver,
23
+ opencode: opencodeFailureResolver,
24
+ gemini_cli: geminiCliFailureResolver,
25
+ shell: shellFailureResolver,
26
+ http: httpFailureResolver
27
+ };
28
+
29
+ export function normalizeProviderFailureDetail(provider: AgentProviderType, detail: string) {
30
+ const resolver = providerFailureResolvers[provider];
31
+ if (!resolver) {
32
+ return detail.replace(/\s+/g, " ").trim() || detail;
33
+ }
34
+ return resolver.normalize(detail);
35
+ }
36
+
37
+ export function classifyProviderFailure(
38
+ provider: AgentProviderType,
39
+ input: ProviderFailureInput
40
+ ): ProviderFailureClassification {
41
+ const resolver = providerFailureResolvers[provider];
42
+ if (!resolver) {
43
+ const normalized = input.detail.replace(/\s+/g, " ").trim() || input.detail;
44
+ return {
45
+ detail: normalized,
46
+ blockerCode: input.failureType?.trim() || "runtime_failed",
47
+ retryable: input.failureType !== "auth" && input.failureType !== "bad_response",
48
+ providerUsageLimited: false
49
+ };
50
+ }
51
+ return resolver.classify(input);
52
+ }
53
+
54
+ export type { ProviderFailureClassification, ProviderFailureInput } from "./types";
@@ -0,0 +1,20 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeOpenAiFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
9
+ return "Selected model is unavailable for this account. Choose a supported model.";
10
+ }
11
+ if (/(auth|unauthorized|api key|invalid_api_key|permission denied)/.test(lower)) {
12
+ return "Authentication failed for provider runtime. Verify credentials and account access.";
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ export const openAiApiFailureResolver = {
18
+ normalize: normalizeOpenAiFailureDetail,
19
+ classify: createClassifier(normalizeOpenAiFailureDetail)
20
+ };
@@ -0,0 +1,20 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeOpenCodeFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ if (!normalized) return detail;
7
+ const lower = normalized.toLowerCase();
8
+ if (/model/.test(lower) && /(not supported|does not exist|not found|unavailable|unsupported)/.test(lower)) {
9
+ return "Selected model is unavailable for this account. Choose a supported model.";
10
+ }
11
+ if (/(auth|unauthorized|api key|invalid_api_key|permission denied)/.test(lower)) {
12
+ return "Authentication failed for provider runtime. Verify credentials and account access.";
13
+ }
14
+ return normalized;
15
+ };
16
+
17
+ export const opencodeFailureResolver = {
18
+ normalize: normalizeOpenCodeFailureDetail,
19
+ classify: createClassifier(normalizeOpenCodeFailureDetail)
20
+ };
@@ -0,0 +1,12 @@
1
+ import { createClassifier, normalizeWhitespace } from "./common";
2
+ import type { ProviderFailureNormalizer } from "./types";
3
+
4
+ const normalizeShellFailureDetail: ProviderFailureNormalizer = (detail) => {
5
+ const normalized = normalizeWhitespace(detail);
6
+ return normalized || detail;
7
+ };
8
+
9
+ export const shellFailureResolver = {
10
+ normalize: normalizeShellFailureDetail,
11
+ classify: createClassifier(normalizeShellFailureDetail)
12
+ };
@@ -0,0 +1,28 @@
1
+ import type { AgentProviderType } from "../types";
2
+
3
+ export type ProviderFailureInput = {
4
+ detail: string;
5
+ stderr?: string;
6
+ stdout?: string;
7
+ failureType?: string | null;
8
+ };
9
+
10
+ export type ProviderFailureClassification = {
11
+ detail: string;
12
+ blockerCode: string;
13
+ retryable: boolean;
14
+ providerUsageLimited: boolean;
15
+ };
16
+
17
+ export type ProviderFailureClassifier = (
18
+ input: ProviderFailureInput
19
+ ) => ProviderFailureClassification;
20
+
21
+ export type ProviderFailureNormalizer = (detail: string) => string;
22
+
23
+ export type ProviderFailureResolver = {
24
+ normalize: ProviderFailureNormalizer;
25
+ classify: ProviderFailureClassifier;
26
+ };
27
+
28
+ export type ProviderFailureResolverMap = Partial<Record<AgentProviderType, ProviderFailureResolver>>;
@@ -4,4 +4,10 @@ export type {
4
4
  RuntimeExecutionOutput,
5
5
  RuntimeTranscriptEvent
6
6
  } from "./runtime";
7
- export { checkRuntimeCommandHealth, containsRateLimitFailure, executeAgentRuntime, executePromptRuntime } from "./runtime";
7
+ export {
8
+ checkRuntimeCommandHealth,
9
+ containsUsageLimitHardStopFailure,
10
+ containsRateLimitFailure,
11
+ executeAgentRuntime,
12
+ executePromptRuntime
13
+ } from "./runtime";
@@ -1,4 +1,6 @@
1
1
  import type { AgentRuntimeConfig } from "./types";
2
+ import type { AgentFinalRunOutput } from "bopodev-contracts";
3
+ import { containsUsageLimitHardStopFailure, parseAgentFinalRunOutput } from "./runtime";
2
4
 
3
5
  export type DirectApiProvider = "openai_api" | "anthropic_api";
4
6
 
@@ -10,10 +12,19 @@ export type DirectApiExecutionOutput = {
10
12
  elapsedMs: number;
11
13
  statusCode: number;
12
14
  summary?: string;
15
+ finalRunOutput?: AgentFinalRunOutput;
13
16
  tokenInput?: number;
14
17
  tokenOutput?: number;
15
18
  usdCost?: number;
16
- failureType?: "auth" | "rate_limit" | "timeout" | "network" | "bad_response" | "http_error";
19
+ failureType?:
20
+ | "auth"
21
+ | "rate_limit"
22
+ | "out_of_funds"
23
+ | "quota_exhausted"
24
+ | "timeout"
25
+ | "network"
26
+ | "bad_response"
27
+ | "http_error";
17
28
  error?: string;
18
29
  responsePreview?: string;
19
30
  attemptCount: number;
@@ -21,7 +32,15 @@ export type DirectApiExecutionOutput = {
21
32
  attempt: number;
22
33
  statusCode: number;
23
34
  elapsedMs: number;
24
- failureType?: "auth" | "rate_limit" | "timeout" | "network" | "bad_response" | "http_error";
35
+ failureType?:
36
+ | "auth"
37
+ | "rate_limit"
38
+ | "out_of_funds"
39
+ | "quota_exhausted"
40
+ | "timeout"
41
+ | "network"
42
+ | "bad_response"
43
+ | "http_error";
25
44
  error?: string;
26
45
  }>;
27
46
  };
@@ -133,8 +152,9 @@ export async function executeDirectApiRuntime(
133
152
  const preview = toPreview(text);
134
153
  const parsed = tryParseJson(text);
135
154
  if (!response.ok) {
136
- const failureType = classifyHttpFailure(response.status);
137
155
  const error = extractErrorMessage(provider, parsed, text) || `HTTP ${response.status}`;
156
+ const providerUsageLimited = containsUsageLimitHardStopFailure(`${error}\n${preview}\n${text}`);
157
+ const failureType = classifyHttpFailure(response.status, providerUsageLimited, error);
138
158
  attempts.push({
139
159
  attempt,
140
160
  statusCode: response.status,
@@ -149,7 +169,7 @@ export async function executeDirectApiRuntime(
149
169
  error,
150
170
  responsePreview: preview
151
171
  };
152
- if (!isRetryableFailure(failureType, response.status) || attempt >= maxAttempts) {
172
+ if (providerUsageLimited || !isRetryableFailure(failureType, response.status) || attempt >= maxAttempts) {
153
173
  break;
154
174
  }
155
175
  await sleep(retryBackoffMs * attempt);
@@ -175,6 +195,7 @@ export async function executeDirectApiRuntime(
175
195
  break;
176
196
  }
177
197
  const summary = provider === "openai_api" ? extractOpenAiSummary(parsed) : extractAnthropicSummary(parsed);
198
+ const finalRunOutput = parseAgentFinalRunOutput(summary);
178
199
  const usage = provider === "openai_api" ? extractOpenAiUsage(parsed) : extractAnthropicUsage(parsed);
179
200
  attempts.push({
180
201
  attempt,
@@ -189,6 +210,7 @@ export async function executeDirectApiRuntime(
189
210
  elapsedMs,
190
211
  statusCode: response.status,
191
212
  summary,
213
+ finalRunOutput: finalRunOutput.output,
192
214
  tokenInput: usage.tokenInput,
193
215
  tokenOutput: usage.tokenOutput,
194
216
  usdCost: usage.usdCost ?? 0,
@@ -349,14 +371,22 @@ function extractAnthropicUsage(parsed: Record<string, unknown>) {
349
371
  function extractErrorMessage(provider: DirectApiProvider, parsed: Record<string, unknown> | null, fallback: string) {
350
372
  const fallbackMessage = toPreview(fallback, 320);
351
373
  if (!parsed) return fallbackMessage;
374
+ if (typeof parsed.detail === "string" && parsed.detail.trim()) {
375
+ return parsed.detail.trim();
376
+ }
377
+ if (typeof parsed.reason === "string" && parsed.reason.trim()) {
378
+ return parsed.reason.trim();
379
+ }
352
380
  const error = parsed.error;
353
381
  if (typeof error === "string" && error.trim()) return error.trim();
354
382
  if (error && typeof error === "object") {
355
383
  const errorRecord = error as Record<string, unknown>;
356
384
  const candidates = [
357
385
  errorRecord.message,
386
+ errorRecord.detail,
358
387
  errorRecord.error?.toString(),
359
- (errorRecord.details as string | undefined) ?? undefined
388
+ (errorRecord.details as string | undefined) ?? undefined,
389
+ errorRecord.reason
360
390
  ];
361
391
  for (const candidate of candidates) {
362
392
  if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
@@ -368,8 +398,23 @@ function extractErrorMessage(provider: DirectApiProvider, parsed: Record<string,
368
398
  return fallbackMessage;
369
399
  }
370
400
 
371
- function classifyHttpFailure(statusCode: number): "auth" | "rate_limit" | "http_error" {
401
+ function classifyHttpFailure(
402
+ statusCode: number,
403
+ providerUsageLimited: boolean,
404
+ detail: string
405
+ ): "auth" | "rate_limit" | "out_of_funds" | "quota_exhausted" | "http_error" {
372
406
  if (statusCode === 401 || statusCode === 403) return "auth";
407
+ if (providerUsageLimited) {
408
+ const normalized = detail.toLowerCase();
409
+ if (
410
+ normalized.includes("insufficient_quota") ||
411
+ normalized.includes("billing_hard_limit_reached") ||
412
+ normalized.includes("out of funds")
413
+ ) {
414
+ return "out_of_funds";
415
+ }
416
+ return "quota_exhausted";
417
+ }
373
418
  if (statusCode === 429) return "rate_limit";
374
419
  return "http_error";
375
420
  }
@@ -1,4 +1,5 @@
1
1
  export {
2
+ parseAgentFinalRunOutput,
2
3
  parseClaudeStreamOutput,
3
4
  parseCursorStreamOutput,
4
5
  parseGeminiStreamOutput,