@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,114 @@
|
|
|
1
|
+
export function asRecord(value) {
|
|
2
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
export function asString(value) {
|
|
5
|
+
return typeof value === 'string' ? value : undefined;
|
|
6
|
+
}
|
|
7
|
+
export function asNumber(value) {
|
|
8
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
export function promptChars(messages) {
|
|
11
|
+
return messages.reduce((sum, message) => {
|
|
12
|
+
if (typeof message.content === 'string')
|
|
13
|
+
return sum + message.content.length;
|
|
14
|
+
return sum + message.content.reduce((partSum, part) => partSum + (part.text?.length ?? 0) + (part.imageBase64?.length ?? 0), 0);
|
|
15
|
+
}, 0);
|
|
16
|
+
}
|
|
17
|
+
export function textFromMessages(messages) {
|
|
18
|
+
return messages.map((message) => {
|
|
19
|
+
if (typeof message.content === 'string')
|
|
20
|
+
return message.content;
|
|
21
|
+
return message.content.map((part) => part.text ?? '').join('');
|
|
22
|
+
}).join('\n');
|
|
23
|
+
}
|
|
24
|
+
export function globalEnv() {
|
|
25
|
+
const maybeProcess = globalThis;
|
|
26
|
+
return maybeProcess.process?.env ?? {};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Merges app-supplied {@link ChatRequest.providerOptions} into a provider
|
|
30
|
+
* request body: shallow at the top level, and one level deep for each key in
|
|
31
|
+
* `nestedKeys` where both sides are plain objects (Ollama `options`, Google
|
|
32
|
+
* `generationConfig`). Values in `providerOptions` win on collision.
|
|
33
|
+
*/
|
|
34
|
+
export function applyProviderOptions(body, providerOptions, nestedKeys = []) {
|
|
35
|
+
if (!providerOptions)
|
|
36
|
+
return body;
|
|
37
|
+
const merged = { ...body };
|
|
38
|
+
for (const [key, value] of Object.entries(providerOptions)) {
|
|
39
|
+
const base = asRecord(merged[key]);
|
|
40
|
+
if (nestedKeys.includes(key) && base && asRecord(value)) {
|
|
41
|
+
merged[key] = { ...base, ...value };
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
merged[key] = value;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
/** Joins a base URL and path without producing a double slash, even if `base` ends with `/`. */
|
|
50
|
+
export function joinUrl(base, path) {
|
|
51
|
+
return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Runs `fn` over `items` with at most `limit` in flight at once, preserving
|
|
55
|
+
* input order in the returned array. Used to probe many local models
|
|
56
|
+
* concurrently without opening an unbounded number of sockets.
|
|
57
|
+
*/
|
|
58
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
59
|
+
const results = new Array(items.length);
|
|
60
|
+
let next = 0;
|
|
61
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
62
|
+
const worker = async () => {
|
|
63
|
+
for (;;) {
|
|
64
|
+
const index = next;
|
|
65
|
+
next += 1;
|
|
66
|
+
if (index >= items.length)
|
|
67
|
+
return;
|
|
68
|
+
results[index] = await fn(items[index], index);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
await Promise.all(Array.from({ length: workerCount }, worker));
|
|
72
|
+
return results;
|
|
73
|
+
}
|
|
74
|
+
export function sleep(ms, signal) {
|
|
75
|
+
if (ms <= 0)
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
let settled = false;
|
|
79
|
+
const cleanup = () => signal?.removeEventListener('abort', onAbort);
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
settled = true;
|
|
82
|
+
cleanup();
|
|
83
|
+
resolve();
|
|
84
|
+
}, ms);
|
|
85
|
+
const onAbort = () => {
|
|
86
|
+
if (settled)
|
|
87
|
+
return;
|
|
88
|
+
settled = true;
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
cleanup();
|
|
91
|
+
reject(new DOMException('Sleep aborted', 'AbortError'));
|
|
92
|
+
};
|
|
93
|
+
if (signal) {
|
|
94
|
+
if (signal.aborted)
|
|
95
|
+
onAbort();
|
|
96
|
+
else
|
|
97
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Token budgets behind the {@link ReasoningEffort} tiers for providers that
|
|
103
|
+
* express effort as a thinking-token budget (Anthropic `budget_tokens`, Google
|
|
104
|
+
* `thinkingBudget`). `'none'` is 0. Deliberately conservative: they are floors
|
|
105
|
+
* a model may stop under, not ceilings it fills.
|
|
106
|
+
*/
|
|
107
|
+
export const REASONING_BUDGET_TOKENS = {
|
|
108
|
+
none: 0,
|
|
109
|
+
minimal: 1024,
|
|
110
|
+
low: 2048,
|
|
111
|
+
medium: 8192,
|
|
112
|
+
high: 16384,
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=util.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
|
-
import { postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import type { ChatMessage, ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
6
|
-
import { streamError, streamTimeout } from './base.js';
|
|
3
|
+
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
+
import type { ChatMessage, ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
+
import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
6
|
+
import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
7
7
|
|
|
8
8
|
const JSON_MODE_TOOL = 'json_output';
|
|
9
9
|
|
|
@@ -31,11 +31,22 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
31
31
|
let inputTokens: number | undefined;
|
|
32
32
|
let outputTokens: number | undefined;
|
|
33
33
|
let stopReason: string | undefined;
|
|
34
|
+
let sawTerminal = false;
|
|
34
35
|
const emittedTools: ToolCall[] = [];
|
|
35
36
|
// Partial tool_use blocks keyed by content-block index (input_json_delta accumulation).
|
|
36
37
|
const blocks = new Map<number, { id: string; name: string; raw: string }>();
|
|
37
38
|
const timeout = streamTimeout(conn, req.signal);
|
|
38
39
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
40
|
+
// Anthropic JSON mode is implemented as a forced `json_output` tool, which
|
|
41
|
+
// cannot coexist with the caller's own tools — same silent-downgrade shape
|
|
42
|
+
// as Gemini, so it gets the same signal rather than quietly dropping.
|
|
43
|
+
if (req.responseFormat?.type === 'json' && req.tools?.length) {
|
|
44
|
+
yield {
|
|
45
|
+
type: 'context',
|
|
46
|
+
kind: 'json_mode_downgraded',
|
|
47
|
+
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 },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
39
50
|
try {
|
|
40
51
|
const res = await postResponse(`${conn.baseUrl}/v1/messages`, body(req, jsonMode), conn.headers, timeout.signal, conn.provider);
|
|
41
52
|
const contentType = res.headers.get('content-type') ?? '';
|
|
@@ -45,6 +56,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
45
56
|
inputTokens = parsed.inputTokens;
|
|
46
57
|
outputTokens = parsed.outputTokens;
|
|
47
58
|
stopReason = parsed.stopReason;
|
|
59
|
+
sawTerminal = true;
|
|
48
60
|
if (jsonMode) {
|
|
49
61
|
text = jsonTextFrom(parsed.toolCalls, parsed.text);
|
|
50
62
|
if (text) yield { type: 'delta', text };
|
|
@@ -58,6 +70,7 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
58
70
|
}
|
|
59
71
|
} else {
|
|
60
72
|
for await (const line of sseLines(res)) {
|
|
73
|
+
timeout.bump();
|
|
61
74
|
const record = asRecord(safeParse(line));
|
|
62
75
|
if (!record) continue;
|
|
63
76
|
const type = asString(record.type);
|
|
@@ -81,6 +94,10 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
81
94
|
text += piece;
|
|
82
95
|
yield { type: 'delta', text: piece };
|
|
83
96
|
}
|
|
97
|
+
} else if (delta?.type === 'thinking_delta') {
|
|
98
|
+
// Extended-thinking tokens ride the reasoning channel, kept out of `text`.
|
|
99
|
+
const piece = asString(delta.thinking) ?? '';
|
|
100
|
+
if (piece) yield { type: 'reasoning', text: piece };
|
|
84
101
|
} else if (delta?.type === 'input_json_delta') {
|
|
85
102
|
const partial = blocks.get(index);
|
|
86
103
|
if (partial) partial.raw += asString(delta.partial_json) ?? '';
|
|
@@ -100,14 +117,18 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
100
117
|
yield { type: 'tool_call', call };
|
|
101
118
|
}
|
|
102
119
|
}
|
|
120
|
+
} else if (type === 'message_stop') {
|
|
121
|
+
sawTerminal = true;
|
|
103
122
|
} else if (type === 'message_delta') {
|
|
104
123
|
const delta = asRecord(record.delta);
|
|
124
|
+
if (asString(delta?.stop_reason)) sawTerminal = true;
|
|
105
125
|
stopReason = asString(delta?.stop_reason) ?? stopReason;
|
|
106
126
|
const usage = asRecord(record.usage);
|
|
107
127
|
outputTokens = asNumber(usage?.output_tokens) ?? outputTokens;
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
}
|
|
131
|
+
if (!sawTerminal) yield streamAnomaly('stream ended without a stop_reason or message_stop');
|
|
111
132
|
const hasTools = emittedTools.length > 0;
|
|
112
133
|
yield { type: 'done', result: {
|
|
113
134
|
text,
|
|
@@ -124,6 +145,25 @@ export class AnthropicAdapter implements ProviderAdapter {
|
|
|
124
145
|
timeout.done();
|
|
125
146
|
}
|
|
126
147
|
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Lists models from Anthropic's `/v1/models` endpoint (auth + version headers
|
|
151
|
+
* already applied to `conn.headers`). The endpoint does not report a context
|
|
152
|
+
* window, so `contextWindow` is left undefined rather than guessed.
|
|
153
|
+
*/
|
|
154
|
+
async listModels(conn: ResolvedConnection): Promise<ModelInfo[]> {
|
|
155
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1/models'), {
|
|
156
|
+
method: 'GET',
|
|
157
|
+
headers: conn.headers,
|
|
158
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
159
|
+
provider: conn.provider,
|
|
160
|
+
});
|
|
161
|
+
const rows = Array.isArray(asRecord(data)?.data) ? asRecord(data)!.data as unknown[] : [];
|
|
162
|
+
return rows
|
|
163
|
+
.map((row) => asString(asRecord(row)?.id))
|
|
164
|
+
.filter((id): id is string => Boolean(id))
|
|
165
|
+
.map((id) => ({ id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } }));
|
|
166
|
+
}
|
|
127
167
|
}
|
|
128
168
|
|
|
129
169
|
function body(req: ChatRequest, jsonMode: boolean): Record<string, unknown> {
|
|
@@ -149,7 +189,25 @@ function body(req: ChatRequest, jsonMode: boolean): Record<string, unknown> {
|
|
|
149
189
|
else if (req.toolChoice === 'auto') base.tool_choice = { type: 'auto' };
|
|
150
190
|
else if (req.toolChoice === 'none') base.tool_choice = { type: 'none' };
|
|
151
191
|
}
|
|
152
|
-
|
|
192
|
+
if (req.reasoningEffort !== undefined) {
|
|
193
|
+
// First-class reasoning effort → extended thinking. Anthropic requires
|
|
194
|
+
// `budget_tokens < max_tokens` and rejects temperature/top_p alongside
|
|
195
|
+
// thinking, so the budget is fitted under max_tokens (raising max_tokens
|
|
196
|
+
// when the caller's value leaves no room) and the samplers are dropped.
|
|
197
|
+
if (req.reasoningEffort === 'none') {
|
|
198
|
+
base.thinking = { type: 'disabled' };
|
|
199
|
+
} else {
|
|
200
|
+
const budget = REASONING_BUDGET_TOKENS[req.reasoningEffort];
|
|
201
|
+
const maxTokens = typeof base.max_tokens === 'number' ? base.max_tokens : 4096;
|
|
202
|
+
if (maxTokens <= budget) base.max_tokens = budget + 1024;
|
|
203
|
+
base.thinking = { type: 'enabled', budget_tokens: budget };
|
|
204
|
+
delete base.temperature;
|
|
205
|
+
delete base.top_p;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// providerOptions carries Anthropic-native fields (`thinking`, `metadata`,
|
|
209
|
+
// top-level `cache_control` extras, `service_tier`, …) without a release.
|
|
210
|
+
return applyProviderOptions(base, req.providerOptions);
|
|
153
211
|
}
|
|
154
212
|
|
|
155
213
|
function toAnthropicMessages(messages: ChatMessage[]): Record<string, unknown>[] {
|
|
@@ -173,28 +231,30 @@ function toAnthropicMessages(messages: ChatMessage[]): Record<string, unknown>[]
|
|
|
173
231
|
}
|
|
174
232
|
|
|
175
233
|
function toAnthropicMessage(m: ChatMessage): Record<string, unknown> {
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// assistant turns that carried tool calls replay them as tool_use blocks.
|
|
234
|
+
// Tool-role messages are handled by toAnthropicMessages' batching loop and
|
|
235
|
+
// never reach here, so no tool branch is needed.
|
|
236
|
+
|
|
237
|
+
// assistant turns that carried tool calls replay them as tool_use blocks,
|
|
238
|
+
// preserving any text/image content parts that accompanied the calls.
|
|
184
239
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
185
|
-
const content: unknown[] = [];
|
|
186
|
-
if (typeof m.content === 'string' && m.content) content.push({ type: 'text', text: m.content });
|
|
240
|
+
const content: unknown[] = [...contentBlocks(m.content)];
|
|
187
241
|
for (const call of m.toolCalls) content.push({ type: 'tool_use', id: call.id, name: call.name, input: call.arguments ?? {} });
|
|
188
242
|
return { role: 'assistant', content };
|
|
189
243
|
}
|
|
190
244
|
return {
|
|
191
245
|
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
192
|
-
content: typeof m.content === 'string' ? m.content : m.content
|
|
193
|
-
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
194
|
-
: { type: 'text', text: part.text ?? '' }),
|
|
246
|
+
content: typeof m.content === 'string' ? m.content : contentBlocks(m.content),
|
|
195
247
|
};
|
|
196
248
|
}
|
|
197
249
|
|
|
250
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Anthropic content blocks. */
|
|
251
|
+
function contentBlocks(content: ChatMessage['content']): unknown[] {
|
|
252
|
+
if (typeof content === 'string') return content ? [{ type: 'text', text: content }] : [];
|
|
253
|
+
return content.map((part) => part.type === 'image'
|
|
254
|
+
? { type: 'image', source: { type: 'base64', media_type: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
255
|
+
: { type: 'text', text: part.text ?? '' });
|
|
256
|
+
}
|
|
257
|
+
|
|
198
258
|
function textContent(content: ChatMessage['content']): string {
|
|
199
259
|
if (typeof content === 'string') return content;
|
|
200
260
|
if (!content) return '';
|
|
@@ -239,22 +299,3 @@ function mapStop(stop: string | undefined, hasTools: boolean): ChatResult['finis
|
|
|
239
299
|
return 'stop';
|
|
240
300
|
}
|
|
241
301
|
|
|
242
|
-
function parseArgs(raw: string): unknown {
|
|
243
|
-
try {
|
|
244
|
-
return raw ? JSON.parse(raw) as unknown : {};
|
|
245
|
-
} catch {
|
|
246
|
-
return {};
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function safeParse(line: string): unknown {
|
|
251
|
-
try {
|
|
252
|
-
return JSON.parse(line) as unknown;
|
|
253
|
-
} catch {
|
|
254
|
-
return undefined;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function randomId(): string {
|
|
259
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
260
|
-
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AIError, fromUnknown } from '../../errors.js';
|
|
2
2
|
import { fetchJson, withTimeout } from '../../transport.js';
|
|
3
3
|
import type { ModelInfo, ResolvedConnection } from '../../types.js';
|
|
4
|
-
import { asRecord, asString } from '../../util.js';
|
|
4
|
+
import { asRecord, asString, joinUrl } from '../../util.js';
|
|
5
5
|
import type { ProviderProfile } from '../profiles.js';
|
|
6
6
|
|
|
7
7
|
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
@@ -17,14 +17,18 @@ export function streamError(error: unknown, timeout: { timedOut(): boolean }, pr
|
|
|
17
17
|
return fromUnknown(error, provider);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Create a timeout/abort scope covering the full stream lifetime, plus an
|
|
22
|
+
* optional idle timeout (`conn.idleTimeoutMs`) that the engine rearms via
|
|
23
|
+
* `bump()` on each received chunk.
|
|
24
|
+
*/
|
|
21
25
|
export function streamTimeout(conn: ResolvedConnection, signal?: AbortSignal): ReturnType<typeof withTimeout> {
|
|
22
|
-
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal);
|
|
26
|
+
return withTimeout(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, signal, conn.idleTimeoutMs);
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export async function listOpenModels(conn: ResolvedConnection, profile: ProviderProfile): Promise<ModelInfo[]> {
|
|
26
30
|
if (!profile.listModelsPath) return [];
|
|
27
|
-
const { data } = await fetchJson(
|
|
31
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, profile.listModelsPath), {
|
|
28
32
|
method: 'GET',
|
|
29
33
|
headers: conn.headers,
|
|
30
34
|
timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
@@ -54,7 +58,7 @@ export async function health(conn: ResolvedConnection, profile: ProviderProfile)
|
|
|
54
58
|
const path = profile.healthPath ?? profile.listModelsPath;
|
|
55
59
|
if (!path) return { ok: true };
|
|
56
60
|
try {
|
|
57
|
-
await fetchJson(
|
|
61
|
+
await fetchJson(joinUrl(conn.baseUrl, path), {
|
|
58
62
|
method: 'GET',
|
|
59
63
|
headers: conn.headers,
|
|
60
64
|
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
@@ -66,8 +70,47 @@ export async function health(conn: ResolvedConnection, profile: ProviderProfile)
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Coerce a provider's tool-call `arguments` into an object. Providers disagree
|
|
75
|
+
* on the shape: OpenAI/Anthropic stream a JSON *string*, Google and Ollama send
|
|
76
|
+
* a native object — but Ollama-compatible backends (llama.cpp and friends)
|
|
77
|
+
* sometimes send a string too. Malformed JSON degrades to `{}` so a single bad
|
|
78
|
+
* tool call surfaces as an argument-validation error rather than a stream crash.
|
|
79
|
+
*/
|
|
80
|
+
export function parseArgs(raw: unknown): unknown {
|
|
81
|
+
if (raw === undefined || raw === null) return {};
|
|
82
|
+
if (typeof raw !== 'string') return raw;
|
|
83
|
+
if (!raw.trim()) return {};
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(raw) as unknown;
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** JSON.parse that returns undefined instead of throwing — for per-line stream frames. */
|
|
92
|
+
export function safeParse(line: string): unknown {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(line) as unknown;
|
|
95
|
+
} catch {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Stable-enough id for a tool call a provider didn't give one for. */
|
|
101
|
+
export function randomId(): string {
|
|
102
|
+
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A stream that ended without the provider's terminal marker (an OpenAI
|
|
107
|
+
* `finish_reason`, an Anthropic `message_delta.stop_reason`, a Google
|
|
108
|
+
* `finishReason`, an Ollama `done`) was almost certainly truncated. Every
|
|
109
|
+
* engine yields this so a dropped connection is diagnosable identically
|
|
110
|
+
* regardless of provider.
|
|
111
|
+
*/
|
|
112
|
+
export function streamAnomaly(reason = 'stream ended without a terminal finish reason'): { type: 'context'; kind: string; data: unknown } {
|
|
113
|
+
return { type: 'context', kind: 'stream_anomaly', data: { reason } };
|
|
71
114
|
}
|
|
72
115
|
|
|
73
116
|
/**
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { AIError } from '../../errors.js';
|
|
2
2
|
import { estimatedUsage } from '../../tokens.js';
|
|
3
3
|
import { fetchJson, postResponse, sseLines } from '../../transport.js';
|
|
4
|
-
import type { ChatMessage, ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
-
import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
|
|
6
|
-
import { DEFAULT_TIMEOUT_MS, streamError, streamTimeout } from './base.js';
|
|
4
|
+
import type { ChatMessage, ChatRequest, ChatResult, ModelInfo, ProviderAdapter, ResolvedConnection, StreamEvent, ToolCall } from '../../types.js';
|
|
5
|
+
import { applyProviderOptions, REASONING_BUDGET_TOKENS, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
|
|
6
|
+
import { DEFAULT_TIMEOUT_MS, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
|
|
7
7
|
|
|
8
8
|
export class GoogleAdapter implements ProviderAdapter {
|
|
9
9
|
readonly provider: string;
|
|
@@ -31,12 +31,23 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
31
31
|
const toolCalls: ToolCall[] = [];
|
|
32
32
|
const timeout = streamTimeout(conn, req.signal);
|
|
33
33
|
yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
|
|
34
|
+
// Gemini rejects `responseMimeType: application/json` combined with function
|
|
35
|
+
// declarations, so `body()` drops JSON mode when tools are present. Surface
|
|
36
|
+
// that downgrade instead of silently returning unstructured text.
|
|
37
|
+
if (jsonModeDowngraded(req)) {
|
|
38
|
+
yield {
|
|
39
|
+
type: 'context',
|
|
40
|
+
kind: 'json_mode_downgraded',
|
|
41
|
+
data: { reason: 'Gemini does not accept JSON response mode together with tools; the request was sent without JSON mode', provider: conn.provider },
|
|
42
|
+
};
|
|
43
|
+
}
|
|
34
44
|
try {
|
|
35
45
|
const streamUrl = `${conn.baseUrl}/v1beta/models/${encodeURIComponent(req.model)}:streamGenerateContent?alt=sse`;
|
|
36
46
|
const res = await postResponse(streamUrl, body(req), conn.headers, timeout.signal, conn.provider);
|
|
37
47
|
const contentType = res.headers.get('content-type') ?? '';
|
|
38
48
|
const chunks = contentType.includes('text/event-stream') ? sseLines(res) : singleJsonLine(res);
|
|
39
49
|
for await (const line of chunks) {
|
|
50
|
+
timeout.bump();
|
|
40
51
|
const parsed = safeParse(line);
|
|
41
52
|
const record = asRecord(parsed);
|
|
42
53
|
const promptFeedback = asRecord(record?.promptFeedback);
|
|
@@ -44,6 +55,12 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
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) firstTokenMs = Date.now() - started;
|
|
48
65
|
text += partText;
|
|
49
66
|
yield { type: 'delta', text: partText };
|
|
@@ -65,6 +82,7 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
65
82
|
inputTokens = asNumber(usage?.promptTokenCount) ?? inputTokens;
|
|
66
83
|
outputTokens = asNumber(usage?.candidatesTokenCount) ?? outputTokens;
|
|
67
84
|
}
|
|
85
|
+
if (!finish) yield streamAnomaly('stream ended without a finishReason');
|
|
68
86
|
const hasTools = toolCalls.length > 0;
|
|
69
87
|
yield { type: 'done', result: {
|
|
70
88
|
text,
|
|
@@ -90,7 +108,7 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
90
108
|
*/
|
|
91
109
|
async health(conn: ResolvedConnection): Promise<{ ok: boolean; detail?: string }> {
|
|
92
110
|
try {
|
|
93
|
-
await fetchJson(
|
|
111
|
+
await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
94
112
|
method: 'GET',
|
|
95
113
|
headers: conn.headers,
|
|
96
114
|
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 10_000),
|
|
@@ -101,6 +119,40 @@ export class GoogleAdapter implements ProviderAdapter {
|
|
|
101
119
|
return { ok: false, detail: error instanceof Error ? error.message : 'Health check failed' };
|
|
102
120
|
}
|
|
103
121
|
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Lists models from Google's `/v1beta/models`. Strips the `models/` name
|
|
125
|
+
* prefix, and maps `inputTokenLimit` → `contextWindow` and
|
|
126
|
+
* `supportedGenerationMethods` → `capabilities`.
|
|
127
|
+
*/
|
|
128
|
+
async listModels(conn: ResolvedConnection): Promise<ModelInfo[]> {
|
|
129
|
+
const { data } = await fetchJson(joinUrl(conn.baseUrl, '/v1beta/models'), {
|
|
130
|
+
method: 'GET',
|
|
131
|
+
headers: conn.headers,
|
|
132
|
+
timeoutMs: Math.min(conn.timeoutMs ?? DEFAULT_TIMEOUT_MS, 15_000),
|
|
133
|
+
provider: conn.provider,
|
|
134
|
+
});
|
|
135
|
+
const rows = Array.isArray(asRecord(data)?.models) ? asRecord(data)!.models as unknown[] : [];
|
|
136
|
+
const models: ModelInfo[] = [];
|
|
137
|
+
for (const value of rows) {
|
|
138
|
+
const row = asRecord(value);
|
|
139
|
+
const name = asString(row?.name);
|
|
140
|
+
if (!name) continue;
|
|
141
|
+
const id = name.startsWith('models/') ? name.slice('models/'.length) : name;
|
|
142
|
+
const model: ModelInfo = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
|
|
143
|
+
const contextWindow = asNumber(row?.inputTokenLimit);
|
|
144
|
+
if (contextWindow !== undefined) model.contextWindow = contextWindow;
|
|
145
|
+
const methods = row?.supportedGenerationMethods;
|
|
146
|
+
if (Array.isArray(methods) && methods.every((m) => typeof m === 'string')) model.capabilities = methods as string[];
|
|
147
|
+
models.push(model);
|
|
148
|
+
}
|
|
149
|
+
return models;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** True when the caller asked for JSON mode but tools force it off. */
|
|
154
|
+
function jsonModeDowngraded(req: ChatRequest): boolean {
|
|
155
|
+
return req.responseFormat?.type === 'json' && !!req.tools?.length;
|
|
104
156
|
}
|
|
105
157
|
|
|
106
158
|
function body(req: ChatRequest): Record<string, unknown> {
|
|
@@ -126,7 +178,17 @@ function body(req: ChatRequest): Record<string, unknown> {
|
|
|
126
178
|
if (typeof req.toolChoice === 'object') config.allowedFunctionNames = [req.toolChoice.name];
|
|
127
179
|
payload.toolConfig = { functionCallingConfig: config };
|
|
128
180
|
}
|
|
129
|
-
|
|
181
|
+
if (req.reasoningEffort !== undefined) {
|
|
182
|
+
// First-class reasoning effort → Gemini's thinking budget. 0 disables
|
|
183
|
+
// thinking on models that allow it; others treat it as a minimum.
|
|
184
|
+
(payload.generationConfig as Record<string, unknown>).thinkingConfig = {
|
|
185
|
+
thinkingBudget: REASONING_BUDGET_TOKENS[req.reasoningEffort],
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// providerOptions reaches Google-native fields: top-level `safetySettings`,
|
|
189
|
+
// `cachedContent`, and `generationConfig` extras (`thinkingConfig`,
|
|
190
|
+
// `responseModalities`, …) merged one level deep into generationConfig.
|
|
191
|
+
return applyProviderOptions(payload, req.providerOptions, ['generationConfig']);
|
|
130
192
|
}
|
|
131
193
|
|
|
132
194
|
function toGoogleContents(messages: ChatMessage[]): Record<string, unknown>[] {
|
|
@@ -150,28 +212,27 @@ function toGoogleContents(messages: ChatMessage[]): Record<string, unknown>[] {
|
|
|
150
212
|
}
|
|
151
213
|
|
|
152
214
|
function toGoogleContent(m: ChatMessage): Record<string, unknown> {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
role: 'user',
|
|
156
|
-
parts: [{ functionResponse: { name: m.name ?? 'unknown', response: toolResponseObject(m) } }],
|
|
157
|
-
};
|
|
158
|
-
}
|
|
215
|
+
// Tool-role messages are handled by toGoogleContents' batching loop and never
|
|
216
|
+
// reach here, so no tool branch is needed.
|
|
159
217
|
if (m.role === 'assistant' && m.toolCalls?.length) {
|
|
160
|
-
const parts: unknown[] = [];
|
|
161
|
-
if (typeof m.content === 'string' && m.content) parts.push({ text: m.content });
|
|
218
|
+
const parts: unknown[] = [...googleParts(m.content)];
|
|
162
219
|
for (const call of m.toolCalls) parts.push({ functionCall: { name: call.name, args: call.arguments ?? {} } });
|
|
163
220
|
return { role: 'model', parts };
|
|
164
221
|
}
|
|
165
222
|
return {
|
|
166
223
|
role: m.role === 'assistant' ? 'model' : 'user',
|
|
167
|
-
parts:
|
|
168
|
-
? [{ text: m.content }]
|
|
169
|
-
: m.content.map((part) => part.type === 'image'
|
|
170
|
-
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
171
|
-
: { text: part.text ?? '' }),
|
|
224
|
+
parts: googleParts(m.content),
|
|
172
225
|
};
|
|
173
226
|
}
|
|
174
227
|
|
|
228
|
+
/** Maps text/image content — a string or a ContentPart[] — onto Google parts. */
|
|
229
|
+
function googleParts(content: ChatMessage['content']): unknown[] {
|
|
230
|
+
if (typeof content === 'string') return content ? [{ text: content }] : [];
|
|
231
|
+
return content.map((part) => part.type === 'image'
|
|
232
|
+
? { inlineData: { mimeType: part.mimeType ?? 'image/png', data: part.imageBase64 ?? '' } }
|
|
233
|
+
: { text: part.text ?? '' });
|
|
234
|
+
}
|
|
235
|
+
|
|
175
236
|
function textContent(content: ChatMessage['content']): string {
|
|
176
237
|
if (typeof content === 'string') return content;
|
|
177
238
|
if (!content) return '';
|
|
@@ -216,14 +277,3 @@ function mapFinish(finish: string | undefined, hasTools: boolean): ChatResult['f
|
|
|
216
277
|
return 'stop';
|
|
217
278
|
}
|
|
218
279
|
|
|
219
|
-
function safeParse(line: string): unknown {
|
|
220
|
-
try {
|
|
221
|
-
return JSON.parse(line) as unknown;
|
|
222
|
-
} catch {
|
|
223
|
-
return undefined;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function randomId(): string {
|
|
228
|
-
return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
229
|
-
}
|