@code-yeongyu/senpi-ai 2026.11.2-1 → 2026.11.2-3

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.
@@ -69,12 +69,12 @@ const vertexAuth = {
69
69
  const key = credential?.key ?? (await env("GOOGLE_CLOUD_API_KEY"));
70
70
  if (key)
71
71
  return { auth: { apiKey: key }, source: credential?.key ? "stored credential" : "GOOGLE_CLOUD_API_KEY" };
72
- const adcPath = credential?.env?.GOOGLE_APPLICATION_CREDENTIALS ?? (await env("GOOGLE_APPLICATION_CREDENTIALS"));
72
+ const adcPath = credential?.env?.GOOGLE_APPLICATION_CREDENTIALS || (await env("GOOGLE_APPLICATION_CREDENTIALS"));
73
73
  signal.throwIfAborted();
74
74
  const hasCredentials = await ctx.fileExists(adcPath ?? VERTEX_ADC_PATH);
75
75
  signal.throwIfAborted();
76
- const project = credential?.env?.GOOGLE_CLOUD_PROJECT ?? (await env("GOOGLE_CLOUD_PROJECT")) ?? (await env("GCLOUD_PROJECT"));
77
- const location = credential?.env?.GOOGLE_CLOUD_LOCATION ?? (await env("GOOGLE_CLOUD_LOCATION"));
76
+ const project = credential?.env?.GOOGLE_CLOUD_PROJECT || (await env("GOOGLE_CLOUD_PROJECT")) || (await env("GCLOUD_PROJECT"));
77
+ const location = credential?.env?.GOOGLE_CLOUD_LOCATION || (await env("GOOGLE_CLOUD_LOCATION"));
78
78
  if (hasCredentials && project && location) {
79
79
  return credential
80
80
  ? { auth: {}, env: credential.env, source: "stored credential" }
@@ -88,11 +88,36 @@ const QUOTA_EXHAUSTION_PATTERNS = [
88
88
  escapeRegExp(ANTHROPIC_THIRD_PARTY_BILLING_MARKER),
89
89
  ];
90
90
  const QUOTA_EXHAUSTION_PATTERN = buildProviderErrorPattern(QUOTA_EXHAUSTION_PATTERNS);
91
+ /**
92
+ * Quota markers that prove a spent account on their own. The bare `billing`
93
+ * substring is deliberately excluded: senpi#4632 shows a gateway 503 whose body
94
+ * merely mentions a billing lookup ("resolve Billing Scope: ... dial tcp: i/o
95
+ * timeout") — a dependency failure, not a spent account — and the substring
96
+ * also matches inside the code token `workspace_billing_scope_unavailable`.
97
+ */
98
+ const STRONG_QUOTA_EXHAUSTION_PATTERN = buildProviderErrorPattern(QUOTA_EXHAUSTION_PATTERNS.filter((pattern) => pattern !== "billing"));
99
+ /**
100
+ * Transient server-side evidence that gates bare billing prose (senpi#4632):
101
+ * a 5xx status token in the message, or the OpenAI-envelope `server_error`
102
+ * type. A genuine quota refusal carries an explicit code (insufficient_quota,
103
+ * usage_limit_reached, credits_required, ...) and keeps its verdict via the
104
+ * strong pattern above, whatever status wraps it.
105
+ */
106
+ const TRANSIENT_SERVER_SIGNAL_PATTERN = /(?:^|[^0-9])(?:500|502|503|504|520|522|524)(?:[^0-9]|$)|"type"\s*:\s*"server_error"/i;
91
107
  export function isQuotaExhaustionMessage(errorMessage) {
92
- return errorMessage !== undefined && QUOTA_EXHAUSTION_PATTERN.test(stripProviderRequestIds(errorMessage));
108
+ if (errorMessage === undefined)
109
+ return false;
110
+ const text = stripProviderRequestIds(errorMessage);
111
+ if (!QUOTA_EXHAUSTION_PATTERN.test(text))
112
+ return false;
113
+ // senpi#4632: bare "billing" prose on a transient 5xx is not a quota
114
+ // exhaustion unless a stronger quota marker is also present.
115
+ if (!STRONG_QUOTA_EXHAUSTION_PATTERN.test(text) && TRANSIENT_SERVER_SIGNAL_PATTERN.test(text)) {
116
+ return false;
117
+ }
118
+ return true;
93
119
  }
94
- const NON_RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
95
- ...QUOTA_EXHAUSTION_PATTERNS,
120
+ const NON_QUOTA_NON_RETRYABLE_PATTERNS = [
96
121
  // Request-shape rejections: the provider refused the payload we built, not the
97
122
  // work it describes. Gateways wrap these in whatever status they like — the
98
123
  // observed Apitopia/Kimi case arrives as `500 server_error: Invalid request:
@@ -109,6 +134,11 @@ const NON_RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
109
134
  // Legacy text-only SIWC failures. Structured diagnostics take precedence below;
110
135
  // this code alone proves no reset time.
111
136
  "subscription_sharing_usage_limit_exceeded",
137
+ ];
138
+ const NON_QUOTA_NON_RETRYABLE_PATTERN = buildProviderErrorPattern(NON_QUOTA_NON_RETRYABLE_PATTERNS);
139
+ const NON_RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
140
+ ...QUOTA_EXHAUSTION_PATTERNS,
141
+ ...NON_QUOTA_NON_RETRYABLE_PATTERNS,
112
142
  ]);
113
143
  const RETRYABLE_PROVIDER_ERROR_PATTERN = buildProviderErrorPattern([
114
144
  // Local credential/auth sidecar lock exhaustion is infrastructure contention,
@@ -534,8 +564,20 @@ export function classifyErrorMessage(errorMessage) {
534
564
  if (!errorMessage)
535
565
  return "unknown";
536
566
  const text = stripProviderRequestIds(errorMessage);
537
- if (NON_RETRYABLE_PROVIDER_ERROR_PATTERN.test(text))
567
+ if (NON_RETRYABLE_PROVIDER_ERROR_PATTERN.test(text)) {
568
+ // senpi#4632: when the only non-retryable signal is bare "billing" prose
569
+ // on a transient 5xx, the failure is a server-side transient, not a
570
+ // spent account — let the retryable patterns (503, timeout, ...) decide.
571
+ // A stronger quota marker or a request-shape/subscription marker keeps
572
+ // the terminal verdict, whatever status wraps it.
573
+ if (isQuotaExhaustionMessage(text) || NON_QUOTA_NON_RETRYABLE_PATTERN.test(text)) {
574
+ return "non-retryable";
575
+ }
576
+ if (TRANSIENT_SERVER_SIGNAL_PATTERN.test(text)) {
577
+ return RETRYABLE_PROVIDER_ERROR_PATTERN.test(text) ? "retryable" : "unknown";
578
+ }
538
579
  return "non-retryable";
580
+ }
539
581
  if (RETRYABLE_PROVIDER_ERROR_PATTERN.test(text))
540
582
  return "retryable";
541
583
  return "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/senpi-ai",
3
- "version": "2026.11.02-1",
3
+ "version": "2026.11.2-3",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -88,7 +88,7 @@
88
88
  "dependencies": {
89
89
  "@anthropic-ai/sdk": "0.127.0",
90
90
  "@aws-sdk/client-bedrock-runtime": "3.1136.0",
91
- "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.11.02-1",
91
+ "@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.11.2-3",
92
92
  "@google/genai": "2.23.0",
93
93
  "@smithy/node-http-handler": "4.12.1",
94
94
  "google-auth-library": "10.6.2",