@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,93 @@
|
|
|
1
|
+
import { AIError, fromUnknown } from '../../errors.js';
|
|
2
|
+
import { fetchJson, withTimeout } from '../../transport.js';
|
|
3
|
+
import { asRecord, asString } from '../../util.js';
|
|
4
|
+
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
5
|
+
/**
|
|
6
|
+
* Maps a raw streaming failure onto a typed {@link AIError}. A fired timeout is
|
|
7
|
+
* reported as `timeout` regardless of how the underlying `fetch` surfaced the
|
|
8
|
+
* abort; anything else is normalized through {@link fromUnknown} (which passes
|
|
9
|
+
* an existing {@link AIError} through untouched, preserving retryability).
|
|
10
|
+
*/
|
|
11
|
+
export function streamError(error, timeout, provider) {
|
|
12
|
+
if (timeout.timedOut())
|
|
13
|
+
return new AIError('Request timed out', { kind: 'timeout', provider });
|
|
14
|
+
return fromUnknown(error, provider);
|
|
15
|
+
}
|
|
16
|
+
/** Create a timeout/abort scope covering the full stream lifetime. */
|
|
17
|
+
export function streamTimeout(conn, signal) {
|
|
18
|
+
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
|
|
19
|
+
}
|
|
20
|
+
export async function listOpenModels(conn, profile) {
|
|
21
|
+
if (!profile.listModelsPath)
|
|
22
|
+
return [];
|
|
23
|
+
const { data } = await fetchJson(`${conn.baseUrl}${profile.listModelsPath}`, {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
headers: conn.headers,
|
|
26
|
+
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
27
|
+
provider: conn.provider,
|
|
28
|
+
});
|
|
29
|
+
const record = asRecord(data);
|
|
30
|
+
const items = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
|
|
31
|
+
const models = [];
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const row = asRecord(item);
|
|
34
|
+
const id = asString(row?.id) ?? asString(row?.name);
|
|
35
|
+
if (!id)
|
|
36
|
+
continue;
|
|
37
|
+
const contextWindow = asContextWindow(row);
|
|
38
|
+
const capabilities = asCapabilities(row);
|
|
39
|
+
const model = {
|
|
40
|
+
id,
|
|
41
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
42
|
+
};
|
|
43
|
+
if (contextWindow !== undefined)
|
|
44
|
+
model.contextWindow = contextWindow;
|
|
45
|
+
if (capabilities)
|
|
46
|
+
model.capabilities = capabilities;
|
|
47
|
+
models.push(model);
|
|
48
|
+
}
|
|
49
|
+
return models;
|
|
50
|
+
}
|
|
51
|
+
export async function health(conn, profile) {
|
|
52
|
+
const path = profile.healthPath ?? profile.listModelsPath;
|
|
53
|
+
if (!path)
|
|
54
|
+
return { ok: true };
|
|
55
|
+
try {
|
|
56
|
+
await fetchJson(`${conn.baseUrl}${path}`, {
|
|
57
|
+
method: 'GET',
|
|
58
|
+
headers: conn.headers,
|
|
59
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
60
|
+
provider: conn.provider,
|
|
61
|
+
});
|
|
62
|
+
return { ok: true };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function requireResponse(condition, message) {
|
|
69
|
+
if (!condition)
|
|
70
|
+
throw new AIError(message, { kind: 'invalid_response' });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Pulls a context-window figure out of the many shapes providers use for it
|
|
74
|
+
* (OpenRouter `context_length`, OpenAI-style `context_window`, Ollama
|
|
75
|
+
* `/api/show` `model_info` entries surfaced as `contextWindow`).
|
|
76
|
+
*/
|
|
77
|
+
function asContextWindow(row) {
|
|
78
|
+
if (!row)
|
|
79
|
+
return undefined;
|
|
80
|
+
const direct = row['context_length'] ?? row['context_window'] ?? row['contextWindow'];
|
|
81
|
+
return typeof direct === 'number' && Number.isFinite(direct) ? direct : undefined;
|
|
82
|
+
}
|
|
83
|
+
function asCapabilities(row) {
|
|
84
|
+
if (!row)
|
|
85
|
+
return undefined;
|
|
86
|
+
const value = row['capabilities'] ?? asRecord(row['architecture'])?.['modality'];
|
|
87
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === 'string'))
|
|
88
|
+
return value;
|
|
89
|
+
if (typeof value === 'string')
|
|
90
|
+
return [value];
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=base.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
|
+
export declare class GoogleAdapter implements ProviderAdapter {
|
|
3
|
+
readonly provider: string;
|
|
4
|
+
constructor(provider: string);
|
|
5
|
+
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
|
+
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=google.d.ts.map
|
|
@@ -0,0 +1,217 @@
|
|
|
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 { streamError, streamTimeout } from './base.js';
|
|
6
|
+
export class GoogleAdapter {
|
|
7
|
+
provider;
|
|
8
|
+
constructor(provider) {
|
|
9
|
+
this.provider = provider;
|
|
10
|
+
}
|
|
11
|
+
async chat(conn, req) {
|
|
12
|
+
let result;
|
|
13
|
+
for await (const event of this.stream(conn, req)) {
|
|
14
|
+
if (event.type === 'done')
|
|
15
|
+
result = event.result;
|
|
16
|
+
if (event.type === 'error')
|
|
17
|
+
throw event.error;
|
|
18
|
+
}
|
|
19
|
+
if (!result)
|
|
20
|
+
throw new AIError('Google stream ended without a result', { kind: 'invalid_response', provider: conn.provider });
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
async *stream(conn, req) {
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
let firstTokenMs = null;
|
|
26
|
+
let text = '';
|
|
27
|
+
let inputTokens;
|
|
28
|
+
let outputTokens;
|
|
29
|
+
let finish;
|
|
30
|
+
const toolCalls = [];
|
|
31
|
+
const timeout = streamTimeout(conn, req.signal);
|
|
32
|
+
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
33
|
+
try {
|
|
34
|
+
const streamUrl = `${conn.baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:streamGenerateContent?alt=sse`;
|
|
35
|
+
const res = await postResponse(streamUrl, body(req), conn.headers, timeout.signal, conn.provider);
|
|
36
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
37
|
+
const chunks = contentType.includes('text/event-stream') ? sseLines(res) : singleJsonLine(res);
|
|
38
|
+
for await (const line of chunks) {
|
|
39
|
+
const parsed = safeParse(line);
|
|
40
|
+
const record = asRecord(parsed);
|
|
41
|
+
const promptFeedback = asRecord(record?.promptFeedback);
|
|
42
|
+
if (!firstCandidate(record) && promptFeedback?.blockReason)
|
|
43
|
+
finish = 'SAFETY';
|
|
44
|
+
for (const part of candidateParts(record)) {
|
|
45
|
+
const partText = asString(asRecord(part)?.text);
|
|
46
|
+
if (partText) {
|
|
47
|
+
if (firstTokenMs === null)
|
|
48
|
+
firstTokenMs = Date.now() - started;
|
|
49
|
+
text += partText;
|
|
50
|
+
yield { type: 'delta', text: partText };
|
|
51
|
+
}
|
|
52
|
+
const fnCall = asRecord(asRecord(part)?.functionCall);
|
|
53
|
+
if (fnCall) {
|
|
54
|
+
const call = {
|
|
55
|
+
id: randomId(),
|
|
56
|
+
name: asString(fnCall.name) ?? 'unknown',
|
|
57
|
+
arguments: fnCall.args ?? {},
|
|
58
|
+
raw: JSON.stringify(fnCall.args ?? {}),
|
|
59
|
+
};
|
|
60
|
+
toolCalls.push(call);
|
|
61
|
+
yield { type: 'tool_call', call };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finish = asString(asRecord(firstCandidate(record))?.finishReason) ?? finish;
|
|
65
|
+
const usage = asRecord(record?.usageMetadata);
|
|
66
|
+
inputTokens = asNumber(usage?.promptTokenCount) ?? inputTokens;
|
|
67
|
+
outputTokens = asNumber(usage?.candidatesTokenCount) ?? outputTokens;
|
|
68
|
+
}
|
|
69
|
+
const hasTools = toolCalls.length > 0;
|
|
70
|
+
yield { type: 'done', result: {
|
|
71
|
+
text,
|
|
72
|
+
toolCalls: hasTools ? toolCalls : undefined,
|
|
73
|
+
finishReason: mapFinish(finish, hasTools),
|
|
74
|
+
usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : estimatedUsage(textFromMessages(req.messages), text),
|
|
75
|
+
timing: { firstTokenMs, totalMs: Date.now() - started },
|
|
76
|
+
model: req.model,
|
|
77
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
78
|
+
} };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw streamError(error, timeout, conn.provider);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
timeout.done();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function body(req) {
|
|
89
|
+
const systemText = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n');
|
|
90
|
+
const jsonMode = req.responseFormat?.type === 'json' && !req.tools?.length;
|
|
91
|
+
const responseSchema = req.responseFormat?.type === 'json' ? req.responseFormat.schema : undefined;
|
|
92
|
+
const payload = {
|
|
93
|
+
contents: toGoogleContents(req.messages.filter((m) => m.role !== 'system')),
|
|
94
|
+
generationConfig: {
|
|
95
|
+
temperature: req.temperature,
|
|
96
|
+
topP: req.topP,
|
|
97
|
+
maxOutputTokens: req.maxTokens,
|
|
98
|
+
stopSequences: req.stopSequences,
|
|
99
|
+
responseMimeType: jsonMode ? 'application/json' : undefined,
|
|
100
|
+
responseSchema: jsonMode ? responseSchema : undefined,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
if (systemText)
|
|
104
|
+
payload.systemInstruction = { parts: [{ text: systemText }] };
|
|
105
|
+
if (req.tools?.length) {
|
|
106
|
+
payload.tools = [{ functionDeclarations: req.tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters })) }];
|
|
107
|
+
const mode = req.toolChoice === 'none' ? 'NONE' : typeof req.toolChoice === 'object' ? 'ANY' : 'AUTO';
|
|
108
|
+
const config = { mode };
|
|
109
|
+
if (typeof req.toolChoice === 'object')
|
|
110
|
+
config.allowedFunctionNames = [req.toolChoice.name];
|
|
111
|
+
payload.toolConfig = { functionCallingConfig: config };
|
|
112
|
+
}
|
|
113
|
+
return payload;
|
|
114
|
+
}
|
|
115
|
+
function toGoogleContents(messages) {
|
|
116
|
+
const contents = [];
|
|
117
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
118
|
+
const message = messages[index];
|
|
119
|
+
if (message.role !== 'tool') {
|
|
120
|
+
contents.push(toGoogleContent(message));
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const parts = [];
|
|
124
|
+
while (index < messages.length && messages[index]?.role === 'tool') {
|
|
125
|
+
const toolMessage = messages[index];
|
|
126
|
+
parts.push({ functionResponse: { name: toolMessage.name ?? 'unknown', response: toolResponseObject(toolMessage) } });
|
|
127
|
+
index += 1;
|
|
128
|
+
}
|
|
129
|
+
index -= 1;
|
|
130
|
+
contents.push({ role: 'user', parts });
|
|
131
|
+
}
|
|
132
|
+
return contents;
|
|
133
|
+
}
|
|
134
|
+
function toGoogleContent(m) {
|
|
135
|
+
if (m.role === 'tool') {
|
|
136
|
+
return {
|
|
137
|
+
role: 'user',
|
|
138
|
+
parts: [{ functionResponse: { name: m.name ?? 'unknown', response: toolResponseObject(m) } }],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
142
|
+
const parts = [];
|
|
143
|
+
if (typeof m.content === 'string' && m.content)
|
|
144
|
+
parts.push({ text: m.content });
|
|
145
|
+
for (const call of m.toolCalls)
|
|
146
|
+
parts.push({ functionCall: { name: call.name, args: call.arguments ?? {} } });
|
|
147
|
+
return { role: 'model', parts };
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
151
|
+
parts: typeof m.content === 'string'
|
|
152
|
+
? [{ text: m.content }]
|
|
153
|
+
: m.content.map((part) => part.type === 'image'
|
|
154
|
+
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
155
|
+
: { text: part.text ?? '' }),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function textContent(content) {
|
|
159
|
+
if (typeof content === 'string')
|
|
160
|
+
return content;
|
|
161
|
+
if (!content)
|
|
162
|
+
return '';
|
|
163
|
+
return content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n');
|
|
164
|
+
}
|
|
165
|
+
function toolResponseObject(message) {
|
|
166
|
+
if (typeof message.content !== 'string')
|
|
167
|
+
return { content: textContent(message.content) };
|
|
168
|
+
try {
|
|
169
|
+
const parsed = JSON.parse(message.content);
|
|
170
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
171
|
+
return parsed;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// keep string fallback
|
|
175
|
+
}
|
|
176
|
+
return { content: message.content };
|
|
177
|
+
}
|
|
178
|
+
async function* singleJsonLine(res) {
|
|
179
|
+
const data = await res.json();
|
|
180
|
+
// Non-SSE responses may be a single object or an array of streamed chunks.
|
|
181
|
+
if (Array.isArray(data)) {
|
|
182
|
+
for (const item of data)
|
|
183
|
+
yield JSON.stringify(item);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
yield JSON.stringify(data);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function firstCandidate(record) {
|
|
190
|
+
const candidates = Array.isArray(record?.candidates) ? record.candidates : [];
|
|
191
|
+
return candidates[0];
|
|
192
|
+
}
|
|
193
|
+
function candidateParts(record) {
|
|
194
|
+
const content = asRecord(asRecord(firstCandidate(record))?.content);
|
|
195
|
+
return Array.isArray(content?.parts) ? content.parts : [];
|
|
196
|
+
}
|
|
197
|
+
function mapFinish(finish, hasTools) {
|
|
198
|
+
if (hasTools)
|
|
199
|
+
return 'tool';
|
|
200
|
+
if (finish === 'MAX_TOKENS')
|
|
201
|
+
return 'length';
|
|
202
|
+
if (finish === 'SAFETY' || finish === 'RECITATION' || finish === 'BLOCKLIST' || finish === 'PROHIBITED_CONTENT')
|
|
203
|
+
return 'content_filter';
|
|
204
|
+
return 'stop';
|
|
205
|
+
}
|
|
206
|
+
function safeParse(line) {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(line);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function randomId() {
|
|
215
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=google.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
|
+
export declare class OllamaAdapter implements ProviderAdapter {
|
|
3
|
+
readonly provider: string;
|
|
4
|
+
constructor(provider: string);
|
|
5
|
+
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
|
+
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
|
+
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
8
|
+
private showModel;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=ollama.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { AIError } from '../../errors.js';
|
|
2
|
+
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
+
import { fetchJson, ndjsonLines, postResponse } from '../../transport.js';
|
|
4
|
+
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS, streamError, streamTimeout } from './base.js';
|
|
6
|
+
export class OllamaAdapter {
|
|
7
|
+
provider;
|
|
8
|
+
constructor(provider) {
|
|
9
|
+
this.provider = provider;
|
|
10
|
+
}
|
|
11
|
+
async chat(conn, req) {
|
|
12
|
+
let result;
|
|
13
|
+
for await (const event of this.stream(conn, req)) {
|
|
14
|
+
if (event.type === 'done')
|
|
15
|
+
result = event.result;
|
|
16
|
+
if (event.type === 'error')
|
|
17
|
+
throw event.error;
|
|
18
|
+
}
|
|
19
|
+
if (!result)
|
|
20
|
+
throw new AIError('Ollama stream ended without a result', { kind: 'invalid_response', provider: conn.provider });
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
async *stream(conn, req) {
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
let firstTokenMs = null;
|
|
26
|
+
let text = '';
|
|
27
|
+
let inputTokens;
|
|
28
|
+
let outputTokens;
|
|
29
|
+
let doneReason;
|
|
30
|
+
const toolCalls = [];
|
|
31
|
+
const timeout = streamTimeout(conn, req.signal);
|
|
32
|
+
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
33
|
+
try {
|
|
34
|
+
const res = await postResponse(`${conn.baseUrl}/api/chat`, body(req), conn.headers, timeout.signal, conn.provider);
|
|
35
|
+
for await (const value of ndjsonLines(res)) {
|
|
36
|
+
const record = asRecord(value);
|
|
37
|
+
const message = asRecord(record?.message);
|
|
38
|
+
const piece = asString(message?.content);
|
|
39
|
+
if (piece) {
|
|
40
|
+
if (firstTokenMs === null)
|
|
41
|
+
firstTokenMs = Date.now() - started;
|
|
42
|
+
text += piece;
|
|
43
|
+
yield { type: 'delta', text: piece };
|
|
44
|
+
}
|
|
45
|
+
inputTokens = asNumber(record?.prompt_eval_count) ?? inputTokens;
|
|
46
|
+
outputTokens = asNumber(record?.eval_count) ?? outputTokens;
|
|
47
|
+
doneReason = asString(record?.done_reason) ?? doneReason;
|
|
48
|
+
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
49
|
+
for (const callValue of calls) {
|
|
50
|
+
const fn = asRecord(asRecord(callValue)?.function);
|
|
51
|
+
const args = fn?.arguments ?? {};
|
|
52
|
+
const toolCall = {
|
|
53
|
+
id: randomId(),
|
|
54
|
+
name: asString(fn?.name) ?? 'unknown',
|
|
55
|
+
arguments: args,
|
|
56
|
+
raw: typeof args === 'string' ? args : JSON.stringify(args),
|
|
57
|
+
};
|
|
58
|
+
toolCalls.push(toolCall);
|
|
59
|
+
yield { type: 'tool_call', call: toolCall };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const hasTools = toolCalls.length > 0;
|
|
63
|
+
yield { type: 'done', result: {
|
|
64
|
+
text,
|
|
65
|
+
toolCalls: hasTools ? toolCalls : undefined,
|
|
66
|
+
finishReason: hasTools ? 'tool' : doneReason === 'length' ? 'length' : 'stop',
|
|
67
|
+
usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : estimatedUsage(textFromMessages(req.messages), text),
|
|
68
|
+
timing: { firstTokenMs, totalMs: Date.now() - started },
|
|
69
|
+
model: req.model,
|
|
70
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
71
|
+
} };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw streamError(error, timeout, conn.provider);
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
timeout.done();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async listModels(conn) {
|
|
81
|
+
const { data } = await fetchJson(`${conn.baseUrl}/api/tags`, {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers: conn.headers,
|
|
84
|
+
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
85
|
+
provider: conn.provider,
|
|
86
|
+
});
|
|
87
|
+
const models = Array.isArray(asRecord(data)?.models) ? asRecord(data).models : [];
|
|
88
|
+
const ids = models.map((model) => asString(asRecord(model)?.name) ?? asString(asRecord(model)?.model) ?? '').filter(Boolean);
|
|
89
|
+
// Probe /api/show per model for context window + capabilities (best effort, sequential
|
|
90
|
+
// to avoid overwhelming a local Ollama instance).
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const id of ids) {
|
|
93
|
+
const info = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
94
|
+
const probed = await this.showModel(conn, id).catch(() => undefined);
|
|
95
|
+
if (probed?.contextWindow !== undefined)
|
|
96
|
+
info.contextWindow = probed.contextWindow;
|
|
97
|
+
if (probed?.capabilities)
|
|
98
|
+
info.capabilities = probed.capabilities;
|
|
99
|
+
results.push(info);
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
async showModel(conn, model) {
|
|
104
|
+
const { data } = await fetchJson(`${conn.baseUrl}/api/show`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'content-type': 'application/json', ...conn.headers },
|
|
107
|
+
body: JSON.stringify({ model }),
|
|
108
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
109
|
+
provider: conn.provider,
|
|
110
|
+
});
|
|
111
|
+
const record = asRecord(data);
|
|
112
|
+
const modelInfo = asRecord(record?.model_info);
|
|
113
|
+
let contextWindow;
|
|
114
|
+
if (modelInfo) {
|
|
115
|
+
for (const [key, value] of Object.entries(modelInfo)) {
|
|
116
|
+
if (key.endsWith('.context_length') && typeof value === 'number') {
|
|
117
|
+
contextWindow = value;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const caps = record?.capabilities;
|
|
123
|
+
const capabilities = Array.isArray(caps) && caps.every((c) => typeof c === 'string') ? caps : undefined;
|
|
124
|
+
const result = {};
|
|
125
|
+
if (contextWindow !== undefined)
|
|
126
|
+
result.contextWindow = contextWindow;
|
|
127
|
+
if (capabilities)
|
|
128
|
+
result.capabilities = capabilities;
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function body(req) {
|
|
133
|
+
return {
|
|
134
|
+
model: req.model,
|
|
135
|
+
messages: req.messages.map(toOllamaMessage),
|
|
136
|
+
stream: true,
|
|
137
|
+
options: { temperature: req.temperature, num_predict: req.maxTokens, top_p: req.topP, stop: req.stopSequences },
|
|
138
|
+
format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
|
|
139
|
+
tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function toOllamaMessage(m) {
|
|
143
|
+
const images = typeof m.content === 'string' ? undefined : m.content.filter((part) => part.type === 'image').map((part) => part.imageBase64).filter(Boolean);
|
|
144
|
+
const message = {
|
|
145
|
+
role: m.role,
|
|
146
|
+
content: typeof m.content === 'string' ? m.content : m.content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n'),
|
|
147
|
+
};
|
|
148
|
+
if (images && images.length)
|
|
149
|
+
message.images = images;
|
|
150
|
+
if (m.role === 'tool' && m.name)
|
|
151
|
+
message.tool_name = m.name;
|
|
152
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
153
|
+
message.tool_calls = m.toolCalls.map((call) => ({ function: { name: call.name, arguments: call.arguments ?? {} } }));
|
|
154
|
+
}
|
|
155
|
+
return message;
|
|
156
|
+
}
|
|
157
|
+
function randomId() {
|
|
158
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=ollama.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
|
+
import type { ProviderProfile } from '../profiles.js';
|
|
3
|
+
export declare class OpenAIChatAdapter implements ProviderAdapter {
|
|
4
|
+
private profile;
|
|
5
|
+
readonly provider: string;
|
|
6
|
+
constructor(provider: string, profile: ProviderProfile);
|
|
7
|
+
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
8
|
+
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
9
|
+
listModels(conn: ResolvedConnection): Promise<import("../../types.js").ModelInfo[]>;
|
|
10
|
+
health(conn: ResolvedConnection): Promise<{
|
|
11
|
+
ok: boolean;
|
|
12
|
+
detail?: string;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=openaiChat.d.ts.map
|