@aragon-agent/core 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/dist/engine/agent-loop.d.ts +52 -0
- package/dist/engine/agent-loop.js +260 -0
- package/dist/engine/agent.d.ts +129 -0
- package/dist/engine/agent.js +283 -0
- package/dist/engine/message-manager.d.ts +26 -0
- package/dist/engine/message-manager.js +39 -0
- package/dist/engine/steering.d.ts +27 -0
- package/dist/engine/steering.js +51 -0
- package/dist/engine/watchdog.d.ts +24 -0
- package/dist/engine/watchdog.js +50 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +63 -0
- package/dist/llm/model-registry.d.ts +51 -0
- package/dist/llm/model-registry.js +277 -0
- package/dist/llm/provider.d.ts +72 -0
- package/dist/llm/provider.js +77 -0
- package/dist/llm/providers/anthropic.d.ts +23 -0
- package/dist/llm/providers/anthropic.js +420 -0
- package/dist/llm/providers/google.d.ts +22 -0
- package/dist/llm/providers/google.js +336 -0
- package/dist/llm/providers/index.d.ts +67 -0
- package/dist/llm/providers/index.js +127 -0
- package/dist/llm/providers/openai.d.ts +22 -0
- package/dist/llm/providers/openai.js +353 -0
- package/dist/llm/stream-utils.d.ts +33 -0
- package/dist/llm/stream-utils.js +132 -0
- package/dist/llm/types.d.ts +129 -0
- package/dist/llm/types.js +17 -0
- package/dist/sandbox/console-capture.d.ts +29 -0
- package/dist/sandbox/console-capture.js +56 -0
- package/dist/sandbox/http-bridge.d.ts +47 -0
- package/dist/sandbox/http-bridge.js +143 -0
- package/dist/sandbox/isolated-vm-sandbox.d.ts +50 -0
- package/dist/sandbox/isolated-vm-sandbox.js +226 -0
- package/dist/sandbox/tool-bridge.d.ts +42 -0
- package/dist/sandbox/tool-bridge.js +98 -0
- package/dist/sandbox/types.d.ts +84 -0
- package/dist/sandbox/types.js +8 -0
- package/dist/skills/constants.d.ts +122 -0
- package/dist/skills/constants.js +179 -0
- package/dist/skills/disclosure.d.ts +146 -0
- package/dist/skills/disclosure.js +788 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +177 -0
- package/dist/skills/index.d.ts +21 -0
- package/dist/skills/index.js +18 -0
- package/dist/skills/skill-find-tool.d.ts +41 -0
- package/dist/skills/skill-find-tool.js +117 -0
- package/dist/skills/skill-registry.d.ts +96 -0
- package/dist/skills/skill-registry.js +208 -0
- package/dist/skills/skill-tool.d.ts +44 -0
- package/dist/skills/skill-tool.js +96 -0
- package/dist/skills/tool-policy.d.ts +91 -0
- package/dist/skills/tool-policy.js +272 -0
- package/dist/skills/types.d.ts +271 -0
- package/dist/skills/types.js +8 -0
- package/dist/skills/validate.d.ts +37 -0
- package/dist/skills/validate.js +264 -0
- package/dist/tools/executor.d.ts +54 -0
- package/dist/tools/executor.js +206 -0
- package/dist/tools/helpers.d.ts +69 -0
- package/dist/tools/helpers.js +87 -0
- package/dist/tools/registry.d.ts +58 -0
- package/dist/tools/registry.js +119 -0
- package/dist/tools/types.d.ts +140 -0
- package/dist/tools/types.js +7 -0
- package/dist/tools/validator.d.ts +46 -0
- package/dist/tools/validator.js +210 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +7 -0
- package/package.json +115 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Gemini (Generative AI) adapter.
|
|
3
|
+
*
|
|
4
|
+
* Uses `POST /v1beta/models/{model}:streamGenerateContent?alt=sse` for
|
|
5
|
+
* streaming and maps the chunked response into unified StreamEvents.
|
|
6
|
+
*
|
|
7
|
+
* Key differences from Anthropic/OpenAI:
|
|
8
|
+
* - Tool calls (functionCall) arrive as complete objects in a single chunk,
|
|
9
|
+
* not incrementally — so we emit tool_call_start + tool_call_end together.
|
|
10
|
+
* - System instructions use a dedicated `system_instruction` field.
|
|
11
|
+
* - The API key is passed as a query parameter, not a header.
|
|
12
|
+
*/
|
|
13
|
+
import { randomUUID } from 'node:crypto';
|
|
14
|
+
import { LLMError, classifyHttpError, wrapFetchError } from '../provider.js';
|
|
15
|
+
import { parseSSEStream } from '../stream-utils.js';
|
|
16
|
+
import { consumeStream } from '../stream-utils.js';
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Constants
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
const DEFAULT_BASE_URL = 'https://generativelanguage.googleapis.com';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Google provider
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
export class GoogleProvider {
|
|
25
|
+
id = 'google';
|
|
26
|
+
displayName = 'Google';
|
|
27
|
+
defaultBaseUrl = DEFAULT_BASE_URL;
|
|
28
|
+
// -----------------------------------------------------------------------
|
|
29
|
+
// stream
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
async *stream(request) {
|
|
32
|
+
const baseUrl = (request.baseUrl || this.defaultBaseUrl).replace(/\/+$/, '');
|
|
33
|
+
const url = `${baseUrl}/v1beta/models/${request.model}:streamGenerateContent?alt=sse&key=${encodeURIComponent(request.apiKey)}`;
|
|
34
|
+
const body = buildRequestBody(request);
|
|
35
|
+
let response;
|
|
36
|
+
try {
|
|
37
|
+
response = await fetch(url, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(body),
|
|
41
|
+
signal: request.signal,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
yield { type: 'error', error: wrapFetchError(err, this.id) };
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
const text = await response.text().catch(() => '');
|
|
50
|
+
yield { type: 'error', error: classifyHttpError(response.status, text, this.id) };
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!response.body) {
|
|
54
|
+
yield { type: 'error', error: new LLMError('Response body is null', this.id, 'network_error', true) };
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// -- Parse SSE events --
|
|
58
|
+
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
59
|
+
const contentBlocks = [];
|
|
60
|
+
let textAccumulator = '';
|
|
61
|
+
let stopReason;
|
|
62
|
+
try {
|
|
63
|
+
for await (const sse of parseSSEStream(response.body, request.signal)) {
|
|
64
|
+
const raw = sse.data.trim();
|
|
65
|
+
if (!raw)
|
|
66
|
+
continue;
|
|
67
|
+
let data;
|
|
68
|
+
try {
|
|
69
|
+
data = JSON.parse(raw);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// Usage metadata
|
|
75
|
+
const usageMeta = data.usageMetadata;
|
|
76
|
+
if (usageMeta) {
|
|
77
|
+
usage.inputTokens = usageMeta.promptTokenCount ?? usage.inputTokens;
|
|
78
|
+
usage.outputTokens = usageMeta.candidatesTokenCount ?? usage.outputTokens;
|
|
79
|
+
}
|
|
80
|
+
// Candidates
|
|
81
|
+
const candidates = data.candidates;
|
|
82
|
+
if (!candidates || candidates.length === 0)
|
|
83
|
+
continue;
|
|
84
|
+
const candidate = candidates[0];
|
|
85
|
+
// Finish reason
|
|
86
|
+
const finishReason = candidate.finishReason;
|
|
87
|
+
if (finishReason) {
|
|
88
|
+
stopReason = mapFinishReason(finishReason);
|
|
89
|
+
}
|
|
90
|
+
const content = candidate.content;
|
|
91
|
+
if (!content)
|
|
92
|
+
continue;
|
|
93
|
+
const parts = content.parts;
|
|
94
|
+
if (!parts)
|
|
95
|
+
continue;
|
|
96
|
+
for (const part of parts) {
|
|
97
|
+
// Text part
|
|
98
|
+
if (typeof part.text === 'string') {
|
|
99
|
+
textAccumulator += part.text;
|
|
100
|
+
yield { type: 'text_delta', delta: part.text };
|
|
101
|
+
}
|
|
102
|
+
// Function call — arrives complete in a single chunk
|
|
103
|
+
const functionCall = part.functionCall;
|
|
104
|
+
if (functionCall) {
|
|
105
|
+
const toolName = functionCall.name || '';
|
|
106
|
+
const args = functionCall.args || {};
|
|
107
|
+
const toolCallId = `call_${randomUUID().slice(0, 8)}`;
|
|
108
|
+
yield { type: 'tool_call_start', toolCallId, toolName };
|
|
109
|
+
yield { type: 'tool_call_end', toolCallId, toolName, args };
|
|
110
|
+
contentBlocks.push({
|
|
111
|
+
type: 'tool_call',
|
|
112
|
+
toolCallId,
|
|
113
|
+
toolName,
|
|
114
|
+
args,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Flush text
|
|
120
|
+
if (textAccumulator) {
|
|
121
|
+
contentBlocks.push({ type: 'text', text: textAccumulator });
|
|
122
|
+
}
|
|
123
|
+
const message = {
|
|
124
|
+
role: 'assistant',
|
|
125
|
+
content: contentBlocks,
|
|
126
|
+
usage,
|
|
127
|
+
stopReason,
|
|
128
|
+
};
|
|
129
|
+
yield { type: 'done', message, usage };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
yield { type: 'error', error: wrapFetchError(err, this.id) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// -----------------------------------------------------------------------
|
|
136
|
+
// complete
|
|
137
|
+
// -----------------------------------------------------------------------
|
|
138
|
+
async complete(request) {
|
|
139
|
+
return consumeStream(this.stream(request));
|
|
140
|
+
}
|
|
141
|
+
// -----------------------------------------------------------------------
|
|
142
|
+
// listModels
|
|
143
|
+
// -----------------------------------------------------------------------
|
|
144
|
+
async listModels(apiKey, baseUrl) {
|
|
145
|
+
const base = (baseUrl || this.defaultBaseUrl).replace(/\/+$/, '');
|
|
146
|
+
const url = `${base}/v1beta/models?key=${encodeURIComponent(apiKey)}`;
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch(url, {
|
|
149
|
+
method: 'GET',
|
|
150
|
+
signal: AbortSignal.timeout(15_000),
|
|
151
|
+
});
|
|
152
|
+
if (!res.ok)
|
|
153
|
+
return [];
|
|
154
|
+
const data = await res.json();
|
|
155
|
+
return (data.models || [])
|
|
156
|
+
.filter((m) => {
|
|
157
|
+
const methods = (m.supportedGenerationMethods || []);
|
|
158
|
+
return methods.includes('generateContent') || methods.includes('streamGenerateContent');
|
|
159
|
+
})
|
|
160
|
+
.map((m) => {
|
|
161
|
+
const id = String(m.name || '').replace(/^models\//, '');
|
|
162
|
+
return {
|
|
163
|
+
id,
|
|
164
|
+
name: String(m.displayName || id),
|
|
165
|
+
provider: this.id,
|
|
166
|
+
contextWindow: m.inputTokenLimit || 128_000,
|
|
167
|
+
maxOutputTokens: m.outputTokenLimit || 8192,
|
|
168
|
+
supportsThinking: false,
|
|
169
|
+
supportsTools: true,
|
|
170
|
+
supportsImages: true,
|
|
171
|
+
cost: { input: 0, output: 0 },
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Internal: request body builder
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
function buildRequestBody(request) {
|
|
184
|
+
const body = {
|
|
185
|
+
contents: convertMessages(request.messages),
|
|
186
|
+
};
|
|
187
|
+
if (request.systemPrompt) {
|
|
188
|
+
body.system_instruction = {
|
|
189
|
+
parts: [{ text: request.systemPrompt }],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (request.tools && request.tools.length > 0) {
|
|
193
|
+
body.tools = [
|
|
194
|
+
{
|
|
195
|
+
functionDeclarations: request.tools.map(convertToolDefinition),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}
|
|
199
|
+
// Generation config
|
|
200
|
+
const genConfig = {};
|
|
201
|
+
if (request.maxTokens) {
|
|
202
|
+
genConfig.maxOutputTokens = request.maxTokens;
|
|
203
|
+
}
|
|
204
|
+
if (request.temperature !== undefined) {
|
|
205
|
+
genConfig.temperature = request.temperature;
|
|
206
|
+
}
|
|
207
|
+
if (request.stopSequences && request.stopSequences.length > 0) {
|
|
208
|
+
genConfig.stopSequences = request.stopSequences;
|
|
209
|
+
}
|
|
210
|
+
if (Object.keys(genConfig).length > 0) {
|
|
211
|
+
body.generationConfig = genConfig;
|
|
212
|
+
}
|
|
213
|
+
return body;
|
|
214
|
+
}
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
// Internal: message format conversion
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
function convertMessages(messages) {
|
|
219
|
+
const result = [];
|
|
220
|
+
for (const msg of messages) {
|
|
221
|
+
switch (msg.role) {
|
|
222
|
+
case 'user':
|
|
223
|
+
result.push({
|
|
224
|
+
role: 'user',
|
|
225
|
+
parts: typeof msg.content === 'string'
|
|
226
|
+
? [{ text: msg.content }]
|
|
227
|
+
: convertContentParts(msg.content),
|
|
228
|
+
});
|
|
229
|
+
break;
|
|
230
|
+
case 'assistant':
|
|
231
|
+
result.push({
|
|
232
|
+
role: 'model',
|
|
233
|
+
parts: convertAssistantParts(msg.content),
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
case 'tool_result': {
|
|
237
|
+
const textContent = typeof msg.content === 'string'
|
|
238
|
+
? msg.content
|
|
239
|
+
: msg.content.map((p) => p.type === 'text' ? p.text : '').join('');
|
|
240
|
+
// Gemini requires the function *name* here, not the call ID.
|
|
241
|
+
// Look backwards through the conversation for the matching
|
|
242
|
+
// tool_call block so we can resolve the actual name.
|
|
243
|
+
const toolName = resolveToolName(messages, msg.toolCallId);
|
|
244
|
+
result.push({
|
|
245
|
+
role: 'function',
|
|
246
|
+
parts: [
|
|
247
|
+
{
|
|
248
|
+
functionResponse: {
|
|
249
|
+
name: toolName,
|
|
250
|
+
response: {
|
|
251
|
+
content: textContent,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
});
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
function convertContentParts(parts) {
|
|
264
|
+
return parts.map((part) => {
|
|
265
|
+
if (part.type === 'text') {
|
|
266
|
+
return { text: part.text };
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
inlineData: {
|
|
270
|
+
mimeType: part.mediaType,
|
|
271
|
+
data: part.data,
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function convertAssistantParts(blocks) {
|
|
277
|
+
const parts = [];
|
|
278
|
+
for (const block of blocks) {
|
|
279
|
+
switch (block.type) {
|
|
280
|
+
case 'text':
|
|
281
|
+
parts.push({ text: block.text });
|
|
282
|
+
break;
|
|
283
|
+
case 'thinking':
|
|
284
|
+
// Gemini doesn't have thinking blocks — emit as text
|
|
285
|
+
parts.push({ text: block.text });
|
|
286
|
+
break;
|
|
287
|
+
case 'tool_call':
|
|
288
|
+
parts.push({
|
|
289
|
+
functionCall: {
|
|
290
|
+
name: block.toolName,
|
|
291
|
+
args: block.args,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return parts;
|
|
298
|
+
}
|
|
299
|
+
function convertToolDefinition(tool) {
|
|
300
|
+
return {
|
|
301
|
+
name: tool.name,
|
|
302
|
+
description: tool.description,
|
|
303
|
+
parameters: tool.parameters,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// Internal: helpers
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
/**
|
|
310
|
+
* Resolve a toolCallId back to the function name by scanning the conversation
|
|
311
|
+
* history for a matching tool_call block in an assistant message.
|
|
312
|
+
*
|
|
313
|
+
* Falls back to the toolCallId itself if no match is found (best-effort).
|
|
314
|
+
*/
|
|
315
|
+
function resolveToolName(messages, toolCallId) {
|
|
316
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
317
|
+
const m = messages[i];
|
|
318
|
+
if (m.role === 'assistant') {
|
|
319
|
+
for (const block of m.content) {
|
|
320
|
+
if (block.type === 'tool_call' && block.toolCallId === toolCallId) {
|
|
321
|
+
return block.toolName;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return toolCallId;
|
|
327
|
+
}
|
|
328
|
+
function mapFinishReason(reason) {
|
|
329
|
+
switch (reason) {
|
|
330
|
+
case 'STOP': return 'end_turn';
|
|
331
|
+
case 'MAX_TOKENS': return 'max_tokens';
|
|
332
|
+
case 'SAFETY': return 'end_turn';
|
|
333
|
+
case 'RECITATION': return 'end_turn';
|
|
334
|
+
default: return 'end_turn';
|
|
335
|
+
}
|
|
336
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry — manages all registered LLM providers and provides
|
|
3
|
+
* convenience methods for streaming and completion.
|
|
4
|
+
*/
|
|
5
|
+
import type { AssistantMessage, ModelInfo, StreamEvent } from '../types.js';
|
|
6
|
+
import type { LLMProvider, LLMRequest } from '../provider.js';
|
|
7
|
+
export declare class ProviderRegistry {
|
|
8
|
+
private readonly providers;
|
|
9
|
+
/** Register a provider. Overwrites any existing registration with the same id. */
|
|
10
|
+
register(provider: LLMProvider): void;
|
|
11
|
+
/** Retrieve a provider by id. */
|
|
12
|
+
get(id: string): LLMProvider | undefined;
|
|
13
|
+
/** Return all registered providers. */
|
|
14
|
+
getAll(): LLMProvider[];
|
|
15
|
+
/** Check whether a provider is registered. */
|
|
16
|
+
has(id: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Convenience: stream a completion through the named provider.
|
|
19
|
+
*
|
|
20
|
+
* @throws {LLMError} if the provider is not registered.
|
|
21
|
+
*/
|
|
22
|
+
stream(providerId: string, request: LLMRequest): AsyncIterableIterator<StreamEvent>;
|
|
23
|
+
/**
|
|
24
|
+
* Convenience: non-streaming completion through the named provider.
|
|
25
|
+
*
|
|
26
|
+
* @throws {LLMError} if the provider is not registered.
|
|
27
|
+
*/
|
|
28
|
+
complete(providerId: string, request: LLMRequest): Promise<AssistantMessage>;
|
|
29
|
+
/**
|
|
30
|
+
* Convenience: list models from the named provider.
|
|
31
|
+
*
|
|
32
|
+
* @throws {LLMError} if the provider is not registered.
|
|
33
|
+
*/
|
|
34
|
+
listModels(providerId: string, apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
35
|
+
/** Get the default base URL for a provider. */
|
|
36
|
+
getDefaultBaseUrl(providerId: string): string;
|
|
37
|
+
private resolveProvider;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create and populate a {@link ProviderRegistry} with all built-in providers.
|
|
41
|
+
* Call this once at server startup.
|
|
42
|
+
*/
|
|
43
|
+
export declare function initProviders(): ProviderRegistry;
|
|
44
|
+
/**
|
|
45
|
+
* Get (or lazily create) the global singleton provider registry.
|
|
46
|
+
*
|
|
47
|
+
* Prefer calling `initProviders()` explicitly at startup and passing the
|
|
48
|
+
* registry around. This singleton exists as a convenience for modules that
|
|
49
|
+
* cannot easily receive the registry through dependency injection.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getProviderRegistry(): ProviderRegistry;
|
|
52
|
+
/**
|
|
53
|
+
* Replace the global singleton (useful for testing).
|
|
54
|
+
*/
|
|
55
|
+
export declare function setProviderRegistry(registry: ProviderRegistry): void;
|
|
56
|
+
/**
|
|
57
|
+
* Stream a completion — drop-in replacement for `pi-ai` `streamSimple`.
|
|
58
|
+
*
|
|
59
|
+
* Uses the global singleton registry.
|
|
60
|
+
*/
|
|
61
|
+
export declare function streamLLM(providerId: string, request: LLMRequest): AsyncIterableIterator<StreamEvent>;
|
|
62
|
+
/**
|
|
63
|
+
* Non-streaming completion — drop-in replacement for `pi-ai` `completeSimple`.
|
|
64
|
+
*
|
|
65
|
+
* Uses the global singleton registry.
|
|
66
|
+
*/
|
|
67
|
+
export declare function completeLLM(providerId: string, request: LLMRequest): Promise<AssistantMessage>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry — manages all registered LLM providers and provides
|
|
3
|
+
* convenience methods for streaming and completion.
|
|
4
|
+
*/
|
|
5
|
+
import { LLMError } from '../provider.js';
|
|
6
|
+
import { AnthropicProvider } from './anthropic.js';
|
|
7
|
+
import { OpenAIProvider } from './openai.js';
|
|
8
|
+
import { GoogleProvider } from './google.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// ProviderRegistry
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export class ProviderRegistry {
|
|
13
|
+
providers = new Map();
|
|
14
|
+
/** Register a provider. Overwrites any existing registration with the same id. */
|
|
15
|
+
register(provider) {
|
|
16
|
+
this.providers.set(provider.id, provider);
|
|
17
|
+
}
|
|
18
|
+
/** Retrieve a provider by id. */
|
|
19
|
+
get(id) {
|
|
20
|
+
return this.providers.get(id);
|
|
21
|
+
}
|
|
22
|
+
/** Return all registered providers. */
|
|
23
|
+
getAll() {
|
|
24
|
+
return Array.from(this.providers.values());
|
|
25
|
+
}
|
|
26
|
+
/** Check whether a provider is registered. */
|
|
27
|
+
has(id) {
|
|
28
|
+
return this.providers.has(id);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Convenience: stream a completion through the named provider.
|
|
32
|
+
*
|
|
33
|
+
* @throws {LLMError} if the provider is not registered.
|
|
34
|
+
*/
|
|
35
|
+
stream(providerId, request) {
|
|
36
|
+
const provider = this.resolveProvider(providerId);
|
|
37
|
+
return provider.stream(request);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Convenience: non-streaming completion through the named provider.
|
|
41
|
+
*
|
|
42
|
+
* @throws {LLMError} if the provider is not registered.
|
|
43
|
+
*/
|
|
44
|
+
async complete(providerId, request) {
|
|
45
|
+
const provider = this.resolveProvider(providerId);
|
|
46
|
+
return provider.complete(request);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Convenience: list models from the named provider.
|
|
50
|
+
*
|
|
51
|
+
* @throws {LLMError} if the provider is not registered.
|
|
52
|
+
*/
|
|
53
|
+
async listModels(providerId, apiKey, baseUrl) {
|
|
54
|
+
const provider = this.resolveProvider(providerId);
|
|
55
|
+
return provider.listModels(apiKey, baseUrl);
|
|
56
|
+
}
|
|
57
|
+
/** Get the default base URL for a provider. */
|
|
58
|
+
getDefaultBaseUrl(providerId) {
|
|
59
|
+
return this.providers.get(providerId)?.defaultBaseUrl ?? '';
|
|
60
|
+
}
|
|
61
|
+
// -----------------------------------------------------------------------
|
|
62
|
+
// Internal
|
|
63
|
+
// -----------------------------------------------------------------------
|
|
64
|
+
resolveProvider(id) {
|
|
65
|
+
const provider = this.providers.get(id);
|
|
66
|
+
if (!provider) {
|
|
67
|
+
throw new LLMError(`Unknown LLM provider: "${id}". Registered providers: ${Array.from(this.providers.keys()).join(', ')}`, id, 'invalid_request', false);
|
|
68
|
+
}
|
|
69
|
+
return provider;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// Factory
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* Create and populate a {@link ProviderRegistry} with all built-in providers.
|
|
77
|
+
* Call this once at server startup.
|
|
78
|
+
*/
|
|
79
|
+
export function initProviders() {
|
|
80
|
+
const registry = new ProviderRegistry();
|
|
81
|
+
registry.register(new AnthropicProvider());
|
|
82
|
+
registry.register(new OpenAIProvider());
|
|
83
|
+
registry.register(new GoogleProvider());
|
|
84
|
+
return registry;
|
|
85
|
+
}
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Singleton
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
let _singleton;
|
|
90
|
+
/**
|
|
91
|
+
* Get (or lazily create) the global singleton provider registry.
|
|
92
|
+
*
|
|
93
|
+
* Prefer calling `initProviders()` explicitly at startup and passing the
|
|
94
|
+
* registry around. This singleton exists as a convenience for modules that
|
|
95
|
+
* cannot easily receive the registry through dependency injection.
|
|
96
|
+
*/
|
|
97
|
+
export function getProviderRegistry() {
|
|
98
|
+
if (!_singleton) {
|
|
99
|
+
_singleton = initProviders();
|
|
100
|
+
}
|
|
101
|
+
return _singleton;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Replace the global singleton (useful for testing).
|
|
105
|
+
*/
|
|
106
|
+
export function setProviderRegistry(registry) {
|
|
107
|
+
_singleton = registry;
|
|
108
|
+
}
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Top-level convenience functions (replacements for pi-ai helpers)
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/**
|
|
113
|
+
* Stream a completion — drop-in replacement for `pi-ai` `streamSimple`.
|
|
114
|
+
*
|
|
115
|
+
* Uses the global singleton registry.
|
|
116
|
+
*/
|
|
117
|
+
export function streamLLM(providerId, request) {
|
|
118
|
+
return getProviderRegistry().stream(providerId, request);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Non-streaming completion — drop-in replacement for `pi-ai` `completeSimple`.
|
|
122
|
+
*
|
|
123
|
+
* Uses the global singleton registry.
|
|
124
|
+
*/
|
|
125
|
+
export async function completeLLM(providerId, request) {
|
|
126
|
+
return getProviderRegistry().complete(providerId, request);
|
|
127
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI Chat Completions adapter.
|
|
3
|
+
*
|
|
4
|
+
* Handles all OpenAI-compatible APIs: OpenAI, xAI, Groq, DeepSeek, Cerebras,
|
|
5
|
+
* Mistral, and any custom endpoint that speaks the same wire format.
|
|
6
|
+
*
|
|
7
|
+
* Key challenges this adapter solves:
|
|
8
|
+
* - Tool call argument chunks arrive incrementally — we accumulate and parse
|
|
9
|
+
* the full JSON only when the tool call finishes.
|
|
10
|
+
* - Multiple tool calls can be in-flight in a single completion chunk.
|
|
11
|
+
* - The `[DONE]` sentinel terminates the SSE stream.
|
|
12
|
+
*/
|
|
13
|
+
import type { AssistantMessage, ModelInfo, StreamEvent } from '../types.js';
|
|
14
|
+
import type { LLMProvider, LLMRequest } from '../provider.js';
|
|
15
|
+
export declare class OpenAIProvider implements LLMProvider {
|
|
16
|
+
readonly id = "openai";
|
|
17
|
+
readonly displayName = "OpenAI";
|
|
18
|
+
readonly defaultBaseUrl = "https://api.openai.com/v1";
|
|
19
|
+
stream(request: LLMRequest): AsyncIterableIterator<StreamEvent>;
|
|
20
|
+
complete(request: LLMRequest): Promise<AssistantMessage>;
|
|
21
|
+
listModels(apiKey: string, baseUrl?: string): Promise<ModelInfo[]>;
|
|
22
|
+
}
|