@code-yeongyu/senpi-ai 2026.10.4 → 2026.10.6
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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +16 -0
- package/dist/model.js +10 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/nvidia.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/utils/overflow.d.ts +3 -0
- package/dist/utils/overflow.js +15 -1
- package/dist/utils/retry-hint.d.ts +1 -1
- package/dist/utils/retry-hint.js +5 -1
- package/dist/utils/tool-choice-fallback.d.ts +10 -0
- package/dist/utils/tool-choice-fallback.js +40 -1
- package/package.json +2 -2
package/dist/utils/overflow.d.ts
CHANGED
|
@@ -67,6 +67,7 @@ export declare function isRecoverableLength(message: AssistantMessage, desiredMa
|
|
|
67
67
|
*/
|
|
68
68
|
export declare function getOverflowPatterns(): RegExp[];
|
|
69
69
|
export declare function isCursorPayloadResourceExhausted(message: {
|
|
70
|
+
provider?: string;
|
|
70
71
|
stopReason?: string;
|
|
71
72
|
errorMessage?: string;
|
|
72
73
|
usage?: {
|
|
@@ -83,6 +84,7 @@ export declare function isCursorPayloadResourceExhausted(message: {
|
|
|
83
84
|
* context window.
|
|
84
85
|
*/
|
|
85
86
|
export declare function isCursorQuotaResourceExhausted(message: {
|
|
87
|
+
provider?: string;
|
|
86
88
|
stopReason?: string;
|
|
87
89
|
errorMessage?: string;
|
|
88
90
|
usage?: {
|
|
@@ -94,6 +96,7 @@ export declare function isCursorQuotaResourceExhausted(message: {
|
|
|
94
96
|
};
|
|
95
97
|
}, contextWindow: number): boolean;
|
|
96
98
|
export declare function isCursorZeroTokenResourceExhausted(message: {
|
|
99
|
+
provider?: string;
|
|
97
100
|
stopReason?: string;
|
|
98
101
|
errorMessage?: string;
|
|
99
102
|
usage?: {
|
package/dist/utils/overflow.js
CHANGED
|
@@ -215,6 +215,15 @@ export function isRecoverableLength(message, desiredMaxOutput) {
|
|
|
215
215
|
export function getOverflowPatterns() {
|
|
216
216
|
return [...OVERFLOW_PATTERNS];
|
|
217
217
|
}
|
|
218
|
+
const CURSOR_PROVIDERS = new Set(["cursor", "cursor-cli-oauth"]);
|
|
219
|
+
/**
|
|
220
|
+
* The zero-token / quota `resource_exhausted` signatures below describe Cursor's backend only.
|
|
221
|
+
* Other providers send `resource_exhausted` for their own rate and usage limits (senpi#2660), and
|
|
222
|
+
* reading those as a Cursor payload overflow or re-mint skips the fallback chain.
|
|
223
|
+
*/
|
|
224
|
+
function isCursorProviderMessage(message) {
|
|
225
|
+
return message.provider === undefined || CURSOR_PROVIDERS.has(message.provider);
|
|
226
|
+
}
|
|
218
227
|
function cursorZeroTokenCount(message) {
|
|
219
228
|
const usage = message.usage;
|
|
220
229
|
if (!usage)
|
|
@@ -222,6 +231,8 @@ function cursorZeroTokenCount(message) {
|
|
|
222
231
|
return (usage.totalTokens || (usage.input ?? 0) + (usage.output ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0));
|
|
223
232
|
}
|
|
224
233
|
export function isCursorPayloadResourceExhausted(message, _estimateTokens) {
|
|
234
|
+
if (!isCursorProviderMessage(message))
|
|
235
|
+
return false;
|
|
225
236
|
if (message.stopReason !== "error" || !/resource.?exhausted/i.test(message.errorMessage || "")) {
|
|
226
237
|
return false;
|
|
227
238
|
}
|
|
@@ -234,13 +245,16 @@ export function isCursorPayloadResourceExhausted(message, _estimateTokens) {
|
|
|
234
245
|
*/
|
|
235
246
|
export function isCursorQuotaResourceExhausted(message, contextWindow) {
|
|
236
247
|
const tokens = cursorZeroTokenCount(message);
|
|
237
|
-
return (message
|
|
248
|
+
return (isCursorProviderMessage(message) &&
|
|
249
|
+
message.stopReason === "error" &&
|
|
238
250
|
RESOURCE_EXHAUSTED_PATTERN.test(message.errorMessage || "") &&
|
|
239
251
|
contextWindow > 0 &&
|
|
240
252
|
tokens > 0 &&
|
|
241
253
|
tokens < contextWindow * 0.5);
|
|
242
254
|
}
|
|
243
255
|
export function isCursorZeroTokenResourceExhausted(message) {
|
|
256
|
+
if (!isCursorProviderMessage(message))
|
|
257
|
+
return false;
|
|
244
258
|
if (message.stopReason !== "error" || !/resource.?exhausted/i.test(message.errorMessage || "")) {
|
|
245
259
|
return false;
|
|
246
260
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. retry-after header (strict integer delta-seconds `^\d+$`, else HTTP-date)
|
|
7
7
|
* 3. x-ratelimit-reset{,-requests,-tokens} (epoch seconds, max-wins)
|
|
8
8
|
* 4. JSON retryDelay strings (ms/s/m units, recursive, max-wins; numeric rejected)
|
|
9
|
-
* 5. body prose (ms-marker, seconds-marker, relative, resets-at ISO8601)
|
|
9
|
+
* 5. body prose (ms-marker, seconds-marker, relative, resets-in, resets-at ISO8601)
|
|
10
10
|
*
|
|
11
11
|
* Eligibility gate: status 429 OR body marks rate-limit error.
|
|
12
12
|
* Explicit zero = 0 (beats lower-precedence positives).
|
package/dist/utils/retry-hint.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 2. retry-after header (strict integer delta-seconds `^\d+$`, else HTTP-date)
|
|
7
7
|
* 3. x-ratelimit-reset{,-requests,-tokens} (epoch seconds, max-wins)
|
|
8
8
|
* 4. JSON retryDelay strings (ms/s/m units, recursive, max-wins; numeric rejected)
|
|
9
|
-
* 5. body prose (ms-marker, seconds-marker, relative, resets-at ISO8601)
|
|
9
|
+
* 5. body prose (ms-marker, seconds-marker, relative, resets-in, resets-at ISO8601)
|
|
10
10
|
*
|
|
11
11
|
* Eligibility gate: status 429 OR body marks rate-limit error.
|
|
12
12
|
* Explicit zero = 0 (beats lower-precedence positives).
|
|
@@ -132,6 +132,10 @@ function fromBodyProse(bodyText, nowMs) {
|
|
|
132
132
|
return Number.parseInt(m[1], 10) * 1000;
|
|
133
133
|
// relative prose: try again | retry | wait after ... [in] N <unit>
|
|
134
134
|
m = bodyText.match(/(?:try again|retry|wait after).*?(?:in\s+)?(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|m|min|minutes?)\b/i);
|
|
135
|
+
if (m)
|
|
136
|
+
return convertUnit(Number.parseFloat(m[1]), m[2]);
|
|
137
|
+
// limit prose: [limit will] reset(s) in N <unit>
|
|
138
|
+
m = bodyText.match(/\bresets?\s+in\s+(\d+(?:\.\d+)?)\s*(ms|s|sec|seconds?|m|min|minutes?)\b/i);
|
|
135
139
|
if (m)
|
|
136
140
|
return convertUnit(Number.parseFloat(m[1]), m[2]);
|
|
137
141
|
// resets at <ISO8601-with-tz>
|
|
@@ -2,6 +2,16 @@ import type { Api, Model } from "../types.ts";
|
|
|
2
2
|
/** The wire identity a forced tool_choice refusal is remembered under. */
|
|
3
3
|
export type ForcedToolChoiceTarget = Pick<Model<Api>, "api" | "provider" | "baseUrl" | "id">;
|
|
4
4
|
export declare function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToolChoice: boolean): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* A 400 to a request that forced a tool and whose message names that very tool: its `tools.N` /
|
|
7
|
+
* `tools[N]` position or its quoted name. Strict-schema gateways refuse the forced tool's schema this
|
|
8
|
+
* way (senpi#2648); the same request without the forced choice is accepted. A 400 that names
|
|
9
|
+
* no tool, or another one, is not this refusal.
|
|
10
|
+
*/
|
|
11
|
+
export declare function refusalNamesForcedTool(error: unknown, params: {
|
|
12
|
+
readonly tool_choice?: unknown;
|
|
13
|
+
readonly tools?: unknown;
|
|
14
|
+
}): boolean;
|
|
5
15
|
export declare function omitToolChoiceParam<TParams extends {
|
|
6
16
|
tool_choice?: unknown;
|
|
7
17
|
}>(params: TParams): TParams;
|
|
@@ -50,6 +50,44 @@ export function isForcedToolChoiceUnsupportedError(error, sentForcedToolChoice)
|
|
|
50
50
|
// cannot force tool use. Use tool_choice 'auto' or 'none'."
|
|
51
51
|
/tool[_\s-]?choice\s+cannot\s+force\s+tool\s+use/is.test(message));
|
|
52
52
|
}
|
|
53
|
+
/** The tool a `tool_choice` forces, in the wire shapes senpi sends (Chat Completions, Responses, Messages). */
|
|
54
|
+
function forcedToolName(toolChoice) {
|
|
55
|
+
if (!isRecord(toolChoice))
|
|
56
|
+
return undefined;
|
|
57
|
+
const named = isRecord(toolChoice.function) ? toolChoice.function.name : toolChoice.name;
|
|
58
|
+
return typeof named === "string" && named.length > 0 ? named : undefined;
|
|
59
|
+
}
|
|
60
|
+
function toolNames(tools) {
|
|
61
|
+
if (!Array.isArray(tools))
|
|
62
|
+
return [];
|
|
63
|
+
return tools.map((tool) => {
|
|
64
|
+
if (!isRecord(tool))
|
|
65
|
+
return undefined;
|
|
66
|
+
const named = isRecord(tool.function) ? tool.function.name : tool.name;
|
|
67
|
+
return typeof named === "string" ? named : undefined;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function escapeRegExp(text) {
|
|
71
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A 400 to a request that forced a tool and whose message names that very tool: its `tools.N` /
|
|
75
|
+
* `tools[N]` position or its quoted name. Strict-schema gateways refuse the forced tool's schema this
|
|
76
|
+
* way (senpi#2648); the same request without the forced choice is accepted. A 400 that names
|
|
77
|
+
* no tool, or another one, is not this refusal.
|
|
78
|
+
*/
|
|
79
|
+
export function refusalNamesForcedTool(error, params) {
|
|
80
|
+
if (extractHttpStatus(error) !== 400)
|
|
81
|
+
return false;
|
|
82
|
+
const name = forcedToolName(params.tool_choice);
|
|
83
|
+
if (name === undefined)
|
|
84
|
+
return false;
|
|
85
|
+
const message = errorMessage(error);
|
|
86
|
+
const index = toolNames(params.tools).indexOf(name);
|
|
87
|
+
if (index >= 0 && new RegExp(`\\btools(?:\\.${index}|\\[${index}\\])(?!\\d)`).test(message))
|
|
88
|
+
return true;
|
|
89
|
+
return new RegExp(`['"\`]${escapeRegExp(name)}['"\`]`).test(message);
|
|
90
|
+
}
|
|
53
91
|
export function omitToolChoiceParam(params) {
|
|
54
92
|
const nextParams = { ...params };
|
|
55
93
|
delete nextParams.tool_choice;
|
|
@@ -82,7 +120,8 @@ export async function sendWithForcedToolChoiceFallback(request) {
|
|
|
82
120
|
return { params: request.params, result: await request.send(request.params) };
|
|
83
121
|
}
|
|
84
122
|
catch (error) {
|
|
85
|
-
if (!isForcedToolChoiceUnsupportedError(error, forced)
|
|
123
|
+
if (!isForcedToolChoiceUnsupportedError(error, forced) &&
|
|
124
|
+
!(forced && refusalNamesForcedTool(error, request.params)))
|
|
86
125
|
throw error;
|
|
87
126
|
const params = omitToolChoiceParam(request.params);
|
|
88
127
|
const result = await request.send(params);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@code-yeongyu/senpi-ai",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.6",
|
|
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.10.
|
|
91
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.6",
|
|
92
92
|
"@google/genai": "2.23.0",
|
|
93
93
|
"@smithy/node-http-handler": "4.12.1",
|
|
94
94
|
"http-proxy-agent": "9.1.0",
|