@lenylvt/pi-ai 0.64.0
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/README.md +203 -0
- package/dist/api-registry.d.ts +20 -0
- package/dist/api-registry.d.ts.map +1 -0
- package/dist/api-registry.js +44 -0
- package/dist/api-registry.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +119 -0
- package/dist/cli.js.map +1 -0
- package/dist/env-api-keys.d.ts +7 -0
- package/dist/env-api-keys.d.ts.map +1 -0
- package/dist/env-api-keys.js +13 -0
- package/dist/env-api-keys.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/models.d.ts +24 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.generated.d.ts +2332 -0
- package/dist/models.generated.d.ts.map +1 -0
- package/dist/models.generated.js +2186 -0
- package/dist/models.generated.js.map +1 -0
- package/dist/models.js +60 -0
- package/dist/models.js.map +1 -0
- package/dist/oauth.d.ts +2 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +2 -0
- package/dist/oauth.js.map +1 -0
- package/dist/providers/anthropic.d.ts +40 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +749 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/faux.d.ts +56 -0
- package/dist/providers/faux.d.ts.map +1 -0
- package/dist/providers/faux.js +367 -0
- package/dist/providers/faux.js.map +1 -0
- package/dist/providers/github-copilot-headers.d.ts +8 -0
- package/dist/providers/github-copilot-headers.d.ts.map +1 -0
- package/dist/providers/github-copilot-headers.js +29 -0
- package/dist/providers/github-copilot-headers.js.map +1 -0
- package/dist/providers/openai-codex-responses.d.ts +9 -0
- package/dist/providers/openai-codex-responses.d.ts.map +1 -0
- package/dist/providers/openai-codex-responses.js +741 -0
- package/dist/providers/openai-codex-responses.js.map +1 -0
- package/dist/providers/openai-completions.d.ts +15 -0
- package/dist/providers/openai-completions.d.ts.map +1 -0
- package/dist/providers/openai-completions.js +687 -0
- package/dist/providers/openai-completions.js.map +1 -0
- package/dist/providers/openai-responses-shared.d.ts +17 -0
- package/dist/providers/openai-responses-shared.d.ts.map +1 -0
- package/dist/providers/openai-responses-shared.js +458 -0
- package/dist/providers/openai-responses-shared.js.map +1 -0
- package/dist/providers/openai-responses.d.ts +13 -0
- package/dist/providers/openai-responses.d.ts.map +1 -0
- package/dist/providers/openai-responses.js +190 -0
- package/dist/providers/openai-responses.js.map +1 -0
- package/dist/providers/register-builtins.d.ts +16 -0
- package/dist/providers/register-builtins.d.ts.map +1 -0
- package/dist/providers/register-builtins.js +140 -0
- package/dist/providers/register-builtins.js.map +1 -0
- package/dist/providers/simple-options.d.ts +8 -0
- package/dist/providers/simple-options.d.ts.map +1 -0
- package/dist/providers/simple-options.js +35 -0
- package/dist/providers/simple-options.js.map +1 -0
- package/dist/providers/transform-messages.d.ts +8 -0
- package/dist/providers/transform-messages.d.ts.map +1 -0
- package/dist/providers/transform-messages.js +155 -0
- package/dist/providers/transform-messages.js.map +1 -0
- package/dist/stream.d.ts +8 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +27 -0
- package/dist/stream.js.map +1 -0
- package/dist/types.d.ts +283 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/event-stream.d.ts +21 -0
- package/dist/utils/event-stream.d.ts.map +1 -0
- package/dist/utils/event-stream.js +81 -0
- package/dist/utils/event-stream.js.map +1 -0
- package/dist/utils/hash.d.ts +3 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +14 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/json-parse.d.ts +9 -0
- package/dist/utils/json-parse.d.ts.map +1 -0
- package/dist/utils/json-parse.js +29 -0
- package/dist/utils/json-parse.js.map +1 -0
- package/dist/utils/oauth/anthropic.d.ts +25 -0
- package/dist/utils/oauth/anthropic.d.ts.map +1 -0
- package/dist/utils/oauth/anthropic.js +335 -0
- package/dist/utils/oauth/anthropic.js.map +1 -0
- package/dist/utils/oauth/github-copilot.d.ts +30 -0
- package/dist/utils/oauth/github-copilot.d.ts.map +1 -0
- package/dist/utils/oauth/github-copilot.js +292 -0
- package/dist/utils/oauth/github-copilot.js.map +1 -0
- package/dist/utils/oauth/index.d.ts +36 -0
- package/dist/utils/oauth/index.d.ts.map +1 -0
- package/dist/utils/oauth/index.js +92 -0
- package/dist/utils/oauth/index.js.map +1 -0
- package/dist/utils/oauth/oauth-page.d.ts +3 -0
- package/dist/utils/oauth/oauth-page.d.ts.map +1 -0
- package/dist/utils/oauth/oauth-page.js +105 -0
- package/dist/utils/oauth/oauth-page.js.map +1 -0
- package/dist/utils/oauth/openai-codex.d.ts +34 -0
- package/dist/utils/oauth/openai-codex.d.ts.map +1 -0
- package/dist/utils/oauth/openai-codex.js +373 -0
- package/dist/utils/oauth/openai-codex.js.map +1 -0
- package/dist/utils/oauth/pkce.d.ts +13 -0
- package/dist/utils/oauth/pkce.d.ts.map +1 -0
- package/dist/utils/oauth/pkce.js +31 -0
- package/dist/utils/oauth/pkce.js.map +1 -0
- package/dist/utils/oauth/types.d.ts +47 -0
- package/dist/utils/oauth/types.d.ts.map +1 -0
- package/dist/utils/oauth/types.js +2 -0
- package/dist/utils/oauth/types.js.map +1 -0
- package/dist/utils/overflow.d.ts +53 -0
- package/dist/utils/overflow.d.ts.map +1 -0
- package/dist/utils/overflow.js +119 -0
- package/dist/utils/overflow.js.map +1 -0
- package/dist/utils/sanitize-unicode.d.ts +22 -0
- package/dist/utils/sanitize-unicode.d.ts.map +1 -0
- package/dist/utils/sanitize-unicode.js +26 -0
- package/dist/utils/sanitize-unicode.js.map +1 -0
- package/dist/utils/typebox-helpers.d.ts +17 -0
- package/dist/utils/typebox-helpers.d.ts.map +1 -0
- package/dist/utils/typebox-helpers.js +21 -0
- package/dist/utils/typebox-helpers.js.map +1 -0
- package/dist/utils/validation.d.ts +18 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +80 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +89 -0
- package/src/api-registry.ts +98 -0
- package/src/cli.ts +136 -0
- package/src/env-api-keys.ts +22 -0
- package/src/index.ts +29 -0
- package/src/models.generated.ts +2188 -0
- package/src/models.ts +82 -0
- package/src/oauth.ts +1 -0
- package/src/providers/anthropic.ts +905 -0
- package/src/providers/faux.ts +498 -0
- package/src/providers/github-copilot-headers.ts +37 -0
- package/src/providers/openai-codex-responses.ts +929 -0
- package/src/providers/openai-completions.ts +811 -0
- package/src/providers/openai-responses-shared.ts +513 -0
- package/src/providers/openai-responses.ts +251 -0
- package/src/providers/register-builtins.ts +232 -0
- package/src/providers/simple-options.ts +46 -0
- package/src/providers/transform-messages.ts +172 -0
- package/src/stream.ts +59 -0
- package/src/types.ts +294 -0
- package/src/utils/event-stream.ts +87 -0
- package/src/utils/hash.ts +13 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/anthropic.ts +402 -0
- package/src/utils/oauth/github-copilot.ts +396 -0
- package/src/utils/oauth/index.ts +123 -0
- package/src/utils/oauth/oauth-page.ts +109 -0
- package/src/utils/oauth/openai-codex.ts +450 -0
- package/src/utils/oauth/pkce.ts +34 -0
- package/src/utils/oauth/types.ts +59 -0
- package/src/utils/overflow.ts +125 -0
- package/src/utils/sanitize-unicode.ts +25 -0
- package/src/utils/typebox-helpers.ts +24 -0
- package/src/utils/validation.ts +93 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
import { registerApiProvider, unregisterApiProviders } from "../api-registry.js";
|
|
2
|
+
import type {
|
|
3
|
+
AssistantMessage,
|
|
4
|
+
AssistantMessageEventStream,
|
|
5
|
+
Context,
|
|
6
|
+
ImageContent,
|
|
7
|
+
Message,
|
|
8
|
+
Model,
|
|
9
|
+
SimpleStreamOptions,
|
|
10
|
+
StreamFunction,
|
|
11
|
+
StreamOptions,
|
|
12
|
+
TextContent,
|
|
13
|
+
ThinkingContent,
|
|
14
|
+
ToolCall,
|
|
15
|
+
ToolResultMessage,
|
|
16
|
+
Usage,
|
|
17
|
+
} from "../types.js";
|
|
18
|
+
import { createAssistantMessageEventStream } from "../utils/event-stream.js";
|
|
19
|
+
|
|
20
|
+
const DEFAULT_API = "faux";
|
|
21
|
+
const DEFAULT_PROVIDER = "faux";
|
|
22
|
+
const DEFAULT_MODEL_ID = "faux-1";
|
|
23
|
+
const DEFAULT_MODEL_NAME = "Faux Model";
|
|
24
|
+
const DEFAULT_BASE_URL = "http://localhost:0";
|
|
25
|
+
const DEFAULT_MIN_TOKEN_SIZE = 3;
|
|
26
|
+
const DEFAULT_MAX_TOKEN_SIZE = 5;
|
|
27
|
+
|
|
28
|
+
const DEFAULT_USAGE: Usage = {
|
|
29
|
+
input: 0,
|
|
30
|
+
output: 0,
|
|
31
|
+
cacheRead: 0,
|
|
32
|
+
cacheWrite: 0,
|
|
33
|
+
totalTokens: 0,
|
|
34
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface FauxModelDefinition {
|
|
38
|
+
id: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
reasoning?: boolean;
|
|
41
|
+
input?: ("text" | "image")[];
|
|
42
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
43
|
+
contextWindow?: number;
|
|
44
|
+
maxTokens?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type FauxContentBlock = TextContent | ThinkingContent | ToolCall;
|
|
48
|
+
|
|
49
|
+
export function fauxText(text: string): TextContent {
|
|
50
|
+
return { type: "text", text };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function fauxThinking(thinking: string): ThinkingContent {
|
|
54
|
+
return { type: "thinking", thinking };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function fauxToolCall(name: string, arguments_: ToolCall["arguments"], options: { id?: string } = {}): ToolCall {
|
|
58
|
+
return {
|
|
59
|
+
type: "toolCall",
|
|
60
|
+
id: options.id ?? randomId("tool"),
|
|
61
|
+
name,
|
|
62
|
+
arguments: arguments_,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeFauxAssistantContent(content: string | FauxContentBlock | FauxContentBlock[]): FauxContentBlock[] {
|
|
67
|
+
if (typeof content === "string") {
|
|
68
|
+
return [fauxText(content)];
|
|
69
|
+
}
|
|
70
|
+
return Array.isArray(content) ? content : [content];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function fauxAssistantMessage(
|
|
74
|
+
content: string | FauxContentBlock | FauxContentBlock[],
|
|
75
|
+
options: {
|
|
76
|
+
stopReason?: AssistantMessage["stopReason"];
|
|
77
|
+
errorMessage?: string;
|
|
78
|
+
responseId?: string;
|
|
79
|
+
timestamp?: number;
|
|
80
|
+
} = {},
|
|
81
|
+
): AssistantMessage {
|
|
82
|
+
return {
|
|
83
|
+
role: "assistant",
|
|
84
|
+
content: normalizeFauxAssistantContent(content),
|
|
85
|
+
api: DEFAULT_API,
|
|
86
|
+
provider: DEFAULT_PROVIDER,
|
|
87
|
+
model: DEFAULT_MODEL_ID,
|
|
88
|
+
usage: DEFAULT_USAGE,
|
|
89
|
+
stopReason: options.stopReason ?? "stop",
|
|
90
|
+
errorMessage: options.errorMessage,
|
|
91
|
+
responseId: options.responseId,
|
|
92
|
+
timestamp: options.timestamp ?? Date.now(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export type FauxResponseFactory = (
|
|
97
|
+
context: Context,
|
|
98
|
+
options: StreamOptions | undefined,
|
|
99
|
+
state: { callCount: number },
|
|
100
|
+
model: Model<string>,
|
|
101
|
+
) => AssistantMessage | Promise<AssistantMessage>;
|
|
102
|
+
|
|
103
|
+
export type FauxResponseStep = AssistantMessage | FauxResponseFactory;
|
|
104
|
+
|
|
105
|
+
export interface RegisterFauxProviderOptions {
|
|
106
|
+
api?: string;
|
|
107
|
+
provider?: string;
|
|
108
|
+
models?: FauxModelDefinition[];
|
|
109
|
+
tokensPerSecond?: number;
|
|
110
|
+
tokenSize?: {
|
|
111
|
+
min?: number;
|
|
112
|
+
max?: number;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface FauxProviderRegistration {
|
|
117
|
+
api: string;
|
|
118
|
+
models: [Model<string>, ...Model<string>[]];
|
|
119
|
+
getModel(): Model<string>;
|
|
120
|
+
getModel(modelId: string): Model<string> | undefined;
|
|
121
|
+
state: { callCount: number };
|
|
122
|
+
setResponses: (responses: FauxResponseStep[]) => void;
|
|
123
|
+
appendResponses: (responses: FauxResponseStep[]) => void;
|
|
124
|
+
getPendingResponseCount: () => number;
|
|
125
|
+
unregister: () => void;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function estimateTokens(text: string): number {
|
|
129
|
+
return Math.ceil(text.length / 4);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function randomId(prefix: string): string {
|
|
133
|
+
return `${prefix}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function contentToText(content: string | Array<TextContent | ImageContent>): string {
|
|
137
|
+
if (typeof content === "string") {
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
return content
|
|
141
|
+
.map((block) => {
|
|
142
|
+
if (block.type === "text") {
|
|
143
|
+
return block.text;
|
|
144
|
+
}
|
|
145
|
+
return `[image:${block.mimeType}:${block.data.length}]`;
|
|
146
|
+
})
|
|
147
|
+
.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function assistantContentToText(content: Array<TextContent | ThinkingContent | ToolCall>): string {
|
|
151
|
+
return content
|
|
152
|
+
.map((block) => {
|
|
153
|
+
if (block.type === "text") {
|
|
154
|
+
return block.text;
|
|
155
|
+
}
|
|
156
|
+
if (block.type === "thinking") {
|
|
157
|
+
return block.thinking;
|
|
158
|
+
}
|
|
159
|
+
return `${block.name}:${JSON.stringify(block.arguments)}`;
|
|
160
|
+
})
|
|
161
|
+
.join("\n");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toolResultToText(message: ToolResultMessage): string {
|
|
165
|
+
return [message.toolName, ...message.content.map((block) => contentToText([block]))].join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function messageToText(message: Message): string {
|
|
169
|
+
if (message.role === "user") {
|
|
170
|
+
return contentToText(message.content);
|
|
171
|
+
}
|
|
172
|
+
if (message.role === "assistant") {
|
|
173
|
+
return assistantContentToText(message.content);
|
|
174
|
+
}
|
|
175
|
+
return toolResultToText(message);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function serializeContext(context: Context): string {
|
|
179
|
+
const parts: string[] = [];
|
|
180
|
+
if (context.systemPrompt) {
|
|
181
|
+
parts.push(`system:${context.systemPrompt}`);
|
|
182
|
+
}
|
|
183
|
+
for (const message of context.messages) {
|
|
184
|
+
parts.push(`${message.role}:${messageToText(message)}`);
|
|
185
|
+
}
|
|
186
|
+
if (context.tools?.length) {
|
|
187
|
+
parts.push(`tools:${JSON.stringify(context.tools)}`);
|
|
188
|
+
}
|
|
189
|
+
return parts.join("\n\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function commonPrefixLength(a: string, b: string): number {
|
|
193
|
+
const length = Math.min(a.length, b.length);
|
|
194
|
+
let index = 0;
|
|
195
|
+
while (index < length && a[index] === b[index]) {
|
|
196
|
+
index++;
|
|
197
|
+
}
|
|
198
|
+
return index;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function withUsageEstimate(
|
|
202
|
+
message: AssistantMessage,
|
|
203
|
+
context: Context,
|
|
204
|
+
options: StreamOptions | undefined,
|
|
205
|
+
promptCache: Map<string, string>,
|
|
206
|
+
): AssistantMessage {
|
|
207
|
+
const promptText = serializeContext(context);
|
|
208
|
+
const promptTokens = estimateTokens(promptText);
|
|
209
|
+
const outputTokens = estimateTokens(assistantContentToText(message.content));
|
|
210
|
+
let input = promptTokens;
|
|
211
|
+
let cacheRead = 0;
|
|
212
|
+
let cacheWrite = 0;
|
|
213
|
+
const sessionId = options?.sessionId;
|
|
214
|
+
|
|
215
|
+
if (sessionId && options?.cacheRetention !== "none") {
|
|
216
|
+
const previousPrompt = promptCache.get(sessionId);
|
|
217
|
+
if (previousPrompt) {
|
|
218
|
+
const cachedChars = commonPrefixLength(previousPrompt, promptText);
|
|
219
|
+
cacheRead = estimateTokens(previousPrompt.slice(0, cachedChars));
|
|
220
|
+
cacheWrite = estimateTokens(promptText.slice(cachedChars));
|
|
221
|
+
input = Math.max(0, promptTokens - cacheRead);
|
|
222
|
+
} else {
|
|
223
|
+
cacheWrite = promptTokens;
|
|
224
|
+
}
|
|
225
|
+
promptCache.set(sessionId, promptText);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
...message,
|
|
230
|
+
usage: {
|
|
231
|
+
input,
|
|
232
|
+
output: outputTokens,
|
|
233
|
+
cacheRead,
|
|
234
|
+
cacheWrite,
|
|
235
|
+
totalTokens: input + outputTokens + cacheRead + cacheWrite,
|
|
236
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitStringByTokenSize(text: string, minTokenSize: number, maxTokenSize: number): string[] {
|
|
242
|
+
const chunks: string[] = [];
|
|
243
|
+
let index = 0;
|
|
244
|
+
while (index < text.length) {
|
|
245
|
+
const tokenSize = minTokenSize + Math.floor(Math.random() * (maxTokenSize - minTokenSize + 1));
|
|
246
|
+
const charSize = Math.max(1, tokenSize * 4);
|
|
247
|
+
chunks.push(text.slice(index, index + charSize));
|
|
248
|
+
index += charSize;
|
|
249
|
+
}
|
|
250
|
+
return chunks.length > 0 ? chunks : [""];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function cloneMessage(message: AssistantMessage, api: string, provider: string, modelId: string): AssistantMessage {
|
|
254
|
+
const cloned = structuredClone(message);
|
|
255
|
+
return {
|
|
256
|
+
...cloned,
|
|
257
|
+
api,
|
|
258
|
+
provider,
|
|
259
|
+
model: modelId,
|
|
260
|
+
timestamp: cloned.timestamp ?? Date.now(),
|
|
261
|
+
usage: cloned.usage ?? DEFAULT_USAGE,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function createErrorMessage(error: unknown, api: string, provider: string, modelId: string): AssistantMessage {
|
|
266
|
+
return {
|
|
267
|
+
role: "assistant",
|
|
268
|
+
content: [],
|
|
269
|
+
api,
|
|
270
|
+
provider,
|
|
271
|
+
model: modelId,
|
|
272
|
+
usage: DEFAULT_USAGE,
|
|
273
|
+
stopReason: "error",
|
|
274
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
275
|
+
timestamp: Date.now(),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function createAbortedMessage(partial: AssistantMessage): AssistantMessage {
|
|
280
|
+
return {
|
|
281
|
+
...partial,
|
|
282
|
+
stopReason: "aborted",
|
|
283
|
+
errorMessage: "Request was aborted",
|
|
284
|
+
timestamp: Date.now(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function scheduleChunk(chunk: string, tokensPerSecond: number | undefined): Promise<void> {
|
|
289
|
+
if (!tokensPerSecond || tokensPerSecond <= 0) {
|
|
290
|
+
return new Promise((resolve) => queueMicrotask(resolve));
|
|
291
|
+
}
|
|
292
|
+
const delayMs = (estimateTokens(chunk) / tokensPerSecond) * 1000;
|
|
293
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function streamWithDeltas(
|
|
297
|
+
stream: AssistantMessageEventStream,
|
|
298
|
+
message: AssistantMessage,
|
|
299
|
+
minTokenSize: number,
|
|
300
|
+
maxTokenSize: number,
|
|
301
|
+
tokensPerSecond: number | undefined,
|
|
302
|
+
signal: AbortSignal | undefined,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const partial: AssistantMessage = { ...message, content: [] };
|
|
305
|
+
if (signal?.aborted) {
|
|
306
|
+
const aborted = createAbortedMessage(partial);
|
|
307
|
+
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
308
|
+
stream.end(aborted);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
stream.push({ type: "start", partial: { ...partial } });
|
|
313
|
+
|
|
314
|
+
for (let index = 0; index < message.content.length; index++) {
|
|
315
|
+
if (signal?.aborted) {
|
|
316
|
+
const aborted = createAbortedMessage(partial);
|
|
317
|
+
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
318
|
+
stream.end(aborted);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const block = message.content[index];
|
|
323
|
+
|
|
324
|
+
if (block.type === "thinking") {
|
|
325
|
+
partial.content = [...partial.content, { type: "thinking", thinking: "" }];
|
|
326
|
+
stream.push({ type: "thinking_start", contentIndex: index, partial: { ...partial } });
|
|
327
|
+
for (const chunk of splitStringByTokenSize(block.thinking, minTokenSize, maxTokenSize)) {
|
|
328
|
+
await scheduleChunk(chunk, tokensPerSecond);
|
|
329
|
+
if (signal?.aborted) {
|
|
330
|
+
const aborted = createAbortedMessage(partial);
|
|
331
|
+
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
332
|
+
stream.end(aborted);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
(partial.content[index] as ThinkingContent).thinking += chunk;
|
|
336
|
+
stream.push({ type: "thinking_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
|
|
337
|
+
}
|
|
338
|
+
stream.push({
|
|
339
|
+
type: "thinking_end",
|
|
340
|
+
contentIndex: index,
|
|
341
|
+
content: block.thinking,
|
|
342
|
+
partial: { ...partial },
|
|
343
|
+
});
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (block.type === "text") {
|
|
348
|
+
partial.content = [...partial.content, { type: "text", text: "" }];
|
|
349
|
+
stream.push({ type: "text_start", contentIndex: index, partial: { ...partial } });
|
|
350
|
+
for (const chunk of splitStringByTokenSize(block.text, minTokenSize, maxTokenSize)) {
|
|
351
|
+
await scheduleChunk(chunk, tokensPerSecond);
|
|
352
|
+
if (signal?.aborted) {
|
|
353
|
+
const aborted = createAbortedMessage(partial);
|
|
354
|
+
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
355
|
+
stream.end(aborted);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
(partial.content[index] as TextContent).text += chunk;
|
|
359
|
+
stream.push({ type: "text_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
|
|
360
|
+
}
|
|
361
|
+
stream.push({ type: "text_end", contentIndex: index, content: block.text, partial: { ...partial } });
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
partial.content = [...partial.content, { type: "toolCall", id: block.id, name: block.name, arguments: {} }];
|
|
366
|
+
stream.push({ type: "toolcall_start", contentIndex: index, partial: { ...partial } });
|
|
367
|
+
for (const chunk of splitStringByTokenSize(JSON.stringify(block.arguments), minTokenSize, maxTokenSize)) {
|
|
368
|
+
await scheduleChunk(chunk, tokensPerSecond);
|
|
369
|
+
if (signal?.aborted) {
|
|
370
|
+
const aborted = createAbortedMessage(partial);
|
|
371
|
+
stream.push({ type: "error", reason: "aborted", error: aborted });
|
|
372
|
+
stream.end(aborted);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
stream.push({ type: "toolcall_delta", contentIndex: index, delta: chunk, partial: { ...partial } });
|
|
376
|
+
}
|
|
377
|
+
(partial.content[index] as ToolCall).arguments = block.arguments;
|
|
378
|
+
stream.push({ type: "toolcall_end", contentIndex: index, toolCall: block, partial: { ...partial } });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
382
|
+
stream.push({ type: "error", reason: message.stopReason, error: message });
|
|
383
|
+
stream.end(message);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
stream.push({ type: "done", reason: message.stopReason, message });
|
|
388
|
+
stream.end(message);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function registerFauxProvider(options: RegisterFauxProviderOptions = {}): FauxProviderRegistration {
|
|
392
|
+
const api = options.api ?? randomId(DEFAULT_API);
|
|
393
|
+
const provider = options.provider ?? DEFAULT_PROVIDER;
|
|
394
|
+
const sourceId = randomId("faux-provider");
|
|
395
|
+
const minTokenSize = Math.max(
|
|
396
|
+
1,
|
|
397
|
+
Math.min(options.tokenSize?.min ?? DEFAULT_MIN_TOKEN_SIZE, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE),
|
|
398
|
+
);
|
|
399
|
+
const maxTokenSize = Math.max(minTokenSize, options.tokenSize?.max ?? DEFAULT_MAX_TOKEN_SIZE);
|
|
400
|
+
let pendingResponses: FauxResponseStep[] = [];
|
|
401
|
+
const tokensPerSecond = options.tokensPerSecond;
|
|
402
|
+
const state = { callCount: 0 };
|
|
403
|
+
const promptCache = new Map<string, string>();
|
|
404
|
+
|
|
405
|
+
const modelDefinitions = options.models?.length
|
|
406
|
+
? options.models
|
|
407
|
+
: [
|
|
408
|
+
{
|
|
409
|
+
id: DEFAULT_MODEL_ID,
|
|
410
|
+
name: DEFAULT_MODEL_NAME,
|
|
411
|
+
reasoning: false,
|
|
412
|
+
input: ["text", "image"] as ("text" | "image")[],
|
|
413
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
414
|
+
contextWindow: 128000,
|
|
415
|
+
maxTokens: 16384,
|
|
416
|
+
},
|
|
417
|
+
];
|
|
418
|
+
const models = modelDefinitions.map((definition) => ({
|
|
419
|
+
id: definition.id,
|
|
420
|
+
name: definition.name ?? definition.id,
|
|
421
|
+
api,
|
|
422
|
+
provider,
|
|
423
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
424
|
+
reasoning: definition.reasoning ?? false,
|
|
425
|
+
input: definition.input ?? ["text", "image"],
|
|
426
|
+
cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
427
|
+
contextWindow: definition.contextWindow ?? 128000,
|
|
428
|
+
maxTokens: definition.maxTokens ?? 16384,
|
|
429
|
+
})) as [Model<string>, ...Model<string>[]];
|
|
430
|
+
|
|
431
|
+
const stream: StreamFunction<string, StreamOptions> = (requestModel, context, streamOptions) => {
|
|
432
|
+
const outer = createAssistantMessageEventStream();
|
|
433
|
+
const step = pendingResponses.shift();
|
|
434
|
+
state.callCount++;
|
|
435
|
+
|
|
436
|
+
queueMicrotask(async () => {
|
|
437
|
+
try {
|
|
438
|
+
if (!step) {
|
|
439
|
+
let message = createErrorMessage(
|
|
440
|
+
new Error("No more faux responses queued"),
|
|
441
|
+
api,
|
|
442
|
+
provider,
|
|
443
|
+
requestModel.id,
|
|
444
|
+
);
|
|
445
|
+
message = withUsageEstimate(message, context, streamOptions, promptCache);
|
|
446
|
+
outer.push({ type: "error", reason: "error", error: message });
|
|
447
|
+
outer.end(message);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const resolved =
|
|
452
|
+
typeof step === "function" ? await step(context, streamOptions, state, requestModel) : step;
|
|
453
|
+
let message = cloneMessage(resolved, api, provider, requestModel.id);
|
|
454
|
+
message = withUsageEstimate(message, context, streamOptions, promptCache);
|
|
455
|
+
await streamWithDeltas(outer, message, minTokenSize, maxTokenSize, tokensPerSecond, streamOptions?.signal);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
const message = createErrorMessage(error, api, provider, requestModel.id);
|
|
458
|
+
outer.push({ type: "error", reason: "error", error: message });
|
|
459
|
+
outer.end(message);
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
return outer;
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const streamSimple: StreamFunction<string, SimpleStreamOptions> = (streamModel, context, streamOptions) =>
|
|
467
|
+
stream(streamModel, context, streamOptions);
|
|
468
|
+
|
|
469
|
+
registerApiProvider({ api, stream, streamSimple }, sourceId);
|
|
470
|
+
|
|
471
|
+
function getModel(): Model<string>;
|
|
472
|
+
function getModel(requestedModelId: string): Model<string> | undefined;
|
|
473
|
+
function getModel(requestedModelId?: string): Model<string> | undefined {
|
|
474
|
+
if (!requestedModelId) {
|
|
475
|
+
return models[0];
|
|
476
|
+
}
|
|
477
|
+
return models.find((candidate) => candidate.id === requestedModelId);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
api,
|
|
482
|
+
models,
|
|
483
|
+
getModel,
|
|
484
|
+
state,
|
|
485
|
+
setResponses(responses) {
|
|
486
|
+
pendingResponses = [...responses];
|
|
487
|
+
},
|
|
488
|
+
appendResponses(responses) {
|
|
489
|
+
pendingResponses.push(...responses);
|
|
490
|
+
},
|
|
491
|
+
getPendingResponseCount() {
|
|
492
|
+
return pendingResponses.length;
|
|
493
|
+
},
|
|
494
|
+
unregister() {
|
|
495
|
+
unregisterApiProviders(sourceId);
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Message } from "../types.js";
|
|
2
|
+
|
|
3
|
+
// Copilot expects X-Initiator to indicate whether the request is user-initiated
|
|
4
|
+
// or agent-initiated (e.g. follow-up after assistant/tool messages).
|
|
5
|
+
export function inferCopilotInitiator(messages: Message[]): "user" | "agent" {
|
|
6
|
+
const last = messages[messages.length - 1];
|
|
7
|
+
return last && last.role !== "user" ? "agent" : "user";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Copilot requires Copilot-Vision-Request header when sending images
|
|
11
|
+
export function hasCopilotVisionInput(messages: Message[]): boolean {
|
|
12
|
+
return messages.some((msg) => {
|
|
13
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
14
|
+
return msg.content.some((c) => c.type === "image");
|
|
15
|
+
}
|
|
16
|
+
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
|
17
|
+
return msg.content.some((c) => c.type === "image");
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildCopilotDynamicHeaders(params: {
|
|
24
|
+
messages: Message[];
|
|
25
|
+
hasImages: boolean;
|
|
26
|
+
}): Record<string, string> {
|
|
27
|
+
const headers: Record<string, string> = {
|
|
28
|
+
"X-Initiator": inferCopilotInitiator(params.messages),
|
|
29
|
+
"Openai-Intent": "conversation-edits",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (params.hasImages) {
|
|
33
|
+
headers["Copilot-Vision-Request"] = "true";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return headers;
|
|
37
|
+
}
|