@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.
Files changed (187) hide show
  1. package/README.md +158 -382
  2. package/UPGRADING.md +182 -0
  3. package/dist/adapters/engines/anthropic.d.ts +7 -1
  4. package/dist/adapters/engines/anthropic.js +82 -37
  5. package/dist/adapters/engines/base.d.ts +29 -2
  6. package/dist/adapters/engines/base.js +52 -8
  7. package/dist/adapters/engines/google.d.ts +7 -1
  8. package/dist/adapters/engines/google.js +80 -29
  9. package/dist/adapters/engines/ollama.d.ts +2 -1
  10. package/dist/adapters/engines/ollama.js +57 -15
  11. package/dist/adapters/engines/openaiChat.d.ts +8 -1
  12. package/dist/adapters/engines/openaiChat.js +109 -29
  13. package/dist/adapters/profiles.d.ts +21 -1
  14. package/dist/adapters/profiles.js +12 -2
  15. package/dist/agent/loop.d.ts +45 -1
  16. package/dist/agent/loop.js +117 -12
  17. package/dist/cjs/adapters/engines/anthropic.js +326 -0
  18. package/dist/cjs/adapters/engines/base.js +147 -0
  19. package/dist/cjs/adapters/engines/google.js +291 -0
  20. package/dist/cjs/adapters/engines/ollama.js +205 -0
  21. package/dist/cjs/adapters/engines/openaiChat.js +312 -0
  22. package/dist/cjs/adapters/index.js +22 -0
  23. package/dist/cjs/adapters/profiles.js +125 -0
  24. package/dist/cjs/agent/index.js +18 -0
  25. package/dist/cjs/agent/loop.js +373 -0
  26. package/dist/cjs/agent/tools.js +51 -0
  27. package/dist/cjs/connect.js +27 -0
  28. package/dist/cjs/errors.js +110 -0
  29. package/dist/cjs/handler.js +586 -0
  30. package/dist/cjs/index.js +28 -0
  31. package/dist/cjs/json.js +121 -0
  32. package/dist/cjs/keys.js +81 -0
  33. package/dist/cjs/package.json +3 -0
  34. package/dist/cjs/policy.js +42 -0
  35. package/dist/cjs/reasoning.js +159 -0
  36. package/dist/cjs/redact.js +78 -0
  37. package/dist/cjs/tokens.js +22 -0
  38. package/dist/cjs/transport.js +172 -0
  39. package/dist/cjs/types.js +6 -0
  40. package/dist/cjs/util.js +126 -0
  41. package/dist/errors.js +25 -1
  42. package/dist/handler.d.ts +67 -1
  43. package/dist/handler.js +266 -61
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/reasoning.d.ts +45 -0
  47. package/dist/reasoning.js +154 -0
  48. package/dist/redact.d.ts +18 -0
  49. package/dist/redact.js +29 -1
  50. package/dist/transport.d.ts +3 -1
  51. package/dist/transport.js +21 -1
  52. package/dist/types.d.ts +112 -2
  53. package/dist/util.d.ts +22 -0
  54. package/dist/util.js +59 -0
  55. package/nugget/VERSION.txt +2 -2
  56. package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
  57. package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
  58. package/nugget/dist/adapters/engines/anthropic.js +323 -0
  59. package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
  60. package/nugget/dist/adapters/engines/base.d.ts +50 -0
  61. package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
  62. package/nugget/dist/adapters/engines/base.js +137 -0
  63. package/nugget/dist/adapters/engines/base.js.map +1 -0
  64. package/nugget/dist/adapters/engines/google.d.ts +24 -0
  65. package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
  66. package/nugget/dist/adapters/engines/google.js +288 -0
  67. package/nugget/dist/adapters/engines/google.js.map +1 -0
  68. package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
  69. package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
  70. package/nugget/dist/adapters/engines/ollama.js +202 -0
  71. package/nugget/dist/adapters/engines/ollama.js.map +1 -0
  72. package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
  73. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
  74. package/nugget/dist/adapters/engines/openaiChat.js +308 -0
  75. package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
  76. package/nugget/dist/adapters/index.d.ts +5 -0
  77. package/nugget/dist/adapters/index.d.ts.map +1 -0
  78. package/nugget/dist/adapters/index.js +17 -0
  79. package/nugget/dist/adapters/index.js.map +1 -0
  80. package/nugget/dist/adapters/profiles.d.ts +62 -0
  81. package/nugget/dist/adapters/profiles.d.ts.map +1 -0
  82. package/nugget/dist/adapters/profiles.js +121 -0
  83. package/nugget/dist/adapters/profiles.js.map +1 -0
  84. package/nugget/dist/agent/index.d.ts +3 -0
  85. package/nugget/dist/agent/index.d.ts.map +1 -0
  86. package/nugget/dist/agent/index.js +3 -0
  87. package/nugget/dist/agent/index.js.map +1 -0
  88. package/nugget/dist/agent/loop.d.ts +114 -0
  89. package/nugget/dist/agent/loop.d.ts.map +1 -0
  90. package/nugget/dist/agent/loop.js +371 -0
  91. package/nugget/dist/agent/loop.js.map +1 -0
  92. package/nugget/dist/agent/tools.d.ts +29 -0
  93. package/nugget/dist/agent/tools.d.ts.map +1 -0
  94. package/nugget/dist/agent/tools.js +48 -0
  95. package/nugget/dist/agent/tools.js.map +1 -0
  96. package/nugget/dist/cjs/adapters/engines/anthropic.js +326 -0
  97. package/nugget/dist/cjs/adapters/engines/base.js +147 -0
  98. package/nugget/dist/cjs/adapters/engines/google.js +291 -0
  99. package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
  100. package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -0
  101. package/nugget/dist/cjs/adapters/index.js +22 -0
  102. package/nugget/dist/cjs/adapters/profiles.js +125 -0
  103. package/nugget/dist/cjs/agent/index.js +18 -0
  104. package/nugget/dist/cjs/agent/loop.js +373 -0
  105. package/nugget/dist/cjs/agent/tools.js +51 -0
  106. package/nugget/dist/cjs/connect.js +27 -0
  107. package/nugget/dist/cjs/errors.js +110 -0
  108. package/nugget/dist/cjs/handler.js +586 -0
  109. package/nugget/dist/cjs/index.js +28 -0
  110. package/nugget/dist/cjs/json.js +121 -0
  111. package/nugget/dist/cjs/keys.js +81 -0
  112. package/nugget/dist/cjs/package.json +3 -0
  113. package/nugget/dist/cjs/policy.js +42 -0
  114. package/nugget/dist/cjs/reasoning.js +159 -0
  115. package/nugget/dist/cjs/redact.js +78 -0
  116. package/nugget/dist/cjs/tokens.js +22 -0
  117. package/nugget/dist/cjs/transport.js +172 -0
  118. package/nugget/dist/cjs/types.js +6 -0
  119. package/nugget/dist/cjs/util.js +126 -0
  120. package/nugget/dist/connect.d.ts +31 -0
  121. package/nugget/dist/connect.d.ts.map +1 -0
  122. package/nugget/dist/connect.js +25 -0
  123. package/nugget/dist/connect.js.map +1 -0
  124. package/nugget/dist/errors.d.ts +22 -0
  125. package/nugget/dist/errors.d.ts.map +1 -0
  126. package/nugget/dist/errors.js +104 -0
  127. package/nugget/dist/errors.js.map +1 -0
  128. package/nugget/dist/handler.d.ts +114 -0
  129. package/nugget/dist/handler.d.ts.map +1 -0
  130. package/nugget/dist/handler.js +582 -0
  131. package/nugget/dist/handler.js.map +1 -0
  132. package/nugget/dist/index.d.ts +13 -0
  133. package/nugget/dist/index.d.ts.map +1 -0
  134. package/nugget/dist/index.js +13 -0
  135. package/nugget/dist/index.js.map +1 -0
  136. package/nugget/dist/json.d.ts +7 -0
  137. package/nugget/dist/json.d.ts.map +1 -0
  138. package/nugget/dist/json.js +114 -0
  139. package/nugget/dist/json.js.map +1 -0
  140. package/nugget/dist/keys.d.ts +7 -0
  141. package/nugget/dist/keys.d.ts.map +1 -0
  142. package/nugget/dist/keys.js +75 -0
  143. package/nugget/dist/keys.js.map +1 -0
  144. package/nugget/dist/policy.d.ts +6 -0
  145. package/nugget/dist/policy.d.ts.map +1 -0
  146. package/nugget/dist/policy.js +37 -0
  147. package/nugget/dist/policy.js.map +1 -0
  148. package/nugget/dist/reasoning.d.ts +45 -0
  149. package/nugget/dist/reasoning.d.ts.map +1 -0
  150. package/nugget/dist/reasoning.js +154 -0
  151. package/nugget/dist/reasoning.js.map +1 -0
  152. package/nugget/dist/redact.d.ts +26 -0
  153. package/nugget/dist/redact.d.ts.map +1 -0
  154. package/nugget/dist/redact.js +74 -0
  155. package/nugget/dist/redact.js.map +1 -0
  156. package/nugget/dist/tokens.d.ts +5 -0
  157. package/nugget/dist/tokens.d.ts.map +1 -0
  158. package/nugget/dist/tokens.js +18 -0
  159. package/nugget/dist/tokens.js.map +1 -0
  160. package/nugget/dist/transport.d.ts +30 -0
  161. package/nugget/dist/transport.d.ts.map +1 -0
  162. package/nugget/dist/transport.js +165 -0
  163. package/nugget/dist/transport.js.map +1 -0
  164. package/nugget/dist/types.d.ts +301 -0
  165. package/nugget/dist/types.d.ts.map +1 -0
  166. package/nugget/dist/types.js +4 -0
  167. package/nugget/dist/types.js.map +1 -0
  168. package/nugget/dist/util.d.ts +31 -0
  169. package/nugget/dist/util.d.ts.map +1 -0
  170. package/nugget/dist/util.js +114 -0
  171. package/nugget/dist/util.js.map +1 -0
  172. package/nugget/src/adapters/engines/anthropic.ts +78 -37
  173. package/nugget/src/adapters/engines/base.ts +50 -7
  174. package/nugget/src/adapters/engines/google.ts +79 -29
  175. package/nugget/src/adapters/engines/ollama.ts +56 -17
  176. package/nugget/src/adapters/engines/openaiChat.ts +104 -30
  177. package/nugget/src/adapters/profiles.ts +53 -3
  178. package/nugget/src/agent/loop.ts +156 -13
  179. package/nugget/src/errors.ts +25 -1
  180. package/nugget/src/handler.ts +308 -59
  181. package/nugget/src/index.ts +1 -0
  182. package/nugget/src/reasoning.ts +174 -0
  183. package/nugget/src/redact.ts +32 -1
  184. package/nugget/src/transport.ts +20 -1
  185. package/nugget/src/types.ts +113 -1
  186. package/nugget/src/util.ts +64 -0
  187. 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
+ }