@code-yeongyu/senpi-ai 2026.10.9 → 2026.10.10-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.
- package/dist/api/openai-completions.js +26 -2
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/providers/data/opencode-go.json +1 -1
- package/dist/providers/data/opengateway.json +1 -1
- package/dist/providers/data/openrouter.json +1 -1
- package/dist/providers/data/venice.json +1 -1
- package/dist/providers/data/vercel-ai-gateway.json +1 -1
- package/dist/utils/tool-choice-fallback.d.ts +6 -0
- package/dist/utils/tool-choice-fallback.js +69 -2
- package/package.json +2 -2
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
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
|
+
/**
|
|
5
|
+
* Reads `stream` until it has produced content (or ended, or `maxBuffered` contentless events went
|
|
6
|
+
* by), so a refusal a gateway sends in-band after answering 200 is raised while the request can still
|
|
7
|
+
* be retried. Such an error is marked as a request refusal; the events read are replayed in order.
|
|
8
|
+
*/
|
|
9
|
+
export declare function primeStreamUntilContent<T>(stream: AsyncIterable<T>, hasContent: (event: T) => boolean, maxBuffered?: number): Promise<AsyncIterable<T>>;
|
|
4
10
|
export declare function isForcedToolChoiceUnsupportedError(error: unknown, sentForcedToolChoice: boolean): boolean;
|
|
5
11
|
/**
|
|
6
12
|
* A 400 to a request that forced a tool and whose message names that very tool: its `tools.N` /
|
|
@@ -30,8 +30,75 @@ function errorMessage(error) {
|
|
|
30
30
|
}
|
|
31
31
|
return String(error);
|
|
32
32
|
}
|
|
33
|
+
/** Marks an error a stream raised in-band, after its 200 response but before any content (senpi#2801). */
|
|
34
|
+
const IN_BAND_BEFORE_CONTENT = Symbol.for("senpi.forcedToolChoice.inBandBeforeContent");
|
|
35
|
+
function isInBandBeforeContent(error) {
|
|
36
|
+
return isRecord(error) && error[IN_BAND_BEFORE_CONTENT] === true;
|
|
37
|
+
}
|
|
38
|
+
function markInBandBeforeContent(error) {
|
|
39
|
+
const marked = isRecord(error) ? error : new Error(errorMessage(error));
|
|
40
|
+
try {
|
|
41
|
+
Object.defineProperty(marked, IN_BAND_BEFORE_CONTENT, { value: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// A frozen error keeps its own identity; it is then judged like any other in-band error.
|
|
45
|
+
}
|
|
46
|
+
return marked;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A request refusal: the HTTP 400 that rejects the request, or the same refusal a gateway that has
|
|
50
|
+
* already answered 200 (a keepalive stream) sends in-band before any content (senpi#2801).
|
|
51
|
+
*/
|
|
52
|
+
function isRequestRefusal(error) {
|
|
53
|
+
const status = extractHttpStatus(error);
|
|
54
|
+
return status === 400 || (status === undefined && isInBandBeforeContent(error));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Reads `stream` until it has produced content (or ended, or `maxBuffered` contentless events went
|
|
58
|
+
* by), so a refusal a gateway sends in-band after answering 200 is raised while the request can still
|
|
59
|
+
* be retried. Such an error is marked as a request refusal; the events read are replayed in order.
|
|
60
|
+
*/
|
|
61
|
+
export async function primeStreamUntilContent(stream, hasContent, maxBuffered = 64) {
|
|
62
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
63
|
+
const buffered = [];
|
|
64
|
+
let ended = false;
|
|
65
|
+
try {
|
|
66
|
+
while (buffered.length < maxBuffered) {
|
|
67
|
+
const next = await iterator.next();
|
|
68
|
+
if (next.done) {
|
|
69
|
+
ended = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
buffered.push(next.value);
|
|
73
|
+
if (hasContent(next.value))
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
throw markInBandBeforeContent(error);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
async *[Symbol.asyncIterator]() {
|
|
82
|
+
try {
|
|
83
|
+
yield* buffered;
|
|
84
|
+
if (ended)
|
|
85
|
+
return;
|
|
86
|
+
while (true) {
|
|
87
|
+
const next = await iterator.next();
|
|
88
|
+
if (next.done)
|
|
89
|
+
return;
|
|
90
|
+
yield next.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
if (!ended)
|
|
95
|
+
await iterator.return?.();
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
33
100
|
export function isForcedToolChoiceUnsupportedError(error, sentForcedToolChoice) {
|
|
34
|
-
if (!sentForcedToolChoice ||
|
|
101
|
+
if (!sentForcedToolChoice || !isRequestRefusal(error)) {
|
|
35
102
|
return false;
|
|
36
103
|
}
|
|
37
104
|
const message = errorMessage(error);
|
|
@@ -77,7 +144,7 @@ function escapeRegExp(text) {
|
|
|
77
144
|
* no tool, or another one, is not this refusal.
|
|
78
145
|
*/
|
|
79
146
|
export function refusalNamesForcedTool(error, params) {
|
|
80
|
-
if (
|
|
147
|
+
if (!isRequestRefusal(error))
|
|
81
148
|
return false;
|
|
82
149
|
const name = forcedToolName(params.tool_choice);
|
|
83
150
|
if (name === undefined)
|
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.10-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.10.
|
|
91
|
+
"@earendil-works/pi-telemetry": "npm:@code-yeongyu/senpi-telemetry@2026.10.10-3",
|
|
92
92
|
"@google/genai": "2.23.0",
|
|
93
93
|
"@smithy/node-http-handler": "4.12.1",
|
|
94
94
|
"http-proxy-agent": "9.1.0",
|