@jxburros/ai-nugget 0.4.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +158 -382
- package/UPGRADING.md +182 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +82 -37
- package/dist/adapters/engines/base.d.ts +29 -2
- package/dist/adapters/engines/base.js +52 -8
- package/dist/adapters/engines/google.d.ts +7 -1
- package/dist/adapters/engines/google.js +80 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +57 -15
- package/dist/adapters/engines/openaiChat.d.ts +8 -1
- package/dist/adapters/engines/openaiChat.js +109 -29
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +45 -1
- package/dist/agent/loop.js +117 -12
- package/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +291 -0
- package/dist/cjs/adapters/engines/ollama.js +205 -0
- package/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/dist/cjs/adapters/index.js +22 -0
- package/dist/cjs/adapters/profiles.js +125 -0
- package/dist/cjs/agent/index.js +18 -0
- package/dist/cjs/agent/loop.js +373 -0
- package/dist/cjs/agent/tools.js +51 -0
- package/dist/cjs/connect.js +27 -0
- package/dist/cjs/errors.js +110 -0
- package/dist/cjs/handler.js +586 -0
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/json.js +121 -0
- package/dist/cjs/keys.js +81 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/policy.js +42 -0
- package/dist/cjs/reasoning.js +159 -0
- package/dist/cjs/redact.js +78 -0
- package/dist/cjs/tokens.js +22 -0
- package/dist/cjs/transport.js +172 -0
- package/dist/cjs/types.js +6 -0
- package/dist/cjs/util.js +126 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +67 -1
- package/dist/handler.js +266 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/reasoning.d.ts +45 -0
- package/dist/reasoning.js +154 -0
- package/dist/redact.d.ts +18 -0
- package/dist/redact.js +29 -1
- package/dist/transport.d.ts +3 -1
- package/dist/transport.js +21 -1
- package/dist/types.d.ts +112 -2
- package/dist/util.d.ts +22 -0
- package/dist/util.js +59 -0
- package/nugget/VERSION.txt +2 -2
- package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
- package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/anthropic.js +323 -0
- package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
- package/nugget/dist/adapters/engines/base.d.ts +50 -0
- package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/base.js +137 -0
- package/nugget/dist/adapters/engines/base.js.map +1 -0
- package/nugget/dist/adapters/engines/google.d.ts +24 -0
- package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/google.js +288 -0
- package/nugget/dist/adapters/engines/google.js.map +1 -0
- package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
- package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/ollama.js +202 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +308 -0
- package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
- package/nugget/dist/adapters/index.d.ts +5 -0
- package/nugget/dist/adapters/index.d.ts.map +1 -0
- package/nugget/dist/adapters/index.js +17 -0
- package/nugget/dist/adapters/index.js.map +1 -0
- package/nugget/dist/adapters/profiles.d.ts +62 -0
- package/nugget/dist/adapters/profiles.d.ts.map +1 -0
- package/nugget/dist/adapters/profiles.js +121 -0
- package/nugget/dist/adapters/profiles.js.map +1 -0
- package/nugget/dist/agent/index.d.ts +3 -0
- package/nugget/dist/agent/index.d.ts.map +1 -0
- package/nugget/dist/agent/index.js +3 -0
- package/nugget/dist/agent/index.js.map +1 -0
- package/nugget/dist/agent/loop.d.ts +114 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +371 -0
- package/nugget/dist/agent/loop.js.map +1 -0
- package/nugget/dist/agent/tools.d.ts +29 -0
- package/nugget/dist/agent/tools.d.ts.map +1 -0
- package/nugget/dist/agent/tools.js +48 -0
- package/nugget/dist/agent/tools.js.map +1 -0
- package/nugget/dist/cjs/adapters/engines/anthropic.js +326 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +291 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -0
- package/nugget/dist/cjs/adapters/index.js +22 -0
- package/nugget/dist/cjs/adapters/profiles.js +125 -0
- package/nugget/dist/cjs/agent/index.js +18 -0
- package/nugget/dist/cjs/agent/loop.js +373 -0
- package/nugget/dist/cjs/agent/tools.js +51 -0
- package/nugget/dist/cjs/connect.js +27 -0
- package/nugget/dist/cjs/errors.js +110 -0
- package/nugget/dist/cjs/handler.js +586 -0
- package/nugget/dist/cjs/index.js +28 -0
- package/nugget/dist/cjs/json.js +121 -0
- package/nugget/dist/cjs/keys.js +81 -0
- package/nugget/dist/cjs/package.json +3 -0
- package/nugget/dist/cjs/policy.js +42 -0
- package/nugget/dist/cjs/reasoning.js +159 -0
- package/nugget/dist/cjs/redact.js +78 -0
- package/nugget/dist/cjs/tokens.js +22 -0
- package/nugget/dist/cjs/transport.js +172 -0
- package/nugget/dist/cjs/types.js +6 -0
- package/nugget/dist/cjs/util.js +126 -0
- package/nugget/dist/connect.d.ts +31 -0
- package/nugget/dist/connect.d.ts.map +1 -0
- package/nugget/dist/connect.js +25 -0
- package/nugget/dist/connect.js.map +1 -0
- package/nugget/dist/errors.d.ts +22 -0
- package/nugget/dist/errors.d.ts.map +1 -0
- package/nugget/dist/errors.js +104 -0
- package/nugget/dist/errors.js.map +1 -0
- package/nugget/dist/handler.d.ts +114 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +582 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +13 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +13 -0
- package/nugget/dist/index.js.map +1 -0
- package/nugget/dist/json.d.ts +7 -0
- package/nugget/dist/json.d.ts.map +1 -0
- package/nugget/dist/json.js +114 -0
- package/nugget/dist/json.js.map +1 -0
- package/nugget/dist/keys.d.ts +7 -0
- package/nugget/dist/keys.d.ts.map +1 -0
- package/nugget/dist/keys.js +75 -0
- package/nugget/dist/keys.js.map +1 -0
- package/nugget/dist/policy.d.ts +6 -0
- package/nugget/dist/policy.d.ts.map +1 -0
- package/nugget/dist/policy.js +37 -0
- package/nugget/dist/policy.js.map +1 -0
- package/nugget/dist/reasoning.d.ts +45 -0
- package/nugget/dist/reasoning.d.ts.map +1 -0
- package/nugget/dist/reasoning.js +154 -0
- package/nugget/dist/reasoning.js.map +1 -0
- package/nugget/dist/redact.d.ts +26 -0
- package/nugget/dist/redact.d.ts.map +1 -0
- package/nugget/dist/redact.js +74 -0
- package/nugget/dist/redact.js.map +1 -0
- package/nugget/dist/tokens.d.ts +5 -0
- package/nugget/dist/tokens.d.ts.map +1 -0
- package/nugget/dist/tokens.js +18 -0
- package/nugget/dist/tokens.js.map +1 -0
- package/nugget/dist/transport.d.ts +30 -0
- package/nugget/dist/transport.d.ts.map +1 -0
- package/nugget/dist/transport.js +165 -0
- package/nugget/dist/transport.js.map +1 -0
- package/nugget/dist/types.d.ts +301 -0
- package/nugget/dist/types.d.ts.map +1 -0
- package/nugget/dist/types.js +4 -0
- package/nugget/dist/types.js.map +1 -0
- package/nugget/dist/util.d.ts +31 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +114 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +78 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +79 -29
- package/nugget/src/adapters/engines/ollama.ts +56 -17
- package/nugget/src/adapters/engines/openaiChat.ts +104 -30
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +156 -13
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +308 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/reasoning.ts +174 -0
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +113 -1
- package/nugget/src/util.ts +64 -0
- package/package.json +18 -5
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
4
|
+
exports.streamError = streamError;
|
|
5
|
+
exports.streamTimeout = streamTimeout;
|
|
6
|
+
exports.listOpenModels = listOpenModels;
|
|
7
|
+
exports.health = health;
|
|
8
|
+
exports.parseArgs = parseArgs;
|
|
9
|
+
exports.safeParse = safeParse;
|
|
10
|
+
exports.randomId = randomId;
|
|
11
|
+
exports.streamAnomaly = streamAnomaly;
|
|
12
|
+
const errors_js_1 = require("../../errors.js");
|
|
13
|
+
const transport_js_1 = require("../../transport.js");
|
|
14
|
+
const util_js_1 = require("../../util.js");
|
|
15
|
+
exports.DEFAULT_TIMEOUT_MS = 120_000;
|
|
16
|
+
/**
|
|
17
|
+
* Maps a raw streaming failure onto a typed {@link AIError}. A fired timeout is
|
|
18
|
+
* reported as `timeout` regardless of how the underlying `fetch` surfaced the
|
|
19
|
+
* abort; anything else is normalized through {@link fromUnknown} (which passes
|
|
20
|
+
* an existing {@link AIError} through untouched, preserving retryability).
|
|
21
|
+
*/
|
|
22
|
+
function streamError(error, timeout, provider) {
|
|
23
|
+
if (timeout.timedOut())
|
|
24
|
+
return new errors_js_1.AIError('Request timed out', { kind: 'timeout', provider });
|
|
25
|
+
return (0, errors_js_1.fromUnknown)(error, provider);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
29
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
30
|
+
* `bump()` on each received chunk.
|
|
31
|
+
*/
|
|
32
|
+
function streamTimeout(conn, signal) {
|
|
33
|
+
return (0, transport_js_1.withTimeout)(conn.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS, signal, conn.idleTimeoutMs);
|
|
34
|
+
}
|
|
35
|
+
async function listOpenModels(conn, profile) {
|
|
36
|
+
if (!profile.listModelsPath)
|
|
37
|
+
return [];
|
|
38
|
+
const { data } = await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, profile.listModelsPath), {
|
|
39
|
+
method: 'GET',
|
|
40
|
+
headers: conn.headers,
|
|
41
|
+
timeoutMs: conn.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS,
|
|
42
|
+
provider: conn.provider,
|
|
43
|
+
});
|
|
44
|
+
const record = (0, util_js_1.asRecord)(data);
|
|
45
|
+
const items = Array.isArray(record?.data) ? record.data : Array.isArray(record?.models) ? record.models : [];
|
|
46
|
+
const models = [];
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
const row = (0, util_js_1.asRecord)(item);
|
|
49
|
+
const id = (0, util_js_1.asString)(row?.id) ?? (0, util_js_1.asString)(row?.name);
|
|
50
|
+
if (!id)
|
|
51
|
+
continue;
|
|
52
|
+
const contextWindow = asContextWindow(row);
|
|
53
|
+
const capabilities = asCapabilities(row);
|
|
54
|
+
const model = {
|
|
55
|
+
id,
|
|
56
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
57
|
+
};
|
|
58
|
+
if (contextWindow !== undefined)
|
|
59
|
+
model.contextWindow = contextWindow;
|
|
60
|
+
if (capabilities)
|
|
61
|
+
model.capabilities = capabilities;
|
|
62
|
+
models.push(model);
|
|
63
|
+
}
|
|
64
|
+
return models;
|
|
65
|
+
}
|
|
66
|
+
async function health(conn, profile) {
|
|
67
|
+
const path = profile.healthPath ?? profile.listModelsPath;
|
|
68
|
+
if (!path)
|
|
69
|
+
return { ok: true };
|
|
70
|
+
try {
|
|
71
|
+
await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, path), {
|
|
72
|
+
method: 'GET',
|
|
73
|
+
headers: conn.headers,
|
|
74
|
+
timeoutMs: Math.min(conn.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS, 10_000),
|
|
75
|
+
provider: conn.provider,
|
|
76
|
+
});
|
|
77
|
+
return { ok: true };
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
85
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
86
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
87
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
88
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
89
|
+
*/
|
|
90
|
+
function parseArgs(raw) {
|
|
91
|
+
if (raw === undefined || raw === null)
|
|
92
|
+
return {};
|
|
93
|
+
if (typeof raw !== 'string')
|
|
94
|
+
return raw;
|
|
95
|
+
if (!raw.trim())
|
|
96
|
+
return {};
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(raw);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
105
|
+
function safeParse(line) {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(line);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
114
|
+
function randomId() {
|
|
115
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
119
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
120
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
121
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
122
|
+
* regardless of provider.
|
|
123
|
+
*/
|
|
124
|
+
function streamAnomaly(reason = 'stream ended without a terminal finish reason') {
|
|
125
|
+
return { type: 'context', kind: 'stream_anomaly', data: { reason } };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Pulls a context-window figure out of the many shapes providers use for it
|
|
129
|
+
* (OpenRouter `context_length`, OpenAI-style `context_window`, Ollama
|
|
130
|
+
* `/api/show` `model_info` entries surfaced as `contextWindow`).
|
|
131
|
+
*/
|
|
132
|
+
function asContextWindow(row) {
|
|
133
|
+
if (!row)
|
|
134
|
+
return undefined;
|
|
135
|
+
const direct = row['context_length'] ?? row['context_window'] ?? row['contextWindow'];
|
|
136
|
+
return typeof direct === 'number' && Number.isFinite(direct) ? direct : undefined;
|
|
137
|
+
}
|
|
138
|
+
function asCapabilities(row) {
|
|
139
|
+
if (!row)
|
|
140
|
+
return undefined;
|
|
141
|
+
const value = row['capabilities'] ?? (0, util_js_1.asRecord)(row['architecture'])?.['modality'];
|
|
142
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === 'string'))
|
|
143
|
+
return value;
|
|
144
|
+
if (typeof value === 'string')
|
|
145
|
+
return [value];
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GoogleAdapter = void 0;
|
|
4
|
+
const errors_js_1 = require("../../errors.js");
|
|
5
|
+
const tokens_js_1 = require("../../tokens.js");
|
|
6
|
+
const transport_js_1 = require("../../transport.js");
|
|
7
|
+
const util_js_1 = require("../../util.js");
|
|
8
|
+
const base_js_1 = require("./base.js");
|
|
9
|
+
class GoogleAdapter {
|
|
10
|
+
provider;
|
|
11
|
+
constructor(provider) {
|
|
12
|
+
this.provider = provider;
|
|
13
|
+
}
|
|
14
|
+
async chat(conn, req) {
|
|
15
|
+
let result;
|
|
16
|
+
for await (const event of this.stream(conn, req)) {
|
|
17
|
+
if (event.type === 'done')
|
|
18
|
+
result = event.result;
|
|
19
|
+
if (event.type === 'error')
|
|
20
|
+
throw event.error;
|
|
21
|
+
}
|
|
22
|
+
if (!result)
|
|
23
|
+
throw new errors_js_1.AIError('Google stream ended without a result', { kind: 'invalid_response', provider: conn.provider });
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
async *stream(conn, req) {
|
|
27
|
+
const started = Date.now();
|
|
28
|
+
let firstTokenMs = null;
|
|
29
|
+
let text = '';
|
|
30
|
+
let inputTokens;
|
|
31
|
+
let outputTokens;
|
|
32
|
+
let finish;
|
|
33
|
+
const toolCalls = [];
|
|
34
|
+
const timeout = (0, base_js_1.streamTimeout)(conn, req.signal);
|
|
35
|
+
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
36
|
+
// Gemini rejects `responseMimeType: application/json` combined with function
|
|
37
|
+
// declarations, so `body()` drops JSON mode when tools are present. Surface
|
|
38
|
+
// that downgrade instead of silently returning unstructured text.
|
|
39
|
+
if (jsonModeDowngraded(req)) {
|
|
40
|
+
yield {
|
|
41
|
+
type: 'context',
|
|
42
|
+
kind: 'json_mode_downgraded',
|
|
43
|
+
data: { reason: 'Gemini does not accept JSON response mode together with tools; the request was sent without JSON mode', provider: conn.provider },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const streamUrl = `${conn.baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:streamGenerateContent?alt=sse`;
|
|
48
|
+
const res = await (0, transport_js_1.postResponse)(streamUrl, body(req), conn.headers, timeout.signal, conn.provider);
|
|
49
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
50
|
+
const chunks = contentType.includes('text/event-stream') ? (0, transport_js_1.sseLines)(res) : singleJsonLine(res);
|
|
51
|
+
for await (const line of chunks) {
|
|
52
|
+
timeout.bump();
|
|
53
|
+
const parsed = (0, base_js_1.safeParse)(line);
|
|
54
|
+
const record = (0, util_js_1.asRecord)(parsed);
|
|
55
|
+
const promptFeedback = (0, util_js_1.asRecord)(record?.promptFeedback);
|
|
56
|
+
if (!firstCandidate(record) && promptFeedback?.blockReason)
|
|
57
|
+
finish = 'SAFETY';
|
|
58
|
+
for (const part of candidateParts(record)) {
|
|
59
|
+
const partText = (0, util_js_1.asString)((0, util_js_1.asRecord)(part)?.text);
|
|
60
|
+
if (partText) {
|
|
61
|
+
// Gemini marks reasoning parts with `thought: true`; route those to
|
|
62
|
+
// the reasoning channel so they don't blend into the answer text.
|
|
63
|
+
if ((0, util_js_1.asRecord)(part)?.thought === true) {
|
|
64
|
+
yield { type: 'reasoning', text: partText };
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (firstTokenMs === null)
|
|
68
|
+
firstTokenMs = Date.now() - started;
|
|
69
|
+
text += partText;
|
|
70
|
+
yield { type: 'delta', text: partText };
|
|
71
|
+
}
|
|
72
|
+
const fnCall = (0, util_js_1.asRecord)((0, util_js_1.asRecord)(part)?.functionCall);
|
|
73
|
+
if (fnCall) {
|
|
74
|
+
const call = {
|
|
75
|
+
id: (0, base_js_1.randomId)(),
|
|
76
|
+
name: (0, util_js_1.asString)(fnCall.name) ?? 'unknown',
|
|
77
|
+
arguments: fnCall.args ?? {},
|
|
78
|
+
raw: JSON.stringify(fnCall.args ?? {}),
|
|
79
|
+
};
|
|
80
|
+
toolCalls.push(call);
|
|
81
|
+
yield { type: 'tool_call', call };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
finish = (0, util_js_1.asString)((0, util_js_1.asRecord)(firstCandidate(record))?.finishReason) ?? finish;
|
|
85
|
+
const usage = (0, util_js_1.asRecord)(record?.usageMetadata);
|
|
86
|
+
inputTokens = (0, util_js_1.asNumber)(usage?.promptTokenCount) ?? inputTokens;
|
|
87
|
+
outputTokens = (0, util_js_1.asNumber)(usage?.candidatesTokenCount) ?? outputTokens;
|
|
88
|
+
}
|
|
89
|
+
if (!finish)
|
|
90
|
+
yield (0, base_js_1.streamAnomaly)('stream ended without a finishReason');
|
|
91
|
+
const hasTools = toolCalls.length > 0;
|
|
92
|
+
yield { type: 'done', result: {
|
|
93
|
+
text,
|
|
94
|
+
toolCalls: hasTools ? toolCalls : undefined,
|
|
95
|
+
finishReason: mapFinish(finish, hasTools),
|
|
96
|
+
usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : (0, tokens_js_1.estimatedUsage)((0, util_js_1.textFromMessages)(req.messages), text),
|
|
97
|
+
timing: { firstTokenMs, totalMs: Date.now() - started },
|
|
98
|
+
model: req.model,
|
|
99
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
100
|
+
} };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw (0, base_js_1.streamError)(error, timeout, conn.provider);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
timeout.done();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `GoogleAdapter` has no `listModels`, so without a `health` probe
|
|
111
|
+
* `AIHandler.testConnection` would report `ok: true` without ever making a
|
|
112
|
+
* network call. `/v1beta/models` is a lightweight, key-authenticated GET
|
|
113
|
+
* that gives an honest connectivity/auth check.
|
|
114
|
+
*/
|
|
115
|
+
async health(conn) {
|
|
116
|
+
try {
|
|
117
|
+
await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, '/v1beta/models'), {
|
|
118
|
+
method: 'GET',
|
|
119
|
+
headers: conn.headers,
|
|
120
|
+
timeoutMs: Math.min(conn.timeoutMs ?? base_js_1.DEFAULT_TIMEOUT_MS, 10_000),
|
|
121
|
+
provider: conn.provider,
|
|
122
|
+
});
|
|
123
|
+
return { ok: true };
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
131
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
132
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
133
|
+
*/
|
|
134
|
+
async listModels(conn) {
|
|
135
|
+
const { data } = await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, '/v1beta/models'), {
|
|
136
|
+
method: 'GET',
|
|
137
|
+
headers: conn.headers,
|
|
138
|
+
timeoutMs: Math.min(conn.timeoutMs ?? base_js_1.DEFAULT_TIMEOUT_MS, 15_000),
|
|
139
|
+
provider: conn.provider,
|
|
140
|
+
});
|
|
141
|
+
const rows = Array.isArray((0, util_js_1.asRecord)(data)?.models) ? (0, util_js_1.asRecord)(data).models : [];
|
|
142
|
+
const models = [];
|
|
143
|
+
for (const value of rows) {
|
|
144
|
+
const row = (0, util_js_1.asRecord)(value);
|
|
145
|
+
const name = (0, util_js_1.asString)(row?.name);
|
|
146
|
+
if (!name)
|
|
147
|
+
continue;
|
|
148
|
+
const id = name.startsWith('models/') ? name.slice('models/'.length) : name;
|
|
149
|
+
const model = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
150
|
+
const contextWindow = (0, util_js_1.asNumber)(row?.inputTokenLimit);
|
|
151
|
+
if (contextWindow !== undefined)
|
|
152
|
+
model.contextWindow = contextWindow;
|
|
153
|
+
const methods = row?.supportedGenerationMethods;
|
|
154
|
+
if (Array.isArray(methods) && methods.every((m) => typeof m === 'string'))
|
|
155
|
+
model.capabilities = methods;
|
|
156
|
+
models.push(model);
|
|
157
|
+
}
|
|
158
|
+
return models;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
exports.GoogleAdapter = GoogleAdapter;
|
|
162
|
+
/** True when the caller asked for JSON mode but tools force it off. */
|
|
163
|
+
function jsonModeDowngraded(req) {
|
|
164
|
+
return req.responseFormat?.type === 'json' && !!req.tools?.length;
|
|
165
|
+
}
|
|
166
|
+
function body(req) {
|
|
167
|
+
const systemText = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n');
|
|
168
|
+
const jsonMode = req.responseFormat?.type === 'json' && !req.tools?.length;
|
|
169
|
+
const responseSchema = req.responseFormat?.type === 'json' ? req.responseFormat.schema : undefined;
|
|
170
|
+
const payload = {
|
|
171
|
+
contents: toGoogleContents(req.messages.filter((m) => m.role !== 'system')),
|
|
172
|
+
generationConfig: {
|
|
173
|
+
temperature: req.temperature,
|
|
174
|
+
topP: req.topP,
|
|
175
|
+
maxOutputTokens: req.maxTokens,
|
|
176
|
+
stopSequences: req.stopSequences,
|
|
177
|
+
responseMimeType: jsonMode ? 'application/json' : undefined,
|
|
178
|
+
responseSchema: jsonMode ? responseSchema : undefined,
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
if (systemText)
|
|
182
|
+
payload.systemInstruction = { parts: [{ text: systemText }] };
|
|
183
|
+
if (req.tools?.length) {
|
|
184
|
+
payload.tools = [{ functionDeclarations: req.tools.map((tool) => ({ name: tool.name, description: tool.description, parameters: tool.parameters })) }];
|
|
185
|
+
const mode = req.toolChoice === 'none' ? 'NONE' : typeof req.toolChoice === 'object' ? 'ANY' : 'AUTO';
|
|
186
|
+
const config = { mode };
|
|
187
|
+
if (typeof req.toolChoice === 'object')
|
|
188
|
+
config.allowedFunctionNames = [req.toolChoice.name];
|
|
189
|
+
payload.toolConfig = { functionCallingConfig: config };
|
|
190
|
+
}
|
|
191
|
+
if (req.reasoningEffort !== undefined) {
|
|
192
|
+
// First-class reasoning effort → Gemini's thinking budget. 0 disables
|
|
193
|
+
// thinking on models that allow it; others treat it as a minimum.
|
|
194
|
+
payload.generationConfig.thinkingConfig = {
|
|
195
|
+
thinkingBudget: util_js_1.REASONING_BUDGET_TOKENS[req.reasoningEffort],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// providerOptions reaches Google-native fields: top-level `safetySettings`,
|
|
199
|
+
// `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
|
|
200
|
+
// `responseModalities`, …) merged one level deep into generationConfig.
|
|
201
|
+
return (0, util_js_1.applyProviderOptions)(payload, req.providerOptions, ['generationConfig']);
|
|
202
|
+
}
|
|
203
|
+
function toGoogleContents(messages) {
|
|
204
|
+
const contents = [];
|
|
205
|
+
for (let index = 0; index < messages.length; index += 1) {
|
|
206
|
+
const message = messages[index];
|
|
207
|
+
if (message.role !== 'tool') {
|
|
208
|
+
contents.push(toGoogleContent(message));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const parts = [];
|
|
212
|
+
while (index < messages.length && messages[index]?.role === 'tool') {
|
|
213
|
+
const toolMessage = messages[index];
|
|
214
|
+
parts.push({ functionResponse: { name: toolMessage.name ?? 'unknown', response: toolResponseObject(toolMessage) } });
|
|
215
|
+
index += 1;
|
|
216
|
+
}
|
|
217
|
+
index -= 1;
|
|
218
|
+
contents.push({ role: 'user', parts });
|
|
219
|
+
}
|
|
220
|
+
return contents;
|
|
221
|
+
}
|
|
222
|
+
function toGoogleContent(m) {
|
|
223
|
+
// Tool-role messages are handled by toGoogleContents' batching loop and never
|
|
224
|
+
// reach here, so no tool branch is needed.
|
|
225
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
226
|
+
const parts = [...googleParts(m.content)];
|
|
227
|
+
for (const call of m.toolCalls)
|
|
228
|
+
parts.push({ functionCall: { name: call.name, args: call.arguments ?? {} } });
|
|
229
|
+
return { role: 'model', parts };
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
role: m.role === 'assistant' ? 'model' : 'user',
|
|
233
|
+
parts: googleParts(m.content),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Google parts. */
|
|
237
|
+
function googleParts(content) {
|
|
238
|
+
if (typeof content === 'string')
|
|
239
|
+
return content ? [{ text: content }] : [];
|
|
240
|
+
return content.map((part) => part.type === 'image'
|
|
241
|
+
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
242
|
+
: { text: part.text ?? '' });
|
|
243
|
+
}
|
|
244
|
+
function textContent(content) {
|
|
245
|
+
if (typeof content === 'string')
|
|
246
|
+
return content;
|
|
247
|
+
if (!content)
|
|
248
|
+
return '';
|
|
249
|
+
return content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n');
|
|
250
|
+
}
|
|
251
|
+
function toolResponseObject(message) {
|
|
252
|
+
if (typeof message.content !== 'string')
|
|
253
|
+
return { content: textContent(message.content) };
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(message.content);
|
|
256
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
257
|
+
return parsed;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// keep string fallback
|
|
261
|
+
}
|
|
262
|
+
return { content: message.content };
|
|
263
|
+
}
|
|
264
|
+
async function* singleJsonLine(res) {
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
// Non-SSE responses may be a single object or an array of streamed chunks.
|
|
267
|
+
if (Array.isArray(data)) {
|
|
268
|
+
for (const item of data)
|
|
269
|
+
yield JSON.stringify(item);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
yield JSON.stringify(data);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function firstCandidate(record) {
|
|
276
|
+
const candidates = Array.isArray(record?.candidates) ? record.candidates : [];
|
|
277
|
+
return candidates[0];
|
|
278
|
+
}
|
|
279
|
+
function candidateParts(record) {
|
|
280
|
+
const content = (0, util_js_1.asRecord)((0, util_js_1.asRecord)(firstCandidate(record))?.content);
|
|
281
|
+
return Array.isArray(content?.parts) ? content.parts : [];
|
|
282
|
+
}
|
|
283
|
+
function mapFinish(finish, hasTools) {
|
|
284
|
+
if (hasTools)
|
|
285
|
+
return 'tool';
|
|
286
|
+
if (finish === 'MAX_TOKENS')
|
|
287
|
+
return 'length';
|
|
288
|
+
if (finish === 'SAFETY' || finish === 'RECITATION' || finish === 'BLOCKLIST' || finish === 'PROHIBITED_CONTENT')
|
|
289
|
+
return 'content_filter';
|
|
290
|
+
return 'stop';
|
|
291
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OllamaAdapter = void 0;
|
|
4
|
+
const errors_js_1 = require("../../errors.js");
|
|
5
|
+
const tokens_js_1 = require("../../tokens.js");
|
|
6
|
+
const transport_js_1 = require("../../transport.js");
|
|
7
|
+
const util_js_1 = require("../../util.js");
|
|
8
|
+
const base_js_1 = require("./base.js");
|
|
9
|
+
/** Max concurrent /api/show probes when listing models — enough to be fast, few enough to be polite to a local daemon. */
|
|
10
|
+
const SHOW_CONCURRENCY = 6;
|
|
11
|
+
class OllamaAdapter {
|
|
12
|
+
provider;
|
|
13
|
+
constructor(provider) {
|
|
14
|
+
this.provider = provider;
|
|
15
|
+
}
|
|
16
|
+
async chat(conn, req) {
|
|
17
|
+
let result;
|
|
18
|
+
for await (const event of this.stream(conn, req)) {
|
|
19
|
+
if (event.type === 'done')
|
|
20
|
+
result = event.result;
|
|
21
|
+
if (event.type === 'error')
|
|
22
|
+
throw event.error;
|
|
23
|
+
}
|
|
24
|
+
if (!result)
|
|
25
|
+
throw new errors_js_1.AIError('Ollama stream ended without a result', { kind: 'invalid_response', provider: conn.provider });
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
async *stream(conn, req) {
|
|
29
|
+
const started = Date.now();
|
|
30
|
+
let firstTokenMs = null;
|
|
31
|
+
let text = '';
|
|
32
|
+
let inputTokens;
|
|
33
|
+
let outputTokens;
|
|
34
|
+
let doneReason;
|
|
35
|
+
let sawTerminal = false;
|
|
36
|
+
const toolCalls = [];
|
|
37
|
+
const timeout = (0, base_js_1.streamTimeout)(conn, req.signal);
|
|
38
|
+
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
39
|
+
try {
|
|
40
|
+
const res = await (0, transport_js_1.postResponse)((0, util_js_1.joinUrl)(conn.baseUrl, '/api/chat'), body(req), conn.headers, timeout.signal, conn.provider);
|
|
41
|
+
for await (const value of (0, transport_js_1.ndjsonLines)(res)) {
|
|
42
|
+
timeout.bump();
|
|
43
|
+
const record = (0, util_js_1.asRecord)(value);
|
|
44
|
+
const message = (0, util_js_1.asRecord)(record?.message);
|
|
45
|
+
const thinking = (0, util_js_1.asString)(message?.thinking);
|
|
46
|
+
if (thinking)
|
|
47
|
+
yield { type: 'reasoning', text: thinking };
|
|
48
|
+
const piece = (0, util_js_1.asString)(message?.content);
|
|
49
|
+
if (piece) {
|
|
50
|
+
if (firstTokenMs === null)
|
|
51
|
+
firstTokenMs = Date.now() - started;
|
|
52
|
+
text += piece;
|
|
53
|
+
yield { type: 'delta', text: piece };
|
|
54
|
+
}
|
|
55
|
+
inputTokens = (0, util_js_1.asNumber)(record?.prompt_eval_count) ?? inputTokens;
|
|
56
|
+
outputTokens = (0, util_js_1.asNumber)(record?.eval_count) ?? outputTokens;
|
|
57
|
+
doneReason = (0, util_js_1.asString)(record?.done_reason) ?? doneReason;
|
|
58
|
+
if (record?.done === true)
|
|
59
|
+
sawTerminal = true;
|
|
60
|
+
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
|
|
61
|
+
for (const callValue of calls) {
|
|
62
|
+
const fn = (0, util_js_1.asRecord)((0, util_js_1.asRecord)(callValue)?.function);
|
|
63
|
+
const args = fn?.arguments ?? {};
|
|
64
|
+
const toolCall = {
|
|
65
|
+
id: (0, base_js_1.randomId)(),
|
|
66
|
+
name: (0, util_js_1.asString)(fn?.name) ?? 'unknown',
|
|
67
|
+
// Ollama itself sends a native object, but llama.cpp-style backends
|
|
68
|
+
// behind the same protocol send a JSON *string* — coerce either into
|
|
69
|
+
// the object `validateToolArgs` expects.
|
|
70
|
+
arguments: (0, base_js_1.parseArgs)(args),
|
|
71
|
+
raw: typeof args === 'string' ? args : JSON.stringify(args),
|
|
72
|
+
};
|
|
73
|
+
toolCalls.push(toolCall);
|
|
74
|
+
yield { type: 'tool_call', call: toolCall };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!sawTerminal)
|
|
78
|
+
yield (0, base_js_1.streamAnomaly)('NDJSON stream ended without a done record');
|
|
79
|
+
const hasTools = toolCalls.length > 0;
|
|
80
|
+
yield { type: 'done', result: {
|
|
81
|
+
text,
|
|
82
|
+
toolCalls: hasTools ? toolCalls : undefined,
|
|
83
|
+
finishReason: hasTools ? 'tool' : doneReason === 'length' ? 'length' : 'stop',
|
|
84
|
+
usage: inputTokens !== undefined || outputTokens !== undefined ? { inputTokens, outputTokens, estimated: false } : (0, tokens_js_1.estimatedUsage)((0, util_js_1.textFromMessages)(req.messages), text),
|
|
85
|
+
timing: { firstTokenMs, totalMs: Date.now() - started },
|
|
86
|
+
model: req.model,
|
|
87
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
88
|
+
} };
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw (0, base_js_1.streamError)(error, timeout, conn.provider);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
timeout.done();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async listModels(conn) {
|
|
98
|
+
const { data } = await (0, transport_js_1.fetchJson)(`${conn.baseUrl}/api/tags`, {
|
|
99
|
+
method: 'GET',
|
|
100
|
+
headers: conn.headers,
|
|
101
|
+
timeoutMs: conn.timeoutMs ?? base_js_1.DEFAULT_TIMEOUT_MS,
|
|
102
|
+
provider: conn.provider,
|
|
103
|
+
});
|
|
104
|
+
const models = Array.isArray((0, util_js_1.asRecord)(data)?.models) ? (0, util_js_1.asRecord)(data).models : [];
|
|
105
|
+
const ids = models.map((model) => (0, util_js_1.asString)((0, util_js_1.asRecord)(model)?.name) ?? (0, util_js_1.asString)((0, util_js_1.asRecord)(model)?.model) ?? '').filter(Boolean);
|
|
106
|
+
// Probe /api/show per model for context window + capabilities (best effort),
|
|
107
|
+
// with bounded concurrency so a large model list resolves quickly without
|
|
108
|
+
// opening an unbounded number of sockets against a local daemon.
|
|
109
|
+
return (0, util_js_1.mapWithConcurrency)(ids, SHOW_CONCURRENCY, async (id) => {
|
|
110
|
+
const info = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
111
|
+
const probed = await this.showModel(conn, id).catch(() => undefined);
|
|
112
|
+
if (probed?.contextWindow !== undefined)
|
|
113
|
+
info.contextWindow = probed.contextWindow;
|
|
114
|
+
if (probed?.capabilities)
|
|
115
|
+
info.capabilities = probed.capabilities;
|
|
116
|
+
return info;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async embed(conn, req) {
|
|
120
|
+
const inputs = Array.isArray(req.input) ? req.input : [req.input];
|
|
121
|
+
const { data } = await (0, transport_js_1.fetchJson)((0, util_js_1.joinUrl)(conn.baseUrl, '/api/embed'), {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'content-type': 'application/json', ...conn.headers },
|
|
124
|
+
body: JSON.stringify((0, util_js_1.applyProviderOptions)({ model: req.model, input: inputs }, req.providerOptions)),
|
|
125
|
+
timeoutMs: conn.timeoutMs ?? base_js_1.DEFAULT_TIMEOUT_MS,
|
|
126
|
+
provider: conn.provider,
|
|
127
|
+
});
|
|
128
|
+
const record = (0, util_js_1.asRecord)(data);
|
|
129
|
+
const rows = Array.isArray(record?.embeddings) ? record.embeddings : [];
|
|
130
|
+
const embeddings = rows.map((row) => (Array.isArray(row) ? row.filter((n) => typeof n === 'number') : []));
|
|
131
|
+
const inputTokens = (0, util_js_1.asNumber)(record?.prompt_eval_count);
|
|
132
|
+
return {
|
|
133
|
+
embeddings,
|
|
134
|
+
model: req.model,
|
|
135
|
+
usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
|
|
136
|
+
source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
|
|
137
|
+
raw: data,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async showModel(conn, model) {
|
|
141
|
+
const { data } = await (0, transport_js_1.fetchJson)(`${conn.baseUrl}/api/show`, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { 'content-type': 'application/json', ...conn.headers },
|
|
144
|
+
body: JSON.stringify({ model }),
|
|
145
|
+
timeoutMs: Math.min(conn.timeoutMs ?? base_js_1.DEFAULT_TIMEOUT_MS, 15_000),
|
|
146
|
+
provider: conn.provider,
|
|
147
|
+
});
|
|
148
|
+
const record = (0, util_js_1.asRecord)(data);
|
|
149
|
+
const modelInfo = (0, util_js_1.asRecord)(record?.model_info);
|
|
150
|
+
let contextWindow;
|
|
151
|
+
if (modelInfo) {
|
|
152
|
+
for (const [key, value] of Object.entries(modelInfo)) {
|
|
153
|
+
if (key.endsWith('.context_length') && typeof value === 'number') {
|
|
154
|
+
contextWindow = value;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const caps = record?.capabilities;
|
|
160
|
+
const capabilities = Array.isArray(caps) && caps.every((c) => typeof c === 'string') ? caps : undefined;
|
|
161
|
+
const result = {};
|
|
162
|
+
if (contextWindow !== undefined)
|
|
163
|
+
result.contextWindow = contextWindow;
|
|
164
|
+
if (capabilities)
|
|
165
|
+
result.capabilities = capabilities;
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.OllamaAdapter = OllamaAdapter;
|
|
170
|
+
function body(req) {
|
|
171
|
+
const base = {
|
|
172
|
+
model: req.model,
|
|
173
|
+
messages: req.messages.map(toOllamaMessage),
|
|
174
|
+
stream: true,
|
|
175
|
+
options: { temperature: req.temperature, num_predict: req.maxTokens, top_p: req.topP, stop: req.stopSequences },
|
|
176
|
+
format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
|
|
177
|
+
tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
|
|
178
|
+
};
|
|
179
|
+
// First-class reasoning effort → Ollama's `think` flag (reasoning arrives in
|
|
180
|
+
// `message.thinking`, which this engine already routes to `reasoning` events).
|
|
181
|
+
// Ollama has no graded effort for most models, so anything but `'none'` is
|
|
182
|
+
// `true`. Only sent when asked for: a model without thinking support rejects it.
|
|
183
|
+
if (req.reasoningEffort !== undefined)
|
|
184
|
+
base.think = req.reasoningEffort !== 'none';
|
|
185
|
+
// providerOptions reaches Ollama's real request fields — `options.num_ctx`,
|
|
186
|
+
// `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
|
|
187
|
+
// merged one level deep so num_ctx joins the samplers above rather than
|
|
188
|
+
// replacing them.
|
|
189
|
+
return (0, util_js_1.applyProviderOptions)(base, req.providerOptions, ['options']);
|
|
190
|
+
}
|
|
191
|
+
function toOllamaMessage(m) {
|
|
192
|
+
const images = typeof m.content === 'string' ? undefined : m.content.filter((part) => part.type === 'image').map((part) => part.imageBase64).filter(Boolean);
|
|
193
|
+
const message = {
|
|
194
|
+
role: m.role,
|
|
195
|
+
content: typeof m.content === 'string' ? m.content : m.content.filter((part) => part.type === 'text').map((part) => part.text ?? '').join('\n'),
|
|
196
|
+
};
|
|
197
|
+
if (images && images.length)
|
|
198
|
+
message.images = images;
|
|
199
|
+
if (m.role === 'tool' && m.name)
|
|
200
|
+
message.tool_name = m.name;
|
|
201
|
+
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
202
|
+
message.tool_calls = m.toolCalls.map((call) => ({ function: { name: call.name, arguments: call.arguments ?? {} } }));
|
|
203
|
+
}
|
|
204
|
+
return message;
|
|
205
|
+
}
|