@jxburros/ai-nugget 0.3.1
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/README.md +316 -0
- package/dist/adapters/engines/anthropic.d.ts +8 -0
- package/dist/adapters/engines/anthropic.js +278 -0
- package/dist/adapters/engines/base.d.ts +23 -0
- package/dist/adapters/engines/base.js +93 -0
- package/dist/adapters/engines/google.d.ts +8 -0
- package/dist/adapters/engines/google.js +217 -0
- package/dist/adapters/engines/ollama.d.ts +10 -0
- package/dist/adapters/engines/ollama.js +160 -0
- package/dist/adapters/engines/openaiChat.d.ts +15 -0
- package/dist/adapters/engines/openaiChat.js +228 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/profiles.d.ts +42 -0
- package/dist/adapters/profiles.js +111 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +3 -0
- package/dist/agent/loop.d.ts +70 -0
- package/dist/agent/loop.js +263 -0
- package/dist/agent/tools.d.ts +29 -0
- package/dist/agent/tools.js +48 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +80 -0
- package/dist/handler.d.ts +48 -0
- package/dist/handler.js +371 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/json.d.ts +7 -0
- package/dist/json.js +114 -0
- package/dist/keys.d.ts +7 -0
- package/dist/keys.js +78 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.js +37 -0
- package/dist/redact.d.ts +8 -0
- package/dist/redact.js +46 -0
- package/dist/tokens.d.ts +5 -0
- package/dist/tokens.js +18 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +145 -0
- package/dist/types.d.ts +191 -0
- package/dist/types.js +4 -0
- package/dist/util.d.ts +8 -0
- package/dist/util.js +51 -0
- package/nugget/VERSION.txt +2 -0
- package/nugget/src/adapters/engines/anthropic.ts +260 -0
- package/nugget/src/adapters/engines/base.ts +90 -0
- package/nugget/src/adapters/engines/google.ts +209 -0
- package/nugget/src/adapters/engines/ollama.ts +153 -0
- package/nugget/src/adapters/engines/openaiChat.ts +223 -0
- package/nugget/src/adapters/index.ts +17 -0
- package/nugget/src/adapters/profiles.ts +149 -0
- package/nugget/src/agent/index.ts +2 -0
- package/nugget/src/agent/loop.ts +314 -0
- package/nugget/src/agent/tools.ts +55 -0
- package/nugget/src/errors.ts +80 -0
- package/nugget/src/handler.ts +408 -0
- package/nugget/src/index.ts +10 -0
- package/nugget/src/json.ts +108 -0
- package/nugget/src/keys.ts +70 -0
- package/nugget/src/policy.ts +39 -0
- package/nugget/src/redact.ts +48 -0
- package/nugget/src/tokens.ts +21 -0
- package/nugget/src/transport.ts +148 -0
- package/nugget/src/types.ts +175 -0
- package/nugget/src/util.ts +51 -0
- package/package.json +69 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { AIError } from '../../errors.js';
|
|
2
|
+
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
+
import { postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
5
|
+
import { health, listOpenModels, streamError, streamTimeout } from './base.js';
|
|
6
|
+
export class OpenAIChatAdapter {
|
|
7
|
+
profile;
|
|
8
|
+
provider;
|
|
9
|
+
constructor(provider, profile) {
|
|
10
|
+
this.profile = profile;
|
|
11
|
+
this.provider = provider;
|
|
12
|
+
}
|
|
13
|
+
async chat(conn, req) {
|
|
14
|
+
let final;
|
|
15
|
+
for await (const event of this.stream(conn, req)) {
|
|
16
|
+
if (event.type === 'done')
|
|
17
|
+
final = event.result;
|
|
18
|
+
if (event.type === 'error')
|
|
19
|
+
throw event.error;
|
|
20
|
+
}
|
|
21
|
+
if (!final)
|
|
22
|
+
throw new AIError('Provider stream ended without a result', { kind: 'invalid_response', provider: conn.provider });
|
|
23
|
+
return final;
|
|
24
|
+
}
|
|
25
|
+
async *stream(conn, req) {
|
|
26
|
+
const started = Date.now();
|
|
27
|
+
let firstTokenMs = null;
|
|
28
|
+
let text = '';
|
|
29
|
+
let toolCalls = [];
|
|
30
|
+
let inputTokens;
|
|
31
|
+
let outputTokens;
|
|
32
|
+
let finish;
|
|
33
|
+
let sawTerminal = false;
|
|
34
|
+
const timeout = streamTimeout(conn, req.signal);
|
|
35
|
+
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
36
|
+
try {
|
|
37
|
+
const res = await postResponse(urlFor(conn, this.profile, req.model), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
|
|
38
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
39
|
+
if (!contentType.includes('text/event-stream')) {
|
|
40
|
+
// Server ignored stream:true (or is a buffered gateway) — recover the whole body.
|
|
41
|
+
const rawText = await res.text().catch(() => '');
|
|
42
|
+
const raw = safeParse(rawText) ?? { text: rawText };
|
|
43
|
+
const parsed = parseOpenAiResponse(raw);
|
|
44
|
+
text = parsed.text;
|
|
45
|
+
toolCalls = parsed.toolCalls;
|
|
46
|
+
inputTokens = parsed.inputTokens;
|
|
47
|
+
outputTokens = parsed.outputTokens;
|
|
48
|
+
finish = parsed.finish;
|
|
49
|
+
sawTerminal = true;
|
|
50
|
+
if (text)
|
|
51
|
+
yield { type: 'delta', text };
|
|
52
|
+
for (const call of toolCalls)
|
|
53
|
+
yield { type: 'tool_call', call };
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const partialTools = new Map();
|
|
57
|
+
for await (const line of sseLines(res)) {
|
|
58
|
+
const chunk = safeParse(line);
|
|
59
|
+
const record = asRecord(chunk);
|
|
60
|
+
if (!record)
|
|
61
|
+
continue;
|
|
62
|
+
const usage = asRecord(record.usage);
|
|
63
|
+
inputTokens = asNumber(usage?.prompt_tokens) ?? inputTokens;
|
|
64
|
+
outputTokens = asNumber(usage?.completion_tokens) ?? outputTokens;
|
|
65
|
+
const choices = Array.isArray(record.choices) ? record.choices : [];
|
|
66
|
+
const choice = asRecord(choices[0]);
|
|
67
|
+
finish = asString(choice?.finish_reason) ?? finish;
|
|
68
|
+
if (choice?.finish_reason)
|
|
69
|
+
sawTerminal = true;
|
|
70
|
+
const delta = asRecord(choice?.delta);
|
|
71
|
+
const piece = asString(delta?.content);
|
|
72
|
+
if (piece) {
|
|
73
|
+
if (firstTokenMs === null)
|
|
74
|
+
firstTokenMs = Date.now() - started;
|
|
75
|
+
text += piece;
|
|
76
|
+
yield { type: 'delta', text: piece };
|
|
77
|
+
}
|
|
78
|
+
const calls = Array.isArray(delta?.tool_calls) ? delta.tool_calls : [];
|
|
79
|
+
for (const callValue of calls) {
|
|
80
|
+
const call = asRecord(callValue);
|
|
81
|
+
const index = asNumber(call?.index) ?? 0;
|
|
82
|
+
const current = partialTools.get(index) ?? { raw: '' };
|
|
83
|
+
current.id = asString(call?.id) ?? current.id;
|
|
84
|
+
const fn = asRecord(call?.function);
|
|
85
|
+
current.name = asString(fn?.name) ?? current.name;
|
|
86
|
+
current.raw += asString(fn?.arguments) ?? '';
|
|
87
|
+
partialTools.set(index, current);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
toolCalls = [...partialTools.values()].filter((call) => call.name).map((call) => ({
|
|
91
|
+
id: call.id ?? randomId(),
|
|
92
|
+
name: call.name,
|
|
93
|
+
raw: call.raw,
|
|
94
|
+
arguments: parseArgs(call.raw),
|
|
95
|
+
}));
|
|
96
|
+
for (const call of toolCalls)
|
|
97
|
+
yield { type: 'tool_call', call };
|
|
98
|
+
}
|
|
99
|
+
if (!sawTerminal) {
|
|
100
|
+
yield { type: 'context', kind: 'stream_anomaly', data: { reason: 'stream ended without a finish_reason' } };
|
|
101
|
+
}
|
|
102
|
+
const result = makeResult(conn, req, text, toolCalls, finish, started, firstTokenMs, inputTokens, outputTokens);
|
|
103
|
+
yield { type: 'done', result };
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw streamError(error, timeout, conn.provider);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
timeout.done();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
listModels(conn) {
|
|
113
|
+
return listOpenModels(conn, this.profile);
|
|
114
|
+
}
|
|
115
|
+
health(conn) {
|
|
116
|
+
return health(conn, this.profile);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function openAiBody(req, profile) {
|
|
120
|
+
const body = {
|
|
121
|
+
model: req.model,
|
|
122
|
+
messages: req.messages.map(toOpenAiMessage),
|
|
123
|
+
temperature: req.temperature,
|
|
124
|
+
top_p: req.topP,
|
|
125
|
+
stop: req.stopSequences,
|
|
126
|
+
response_format: responseFormatFor(req, profile),
|
|
127
|
+
tools: req.tools?.map((tool) => ({ type: 'function', function: tool })),
|
|
128
|
+
tool_choice: typeof req.toolChoice === 'object' ? { type: 'function', function: { name: req.toolChoice.name } } : req.toolChoice,
|
|
129
|
+
stream: true,
|
|
130
|
+
// Many OpenAI-compatible local servers (llama.cpp, LM Studio, vLLM) choke
|
|
131
|
+
// on or ignore stream_options — only send it where the profile confirms
|
|
132
|
+
// the server understands it.
|
|
133
|
+
stream_options: profile.quirks?.supportsUsageInStream ? { include_usage: true } : undefined,
|
|
134
|
+
};
|
|
135
|
+
if (req.maxTokens !== undefined)
|
|
136
|
+
body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
|
|
137
|
+
return body;
|
|
138
|
+
}
|
|
139
|
+
function responseFormatFor(req, profile) {
|
|
140
|
+
if (req.responseFormat?.type !== 'json')
|
|
141
|
+
return undefined;
|
|
142
|
+
if (req.responseFormat.schema && profile.quirks?.supportsJsonSchema) {
|
|
143
|
+
return { type: 'json_schema', json_schema: { name: 'response', schema: req.responseFormat.schema } };
|
|
144
|
+
}
|
|
145
|
+
return { type: 'json_object' };
|
|
146
|
+
}
|
|
147
|
+
function toOpenAiMessage(message) {
|
|
148
|
+
return {
|
|
149
|
+
role: message.role,
|
|
150
|
+
content: typeof message.content === 'string'
|
|
151
|
+
? message.content
|
|
152
|
+
: message.content.map((part) => part.type === 'image'
|
|
153
|
+
? { type: 'image_url', image_url: { url: `data:${part.mimeType ?? 'image/png'};base64,${part.imageBase64 ?? ''}` } }
|
|
154
|
+
: { type: 'text', text: part.text ?? '' }),
|
|
155
|
+
name: message.role === 'tool' ? message.name : undefined,
|
|
156
|
+
tool_call_id: message.toolCallId,
|
|
157
|
+
tool_calls: message.toolCalls?.map((call) => ({ id: call.id, type: 'function', function: { name: call.name, arguments: call.raw ?? JSON.stringify(call.arguments) } })),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function urlFor(conn, profile, model) {
|
|
161
|
+
if (profile.quirks?.urlTemplate) {
|
|
162
|
+
return profile.quirks.urlTemplate.replace('{baseUrl}', conn.baseUrl).replace('{model}', encodeURIComponent(model));
|
|
163
|
+
}
|
|
164
|
+
return `${conn.baseUrl}/chat/completions`;
|
|
165
|
+
}
|
|
166
|
+
function parseOpenAiResponse(raw) {
|
|
167
|
+
const record = asRecord(raw);
|
|
168
|
+
const usage = asRecord(record?.usage);
|
|
169
|
+
const choice = asRecord(Array.isArray(record?.choices) ? record.choices[0] : undefined);
|
|
170
|
+
const message = asRecord(choice?.message);
|
|
171
|
+
const text = asString(message?.content) ?? asString(record?.text) ?? '';
|
|
172
|
+
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
173
|
+
return {
|
|
174
|
+
text,
|
|
175
|
+
finish: asString(choice?.finish_reason),
|
|
176
|
+
toolCalls: calls.map((value) => {
|
|
177
|
+
const call = asRecord(value);
|
|
178
|
+
const fn = asRecord(call?.function);
|
|
179
|
+
const rawArgs = asString(fn?.arguments) ?? '{}';
|
|
180
|
+
return { id: asString(call?.id) ?? randomId(), name: asString(fn?.name) ?? 'unknown', raw: rawArgs, arguments: parseArgs(rawArgs) };
|
|
181
|
+
}),
|
|
182
|
+
inputTokens: asNumber(usage?.prompt_tokens),
|
|
183
|
+
outputTokens: asNumber(usage?.completion_tokens),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function makeResult(conn, req, text, toolCalls, finish, started, firstTokenMs, inputTokens, outputTokens) {
|
|
187
|
+
const usage = inputTokens !== undefined || outputTokens !== undefined
|
|
188
|
+
? { inputTokens, outputTokens, estimated: false }
|
|
189
|
+
: estimatedUsage(textFromMessages(req.messages), text);
|
|
190
|
+
return {
|
|
191
|
+
text,
|
|
192
|
+
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
193
|
+
finishReason: mapFinish(finish, toolCalls.length > 0),
|
|
194
|
+
usage,
|
|
195
|
+
timing: { firstTokenMs, totalMs: Date.now() - started },
|
|
196
|
+
model: req.model,
|
|
197
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function mapFinish(finish, hasToolCalls) {
|
|
201
|
+
if (hasToolCalls || finish === 'tool_calls' || finish === 'function_call')
|
|
202
|
+
return 'tool';
|
|
203
|
+
if (finish === 'length')
|
|
204
|
+
return 'length';
|
|
205
|
+
if (finish === 'content_filter')
|
|
206
|
+
return 'content_filter';
|
|
207
|
+
return 'stop';
|
|
208
|
+
}
|
|
209
|
+
function parseArgs(raw) {
|
|
210
|
+
try {
|
|
211
|
+
return raw ? JSON.parse(raw) : {};
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return {};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function safeParse(line) {
|
|
218
|
+
try {
|
|
219
|
+
return JSON.parse(line);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function randomId() {
|
|
226
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=openaiChat.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ProviderAdapter } from '../types.js';
|
|
2
|
+
export declare function adapterFor(provider: string, baseUrl?: string): ProviderAdapter;
|
|
3
|
+
export { PROVIDER_PROFILES, profileFor } from './profiles.js';
|
|
4
|
+
export type { ProviderProfile, EngineName, AuthMode } from './profiles.js';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AnthropicAdapter } from './engines/anthropic.js';
|
|
2
|
+
import { GoogleAdapter } from './engines/google.js';
|
|
3
|
+
import { OllamaAdapter } from './engines/ollama.js';
|
|
4
|
+
import { OpenAIChatAdapter } from './engines/openaiChat.js';
|
|
5
|
+
import { profileFor } from './profiles.js';
|
|
6
|
+
export function adapterFor(provider, baseUrl) {
|
|
7
|
+
const profile = profileFor(provider, baseUrl);
|
|
8
|
+
if (profile.engine === 'anthropic')
|
|
9
|
+
return new AnthropicAdapter(provider);
|
|
10
|
+
if (profile.engine === 'google')
|
|
11
|
+
return new GoogleAdapter(provider);
|
|
12
|
+
if (profile.engine === 'ollama')
|
|
13
|
+
return new OllamaAdapter(provider);
|
|
14
|
+
return new OpenAIChatAdapter(provider, profile);
|
|
15
|
+
}
|
|
16
|
+
export { PROVIDER_PROFILES, profileFor } from './profiles.js';
|
|
17
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type EngineName = 'openaiChat' | 'anthropic' | 'google' | 'ollama';
|
|
2
|
+
export type AuthMode = 'bearer' | 'x-api-key' | 'x-goog-api-key' | 'api-key-header' | 'none';
|
|
3
|
+
/**
|
|
4
|
+
* Answers the capability questions apps need before choosing behavior —
|
|
5
|
+
* distinct from `quirks`, which are wire-format details an adapter consults
|
|
6
|
+
* to build a request. These are per-provider defaults, not per-model: a
|
|
7
|
+
* specific model behind a `true` profile can still lack a capability (a tiny
|
|
8
|
+
* Ollama model ignoring `tools`), and callers that know better should pass an
|
|
9
|
+
* explicit `toolMode` rather than rely on `auto`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ProviderCapabilities {
|
|
12
|
+
/** Native tool/function-calling is reliable for this provider's protocol, not just model-dependent. Drives `toolMode: 'auto'` in the agent loop. */
|
|
13
|
+
nativeTools: boolean;
|
|
14
|
+
/** A provider-enforced structured-output mode exists (not just prompt-and-hope JSON). */
|
|
15
|
+
jsonMode: boolean;
|
|
16
|
+
/** Runs on localhost / user-controlled infrastructure rather than a hosted cloud API. */
|
|
17
|
+
local: boolean;
|
|
18
|
+
/** Meant to run embedded alongside the app (a local sidecar process) rather than accessed as a standalone hosted service. */
|
|
19
|
+
embeddable: boolean;
|
|
20
|
+
}
|
|
21
|
+
export interface ProviderProfile {
|
|
22
|
+
engine: EngineName;
|
|
23
|
+
defaultBaseUrl?: string;
|
|
24
|
+
auth: AuthMode;
|
|
25
|
+
defaultHeaders?: Record<string, string>;
|
|
26
|
+
capabilities: ProviderCapabilities;
|
|
27
|
+
quirks?: {
|
|
28
|
+
keyOptional?: boolean;
|
|
29
|
+
modelOptional?: boolean;
|
|
30
|
+
urlTemplate?: string;
|
|
31
|
+
supportsUsageInStream?: boolean;
|
|
32
|
+
maxTokensRequired?: boolean;
|
|
33
|
+
maxTokensParam?: 'max_tokens' | 'max_completion_tokens';
|
|
34
|
+
supportsJsonSchema?: boolean;
|
|
35
|
+
};
|
|
36
|
+
listModelsPath?: string;
|
|
37
|
+
healthPath?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const PROVIDER_PROFILES: Record<string, ProviderProfile>;
|
|
40
|
+
export declare function profileFor(provider: string, baseUrl?: string): ProviderProfile;
|
|
41
|
+
export declare function applyAuth(profile: ProviderProfile, apiKey: string | null, headers: Record<string, string>): Record<string, string>;
|
|
42
|
+
//# sourceMappingURL=profiles.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const HOSTED_CLOUD = { nativeTools: true, jsonMode: true, local: false, embeddable: false };
|
|
2
|
+
const LOCAL_RUNTIME = { nativeTools: false, jsonMode: false, local: true, embeddable: true };
|
|
3
|
+
export const PROVIDER_PROFILES = {
|
|
4
|
+
openai: {
|
|
5
|
+
engine: 'openaiChat',
|
|
6
|
+
defaultBaseUrl: 'https://api.openai.com/v1',
|
|
7
|
+
auth: 'bearer',
|
|
8
|
+
listModelsPath: '/models',
|
|
9
|
+
capabilities: HOSTED_CLOUD,
|
|
10
|
+
quirks: { supportsUsageInStream: true, maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
|
|
11
|
+
},
|
|
12
|
+
'azure-openai': {
|
|
13
|
+
engine: 'openaiChat',
|
|
14
|
+
auth: 'api-key-header',
|
|
15
|
+
capabilities: HOSTED_CLOUD,
|
|
16
|
+
quirks: { urlTemplate: '{baseUrl}/openai/deployments/{model}/chat/completions?api-version=2024-10-21', maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
|
|
17
|
+
},
|
|
18
|
+
openrouter: {
|
|
19
|
+
engine: 'openaiChat',
|
|
20
|
+
defaultBaseUrl: 'https://openrouter.ai/api/v1',
|
|
21
|
+
auth: 'bearer',
|
|
22
|
+
defaultHeaders: { 'HTTP-Referer': 'https://github.com/jxburros/AI-Nugget', 'X-Title': 'ai-nugget' },
|
|
23
|
+
listModelsPath: '/models',
|
|
24
|
+
capabilities: HOSTED_CLOUD,
|
|
25
|
+
quirks: { supportsUsageInStream: true },
|
|
26
|
+
},
|
|
27
|
+
groq: { engine: 'openaiChat', defaultBaseUrl: 'https://api.groq.com/openai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
28
|
+
deepseek: { engine: 'openaiChat', defaultBaseUrl: 'https://api.deepseek.com/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
29
|
+
mistral: { engine: 'openaiChat', defaultBaseUrl: 'https://api.mistral.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
30
|
+
together: { engine: 'openaiChat', defaultBaseUrl: 'https://api.together.xyz/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
31
|
+
fireworks: { engine: 'openaiChat', defaultBaseUrl: 'https://api.fireworks.ai/inference/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
|
|
32
|
+
lmstudio: {
|
|
33
|
+
engine: 'openaiChat',
|
|
34
|
+
defaultBaseUrl: 'http://localhost:1234/v1',
|
|
35
|
+
auth: 'none',
|
|
36
|
+
listModelsPath: '/models',
|
|
37
|
+
capabilities: LOCAL_RUNTIME,
|
|
38
|
+
quirks: { keyOptional: true },
|
|
39
|
+
},
|
|
40
|
+
llamacpp: {
|
|
41
|
+
engine: 'openaiChat',
|
|
42
|
+
defaultBaseUrl: 'http://localhost:8080/v1',
|
|
43
|
+
auth: 'none',
|
|
44
|
+
listModelsPath: '/models',
|
|
45
|
+
healthPath: '/health',
|
|
46
|
+
capabilities: LOCAL_RUNTIME,
|
|
47
|
+
quirks: { keyOptional: true, modelOptional: true },
|
|
48
|
+
},
|
|
49
|
+
vllm: {
|
|
50
|
+
engine: 'openaiChat',
|
|
51
|
+
defaultBaseUrl: 'http://localhost:8000/v1',
|
|
52
|
+
auth: 'none',
|
|
53
|
+
listModelsPath: '/models',
|
|
54
|
+
capabilities: LOCAL_RUNTIME,
|
|
55
|
+
quirks: { keyOptional: true },
|
|
56
|
+
},
|
|
57
|
+
ollama: {
|
|
58
|
+
engine: 'ollama',
|
|
59
|
+
defaultBaseUrl: 'http://localhost:11434',
|
|
60
|
+
auth: 'none',
|
|
61
|
+
listModelsPath: '/api/tags',
|
|
62
|
+
// Ollama's `format: 'json'` is engine-enforced (reliable regardless of
|
|
63
|
+
// model); its `tools` field is honored only by models trained for tool
|
|
64
|
+
// use, so nativeTools stays conservative like the other local runtimes.
|
|
65
|
+
capabilities: { ...LOCAL_RUNTIME, jsonMode: true },
|
|
66
|
+
quirks: { keyOptional: true },
|
|
67
|
+
},
|
|
68
|
+
anthropic: {
|
|
69
|
+
engine: 'anthropic',
|
|
70
|
+
defaultBaseUrl: 'https://api.anthropic.com',
|
|
71
|
+
auth: 'x-api-key',
|
|
72
|
+
defaultHeaders: { 'anthropic-version': '2023-06-01' },
|
|
73
|
+
capabilities: HOSTED_CLOUD,
|
|
74
|
+
quirks: { maxTokensRequired: true },
|
|
75
|
+
},
|
|
76
|
+
google: {
|
|
77
|
+
engine: 'google',
|
|
78
|
+
defaultBaseUrl: 'https://generativelanguage.googleapis.com',
|
|
79
|
+
auth: 'x-goog-api-key',
|
|
80
|
+
capabilities: HOSTED_CLOUD,
|
|
81
|
+
},
|
|
82
|
+
'openai-compat': {
|
|
83
|
+
engine: 'openaiChat',
|
|
84
|
+
auth: 'bearer',
|
|
85
|
+
listModelsPath: '/models',
|
|
86
|
+
// Unknown endpoint — usually pointed at a local/self-hosted server, so
|
|
87
|
+
// assume the conservative local-runtime defaults rather than the hosted-cloud ones.
|
|
88
|
+
capabilities: LOCAL_RUNTIME,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
export function profileFor(provider, baseUrl) {
|
|
92
|
+
return PROVIDER_PROFILES[provider] ?? {
|
|
93
|
+
...PROVIDER_PROFILES['openai-compat'],
|
|
94
|
+
defaultBaseUrl: baseUrl,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export function applyAuth(profile, apiKey, headers) {
|
|
98
|
+
const next = { ...profile.defaultHeaders, ...headers };
|
|
99
|
+
if (!apiKey)
|
|
100
|
+
return next;
|
|
101
|
+
if (profile.auth === 'bearer')
|
|
102
|
+
next.authorization = `Bearer ${apiKey}`;
|
|
103
|
+
else if (profile.auth === 'x-api-key')
|
|
104
|
+
next['x-api-key'] = apiKey;
|
|
105
|
+
else if (profile.auth === 'x-goog-api-key')
|
|
106
|
+
next['x-goog-api-key'] = apiKey;
|
|
107
|
+
else if (profile.auth === 'api-key-header')
|
|
108
|
+
next['api-key'] = apiKey;
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=profiles.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AIHandler } from '../handler.js';
|
|
2
|
+
import type { ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
|
|
3
|
+
import type { ToolSpec } from './tools.js';
|
|
4
|
+
export interface AgentOptions {
|
|
5
|
+
handler: AIHandler;
|
|
6
|
+
connection: Connection;
|
|
7
|
+
model: string;
|
|
8
|
+
tools: ToolSpec[];
|
|
9
|
+
messages: ChatMessage[];
|
|
10
|
+
/**
|
|
11
|
+
* `native` sends `tools` on the request; `promptJson` describes tools in a
|
|
12
|
+
* system message and parses a JSON directive back out of plain text.
|
|
13
|
+
* `auto` (the default) picks per-call from the connection's provider
|
|
14
|
+
* capability profile (`profileFor(provider).capabilities.nativeTools`) —
|
|
15
|
+
* hosted providers with reliable tool-calling get `native`, local runtimes
|
|
16
|
+
* (Ollama, llama.cpp, LM Studio, vLLM) and the `openai-compat` escape hatch
|
|
17
|
+
* default to `promptJson` since native tool support there is model-
|
|
18
|
+
* dependent, not protocol-guaranteed.
|
|
19
|
+
*/
|
|
20
|
+
toolMode?: 'native' | 'promptJson' | 'auto';
|
|
21
|
+
budget?: {
|
|
22
|
+
maxSteps?: number;
|
|
23
|
+
maxTokens?: number;
|
|
24
|
+
deadlineMs?: number;
|
|
25
|
+
};
|
|
26
|
+
approval?: ApprovalGate;
|
|
27
|
+
onEvent?: (e: AgentEvent) => void;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
metadata?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
export type ApprovalGate = (req: {
|
|
32
|
+
call: ToolCall;
|
|
33
|
+
tool: ToolSpec;
|
|
34
|
+
step: number;
|
|
35
|
+
}) => Promise<'allow' | 'deny' | {
|
|
36
|
+
modifiedArguments: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
export type AgentEvent = StreamEvent | {
|
|
39
|
+
type: 'step_start';
|
|
40
|
+
step: number;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'tool_start';
|
|
43
|
+
step: number;
|
|
44
|
+
call: ToolCall;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'tool_result';
|
|
47
|
+
step: number;
|
|
48
|
+
call: ToolCall;
|
|
49
|
+
result: unknown;
|
|
50
|
+
isError: boolean;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'tool_denied';
|
|
53
|
+
step: number;
|
|
54
|
+
call: ToolCall;
|
|
55
|
+
reason: string;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'agent_done';
|
|
58
|
+
result: AgentResult;
|
|
59
|
+
};
|
|
60
|
+
export interface AgentResult {
|
|
61
|
+
finalText: string;
|
|
62
|
+
messages: ChatMessage[];
|
|
63
|
+
usage: Usage;
|
|
64
|
+
steps: number;
|
|
65
|
+
stopReason: 'finished' | 'max_steps' | 'budget' | 'deadline' | 'canceled' | 'error';
|
|
66
|
+
}
|
|
67
|
+
export declare function runAgent(opts: AgentOptions): AsyncIterable<AgentEvent> & {
|
|
68
|
+
result: Promise<AgentResult>;
|
|
69
|
+
};
|
|
70
|
+
//# sourceMappingURL=loop.d.ts.map
|