@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
|
|
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
|
|
77
|
-
const location = credential?.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" }
|
package/dist/utils/retry.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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",
|