@kevin5251984/guild 0.2.12
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/LICENSE +21 -0
- package/bin/guildd.mjs +20 -0
- package/cordis.yml +24 -0
- package/package.json +52 -0
- package/src/agent-file.ts +125 -0
- package/src/browser.ts +668 -0
- package/src/catalog/default-bots.ts +263 -0
- package/src/catalog/skills.ts +128 -0
- package/src/catalog/subagents.ts +70 -0
- package/src/chat-parts.ts +71 -0
- package/src/cli-args.ts +75 -0
- package/src/cli.ts +60 -0
- package/src/compact.ts +355 -0
- package/src/cordis.d.ts +40 -0
- package/src/db.ts +653 -0
- package/src/generate.ts +673 -0
- package/src/handlers.ts +1623 -0
- package/src/harness.ts +326 -0
- package/src/host-agents.ts +137 -0
- package/src/host-browse.ts +199 -0
- package/src/host-skills.ts +150 -0
- package/src/image-gen.ts +270 -0
- package/src/index.ts +12 -0
- package/src/llm.ts +993 -0
- package/src/mcp.ts +563 -0
- package/src/memory.ts +159 -0
- package/src/mention.ts +176 -0
- package/src/oauth.ts +1474 -0
- package/src/plugins/api.ts +8 -0
- package/src/plugins/chat.ts +31 -0
- package/src/plugins/harness.ts +77 -0
- package/src/plugins/llm.ts +50 -0
- package/src/plugins/mcp.ts +58 -0
- package/src/plugins/memory.ts +42 -0
- package/src/plugins/oauth.ts +47 -0
- package/src/plugins/server.ts +126 -0
- package/src/plugins/store.ts +29 -0
- package/src/plugins/tools.ts +79 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +3045 -0
- package/src/public/chat.html +5834 -0
- package/src/public/favicon-16.png +0 -0
- package/src/public/favicon-16.svg +10 -0
- package/src/public/favicon-32.png +0 -0
- package/src/public/favicon.ico +0 -0
- package/src/public/favicon.svg +13 -0
- package/src/public/i18n.js +663 -0
- package/src/public/index.html +143 -0
- package/src/public/library.html +678 -0
- package/src/public/mcp-add.html +126 -0
- package/src/public/md.js +332 -0
- package/src/public/rpg/inn-street.jpg +0 -0
- package/src/public/settings.html +795 -0
- package/src/public/skills-add.html +212 -0
- package/src/public/studio.html +1181 -0
- package/src/public/style.css +1678 -0
- package/src/public/subagents-add.html +152 -0
- package/src/router.ts +978 -0
- package/src/send-budget.ts +52 -0
- package/src/server.ts +1 -0
- package/src/skill-import.ts +250 -0
- package/src/slash.ts +15 -0
- package/src/start.ts +103 -0
- package/src/store.ts +1208 -0
- package/src/subagent.ts +355 -0
- package/src/tools.ts +818 -0
- package/src/trajectory.ts +339 -0
- package/src/usage.ts +111 -0
- package/vendor/protocol/package.json +19 -0
- package/vendor/protocol/src/index.ts +159 -0
package/src/llm.ts
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
AuxRole,
|
|
5
|
+
LlmApi,
|
|
6
|
+
ModelEntry,
|
|
7
|
+
ModelRef,
|
|
8
|
+
ModelsFile,
|
|
9
|
+
ProviderEntry,
|
|
10
|
+
} from "@guild/protocol";
|
|
11
|
+
import { StoreError } from "./store.ts";
|
|
12
|
+
import { estimateSendTokens, trimSendMessages } from "./send-budget.ts";
|
|
13
|
+
import {
|
|
14
|
+
completeOAuth,
|
|
15
|
+
formatOAuthError,
|
|
16
|
+
listSubscriptions,
|
|
17
|
+
OAUTH_PICKER_IDS,
|
|
18
|
+
storedAccessToken,
|
|
19
|
+
subscriptionByPicker,
|
|
20
|
+
} from "./oauth.ts";
|
|
21
|
+
import {
|
|
22
|
+
openaiTools,
|
|
23
|
+
roundSignal,
|
|
24
|
+
TOOL_LOOP_WRAP,
|
|
25
|
+
type SkillRef,
|
|
26
|
+
type ToolContext,
|
|
27
|
+
type ToolTrace,
|
|
28
|
+
} from "./tools.ts";
|
|
29
|
+
import { runAgentLoop } from "./harness.ts";
|
|
30
|
+
import type { ChatUsage } from "@guild/protocol";
|
|
31
|
+
import {
|
|
32
|
+
addUsage,
|
|
33
|
+
blankUsage,
|
|
34
|
+
fromAnthropicUsage,
|
|
35
|
+
fromOpenAiUsage,
|
|
36
|
+
withDuration,
|
|
37
|
+
} from "./usage.ts";
|
|
38
|
+
|
|
39
|
+
export const AUX_ROLES: { id: AuxRole; name: string; hint: string }[] = [
|
|
40
|
+
{ id: "vision", name: "Vision", hint: "Image analysis" },
|
|
41
|
+
{ id: "web", name: "Web extract", hint: "Page summarization" },
|
|
42
|
+
{ id: "spawn", name: "SubAgent", hint: "explorer / worker / reviewer" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const CONFIGURABLE_AUX = new Set(AUX_ROLES.map((role) => role.id));
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_MODELS: ModelsFile = {
|
|
48
|
+
default: null,
|
|
49
|
+
reasoning: "medium",
|
|
50
|
+
fast: false,
|
|
51
|
+
aux: {},
|
|
52
|
+
recent: [],
|
|
53
|
+
providers: {
|
|
54
|
+
openai: {
|
|
55
|
+
name: "OpenAI",
|
|
56
|
+
baseUrl: "https://api.openai.com/v1",
|
|
57
|
+
api: "openai-completions",
|
|
58
|
+
apiKey: "$OPENAI_API_KEY",
|
|
59
|
+
models: [
|
|
60
|
+
{ id: "gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
61
|
+
{ id: "gpt-4.1", name: "GPT-4.1" },
|
|
62
|
+
],
|
|
63
|
+
},
|
|
64
|
+
xai: {
|
|
65
|
+
name: "xAI",
|
|
66
|
+
baseUrl: "https://api.x.ai/v1",
|
|
67
|
+
api: "openai-completions",
|
|
68
|
+
apiKey: "$XAI_API_KEY",
|
|
69
|
+
models: [
|
|
70
|
+
{ id: "grok-4.6", name: "Grok 4.6" },
|
|
71
|
+
{ id: "grok-4.5", name: "Grok 4.5" },
|
|
72
|
+
{ id: "grok-4.3", name: "Grok 4.3" },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
anthropic: {
|
|
76
|
+
name: "Anthropic",
|
|
77
|
+
baseUrl: "https://api.anthropic.com",
|
|
78
|
+
api: "anthropic-messages",
|
|
79
|
+
apiKey: "$ANTHROPIC_API_KEY",
|
|
80
|
+
models: [{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" }],
|
|
81
|
+
},
|
|
82
|
+
ollama: {
|
|
83
|
+
name: "Ollama",
|
|
84
|
+
baseUrl: "http://localhost:11434/v1",
|
|
85
|
+
api: "openai-completions",
|
|
86
|
+
apiKey: "ollama",
|
|
87
|
+
models: [{ id: "llama3.1:8b", name: "Llama 3.1 8B" }],
|
|
88
|
+
},
|
|
89
|
+
openrouter: {
|
|
90
|
+
name: "OpenRouter",
|
|
91
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
92
|
+
api: "openai-completions",
|
|
93
|
+
apiKey: "$OPENROUTER_API_KEY",
|
|
94
|
+
models: [
|
|
95
|
+
{ id: "anthropic/claude-sonnet-4", name: "Claude Sonnet 4" },
|
|
96
|
+
{ id: "openai/gpt-4.1-mini", name: "GPT-4.1 mini" },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function modelsPath(dataDir: string): string {
|
|
103
|
+
return join(dataDir, "models.json");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function seedModelsFile(dataDir: string): void {
|
|
107
|
+
mkdirSync(dataDir, { recursive: true });
|
|
108
|
+
if (existsSync(modelsPath(dataDir))) return;
|
|
109
|
+
writeModelsFile(dataDir, DEFAULT_MODELS);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function readModelsFile(dataDir: string): ModelsFile {
|
|
113
|
+
seedModelsFile(dataDir);
|
|
114
|
+
try {
|
|
115
|
+
const raw = readFileSync(modelsPath(dataDir), "utf8");
|
|
116
|
+
const parsed = JSON.parse(raw) as ModelsFile;
|
|
117
|
+
if (!parsed || typeof parsed !== "object" || !parsed.providers) {
|
|
118
|
+
return structuredClone(DEFAULT_MODELS);
|
|
119
|
+
}
|
|
120
|
+
return parsed;
|
|
121
|
+
} catch {
|
|
122
|
+
return structuredClone(DEFAULT_MODELS);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function writeModelsFile(dataDir: string, file: ModelsFile): ModelsFile {
|
|
127
|
+
const cleaned = sanitizeModels(file);
|
|
128
|
+
writeFileSync(modelsPath(dataDir), `${JSON.stringify(cleaned, null, 2)}\n`);
|
|
129
|
+
return cleaned;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function mergeModelsFile(
|
|
133
|
+
dataDir: string,
|
|
134
|
+
incoming: Partial<ModelsFile>,
|
|
135
|
+
): ModelsFile {
|
|
136
|
+
const existing = readModelsFile(dataDir);
|
|
137
|
+
const next: ModelsFile = {
|
|
138
|
+
default: incoming.default !== undefined ? incoming.default : existing.default,
|
|
139
|
+
reasoning: incoming.reasoning ?? existing.reasoning ?? "medium",
|
|
140
|
+
fast: incoming.fast ?? existing.fast ?? false,
|
|
141
|
+
aux: incoming.aux !== undefined ? incoming.aux : existing.aux,
|
|
142
|
+
recent: incoming.recent !== undefined ? incoming.recent : existing.recent,
|
|
143
|
+
providers: incoming.providers ? {} : existing.providers,
|
|
144
|
+
};
|
|
145
|
+
if (incoming.default) {
|
|
146
|
+
next.recent = pushRecent(existing.recent, incoming.default);
|
|
147
|
+
}
|
|
148
|
+
if (!incoming.providers) {
|
|
149
|
+
return writeModelsFile(dataDir, next);
|
|
150
|
+
}
|
|
151
|
+
const providersIn: ModelsFile["providers"] = {};
|
|
152
|
+
for (const [id, provider] of Object.entries(incoming.providers)) {
|
|
153
|
+
const prev = existing.providers[id];
|
|
154
|
+
const incomingKey = String(provider.apiKey ?? "").trim();
|
|
155
|
+
const prevKey = prev?.apiKey ?? "";
|
|
156
|
+
const apiKey =
|
|
157
|
+
!incomingKey || (prevKey && incomingKey === maskApiKey(prevKey))
|
|
158
|
+
? prevKey || incomingKey
|
|
159
|
+
: incomingKey;
|
|
160
|
+
providersIn[id] = { ...provider, apiKey };
|
|
161
|
+
}
|
|
162
|
+
next.providers = providersIn;
|
|
163
|
+
return writeModelsFile(dataDir, next);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function pushRecent(list: ModelRef[] | undefined, ref: ModelRef): ModelRef[] {
|
|
167
|
+
const next = [
|
|
168
|
+
ref,
|
|
169
|
+
...(list ?? []).filter(
|
|
170
|
+
(item) => !(item.provider === ref.provider && item.model === ref.model),
|
|
171
|
+
),
|
|
172
|
+
];
|
|
173
|
+
return next.slice(0, 8);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type PublicProvider = ProviderEntry & {
|
|
177
|
+
id: string;
|
|
178
|
+
stored: "empty" | "env" | "literal";
|
|
179
|
+
apiKeyPreview: string;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export function maskApiKey(value: string): string {
|
|
183
|
+
const key = String(value || "");
|
|
184
|
+
if (key.length <= 10) return key;
|
|
185
|
+
return key.slice(0, 5) + "…" + key.slice(-5);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function publicModels(dataDir: string, env: NodeJS.ProcessEnv = process.env) {
|
|
189
|
+
const file = readModelsFile(dataDir);
|
|
190
|
+
const providers: PublicProvider[] = Object.entries(file.providers).map(
|
|
191
|
+
([id, provider]) => {
|
|
192
|
+
const key = provider.apiKey ?? "";
|
|
193
|
+
const stored = !key ? "empty" : key.startsWith("$") ? "env" : "literal";
|
|
194
|
+
return {
|
|
195
|
+
id,
|
|
196
|
+
...provider,
|
|
197
|
+
apiKey: stored === "literal" ? "" : key,
|
|
198
|
+
apiKeyPreview: key ? maskApiKey(key) : "",
|
|
199
|
+
stored,
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
const target = resolveLlm(dataDir, env);
|
|
204
|
+
const subscriptions = listSubscriptions(dataDir);
|
|
205
|
+
const picker = [
|
|
206
|
+
...providers.map((p) => ({
|
|
207
|
+
id: p.id,
|
|
208
|
+
name: p.name || p.id,
|
|
209
|
+
kind: "key" as const,
|
|
210
|
+
ready: Boolean(resolveApiKey(p.apiKey, env) || p.stored === "literal"),
|
|
211
|
+
models: p.models,
|
|
212
|
+
})),
|
|
213
|
+
...subscriptions.map((s) => ({
|
|
214
|
+
id: s.pickerId,
|
|
215
|
+
name: s.name,
|
|
216
|
+
kind: "oauth" as const,
|
|
217
|
+
ready: s.ready,
|
|
218
|
+
models: s.models ?? [],
|
|
219
|
+
})),
|
|
220
|
+
];
|
|
221
|
+
return {
|
|
222
|
+
default: file.default ?? null,
|
|
223
|
+
reasoning: file.reasoning ?? "medium",
|
|
224
|
+
fast: Boolean(file.fast),
|
|
225
|
+
aux: file.aux ?? {},
|
|
226
|
+
auxRoles: AUX_ROLES,
|
|
227
|
+
recent: file.recent ?? [],
|
|
228
|
+
providers,
|
|
229
|
+
subscriptions,
|
|
230
|
+
picker,
|
|
231
|
+
active: target
|
|
232
|
+
? {
|
|
233
|
+
provider: target.providerId,
|
|
234
|
+
model: target.model,
|
|
235
|
+
ready: true,
|
|
236
|
+
}
|
|
237
|
+
: null,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export type LlmTarget = {
|
|
242
|
+
providerId: string;
|
|
243
|
+
model: string;
|
|
244
|
+
baseUrl: string;
|
|
245
|
+
apiKey: string;
|
|
246
|
+
api: LlmApi;
|
|
247
|
+
headers?: Record<string, string>;
|
|
248
|
+
accountId?: string;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
export function resolveApiKey(
|
|
252
|
+
raw: string | undefined,
|
|
253
|
+
env: NodeJS.ProcessEnv,
|
|
254
|
+
): string {
|
|
255
|
+
const value = (raw ?? "").trim();
|
|
256
|
+
if (!value) return "";
|
|
257
|
+
if (value.startsWith("$")) return (env[value.slice(1)] ?? "").trim();
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function oauthTarget(
|
|
262
|
+
dataDir: string,
|
|
263
|
+
providerId: string,
|
|
264
|
+
modelId?: string,
|
|
265
|
+
): LlmTarget | null {
|
|
266
|
+
const sub = subscriptionByPicker(providerId);
|
|
267
|
+
if (!sub) return null;
|
|
268
|
+
if (!storedAccessToken(dataDir, sub.id)) return null;
|
|
269
|
+
const model =
|
|
270
|
+
modelId ||
|
|
271
|
+
listSubscriptions(dataDir).find((item) => item.id === sub.id)?.models[0]
|
|
272
|
+
?.id ||
|
|
273
|
+
"";
|
|
274
|
+
if (!model) return null;
|
|
275
|
+
return {
|
|
276
|
+
providerId,
|
|
277
|
+
model,
|
|
278
|
+
baseUrl: "pi-ai",
|
|
279
|
+
apiKey: "oauth",
|
|
280
|
+
api: "openai-completions",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function resolveLlm(
|
|
285
|
+
dataDir: string,
|
|
286
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
287
|
+
role?: AuxRole | "chat",
|
|
288
|
+
prefer?: ModelRef | null,
|
|
289
|
+
): LlmTarget | null {
|
|
290
|
+
const file = readModelsFile(dataDir);
|
|
291
|
+
const ref: ModelRef | null | undefined =
|
|
292
|
+
prefer ??
|
|
293
|
+
(role && CONFIGURABLE_AUX.has(role as AuxRole)
|
|
294
|
+
? file.aux?.[role as AuxRole]
|
|
295
|
+
: file.default);
|
|
296
|
+
const tryProvider = (id: string, modelId?: string): LlmTarget | null => {
|
|
297
|
+
const oauth = oauthTarget(dataDir, id, modelId);
|
|
298
|
+
if (oauth) return oauth;
|
|
299
|
+
const provider = file.providers[id];
|
|
300
|
+
if (!provider) return null;
|
|
301
|
+
const apiKey = resolveApiKey(provider.apiKey, env);
|
|
302
|
+
if (!apiKey) return null;
|
|
303
|
+
const model = modelId || provider.models[0]?.id || "";
|
|
304
|
+
if (!model) return null;
|
|
305
|
+
return {
|
|
306
|
+
providerId: id,
|
|
307
|
+
model,
|
|
308
|
+
baseUrl: provider.baseUrl.replace(/\/+$/, ""),
|
|
309
|
+
apiKey,
|
|
310
|
+
api: provider.api,
|
|
311
|
+
};
|
|
312
|
+
};
|
|
313
|
+
if (ref?.provider) {
|
|
314
|
+
const hit = tryProvider(ref.provider, ref.model);
|
|
315
|
+
if (hit) return hit;
|
|
316
|
+
}
|
|
317
|
+
if (file.default?.provider) {
|
|
318
|
+
const hit = tryProvider(file.default.provider, file.default.model);
|
|
319
|
+
if (hit) return hit;
|
|
320
|
+
}
|
|
321
|
+
for (const id of Object.keys(file.providers)) {
|
|
322
|
+
const hit = tryProvider(id);
|
|
323
|
+
if (hit) return hit;
|
|
324
|
+
}
|
|
325
|
+
return envFallback(env);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function envFallback(env: NodeJS.ProcessEnv): LlmTarget | null {
|
|
329
|
+
if (env.XAI_API_KEY) {
|
|
330
|
+
return {
|
|
331
|
+
providerId: "xai",
|
|
332
|
+
model: env.XAI_MODEL ?? "grok-4-fast",
|
|
333
|
+
baseUrl: (env.XAI_API_URL ?? "https://api.x.ai/v1").replace(
|
|
334
|
+
/\/chat\/completions$/,
|
|
335
|
+
"",
|
|
336
|
+
),
|
|
337
|
+
apiKey: env.XAI_API_KEY,
|
|
338
|
+
api: "openai-completions",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (env.OPENAI_API_KEY) {
|
|
342
|
+
return {
|
|
343
|
+
providerId: "openai",
|
|
344
|
+
model: env.OPENAI_MODEL ?? "gpt-4.1-mini",
|
|
345
|
+
baseUrl: (env.OPENAI_BASE_URL ?? "https://api.openai.com/v1").replace(
|
|
346
|
+
/\/chat\/completions$/,
|
|
347
|
+
"",
|
|
348
|
+
),
|
|
349
|
+
apiKey: env.OPENAI_API_KEY,
|
|
350
|
+
api: "openai-completions",
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export async function llmComplete(input: {
|
|
357
|
+
dataDir: string;
|
|
358
|
+
env?: NodeJS.ProcessEnv;
|
|
359
|
+
system: string;
|
|
360
|
+
messages: { role: "user" | "assistant"; content: string }[];
|
|
361
|
+
temperature?: number;
|
|
362
|
+
role?: AuxRole | "chat";
|
|
363
|
+
prefer?: ModelRef | null;
|
|
364
|
+
tools?: boolean;
|
|
365
|
+
skills?: SkillRef[];
|
|
366
|
+
toolCtx?: ToolContext;
|
|
367
|
+
}): Promise<{
|
|
368
|
+
text: string;
|
|
369
|
+
provider: string;
|
|
370
|
+
model: string;
|
|
371
|
+
traces: ToolTrace[];
|
|
372
|
+
thinking: string;
|
|
373
|
+
usage?: ChatUsage;
|
|
374
|
+
} | null> {
|
|
375
|
+
const env = input.env ?? process.env;
|
|
376
|
+
const target = resolveLlm(input.dataDir, env, input.role, input.prefer);
|
|
377
|
+
if (!target) return null;
|
|
378
|
+
const useTools = input.tools ?? input.role === "chat";
|
|
379
|
+
const toolCtx: ToolContext = input.toolCtx ?? {
|
|
380
|
+
skills: input.skills,
|
|
381
|
+
dataDir: input.dataDir,
|
|
382
|
+
env,
|
|
383
|
+
spawnDepth: 0,
|
|
384
|
+
allowWrite: true,
|
|
385
|
+
};
|
|
386
|
+
if (OAUTH_PICKER_IDS.has(target.providerId)) {
|
|
387
|
+
try {
|
|
388
|
+
const file = readModelsFile(input.dataDir);
|
|
389
|
+
return await completeOAuth({
|
|
390
|
+
dataDir: input.dataDir,
|
|
391
|
+
pickerId: target.providerId,
|
|
392
|
+
model: target.model,
|
|
393
|
+
system: input.system,
|
|
394
|
+
messages: input.messages,
|
|
395
|
+
temperature: input.temperature ?? 0.4,
|
|
396
|
+
reasoning: file.fast ? "low" : file.reasoning,
|
|
397
|
+
tools: useTools,
|
|
398
|
+
skills: input.skills,
|
|
399
|
+
toolCtx,
|
|
400
|
+
});
|
|
401
|
+
} catch (error) {
|
|
402
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
403
|
+
return {
|
|
404
|
+
text: formatOAuthError(
|
|
405
|
+
target.providerId.replace(/-oauth$/, ""),
|
|
406
|
+
message,
|
|
407
|
+
),
|
|
408
|
+
provider: target.providerId,
|
|
409
|
+
model: target.model,
|
|
410
|
+
traces: [],
|
|
411
|
+
thinking: "",
|
|
412
|
+
usage: { provider: target.providerId, model: target.model },
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
try {
|
|
417
|
+
const done = await dispatchComplete(
|
|
418
|
+
target,
|
|
419
|
+
input.system,
|
|
420
|
+
input.messages,
|
|
421
|
+
input.temperature ?? 0.4,
|
|
422
|
+
useTools,
|
|
423
|
+
toolCtx,
|
|
424
|
+
);
|
|
425
|
+
if (!done) return null;
|
|
426
|
+
return {
|
|
427
|
+
text: done.text,
|
|
428
|
+
provider: target.providerId,
|
|
429
|
+
model: target.model,
|
|
430
|
+
traces: done.traces,
|
|
431
|
+
thinking: done.thinking,
|
|
432
|
+
usage: {
|
|
433
|
+
...(done.usage ?? {}),
|
|
434
|
+
provider: target.providerId,
|
|
435
|
+
model: target.model,
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
} catch {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
type DispatchResult = {
|
|
444
|
+
text: string;
|
|
445
|
+
traces: ToolTrace[];
|
|
446
|
+
thinking: string;
|
|
447
|
+
usage?: ChatUsage;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
async function dispatchComplete(
|
|
451
|
+
target: LlmTarget,
|
|
452
|
+
system: string,
|
|
453
|
+
messages: { role: "user" | "assistant"; content: string }[],
|
|
454
|
+
temperature: number,
|
|
455
|
+
tools: boolean,
|
|
456
|
+
ctx: ToolContext,
|
|
457
|
+
): Promise<DispatchResult | null> {
|
|
458
|
+
if (target.api === "openai-responses") {
|
|
459
|
+
const text = await completeCodex(target, system, messages);
|
|
460
|
+
return text ? { text, traces: [], thinking: "" } : null;
|
|
461
|
+
}
|
|
462
|
+
if (target.api === "anthropic-messages") {
|
|
463
|
+
return tools
|
|
464
|
+
? completeAnthropicTools(target, system, messages, ctx)
|
|
465
|
+
: wrapText(await completeAnthropic(target, system, messages));
|
|
466
|
+
}
|
|
467
|
+
return tools
|
|
468
|
+
? completeOpenAiTools(target, system, messages, temperature, ctx)
|
|
469
|
+
: wrapText(await completeOpenAi(target, system, messages, temperature));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function wrapText(text: string | null): DispatchResult | null {
|
|
473
|
+
return text ? { text, traces: [], thinking: "" } : null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function completeOpenAiTools(
|
|
477
|
+
target: LlmTarget,
|
|
478
|
+
system: string,
|
|
479
|
+
messages: { role: "user" | "assistant"; content: string }[],
|
|
480
|
+
temperature: number,
|
|
481
|
+
ctx: ToolContext,
|
|
482
|
+
): Promise<DispatchResult | null> {
|
|
483
|
+
type ChatMsg = {
|
|
484
|
+
role: string;
|
|
485
|
+
content?: string | null;
|
|
486
|
+
reasoning_content?: string | null;
|
|
487
|
+
reasoning?: string | null;
|
|
488
|
+
tool_calls?: {
|
|
489
|
+
id: string;
|
|
490
|
+
type: "function";
|
|
491
|
+
function: { name: string; arguments: string };
|
|
492
|
+
}[];
|
|
493
|
+
tool_call_id?: string;
|
|
494
|
+
};
|
|
495
|
+
const msgs: ChatMsg[] = [
|
|
496
|
+
{ role: "system", content: system },
|
|
497
|
+
...messages,
|
|
498
|
+
];
|
|
499
|
+
const traces: ToolTrace[] = [];
|
|
500
|
+
const thinkingChunks: string[] = [];
|
|
501
|
+
const catalog = openaiTools(ctx.skills ?? [], ctx);
|
|
502
|
+
const usage = blankUsage();
|
|
503
|
+
const started = Date.now();
|
|
504
|
+
let lastAssistant: ChatMsg | null = null;
|
|
505
|
+
const looped = await runAgentLoop({
|
|
506
|
+
toolCtx: ctx,
|
|
507
|
+
traces,
|
|
508
|
+
thinkingChunks,
|
|
509
|
+
nullIfNoTraces: true,
|
|
510
|
+
ask: async ({ wrap, steer }) => {
|
|
511
|
+
if (wrap) msgs.push({ role: "user", content: TOOL_LOOP_WRAP });
|
|
512
|
+
if (steer) msgs.push({ role: "user", content: steer });
|
|
513
|
+
const extra =
|
|
514
|
+
estimateSendTokens(system) +
|
|
515
|
+
estimateSendTokens(JSON.stringify(catalog)) +
|
|
516
|
+
2048;
|
|
517
|
+
const fitted = trimSendMessages(msgs, extra);
|
|
518
|
+
if (fitted.length < msgs.length) {
|
|
519
|
+
msgs.splice(0, msgs.length, ...fitted);
|
|
520
|
+
}
|
|
521
|
+
const response = await fetch(`${target.baseUrl}/chat/completions`, {
|
|
522
|
+
method: "POST",
|
|
523
|
+
headers: {
|
|
524
|
+
authorization: `Bearer ${target.apiKey}`,
|
|
525
|
+
"content-type": "application/json",
|
|
526
|
+
...(target.headers ?? {}),
|
|
527
|
+
},
|
|
528
|
+
body: JSON.stringify({
|
|
529
|
+
model: target.model,
|
|
530
|
+
temperature,
|
|
531
|
+
messages: msgs,
|
|
532
|
+
tools: catalog,
|
|
533
|
+
tool_choice: "auto",
|
|
534
|
+
}),
|
|
535
|
+
signal: roundSignal(ctx),
|
|
536
|
+
});
|
|
537
|
+
if (!response.ok) return null;
|
|
538
|
+
const data = (await response.json()) as {
|
|
539
|
+
choices?: { message?: ChatMsg; finish_reason?: string }[];
|
|
540
|
+
usage?: {
|
|
541
|
+
prompt_tokens?: number;
|
|
542
|
+
completion_tokens?: number;
|
|
543
|
+
total_tokens?: number;
|
|
544
|
+
prompt_tokens_details?: { cached_tokens?: number };
|
|
545
|
+
input_tokens_details?: { cached_tokens?: number };
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
const message = data.choices?.[0]?.message;
|
|
549
|
+
if (!message) return null;
|
|
550
|
+
addUsage(usage, fromOpenAiUsage(data.usage));
|
|
551
|
+
lastAssistant = message;
|
|
552
|
+
const calls = (message.tool_calls ?? []).map((call) => {
|
|
553
|
+
let args: Record<string, unknown> = {};
|
|
554
|
+
try {
|
|
555
|
+
args = JSON.parse(call.function.arguments || "{}") as Record<
|
|
556
|
+
string,
|
|
557
|
+
unknown
|
|
558
|
+
>;
|
|
559
|
+
} catch {
|
|
560
|
+
args = {};
|
|
561
|
+
}
|
|
562
|
+
return { id: call.id, name: call.function.name, args };
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
calls,
|
|
566
|
+
text: message.content?.trim() ?? "",
|
|
567
|
+
thinking: (message.reasoning_content || message.reasoning || "").trim(),
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
onRetry: (late) => {
|
|
571
|
+
if (lastAssistant) msgs.push(lastAssistant);
|
|
572
|
+
msgs.push({ role: "user", content: late });
|
|
573
|
+
},
|
|
574
|
+
onTools: (calls, outcomes) => {
|
|
575
|
+
if (lastAssistant) msgs.push(lastAssistant);
|
|
576
|
+
for (let i = 0; i < calls.length; i++) {
|
|
577
|
+
msgs.push({
|
|
578
|
+
role: "tool",
|
|
579
|
+
tool_call_id: calls[i].id,
|
|
580
|
+
content: outcomes[i]?.text ?? "",
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
if (!looped) return null;
|
|
586
|
+
return {
|
|
587
|
+
text: looped.text,
|
|
588
|
+
traces: looped.traces,
|
|
589
|
+
thinking: looped.thinking,
|
|
590
|
+
usage: withDuration(usage, started),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function completeAnthropicTools(
|
|
595
|
+
target: LlmTarget,
|
|
596
|
+
system: string,
|
|
597
|
+
messages: { role: "user" | "assistant"; content: string }[],
|
|
598
|
+
ctx: ToolContext,
|
|
599
|
+
): Promise<DispatchResult | null> {
|
|
600
|
+
type Part =
|
|
601
|
+
| { type: "text"; text: string }
|
|
602
|
+
| { type: "tool_use"; id: string; name: string; input: Record<string, unknown> }
|
|
603
|
+
| { type: "tool_result"; tool_use_id: string; content: string; is_error?: boolean };
|
|
604
|
+
type Msg = { role: "user" | "assistant"; content: string | Part[] };
|
|
605
|
+
const msgs: Msg[] = messages.map((item) => ({
|
|
606
|
+
role: item.role,
|
|
607
|
+
content: item.content,
|
|
608
|
+
}));
|
|
609
|
+
const tools = openaiTools(ctx.skills ?? [], ctx).map((tool) => ({
|
|
610
|
+
name: tool.function.name,
|
|
611
|
+
description: tool.function.description,
|
|
612
|
+
input_schema: tool.function.parameters,
|
|
613
|
+
}));
|
|
614
|
+
const headers = anthropicHeaders(target);
|
|
615
|
+
const traces: ToolTrace[] = [];
|
|
616
|
+
const usage = blankUsage();
|
|
617
|
+
const started = Date.now();
|
|
618
|
+
let lastParts: Part[] = [];
|
|
619
|
+
const looped = await runAgentLoop({
|
|
620
|
+
toolCtx: ctx,
|
|
621
|
+
traces,
|
|
622
|
+
nullIfNoTraces: true,
|
|
623
|
+
ask: async ({ wrap, steer }) => {
|
|
624
|
+
if (wrap) msgs.push({ role: "user", content: TOOL_LOOP_WRAP });
|
|
625
|
+
if (steer) msgs.push({ role: "user", content: steer });
|
|
626
|
+
const extra =
|
|
627
|
+
estimateSendTokens(system) +
|
|
628
|
+
estimateSendTokens(JSON.stringify(tools)) +
|
|
629
|
+
2048;
|
|
630
|
+
const fitted = trimSendMessages(msgs, extra);
|
|
631
|
+
if (fitted.length < msgs.length) {
|
|
632
|
+
msgs.splice(0, msgs.length, ...fitted);
|
|
633
|
+
}
|
|
634
|
+
const response = await fetch(
|
|
635
|
+
`${target.baseUrl.replace(/\/v1$/, "")}/v1/messages`,
|
|
636
|
+
{
|
|
637
|
+
method: "POST",
|
|
638
|
+
headers,
|
|
639
|
+
body: JSON.stringify({
|
|
640
|
+
model: target.model,
|
|
641
|
+
max_tokens: 2048,
|
|
642
|
+
system,
|
|
643
|
+
messages: msgs,
|
|
644
|
+
tools,
|
|
645
|
+
}),
|
|
646
|
+
signal: roundSignal(ctx),
|
|
647
|
+
},
|
|
648
|
+
);
|
|
649
|
+
if (!response.ok) return null;
|
|
650
|
+
const data = (await response.json()) as {
|
|
651
|
+
stop_reason?: string;
|
|
652
|
+
content?: Part[];
|
|
653
|
+
usage?: {
|
|
654
|
+
input_tokens?: number;
|
|
655
|
+
output_tokens?: number;
|
|
656
|
+
cache_read_input_tokens?: number;
|
|
657
|
+
cache_creation_input_tokens?: number;
|
|
658
|
+
};
|
|
659
|
+
};
|
|
660
|
+
const parts = data.content ?? [];
|
|
661
|
+
lastParts = parts;
|
|
662
|
+
addUsage(usage, fromAnthropicUsage(data.usage));
|
|
663
|
+
const uses =
|
|
664
|
+
data.stop_reason === "tool_use"
|
|
665
|
+
? parts.filter(
|
|
666
|
+
(part): part is Extract<Part, { type: "tool_use" }> =>
|
|
667
|
+
part.type === "tool_use",
|
|
668
|
+
)
|
|
669
|
+
: [];
|
|
670
|
+
const textPart = parts.find((part) => part.type === "text");
|
|
671
|
+
const body =
|
|
672
|
+
textPart && textPart.type === "text" ? textPart.text.trim() : "";
|
|
673
|
+
return {
|
|
674
|
+
calls: uses.map((call) => ({
|
|
675
|
+
id: call.id,
|
|
676
|
+
name: call.name,
|
|
677
|
+
args: call.input ?? {},
|
|
678
|
+
})),
|
|
679
|
+
text: body,
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
onRetry: (late) => {
|
|
683
|
+
if (lastParts.length) msgs.push({ role: "assistant", content: lastParts });
|
|
684
|
+
msgs.push({ role: "user", content: late });
|
|
685
|
+
},
|
|
686
|
+
onTools: (calls, outcomes) => {
|
|
687
|
+
if (lastParts.length) msgs.push({ role: "assistant", content: lastParts });
|
|
688
|
+
msgs.push({
|
|
689
|
+
role: "user",
|
|
690
|
+
content: calls.map((call, i) => ({
|
|
691
|
+
type: "tool_result" as const,
|
|
692
|
+
tool_use_id: call.id,
|
|
693
|
+
content: outcomes[i]?.text ?? "",
|
|
694
|
+
is_error: outcomes[i]?.isError,
|
|
695
|
+
})),
|
|
696
|
+
});
|
|
697
|
+
},
|
|
698
|
+
});
|
|
699
|
+
if (!looped) return null;
|
|
700
|
+
return {
|
|
701
|
+
text: looped.text,
|
|
702
|
+
traces: looped.traces,
|
|
703
|
+
thinking: looped.thinking,
|
|
704
|
+
usage: withDuration(usage, started),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function anthropicHeaders(target: LlmTarget): Record<string, string> {
|
|
709
|
+
const oauth =
|
|
710
|
+
target.providerId === "anthropic-oauth" ||
|
|
711
|
+
target.apiKey.includes("sk-ant-oat");
|
|
712
|
+
const headers: Record<string, string> = {
|
|
713
|
+
"anthropic-version": "2023-06-01",
|
|
714
|
+
"content-type": "application/json",
|
|
715
|
+
...(target.headers ?? {}),
|
|
716
|
+
};
|
|
717
|
+
if (oauth) {
|
|
718
|
+
headers.authorization = `Bearer ${target.apiKey}`;
|
|
719
|
+
headers["anthropic-beta"] = "claude-code-20250219,oauth-2025-04-20";
|
|
720
|
+
headers["user-agent"] = "claude-cli/2.0.0";
|
|
721
|
+
headers["x-app"] = "cli";
|
|
722
|
+
} else if (!headers.authorization && !headers.Authorization) {
|
|
723
|
+
headers["x-api-key"] = target.apiKey;
|
|
724
|
+
}
|
|
725
|
+
return headers;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async function completeOpenAi(
|
|
729
|
+
target: LlmTarget,
|
|
730
|
+
system: string,
|
|
731
|
+
messages: { role: "user" | "assistant"; content: string }[],
|
|
732
|
+
temperature: number,
|
|
733
|
+
): Promise<string | null> {
|
|
734
|
+
const url = `${target.baseUrl}/chat/completions`;
|
|
735
|
+
const headers: Record<string, string> = {
|
|
736
|
+
authorization: `Bearer ${target.apiKey}`,
|
|
737
|
+
"content-type": "application/json",
|
|
738
|
+
...(target.headers ?? {}),
|
|
739
|
+
};
|
|
740
|
+
const response = await fetch(url, {
|
|
741
|
+
method: "POST",
|
|
742
|
+
headers,
|
|
743
|
+
body: JSON.stringify({
|
|
744
|
+
model: target.model,
|
|
745
|
+
temperature,
|
|
746
|
+
messages: [{ role: "system", content: system }, ...messages],
|
|
747
|
+
}),
|
|
748
|
+
signal: AbortSignal.timeout(25_000),
|
|
749
|
+
});
|
|
750
|
+
if (!response.ok) return null;
|
|
751
|
+
const data = (await response.json()) as {
|
|
752
|
+
choices?: { message?: { content?: string } }[];
|
|
753
|
+
};
|
|
754
|
+
return data.choices?.[0]?.message?.content?.trim() || null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async function completeAnthropic(
|
|
758
|
+
target: LlmTarget,
|
|
759
|
+
system: string,
|
|
760
|
+
messages: { role: "user" | "assistant"; content: string }[],
|
|
761
|
+
): Promise<string | null> {
|
|
762
|
+
const base = target.baseUrl.replace(/\/v1$/, "");
|
|
763
|
+
const oauth = target.providerId === "anthropic-oauth" || target.apiKey.includes("sk-ant-oat");
|
|
764
|
+
const headers: Record<string, string> = {
|
|
765
|
+
"anthropic-version": "2023-06-01",
|
|
766
|
+
"content-type": "application/json",
|
|
767
|
+
...(target.headers ?? {}),
|
|
768
|
+
};
|
|
769
|
+
if (oauth) {
|
|
770
|
+
headers.authorization = `Bearer ${target.apiKey}`;
|
|
771
|
+
headers["anthropic-beta"] = "claude-code-20250219,oauth-2025-04-20";
|
|
772
|
+
headers["user-agent"] = "claude-cli/2.0.0";
|
|
773
|
+
headers["x-app"] = "cli";
|
|
774
|
+
} else if (!headers.authorization && !headers.Authorization) {
|
|
775
|
+
headers["x-api-key"] = target.apiKey;
|
|
776
|
+
}
|
|
777
|
+
const response = await fetch(`${base}/v1/messages`, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers,
|
|
780
|
+
body: JSON.stringify({
|
|
781
|
+
model: target.model,
|
|
782
|
+
max_tokens: 1024,
|
|
783
|
+
system,
|
|
784
|
+
messages,
|
|
785
|
+
}),
|
|
786
|
+
signal: AbortSignal.timeout(25_000),
|
|
787
|
+
});
|
|
788
|
+
if (!response.ok) return null;
|
|
789
|
+
const data = (await response.json()) as {
|
|
790
|
+
content?: { type?: string; text?: string }[];
|
|
791
|
+
};
|
|
792
|
+
const text = data.content?.find((part) => part.type === "text")?.text;
|
|
793
|
+
return text?.trim() || null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async function completeCodex(
|
|
797
|
+
target: LlmTarget,
|
|
798
|
+
system: string,
|
|
799
|
+
messages: { role: "user" | "assistant"; content: string }[],
|
|
800
|
+
): Promise<string | null> {
|
|
801
|
+
const accountId =
|
|
802
|
+
target.accountId || chatgptAccountId(target.apiKey) || "";
|
|
803
|
+
if (!accountId) return null;
|
|
804
|
+
const url = `${target.baseUrl.replace(/\/+$/, "")}/codex/responses`;
|
|
805
|
+
const headers: Record<string, string> = {
|
|
806
|
+
authorization: `Bearer ${target.apiKey}`,
|
|
807
|
+
"chatgpt-account-id": accountId,
|
|
808
|
+
"content-type": "application/json",
|
|
809
|
+
accept: "application/json",
|
|
810
|
+
"OpenAI-Beta": "responses=experimental",
|
|
811
|
+
originator: "guild",
|
|
812
|
+
};
|
|
813
|
+
const input = messages.map((item) => ({
|
|
814
|
+
role: item.role,
|
|
815
|
+
content: item.content,
|
|
816
|
+
}));
|
|
817
|
+
const body = {
|
|
818
|
+
model: target.model,
|
|
819
|
+
stream: false,
|
|
820
|
+
store: false,
|
|
821
|
+
instructions: system,
|
|
822
|
+
input,
|
|
823
|
+
};
|
|
824
|
+
const response = await fetch(url, {
|
|
825
|
+
method: "POST",
|
|
826
|
+
headers,
|
|
827
|
+
body: JSON.stringify(body),
|
|
828
|
+
signal: AbortSignal.timeout(40_000),
|
|
829
|
+
});
|
|
830
|
+
if (response.ok) {
|
|
831
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
832
|
+
return extractResponsesText(data);
|
|
833
|
+
}
|
|
834
|
+
headers.accept = "text/event-stream";
|
|
835
|
+
const streamed = await fetch(url, {
|
|
836
|
+
method: "POST",
|
|
837
|
+
headers,
|
|
838
|
+
body: JSON.stringify({ ...body, stream: true }),
|
|
839
|
+
signal: AbortSignal.timeout(40_000),
|
|
840
|
+
});
|
|
841
|
+
if (!streamed.ok || !streamed.body) return null;
|
|
842
|
+
return readSseText(streamed);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function extractResponsesText(data: Record<string, unknown>): string | null {
|
|
846
|
+
const buckets = [data.output, (data.response as Record<string, unknown> | undefined)?.output];
|
|
847
|
+
for (const output of buckets) {
|
|
848
|
+
if (!Array.isArray(output)) continue;
|
|
849
|
+
const parts: string[] = [];
|
|
850
|
+
for (const item of output) {
|
|
851
|
+
if (!item || typeof item !== "object") continue;
|
|
852
|
+
const content = (item as { content?: unknown }).content;
|
|
853
|
+
if (!Array.isArray(content)) continue;
|
|
854
|
+
for (const part of content) {
|
|
855
|
+
if (!part || typeof part !== "object") continue;
|
|
856
|
+
const rec = part as { type?: string; text?: string };
|
|
857
|
+
if ((rec.type === "output_text" || rec.type === "text") && rec.text) {
|
|
858
|
+
parts.push(rec.text);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (parts.length) return parts.join("").trim();
|
|
863
|
+
}
|
|
864
|
+
const text = data.output_text;
|
|
865
|
+
return typeof text === "string" ? text.trim() : null;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function readSseText(response: Response): Promise<string | null> {
|
|
869
|
+
const reader = response.body?.getReader();
|
|
870
|
+
if (!reader) return null;
|
|
871
|
+
const decoder = new TextDecoder();
|
|
872
|
+
let buffer = "";
|
|
873
|
+
let text = "";
|
|
874
|
+
while (true) {
|
|
875
|
+
const { done, value } = await reader.read();
|
|
876
|
+
if (done) break;
|
|
877
|
+
buffer += decoder.decode(value, { stream: true });
|
|
878
|
+
const chunks = buffer.split("\n");
|
|
879
|
+
buffer = chunks.pop() ?? "";
|
|
880
|
+
for (const line of chunks) {
|
|
881
|
+
if (!line.startsWith("data:")) continue;
|
|
882
|
+
const raw = line.slice(5).trim();
|
|
883
|
+
if (!raw || raw === "[DONE]") continue;
|
|
884
|
+
try {
|
|
885
|
+
const event = JSON.parse(raw) as {
|
|
886
|
+
type?: string;
|
|
887
|
+
delta?: string;
|
|
888
|
+
text?: string;
|
|
889
|
+
};
|
|
890
|
+
if (typeof event.delta === "string") text += event.delta;
|
|
891
|
+
else if (event.type === "response.output_text.delta" && event.text) {
|
|
892
|
+
text += event.text;
|
|
893
|
+
}
|
|
894
|
+
} catch {
|
|
895
|
+
/* ignore */
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return text.trim() || null;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function chatgptAccountId(accessToken: string): string | null {
|
|
903
|
+
try {
|
|
904
|
+
const parts = accessToken.split(".");
|
|
905
|
+
if (parts.length < 2) return null;
|
|
906
|
+
const payload = JSON.parse(
|
|
907
|
+
Buffer.from(parts[1], "base64url").toString("utf8"),
|
|
908
|
+
) as Record<string, unknown>;
|
|
909
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
910
|
+
if (auth && typeof auth === "object") {
|
|
911
|
+
const id = (auth as Record<string, unknown>).chatgpt_account_id;
|
|
912
|
+
if (typeof id === "string" && id) return id;
|
|
913
|
+
}
|
|
914
|
+
} catch {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function sanitizeModels(file: ModelsFile): ModelsFile {
|
|
921
|
+
const providers: Record<string, ProviderEntry> = {};
|
|
922
|
+
const incoming = file.providers ?? {};
|
|
923
|
+
for (const [rawId, provider] of Object.entries(incoming)) {
|
|
924
|
+
const id = rawId
|
|
925
|
+
.trim()
|
|
926
|
+
.toLowerCase()
|
|
927
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
928
|
+
.replace(/^-+|-+$/g, "");
|
|
929
|
+
if (!id) continue;
|
|
930
|
+
if (!provider || typeof provider !== "object") continue;
|
|
931
|
+
const baseUrl = String(provider.baseUrl ?? "").trim();
|
|
932
|
+
if (!baseUrl) {
|
|
933
|
+
throw new StoreError(400, `provider ${id} needs baseUrl`);
|
|
934
|
+
}
|
|
935
|
+
const api: LlmApi =
|
|
936
|
+
provider.api === "anthropic-messages"
|
|
937
|
+
? "anthropic-messages"
|
|
938
|
+
: provider.api === "openai-responses"
|
|
939
|
+
? "openai-responses"
|
|
940
|
+
: "openai-completions";
|
|
941
|
+
const models: ModelEntry[] = (provider.models ?? [])
|
|
942
|
+
.filter((model) => model && typeof model.id === "string" && model.id.trim())
|
|
943
|
+
.map((model) => ({
|
|
944
|
+
id: model.id.trim(),
|
|
945
|
+
name: model.name?.trim() || undefined,
|
|
946
|
+
}));
|
|
947
|
+
if (models.length === 0) {
|
|
948
|
+
throw new StoreError(400, `provider ${id} needs at least one model`);
|
|
949
|
+
}
|
|
950
|
+
providers[id] = {
|
|
951
|
+
name: provider.name?.trim() || id,
|
|
952
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
953
|
+
api,
|
|
954
|
+
apiKey: provider.apiKey?.trim() || undefined,
|
|
955
|
+
models,
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
const oauthModels = (provider: string, model: string): boolean => {
|
|
959
|
+
return OAUTH_PICKER_IDS.has(provider) && Boolean(model);
|
|
960
|
+
};
|
|
961
|
+
const validRef = (ref: ModelRef | null | undefined): ModelRef | null => {
|
|
962
|
+
if (!ref?.provider || !ref.model) return null;
|
|
963
|
+
if (OAUTH_PICKER_IDS.has(ref.provider) && oauthModels(ref.provider, ref.model)) {
|
|
964
|
+
return { provider: ref.provider, model: ref.model };
|
|
965
|
+
}
|
|
966
|
+
if (providers[ref.provider]?.models.some((m) => m.id === ref.model)) {
|
|
967
|
+
return { provider: ref.provider, model: ref.model };
|
|
968
|
+
}
|
|
969
|
+
return null;
|
|
970
|
+
};
|
|
971
|
+
const aux: ModelsFile["aux"] = {};
|
|
972
|
+
for (const [role, ref] of Object.entries(file.aux ?? {})) {
|
|
973
|
+
aux[role as AuxRole] = validRef(ref as ModelRef | null);
|
|
974
|
+
}
|
|
975
|
+
const reasoning =
|
|
976
|
+
file.reasoning === "minimal" ||
|
|
977
|
+
file.reasoning === "low" ||
|
|
978
|
+
file.reasoning === "high"
|
|
979
|
+
? file.reasoning
|
|
980
|
+
: "medium";
|
|
981
|
+
const recent = (file.recent ?? [])
|
|
982
|
+
.map((ref) => validRef(ref))
|
|
983
|
+
.filter((ref): ref is ModelRef => Boolean(ref))
|
|
984
|
+
.slice(0, 8);
|
|
985
|
+
return {
|
|
986
|
+
default: validRef(file.default ?? null),
|
|
987
|
+
reasoning,
|
|
988
|
+
fast: Boolean(file.fast),
|
|
989
|
+
aux,
|
|
990
|
+
recent,
|
|
991
|
+
providers,
|
|
992
|
+
};
|
|
993
|
+
}
|