pi-free 2.0.13 → 2.0.15
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/CHANGELOG.md +28 -0
- package/README.md +9 -5
- package/config.ts +15 -0
- package/constants.ts +3 -0
- package/index.ts +135 -0
- package/lib/built-in-toggle.ts +4 -4
- package/lib/probe-cache.ts +86 -0
- package/lib/provider-compat.ts +33 -0
- package/lib/registry.ts +25 -3
- package/lib/telemetry.ts +328 -0
- package/lib/util.ts +10 -1
- package/package.json +1 -1
- package/provider-failover/benchmark-lookup.ts +94 -8
- package/provider-failover/benchmarks-chunk-0.ts +599 -890
- package/provider-failover/benchmarks-chunk-1.ts +655 -924
- package/provider-failover/benchmarks-chunk-2.ts +675 -966
- package/provider-failover/benchmarks-chunk-3.ts +676 -967
- package/provider-failover/benchmarks-chunk-4.ts +704 -954
- package/provider-failover/benchmarks-chunk-5.ts +1301 -0
- package/provider-failover/hardcoded-benchmarks.ts +9 -3
- package/providers/cline/cline-models.ts +200 -68
- package/providers/cline/cline.ts +3 -3
- package/providers/dynamic-built-in/index.ts +1 -1
- package/providers/kilo/kilo.ts +2 -2
- package/providers/model-fetcher.ts +3 -1
- package/providers/nvidia/nvidia.ts +54 -16
- package/providers/ollama/ollama.ts +103 -46
- package/providers/opencode-session.ts +398 -371
- package/providers/qwen/qwen.ts +2 -2
- package/providers/routeway/routeway.ts +391 -0
|
@@ -1,371 +1,398 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
|
-
import { basename, dirname, join } from "node:path";
|
|
3
|
-
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { pathToFileURL } from "node:url";
|
|
6
|
-
import type {
|
|
7
|
-
Api,
|
|
8
|
-
AssistantMessage,
|
|
9
|
-
AssistantMessageEvent,
|
|
10
|
-
AssistantMessageEventStream,
|
|
11
|
-
Context,
|
|
12
|
-
Model,
|
|
13
|
-
SimpleStreamOptions,
|
|
14
|
-
} from "@earendil-works/pi-ai";
|
|
15
|
-
import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
|
|
17
|
-
export const OPENCODE_DYNAMIC_API = "opencode-dynamic" as const;
|
|
18
|
-
|
|
19
|
-
export const OPENCODE_STATIC_HEADERS = {
|
|
20
|
-
"User-Agent": "opencode/1.15.5",
|
|
21
|
-
"x-opencode-client": "cli",
|
|
22
|
-
} as const;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* OpenCode-native identifier generation.
|
|
26
|
-
*
|
|
27
|
-
* OpenCode's server uses checkHeaders to distinguish native CLI requests from
|
|
28
|
-
* third-party clients. Native identifiers use ULID-style prefixes:
|
|
29
|
-
*
|
|
30
|
-
* Session: ses_<hex><base62> (e.g. ses_a1b2c3d4e5f6g7h8i9j0k1l2m3n4)
|
|
31
|
-
* Request: msg_<hex><base62> (e.g. msg_01KA1B2C3D4E5F6G7H8I9J0K1L2M)
|
|
32
|
-
*
|
|
33
|
-
* If the server does not see the expected prefix it applies a fallback rate
|
|
34
|
-
* limit (~2 req/day) which causes models to "freeze" after a few prompts.
|
|
35
|
-
*/
|
|
36
|
-
function generateOpenCodeId(prefix: string): string {
|
|
37
|
-
// Timestamp in ms as big-endian hex (matches ULID-style sortability).
|
|
38
|
-
const ms = BigInt(Date.now());
|
|
39
|
-
const timeHex = ms.toString(16).padStart(12, "0");
|
|
40
|
-
// Random suffix (crypto) encoded as base62 for compactness.
|
|
41
|
-
const randomLen = 14;
|
|
42
|
-
const base62Chars =
|
|
43
|
-
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
44
|
-
const bytes = randomBytes(randomLen);
|
|
45
|
-
let suffix = "";
|
|
46
|
-
for (let i = 0; i < randomLen; i++) {
|
|
47
|
-
suffix += base62Chars[bytes[i] % 62];
|
|
48
|
-
}
|
|
49
|
-
return `${prefix}${timeHex}${suffix}`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Shared OpenCode session/request tracking.
|
|
54
|
-
*
|
|
55
|
-
* OpenCode endpoints require native-format identifiers (ses_ / msg_ prefix)
|
|
56
|
-
* to receive the full daily rate limit. Without matching prefixes the server
|
|
57
|
-
* falls back to a ~2 req/day limit, causing free models to freeze after a
|
|
58
|
-
* couple of prompts.
|
|
59
|
-
*/
|
|
60
|
-
export function createOpenCodeSessionTracker() {
|
|
61
|
-
let sessionId = "";
|
|
62
|
-
|
|
63
|
-
function getSessionId(): string {
|
|
64
|
-
if (!sessionId) {
|
|
65
|
-
sessionId = generateOpenCodeId("ses_");
|
|
66
|
-
}
|
|
67
|
-
return sessionId;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function nextRequestId(): string {
|
|
71
|
-
return generateOpenCodeId("msg_");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
getSessionId,
|
|
76
|
-
nextRequestId,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export type OpenCodeSessionTracker = ReturnType<
|
|
81
|
-
typeof createOpenCodeSessionTracker
|
|
82
|
-
>;
|
|
83
|
-
|
|
84
|
-
export function createOpenCodeHeaders(
|
|
85
|
-
tracker: OpenCodeSessionTracker,
|
|
86
|
-
existingHeaders?: Record<string, string>,
|
|
87
|
-
): Record<string, string> {
|
|
88
|
-
return {
|
|
89
|
-
...existingHeaders,
|
|
90
|
-
...OPENCODE_STATIC_HEADERS,
|
|
91
|
-
"x-opencode-session": tracker.getSessionId(),
|
|
92
|
-
"x-opencode-request": tracker.nextRequestId(),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function isOpenCodeProvider(providerId: string): boolean {
|
|
97
|
-
return providerId === "opencode" || providerId === "opencode-go";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function stripTrailingSlashes(value: string): string {
|
|
101
|
-
let end = value.length;
|
|
102
|
-
while (end > 0 && value.codePointAt(end - 1) === 47) {
|
|
103
|
-
end--;
|
|
104
|
-
}
|
|
105
|
-
return value.slice(0, end);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function isAnthropicOpenCodeEndpoint(model: Model<Api>): boolean {
|
|
109
|
-
return !stripTrailingSlashes(model.baseUrl).endsWith("/v1");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
type StreamSimpleFn<TApi extends Api> = (
|
|
113
|
-
model: Model<TApi>,
|
|
114
|
-
context: Context,
|
|
115
|
-
options?: SimpleStreamOptions,
|
|
116
|
-
) => AssistantMessageEventStream;
|
|
117
|
-
|
|
118
|
-
type AnthropicStreamModule = {
|
|
119
|
-
streamSimpleAnthropic: StreamSimpleFn<"anthropic-messages">;
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
type OpenAICompletionsStreamModule = {
|
|
123
|
-
streamSimpleOpenAICompletions: StreamSimpleFn<"openai-completions">;
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
const piAiSubpathCache = new Map<string, Promise<unknown>>();
|
|
127
|
-
|
|
128
|
-
async function importPiAiSubpath<T>(subpath: string): Promise<T> {
|
|
129
|
-
const specifier = `@earendil-works/pi-ai/${subpath}`;
|
|
130
|
-
const cached = piAiSubpathCache.get(specifier) as Promise<T> | undefined;
|
|
131
|
-
if (cached) return cached;
|
|
132
|
-
|
|
133
|
-
const promise = importPiAiSubpathUncached<T>(specifier);
|
|
134
|
-
piAiSubpathCache.set(specifier, promise);
|
|
135
|
-
return promise;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function importPiAiSubpathUncached<T>(specifier: string): Promise<T> {
|
|
139
|
-
try {
|
|
140
|
-
return (await import(specifier)) as T;
|
|
141
|
-
} catch (directError) {
|
|
142
|
-
const
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
):
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
1
|
+
import { existsSync, lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import type {
|
|
7
|
+
Api,
|
|
8
|
+
AssistantMessage,
|
|
9
|
+
AssistantMessageEvent,
|
|
10
|
+
AssistantMessageEventStream,
|
|
11
|
+
Context,
|
|
12
|
+
Model,
|
|
13
|
+
SimpleStreamOptions,
|
|
14
|
+
} from "@earendil-works/pi-ai";
|
|
15
|
+
import type { ProviderConfig } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
export const OPENCODE_DYNAMIC_API = "opencode-dynamic" as const;
|
|
18
|
+
|
|
19
|
+
export const OPENCODE_STATIC_HEADERS = {
|
|
20
|
+
"User-Agent": "opencode/1.15.5",
|
|
21
|
+
"x-opencode-client": "cli",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* OpenCode-native identifier generation.
|
|
26
|
+
*
|
|
27
|
+
* OpenCode's server uses checkHeaders to distinguish native CLI requests from
|
|
28
|
+
* third-party clients. Native identifiers use ULID-style prefixes:
|
|
29
|
+
*
|
|
30
|
+
* Session: ses_<hex><base62> (e.g. ses_a1b2c3d4e5f6g7h8i9j0k1l2m3n4)
|
|
31
|
+
* Request: msg_<hex><base62> (e.g. msg_01KA1B2C3D4E5F6G7H8I9J0K1L2M)
|
|
32
|
+
*
|
|
33
|
+
* If the server does not see the expected prefix it applies a fallback rate
|
|
34
|
+
* limit (~2 req/day) which causes models to "freeze" after a few prompts.
|
|
35
|
+
*/
|
|
36
|
+
function generateOpenCodeId(prefix: string): string {
|
|
37
|
+
// Timestamp in ms as big-endian hex (matches ULID-style sortability).
|
|
38
|
+
const ms = BigInt(Date.now());
|
|
39
|
+
const timeHex = ms.toString(16).padStart(12, "0");
|
|
40
|
+
// Random suffix (crypto) encoded as base62 for compactness.
|
|
41
|
+
const randomLen = 14;
|
|
42
|
+
const base62Chars =
|
|
43
|
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
44
|
+
const bytes = randomBytes(randomLen);
|
|
45
|
+
let suffix = "";
|
|
46
|
+
for (let i = 0; i < randomLen; i++) {
|
|
47
|
+
suffix += base62Chars[bytes[i] % 62];
|
|
48
|
+
}
|
|
49
|
+
return `${prefix}${timeHex}${suffix}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Shared OpenCode session/request tracking.
|
|
54
|
+
*
|
|
55
|
+
* OpenCode endpoints require native-format identifiers (ses_ / msg_ prefix)
|
|
56
|
+
* to receive the full daily rate limit. Without matching prefixes the server
|
|
57
|
+
* falls back to a ~2 req/day limit, causing free models to freeze after a
|
|
58
|
+
* couple of prompts.
|
|
59
|
+
*/
|
|
60
|
+
export function createOpenCodeSessionTracker() {
|
|
61
|
+
let sessionId = "";
|
|
62
|
+
|
|
63
|
+
function getSessionId(): string {
|
|
64
|
+
if (!sessionId) {
|
|
65
|
+
sessionId = generateOpenCodeId("ses_");
|
|
66
|
+
}
|
|
67
|
+
return sessionId;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function nextRequestId(): string {
|
|
71
|
+
return generateOpenCodeId("msg_");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
getSessionId,
|
|
76
|
+
nextRequestId,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type OpenCodeSessionTracker = ReturnType<
|
|
81
|
+
typeof createOpenCodeSessionTracker
|
|
82
|
+
>;
|
|
83
|
+
|
|
84
|
+
export function createOpenCodeHeaders(
|
|
85
|
+
tracker: OpenCodeSessionTracker,
|
|
86
|
+
existingHeaders?: Record<string, string>,
|
|
87
|
+
): Record<string, string> {
|
|
88
|
+
return {
|
|
89
|
+
...existingHeaders,
|
|
90
|
+
...OPENCODE_STATIC_HEADERS,
|
|
91
|
+
"x-opencode-session": tracker.getSessionId(),
|
|
92
|
+
"x-opencode-request": tracker.nextRequestId(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function isOpenCodeProvider(providerId: string): boolean {
|
|
97
|
+
return providerId === "opencode" || providerId === "opencode-go";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stripTrailingSlashes(value: string): string {
|
|
101
|
+
let end = value.length;
|
|
102
|
+
while (end > 0 && value.codePointAt(end - 1) === 47) {
|
|
103
|
+
end--;
|
|
104
|
+
}
|
|
105
|
+
return value.slice(0, end);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isAnthropicOpenCodeEndpoint(model: Model<Api>): boolean {
|
|
109
|
+
return !stripTrailingSlashes(model.baseUrl).endsWith("/v1");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type StreamSimpleFn<TApi extends Api> = (
|
|
113
|
+
model: Model<TApi>,
|
|
114
|
+
context: Context,
|
|
115
|
+
options?: SimpleStreamOptions,
|
|
116
|
+
) => AssistantMessageEventStream;
|
|
117
|
+
|
|
118
|
+
type AnthropicStreamModule = {
|
|
119
|
+
streamSimpleAnthropic: StreamSimpleFn<"anthropic-messages">;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type OpenAICompletionsStreamModule = {
|
|
123
|
+
streamSimpleOpenAICompletions: StreamSimpleFn<"openai-completions">;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const piAiSubpathCache = new Map<string, Promise<unknown>>();
|
|
127
|
+
|
|
128
|
+
async function importPiAiSubpath<T>(subpath: string): Promise<T> {
|
|
129
|
+
const specifier = `@earendil-works/pi-ai/${subpath}`;
|
|
130
|
+
const cached = piAiSubpathCache.get(specifier) as Promise<T> | undefined;
|
|
131
|
+
if (cached) return cached;
|
|
132
|
+
|
|
133
|
+
const promise = importPiAiSubpathUncached<T>(specifier);
|
|
134
|
+
piAiSubpathCache.set(specifier, promise);
|
|
135
|
+
return promise;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function importPiAiSubpathUncached<T>(specifier: string): Promise<T> {
|
|
139
|
+
try {
|
|
140
|
+
return (await import(specifier)) as T;
|
|
141
|
+
} catch (directError) {
|
|
142
|
+
const rootFallback = await importPiAiRootFallback<T>(specifier);
|
|
143
|
+
if (rootFallback) return rootFallback;
|
|
144
|
+
|
|
145
|
+
const resolved = resolvePiAiSubpathFromPackage(specifier);
|
|
146
|
+
if (!resolved) throw directError;
|
|
147
|
+
try {
|
|
148
|
+
return (await import(pathToFileURL(resolved).href)) as T;
|
|
149
|
+
} catch {
|
|
150
|
+
throw directError;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function importPiAiRootFallback<T>(
|
|
156
|
+
specifier: string,
|
|
157
|
+
): Promise<T | undefined> {
|
|
158
|
+
const subpath = specifier.replace("@earendil-works/pi-ai/", "");
|
|
159
|
+
const requiredExport: Record<string, string> = {
|
|
160
|
+
anthropic: "streamSimpleAnthropic",
|
|
161
|
+
"openai-completions": "streamSimpleOpenAICompletions",
|
|
162
|
+
};
|
|
163
|
+
const exportName = requiredExport[subpath];
|
|
164
|
+
if (!exportName) return undefined;
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const rootModule = (await import("@earendil-works/pi-ai")) as Record<
|
|
168
|
+
string,
|
|
169
|
+
unknown
|
|
170
|
+
>;
|
|
171
|
+
return typeof rootModule[exportName] === "function"
|
|
172
|
+
? (rootModule as T)
|
|
173
|
+
: undefined;
|
|
174
|
+
} catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const PI_AI_DEPENDENCY_CANARY = "openai";
|
|
180
|
+
|
|
181
|
+
function findPiAiPackageDir(requireBase: string): string | undefined {
|
|
182
|
+
try {
|
|
183
|
+
const require = createRequire(requireBase);
|
|
184
|
+
const resolved = require.resolve(PI_AI_DEPENDENCY_CANARY);
|
|
185
|
+
let dir = dirname(resolved);
|
|
186
|
+
while (dir !== dirname(dir)) {
|
|
187
|
+
if (basename(dir) === "node_modules") {
|
|
188
|
+
const piAiDir = join(dir, "@earendil-works", "pi-ai");
|
|
189
|
+
const pkgJsonPath = join(piAiDir, "package.json");
|
|
190
|
+
if (existsSync(pkgJsonPath) && lstatSync(pkgJsonPath).isFile()) {
|
|
191
|
+
return piAiDir;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
dir = dirname(dir);
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
// Resolution failed — try the next base.
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function resolvePiAiSubpathFromPackage(specifier: string): string | undefined {
|
|
203
|
+
const subpath = specifier.replace("@earendil-works/pi-ai/", "");
|
|
204
|
+
const candidates = [process.argv[1], import.meta.url].filter(
|
|
205
|
+
(value): value is string => Boolean(value),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
for (const candidate of candidates) {
|
|
209
|
+
const pkgDir = findPiAiPackageDir(candidate);
|
|
210
|
+
if (!pkgDir) continue;
|
|
211
|
+
try {
|
|
212
|
+
const pkg = JSON.parse(
|
|
213
|
+
readFileSync(join(pkgDir, "package.json"), "utf-8"),
|
|
214
|
+
);
|
|
215
|
+
const exportEntry = pkg.exports?.[`./${subpath}`];
|
|
216
|
+
const targetPath = exportEntry?.import ?? exportEntry?.default;
|
|
217
|
+
if (typeof targetPath === "string") {
|
|
218
|
+
return join(pkgDir, targetPath);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// Try the next resolution base.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class DeferredAssistantMessageEventStream {
|
|
229
|
+
private queue: AssistantMessageEvent[] = [];
|
|
230
|
+
private waiting: Array<
|
|
231
|
+
(result: IteratorResult<AssistantMessageEvent>) => void
|
|
232
|
+
> = [];
|
|
233
|
+
private done = false;
|
|
234
|
+
private resolveResult!: (message: AssistantMessage) => void;
|
|
235
|
+
private readonly finalResultPromise: Promise<AssistantMessage>;
|
|
236
|
+
|
|
237
|
+
constructor() {
|
|
238
|
+
this.finalResultPromise = new Promise((resolve) => {
|
|
239
|
+
this.resolveResult = resolve;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
push(event: AssistantMessageEvent): void {
|
|
244
|
+
if (this.done) return;
|
|
245
|
+
|
|
246
|
+
if (event.type === "done" || event.type === "error") {
|
|
247
|
+
this.done = true;
|
|
248
|
+
this.resolveResult(event.type === "done" ? event.message : event.error);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const waiter = this.waiting.shift();
|
|
252
|
+
if (waiter) {
|
|
253
|
+
waiter({ value: event, done: false });
|
|
254
|
+
} else {
|
|
255
|
+
this.queue.push(event);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
end(result?: AssistantMessage): void {
|
|
260
|
+
if (this.done) return;
|
|
261
|
+
this.done = true;
|
|
262
|
+
if (result) this.resolveResult(result);
|
|
263
|
+
while (this.waiting.length > 0) {
|
|
264
|
+
this.waiting.shift()?.({ value: undefined, done: true });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async *[Symbol.asyncIterator](): AsyncIterator<AssistantMessageEvent> {
|
|
269
|
+
while (true) {
|
|
270
|
+
if (this.queue.length > 0) {
|
|
271
|
+
yield this.queue.shift()!;
|
|
272
|
+
} else if (this.done) {
|
|
273
|
+
return;
|
|
274
|
+
} else {
|
|
275
|
+
const result = await new Promise<IteratorResult<AssistantMessageEvent>>(
|
|
276
|
+
(resolve) => this.waiting.push(resolve),
|
|
277
|
+
);
|
|
278
|
+
if (result.done) return;
|
|
279
|
+
yield result.value;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
result(): Promise<AssistantMessage> {
|
|
285
|
+
return this.finalResultPromise;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function createErrorMessage(
|
|
290
|
+
model: Model<Api>,
|
|
291
|
+
error: unknown,
|
|
292
|
+
): AssistantMessage {
|
|
293
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
294
|
+
return {
|
|
295
|
+
role: "assistant",
|
|
296
|
+
content: [],
|
|
297
|
+
api: model.api,
|
|
298
|
+
provider: model.provider,
|
|
299
|
+
model: model.id,
|
|
300
|
+
usage: {
|
|
301
|
+
input: 0,
|
|
302
|
+
output: 0,
|
|
303
|
+
cacheRead: 0,
|
|
304
|
+
cacheWrite: 0,
|
|
305
|
+
totalTokens: 0,
|
|
306
|
+
cost: {
|
|
307
|
+
input: 0,
|
|
308
|
+
output: 0,
|
|
309
|
+
cacheRead: 0,
|
|
310
|
+
cacheWrite: 0,
|
|
311
|
+
total: 0,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
stopReason: "error",
|
|
315
|
+
errorMessage: message,
|
|
316
|
+
timestamp: Date.now(),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function pipeStream(
|
|
321
|
+
stream: DeferredAssistantMessageEventStream,
|
|
322
|
+
upstream: AssistantMessageEventStream,
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
let finalMessage: AssistantMessage | undefined;
|
|
325
|
+
try {
|
|
326
|
+
for await (const event of upstream) {
|
|
327
|
+
stream.push(event);
|
|
328
|
+
if (event.type === "done") finalMessage = event.message;
|
|
329
|
+
if (event.type === "error") finalMessage = event.error;
|
|
330
|
+
}
|
|
331
|
+
stream.end(finalMessage ?? (await upstream.result()));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (finalMessage) {
|
|
334
|
+
stream.end(finalMessage);
|
|
335
|
+
} else {
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Pi's static model headers are evaluated at registration time. OpenCode treats
|
|
343
|
+
* x-opencode-request like a per-request id, so reusing one value across turns can
|
|
344
|
+
* leave later requests attached to an old/in-flight generation. Registering a
|
|
345
|
+
* provider-specific stream keeps the normal Pi parsers but refreshes headers for
|
|
346
|
+
* every LLM call.
|
|
347
|
+
*/
|
|
348
|
+
export function createOpenCodeStreamSimple(
|
|
349
|
+
tracker: OpenCodeSessionTracker,
|
|
350
|
+
): NonNullable<ProviderConfig["streamSimple"]> {
|
|
351
|
+
return (model, context, options) => {
|
|
352
|
+
const headers = createOpenCodeHeaders(tracker, options?.headers);
|
|
353
|
+
const stream = new DeferredAssistantMessageEventStream();
|
|
354
|
+
|
|
355
|
+
void (async () => {
|
|
356
|
+
try {
|
|
357
|
+
if (isAnthropicOpenCodeEndpoint(model)) {
|
|
358
|
+
const { streamSimpleAnthropic } =
|
|
359
|
+
await importPiAiSubpath<AnthropicStreamModule>("anthropic");
|
|
360
|
+
await pipeStream(
|
|
361
|
+
stream,
|
|
362
|
+
streamSimpleAnthropic(
|
|
363
|
+
{
|
|
364
|
+
...model,
|
|
365
|
+
api: "anthropic-messages",
|
|
366
|
+
} as Model<"anthropic-messages">,
|
|
367
|
+
context,
|
|
368
|
+
{ ...options, headers },
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const { streamSimpleOpenAICompletions } =
|
|
375
|
+
await importPiAiSubpath<OpenAICompletionsStreamModule>(
|
|
376
|
+
"openai-completions",
|
|
377
|
+
);
|
|
378
|
+
await pipeStream(
|
|
379
|
+
stream,
|
|
380
|
+
streamSimpleOpenAICompletions(
|
|
381
|
+
{
|
|
382
|
+
...model,
|
|
383
|
+
api: "openai-completions",
|
|
384
|
+
} as Model<"openai-completions">,
|
|
385
|
+
context,
|
|
386
|
+
{ ...options, headers },
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
const errorMessage = createErrorMessage(model, error);
|
|
391
|
+
stream.push({ type: "start", partial: errorMessage });
|
|
392
|
+
stream.push({ type: "error", reason: "error", error: errorMessage });
|
|
393
|
+
}
|
|
394
|
+
})();
|
|
395
|
+
|
|
396
|
+
return stream as unknown as AssistantMessageEventStream;
|
|
397
|
+
};
|
|
398
|
+
}
|