@jxburros/ai-nugget 0.3.1 → 0.6.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 +134 -271
- package/UPGRADING.md +158 -0
- package/dist/adapters/engines/anthropic.d.ts +7 -1
- package/dist/adapters/engines/anthropic.js +64 -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 +17 -1
- package/dist/adapters/engines/google.js +93 -29
- package/dist/adapters/engines/ollama.d.ts +2 -1
- package/dist/adapters/engines/ollama.js +51 -15
- package/dist/adapters/engines/openaiChat.d.ts +2 -1
- package/dist/adapters/engines/openaiChat.js +60 -30
- package/dist/adapters/profiles.d.ts +21 -1
- package/dist/adapters/profiles.js +12 -2
- package/dist/agent/loop.d.ts +43 -1
- package/dist/agent/loop.js +120 -13
- package/dist/cjs/adapters/engines/anthropic.js +308 -0
- package/dist/cjs/adapters/engines/base.js +147 -0
- package/dist/cjs/adapters/engines/google.js +284 -0
- package/dist/cjs/adapters/engines/ollama.js +199 -0
- package/dist/cjs/adapters/engines/openaiChat.js +261 -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 +372 -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 +561 -0
- package/dist/cjs/index.js +27 -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/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 +112 -0
- package/dist/connect.d.ts +31 -0
- package/dist/connect.js +25 -0
- package/dist/errors.js +25 -1
- package/dist/handler.d.ts +61 -1
- package/dist/handler.js +247 -61
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/keys.js +1 -4
- 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 +96 -2
- package/dist/util.d.ts +16 -0
- package/dist/util.js +50 -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 +305 -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 +281 -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 +196 -0
- package/nugget/dist/adapters/engines/ollama.js.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
- package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
- package/nugget/dist/adapters/engines/openaiChat.js +258 -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 +112 -0
- package/nugget/dist/agent/loop.d.ts.map +1 -0
- package/nugget/dist/agent/loop.js +370 -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 +308 -0
- package/nugget/dist/cjs/adapters/engines/base.js +147 -0
- package/nugget/dist/cjs/adapters/engines/google.js +284 -0
- package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
- package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -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 +372 -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 +561 -0
- package/nugget/dist/cjs/index.js +27 -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/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 +112 -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 +108 -0
- package/nugget/dist/handler.d.ts.map +1 -0
- package/nugget/dist/handler.js +557 -0
- package/nugget/dist/handler.js.map +1 -0
- package/nugget/dist/index.d.ts +12 -0
- package/nugget/dist/index.d.ts.map +1 -0
- package/nugget/dist/index.js +12 -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/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 +285 -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 +24 -0
- package/nugget/dist/util.d.ts.map +1 -0
- package/nugget/dist/util.js +101 -0
- package/nugget/dist/util.js.map +1 -0
- package/nugget/src/adapters/engines/anthropic.ts +62 -37
- package/nugget/src/adapters/engines/base.ts +50 -7
- package/nugget/src/adapters/engines/google.ts +92 -29
- package/nugget/src/adapters/engines/ollama.ts +51 -17
- package/nugget/src/adapters/engines/openaiChat.ts +61 -32
- package/nugget/src/adapters/profiles.ts +53 -3
- package/nugget/src/agent/loop.ts +158 -14
- package/nugget/src/connect.ts +48 -0
- package/nugget/src/errors.ts +25 -1
- package/nugget/src/handler.ts +286 -59
- package/nugget/src/index.ts +1 -0
- package/nugget/src/keys.ts +1 -5
- package/nugget/src/redact.ts +32 -1
- package/nugget/src/transport.ts +20 -1
- package/nugget/src/types.ts +96 -1
- package/nugget/src/util.ts +55 -0
- package/package.json +20 -6
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
export declare class AnthropicAdapter implements ProviderAdapter {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
constructor(provider: string);
|
|
5
5
|
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
6
|
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
|
+
/**
|
|
8
|
+
* Lists models from Anthropic's `/v1/models` endpoint (auth + version headers
|
|
9
|
+
* already applied to `conn.headers`). The endpoint does not report a context
|
|
10
|
+
* window, so `contextWindow` is left undefined rather than guessed.
|
|
11
|
+
*/
|
|
12
|
+
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
7
13
|
}
|
|
8
14
|
//# sourceMappingURL=anthropic.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
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';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
6
|
const JSON_MODE_TOOL = 'json_output';
|
|
7
7
|
export class AnthropicAdapter {
|
|
8
8
|
provider;
|
|
@@ -29,11 +29,22 @@ export class AnthropicAdapter {
|
|
|
29
29
|
let inputTokens;
|
|
30
30
|
let outputTokens;
|
|
31
31
|
let stopReason;
|
|
32
|
+
let sawTerminal = false;
|
|
32
33
|
const emittedTools = [];
|
|
33
34
|
// Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
|
|
34
35
|
const blocks = new Map();
|
|
35
36
|
const timeout = streamTimeout(conn, req.signal);
|
|
36
37
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
38
|
+
// Anthropic JSON mode is implemented as a forced `json_output` tool, which
|
|
39
|
+
// cannot coexist with the caller's own tools — same silent-downgrade shape
|
|
40
|
+
// as Gemini, so it gets the same signal rather than quietly dropping.
|
|
41
|
+
if (req.responseFormat?.type === 'json' && req.tools?.length) {
|
|
42
|
+
yield {
|
|
43
|
+
type: 'context',
|
|
44
|
+
kind: 'json_mode_downgraded',
|
|
45
|
+
data: { reason: 'Anthropic JSON mode uses a forced tool and cannot be combined with caller tools; the request was sent without JSON mode', provider: conn.provider },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
37
48
|
try {
|
|
38
49
|
const res = await postResponse(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
|
|
39
50
|
const contentType = res.headers.get('content-type') ?? '';
|
|
@@ -43,6 +54,7 @@ export class AnthropicAdapter {
|
|
|
43
54
|
inputTokens = parsed.inputTokens;
|
|
44
55
|
outputTokens = parsed.outputTokens;
|
|
45
56
|
stopReason = parsed.stopReason;
|
|
57
|
+
sawTerminal = true;
|
|
46
58
|
if (jsonMode) {
|
|
47
59
|
text = jsonTextFrom(parsed.toolCalls, parsed.text);
|
|
48
60
|
if (text)
|
|
@@ -60,6 +72,7 @@ export class AnthropicAdapter {
|
|
|
60
72
|
}
|
|
61
73
|
else {
|
|
62
74
|
for await (const line of sseLines(res)) {
|
|
75
|
+
timeout.bump();
|
|
63
76
|
const record = asRecord(safeParse(line));
|
|
64
77
|
if (!record)
|
|
65
78
|
continue;
|
|
@@ -88,6 +101,12 @@ export class AnthropicAdapter {
|
|
|
88
101
|
yield { type: 'delta', text: piece };
|
|
89
102
|
}
|
|
90
103
|
}
|
|
104
|
+
else if (delta?.type === 'thinking_delta') {
|
|
105
|
+
// Extended-thinking tokens ride the reasoning channel, kept out of `text`.
|
|
106
|
+
const piece = asString(delta.thinking) ?? '';
|
|
107
|
+
if (piece)
|
|
108
|
+
yield { type: 'reasoning', text: piece };
|
|
109
|
+
}
|
|
91
110
|
else if (delta?.type === 'input_json_delta') {
|
|
92
111
|
const partial = blocks.get(index);
|
|
93
112
|
if (partial)
|
|
@@ -112,14 +131,21 @@ export class AnthropicAdapter {
|
|
|
112
131
|
}
|
|
113
132
|
}
|
|
114
133
|
}
|
|
134
|
+
else if (type === 'message_stop') {
|
|
135
|
+
sawTerminal = true;
|
|
136
|
+
}
|
|
115
137
|
else if (type === 'message_delta') {
|
|
116
138
|
const delta = asRecord(record.delta);
|
|
139
|
+
if (asString(delta?.stop_reason))
|
|
140
|
+
sawTerminal = true;
|
|
117
141
|
stopReason = asString(delta?.stop_reason) ?? stopReason;
|
|
118
142
|
const usage = asRecord(record.usage);
|
|
119
143
|
outputTokens = asNumber(usage?.output_tokens) ?? outputTokens;
|
|
120
144
|
}
|
|
121
145
|
}
|
|
122
146
|
}
|
|
147
|
+
if (!sawTerminal)
|
|
148
|
+
yield streamAnomaly('stream ended without a stop_reason or message_stop');
|
|
123
149
|
const hasTools = emittedTools.length > 0;
|
|
124
150
|
yield { type: 'done', result: {
|
|
125
151
|
text,
|
|
@@ -138,6 +164,24 @@ export class AnthropicAdapter {
|
|
|
138
164
|
timeout.done();
|
|
139
165
|
}
|
|
140
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Lists models from Anthropic's `/v1/models` endpoint (auth + version headers
|
|
169
|
+
* already applied to `conn.headers`). The endpoint does not report a context
|
|
170
|
+
* window, so `contextWindow` is left undefined rather than guessed.
|
|
171
|
+
*/
|
|
172
|
+
async listModels(conn) {
|
|
173
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1/models'), {
|
|
174
|
+
method: 'GET',
|
|
175
|
+
headers: conn.headers,
|
|
176
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
177
|
+
provider: conn.provider,
|
|
178
|
+
});
|
|
179
|
+
const rows = Array.isArray(asRecord(data)?.data) ? asRecord(data).data : [];
|
|
180
|
+
return rows
|
|
181
|
+
.map((row) => asString(asRecord(row)?.id))
|
|
182
|
+
.filter((id) => Boolean(id))
|
|
183
|
+
.map((id) => ({ id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } }));
|
|
184
|
+
}
|
|
141
185
|
}
|
|
142
186
|
function body(req, jsonMode) {
|
|
143
187
|
const system = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n') || undefined;
|
|
@@ -166,7 +210,9 @@ function body(req, jsonMode) {
|
|
|
166
210
|
else if (req.toolChoice === 'none')
|
|
167
211
|
base.tool_choice = { type: 'none' };
|
|
168
212
|
}
|
|
169
|
-
|
|
213
|
+
// providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
|
|
214
|
+
// top-level `cache_control` extras, `service_tier`, …) without a release.
|
|
215
|
+
return applyProviderOptions(base, req.providerOptions);
|
|
170
216
|
}
|
|
171
217
|
function toAnthropicMessages(messages) {
|
|
172
218
|
const out = [];
|
|
@@ -188,29 +234,29 @@ function toAnthropicMessages(messages) {
|
|
|
188
234
|
return out;
|
|
189
235
|
}
|
|
190
236
|
function toAnthropicMessage(m) {
|
|
191
|
-
//
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
content: [{ type: 'tool_result', tool_use_id: m.toolCallId ?? '', content: typeof m.content === 'string' ? m.content : '' }],
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
// assistant turns that carried tool calls replay them as tool_use blocks.
|
|
237
|
+
// Tool-role messages are handled by toAnthropicMessages' batching loop and
|
|
238
|
+
// never reach here, so no tool branch is needed.
|
|
239
|
+
// assistant turns that carried tool calls replay them as tool_use blocks,
|
|
240
|
+
// preserving any text/image content parts that accompanied the calls.
|
|
199
241
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
200
|
-
const content = [];
|
|
201
|
-
if (typeof m.content === 'string' && m.content)
|
|
202
|
-
content.push({ type: 'text', text: m.content });
|
|
242
|
+
const content = [...contentBlocks(m.content)];
|
|
203
243
|
for (const call of m.toolCalls)
|
|
204
244
|
content.push({ type: 'tool_use', id: call.id, name: call.name, input: call.arguments ?? {} });
|
|
205
245
|
return { role: 'assistant', content };
|
|
206
246
|
}
|
|
207
247
|
return {
|
|
208
248
|
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
209
|
-
content: typeof m.content === 'string' ? m.content : m.content
|
|
210
|
-
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
211
|
-
: { type: 'text', text: part.text ?? '' }),
|
|
249
|
+
content: typeof m.content === 'string' ? m.content : contentBlocks(m.content),
|
|
212
250
|
};
|
|
213
251
|
}
|
|
252
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Anthropic content blocks. */
|
|
253
|
+
function contentBlocks(content) {
|
|
254
|
+
if (typeof content === 'string')
|
|
255
|
+
return content ? [{ type: 'text', text: content }] : [];
|
|
256
|
+
return content.map((part) => part.type === 'image'
|
|
257
|
+
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
258
|
+
: { type: 'text', text: part.text ?? '' });
|
|
259
|
+
}
|
|
214
260
|
function textContent(content) {
|
|
215
261
|
if (typeof content === 'string')
|
|
216
262
|
return content;
|
|
@@ -256,23 +302,4 @@ function mapStop(stop, hasTools) {
|
|
|
256
302
|
return 'length';
|
|
257
303
|
return 'stop';
|
|
258
304
|
}
|
|
259
|
-
function parseArgs(raw) {
|
|
260
|
-
try {
|
|
261
|
-
return raw ? JSON.parse(raw) : {};
|
|
262
|
-
}
|
|
263
|
-
catch {
|
|
264
|
-
return {};
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
function safeParse(line) {
|
|
268
|
-
try {
|
|
269
|
-
return JSON.parse(line);
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
function randomId() {
|
|
276
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
277
|
-
}
|
|
278
305
|
//# sourceMappingURL=anthropic.js.map
|
|
@@ -12,12 +12,39 @@ export declare const DEFAULT_TIMEOUT_MS = 120000;
|
|
|
12
12
|
export declare function streamError(error: unknown, timeout: {
|
|
13
13
|
timedOut(): boolean;
|
|
14
14
|
}, provider: string): AIError;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
17
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
18
|
+
* `bump()` on each received chunk.
|
|
19
|
+
*/
|
|
16
20
|
export declare function streamTimeout(conn: ResolvedConnection, signal?: AbortSignal): ReturnType<typeof withTimeout>;
|
|
17
21
|
export declare function listOpenModels(conn: ResolvedConnection, profile: ProviderProfile): Promise<ModelInfo[]>;
|
|
18
22
|
export declare function health(conn: ResolvedConnection, profile: ProviderProfile): Promise<{
|
|
19
23
|
ok: boolean;
|
|
20
24
|
detail?: string;
|
|
21
25
|
}>;
|
|
22
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
28
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
29
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
30
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
31
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseArgs(raw: unknown): unknown;
|
|
34
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
35
|
+
export declare function safeParse(line: string): unknown;
|
|
36
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
37
|
+
export declare function randomId(): string;
|
|
38
|
+
/**
|
|
39
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
40
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
41
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
42
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
43
|
+
* regardless of provider.
|
|
44
|
+
*/
|
|
45
|
+
export declare function streamAnomaly(reason?: string): {
|
|
46
|
+
type: 'context';
|
|
47
|
+
kind: string;
|
|
48
|
+
data: unknown;
|
|
49
|
+
};
|
|
23
50
|
//# sourceMappingURL=base.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AIError, fromUnknown } from '../../errors.js';
|
|
2
2
|
import { fetchJson, withTimeout } from '../../transport.js';
|
|
3
|
-
import { asRecord, asString } from '../../util.js';
|
|
3
|
+
import { asRecord, asString, joinUrl } from '../../util.js';
|
|
4
4
|
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
5
5
|
/**
|
|
6
6
|
* Maps a raw streaming failure onto a typed {@link AIError}. A fired timeout is
|
|
@@ -13,14 +13,18 @@ export function streamError(error, timeout, provider) {
|
|
|
13
13
|
return new AIError('Request timed out', { kind: 'timeout', provider });
|
|
14
14
|
return fromUnknown(error, provider);
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
18
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
19
|
+
* `bump()` on each received chunk.
|
|
20
|
+
*/
|
|
17
21
|
export function streamTimeout(conn, signal) {
|
|
18
|
-
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
|
|
22
|
+
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal, conn.idleTimeoutMs);
|
|
19
23
|
}
|
|
20
24
|
export async function listOpenModels(conn, profile) {
|
|
21
25
|
if (!profile.listModelsPath)
|
|
22
26
|
return [];
|
|
23
|
-
const { data } = await fetchJson(
|
|
27
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, profile.listModelsPath), {
|
|
24
28
|
method: 'GET',
|
|
25
29
|
headers: conn.headers,
|
|
26
30
|
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -53,7 +57,7 @@ export async function health(conn, profile) {
|
|
|
53
57
|
if (!path)
|
|
54
58
|
return { ok: true };
|
|
55
59
|
try {
|
|
56
|
-
await fetchJson(
|
|
60
|
+
await fetchJson(joinUrl(conn.baseUrl, path), {
|
|
57
61
|
method: 'GET',
|
|
58
62
|
headers: conn.headers,
|
|
59
63
|
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
@@ -65,9 +69,49 @@ export async function health(conn, profile) {
|
|
|
65
69
|
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
74
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
75
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
76
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
77
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
78
|
+
*/
|
|
79
|
+
export function parseArgs(raw) {
|
|
80
|
+
if (raw === undefined || raw === null)
|
|
81
|
+
return {};
|
|
82
|
+
if (typeof raw !== 'string')
|
|
83
|
+
return raw;
|
|
84
|
+
if (!raw.trim())
|
|
85
|
+
return {};
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return {};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
94
|
+
export function safeParse(line) {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
103
|
+
export function randomId() {
|
|
104
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
108
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
109
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
110
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
111
|
+
* regardless of provider.
|
|
112
|
+
*/
|
|
113
|
+
export function streamAnomaly(reason = 'stream ended without a terminal finish reason') {
|
|
114
|
+
return { type: 'context', kind: 'stream_anomaly', data: { reason } };
|
|
71
115
|
}
|
|
72
116
|
/**
|
|
73
117
|
* Pulls a context-window figure out of the many shapes providers use for it
|
|
@@ -1,8 +1,24 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
export declare class GoogleAdapter implements ProviderAdapter {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
constructor(provider: string);
|
|
5
5
|
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
6
|
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
|
+
/**
|
|
8
|
+
* `GoogleAdapter` has no `listModels`, so without a `health` probe
|
|
9
|
+
* `AIHandler.testConnection` would report `ok: true` without ever making a
|
|
10
|
+
* network call. `/v1beta/models` is a lightweight, key-authenticated GET
|
|
11
|
+
* that gives an honest connectivity/auth check.
|
|
12
|
+
*/
|
|
13
|
+
health(conn: ResolvedConnection): Promise<{
|
|
14
|
+
ok: boolean;
|
|
15
|
+
detail?: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
19
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
20
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
21
|
+
*/
|
|
22
|
+
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
7
23
|
}
|
|
8
24
|
//# sourceMappingURL=google.d.ts.map
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
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';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
6
6
|
export class GoogleAdapter {
|
|
7
7
|
provider;
|
|
8
8
|
constructor(provider) {
|
|
@@ -30,12 +30,23 @@ export class GoogleAdapter {
|
|
|
30
30
|
const toolCalls = [];
|
|
31
31
|
const timeout = streamTimeout(conn, req.signal);
|
|
32
32
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
33
|
+
// Gemini rejects `responseMimeType: application/json` combined with function
|
|
34
|
+
// declarations, so `body()` drops JSON mode when tools are present. Surface
|
|
35
|
+
// that downgrade instead of silently returning unstructured text.
|
|
36
|
+
if (jsonModeDowngraded(req)) {
|
|
37
|
+
yield {
|
|
38
|
+
type: 'context',
|
|
39
|
+
kind: 'json_mode_downgraded',
|
|
40
|
+
data: { reason: 'Gemini does not accept JSON response mode together with tools; the request was sent without JSON mode', provider: conn.provider },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
33
43
|
try {
|
|
34
44
|
const streamUrl = `${conn.baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:streamGenerateContent?alt=sse`;
|
|
35
45
|
const res = await postResponse(streamUrl, body(req), conn.headers, timeout.signal, conn.provider);
|
|
36
46
|
const contentType = res.headers.get('content-type') ?? '';
|
|
37
47
|
const chunks = contentType.includes('text/event-stream') ? sseLines(res) : singleJsonLine(res);
|
|
38
48
|
for await (const line of chunks) {
|
|
49
|
+
timeout.bump();
|
|
39
50
|
const parsed = safeParse(line);
|
|
40
51
|
const record = asRecord(parsed);
|
|
41
52
|
const promptFeedback = asRecord(record?.promptFeedback);
|
|
@@ -44,6 +55,12 @@ export class GoogleAdapter {
|
|
|
44
55
|
for (const part of candidateParts(record)) {
|
|
45
56
|
const partText = asString(asRecord(part)?.text);
|
|
46
57
|
if (partText) {
|
|
58
|
+
// Gemini marks reasoning parts with `thought: true`; route those to
|
|
59
|
+
// the reasoning channel so they don't blend into the answer text.
|
|
60
|
+
if (asRecord(part)?.thought === true) {
|
|
61
|
+
yield { type: 'reasoning', text: partText };
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
47
64
|
if (firstTokenMs === null)
|
|
48
65
|
firstTokenMs = Date.now() - started;
|
|
49
66
|
text += partText;
|
|
@@ -66,6 +83,8 @@ export class GoogleAdapter {
|
|
|
66
83
|
inputTokens = asNumber(usage?.promptTokenCount) ?? inputTokens;
|
|
67
84
|
outputTokens = asNumber(usage?.candidatesTokenCount) ?? outputTokens;
|
|
68
85
|
}
|
|
86
|
+
if (!finish)
|
|
87
|
+
yield streamAnomaly('stream ended without a finishReason');
|
|
69
88
|
const hasTools = toolCalls.length > 0;
|
|
70
89
|
yield { type: 'done', result: {
|
|
71
90
|
text,
|
|
@@ -84,6 +103,61 @@ export class GoogleAdapter {
|
|
|
84
103
|
timeout.done();
|
|
85
104
|
}
|
|
86
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* `GoogleAdapter` has no `listModels`, so without a `health` probe
|
|
108
|
+
* `AIHandler.testConnection` would report `ok: true` without ever making a
|
|
109
|
+
* network call. `/v1beta/models` is a lightweight, key-authenticated GET
|
|
110
|
+
* that gives an honest connectivity/auth check.
|
|
111
|
+
*/
|
|
112
|
+
async health(conn) {
|
|
113
|
+
try {
|
|
114
|
+
await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
115
|
+
method: 'GET',
|
|
116
|
+
headers: conn.headers,
|
|
117
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
118
|
+
provider: conn.provider,
|
|
119
|
+
});
|
|
120
|
+
return { ok: true };
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
128
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
129
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
130
|
+
*/
|
|
131
|
+
async listModels(conn) {
|
|
132
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
133
|
+
method: 'GET',
|
|
134
|
+
headers: conn.headers,
|
|
135
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
136
|
+
provider: conn.provider,
|
|
137
|
+
});
|
|
138
|
+
const rows = Array.isArray(asRecord(data)?.models) ? asRecord(data).models : [];
|
|
139
|
+
const models = [];
|
|
140
|
+
for (const value of rows) {
|
|
141
|
+
const row = asRecord(value);
|
|
142
|
+
const name = asString(row?.name);
|
|
143
|
+
if (!name)
|
|
144
|
+
continue;
|
|
145
|
+
const id = name.startsWith('models/') ? name.slice('models/'.length) : name;
|
|
146
|
+
const model = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
147
|
+
const contextWindow = asNumber(row?.inputTokenLimit);
|
|
148
|
+
if (contextWindow !== undefined)
|
|
149
|
+
model.contextWindow = contextWindow;
|
|
150
|
+
const methods = row?.supportedGenerationMethods;
|
|
151
|
+
if (Array.isArray(methods) && methods.every((m) => typeof m === 'string'))
|
|
152
|
+
model.capabilities = methods;
|
|
153
|
+
models.push(model);
|
|
154
|
+
}
|
|
155
|
+
return models;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** True when the caller asked for JSON mode but tools force it off. */
|
|
159
|
+
function jsonModeDowngraded(req) {
|
|
160
|
+
return req.responseFormat?.type === 'json' && !!req.tools?.length;
|
|
87
161
|
}
|
|
88
162
|
function body(req) {
|
|
89
163
|
const systemText = req.messages.filter((m) => m.role === 'system').map((m) => textContent(m.content)).join('\n\n');
|
|
@@ -110,7 +184,10 @@ function body(req) {
|
|
|
110
184
|
config.allowedFunctionNames = [req.toolChoice.name];
|
|
111
185
|
payload.toolConfig = { functionCallingConfig: config };
|
|
112
186
|
}
|
|
113
|
-
|
|
187
|
+
// providerOptions reaches Google-native fields: top-level `safetySettings`,
|
|
188
|
+
// `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
|
|
189
|
+
// `responseModalities`, …) merged one level deep into generationConfig.
|
|
190
|
+
return applyProviderOptions(payload, req.providerOptions, ['generationConfig']);
|
|
114
191
|
}
|
|
115
192
|
function toGoogleContents(messages) {
|
|
116
193
|
const contents = [];
|
|
@@ -132,29 +209,27 @@ function toGoogleContents(messages) {
|
|
|
132
209
|
return contents;
|
|
133
210
|
}
|
|
134
211
|
function toGoogleContent(m) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
role: 'user',
|
|
138
|
-
parts: [{ functionResponse: { name: m.name ?? 'unknown', response: toolResponseObject(m) } }],
|
|
139
|
-
};
|
|
140
|
-
}
|
|
212
|
+
// Tool-role messages are handled by toGoogleContents' batching loop and never
|
|
213
|
+
// reach here, so no tool branch is needed.
|
|
141
214
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
142
|
-
const parts = [];
|
|
143
|
-
if (typeof m.content === 'string' && m.content)
|
|
144
|
-
parts.push({ text: m.content });
|
|
215
|
+
const parts = [...googleParts(m.content)];
|
|
145
216
|
for (const call of m.toolCalls)
|
|
146
217
|
parts.push({ functionCall: { name: call.name, args: call.arguments ?? {} } });
|
|
147
218
|
return { role: 'model', parts };
|
|
148
219
|
}
|
|
149
220
|
return {
|
|
150
221
|
role: m.role === 'assistant' ? 'model' : 'user',
|
|
151
|
-
parts:
|
|
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 ?? '' }),
|
|
222
|
+
parts: googleParts(m.content),
|
|
156
223
|
};
|
|
157
224
|
}
|
|
225
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Google parts. */
|
|
226
|
+
function googleParts(content) {
|
|
227
|
+
if (typeof content === 'string')
|
|
228
|
+
return content ? [{ text: content }] : [];
|
|
229
|
+
return content.map((part) => part.type === 'image'
|
|
230
|
+
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
231
|
+
: { text: part.text ?? '' });
|
|
232
|
+
}
|
|
158
233
|
function textContent(content) {
|
|
159
234
|
if (typeof content === 'string')
|
|
160
235
|
return content;
|
|
@@ -203,15 +278,4 @@ function mapFinish(finish, hasTools) {
|
|
|
203
278
|
return 'content_filter';
|
|
204
279
|
return 'stop';
|
|
205
280
|
}
|
|
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
281
|
//# sourceMappingURL=google.js.map
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
1
|
+
import type { ChatRequest, ChatResult, EmbedRequest, EmbedResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
|
|
2
2
|
export declare class OllamaAdapter implements ProviderAdapter {
|
|
3
3
|
readonly provider: string;
|
|
4
4
|
constructor(provider: string);
|
|
5
5
|
chat(conn: ResolvedConnection, req: ChatRequest): Promise<ChatResult>;
|
|
6
6
|
stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
|
|
7
7
|
listModels(conn: ResolvedConnection): Promise<ModelInfo[]>;
|
|
8
|
+
embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
|
|
8
9
|
private showModel;
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=ollama.d.ts.map
|