melchizedek-agents 0.9.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/AGENT_SETUP.md +182 -0
- package/DOCUMENTATION.md +614 -0
- package/LICENSE +21 -0
- package/QUICKSTART.md +164 -0
- package/README.md +151 -0
- package/config/agents/README.md +27 -0
- package/config/agents/examples/ares.yaml +74 -0
- package/config/agents/examples/cartographers.yaml +115 -0
- package/config/agents/examples/claude.yaml +17 -0
- package/config/agents/examples/council.yaml +65 -0
- package/config/agents/examples/critic.yaml +74 -0
- package/config/agents/examples/delegation.yaml +29 -0
- package/config/agents/examples/hierarchical.yaml +26 -0
- package/config/agents/examples/image_production.yaml +112 -0
- package/config/agents/examples/librarian.yaml +75 -0
- package/config/agents/examples/model_zoo.yaml +81 -0
- package/config/agents/examples/patient_advocate.yaml +107 -0
- package/config/agents/examples/scriptorium.yaml +96 -0
- package/config/agents/examples/style_council.yaml +83 -0
- package/config/agents/examples/syndicate.yaml +34 -0
- package/config/agents/examples/tutor.yaml +66 -0
- package/config/agents/syndicateSchema.yaml +291 -0
- package/dist/lib/config.d.ts +108 -0
- package/dist/lib/config.js +110 -0
- package/dist/lib/dispatch.d.ts +133 -0
- package/dist/lib/dispatch.js +182 -0
- package/dist/lib/grounding.d.ts +28 -0
- package/dist/lib/grounding.js +61 -0
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +13 -0
- package/dist/lib/loadEnv.d.ts +21 -0
- package/dist/lib/loadEnv.js +58 -0
- package/dist/lib/loadSyndicate.d.ts +177 -0
- package/dist/lib/loadSyndicate.js +264 -0
- package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
- package/dist/lib/memory/supabaseMemoryService.js +499 -0
- package/dist/lib/models/claudeLlm.d.ts +93 -0
- package/dist/lib/models/claudeLlm.js +332 -0
- package/dist/lib/models/gptLlm.d.ts +110 -0
- package/dist/lib/models/gptLlm.js +497 -0
- package/dist/lib/models/grokLlm.d.ts +64 -0
- package/dist/lib/models/grokLlm.js +86 -0
- package/dist/lib/models/ollamaLlm.d.ts +105 -0
- package/dist/lib/models/ollamaLlm.js +134 -0
- package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
- package/dist/lib/models/openAiCompatibleLlm.js +331 -0
- package/dist/lib/models/providerMap.d.ts +28 -0
- package/dist/lib/models/providerMap.js +45 -0
- package/dist/lib/models/registry.d.ts +71 -0
- package/dist/lib/models/registry.js +159 -0
- package/dist/lib/models/schemaNormalize.d.ts +22 -0
- package/dist/lib/models/schemaNormalize.js +58 -0
- package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
- package/dist/lib/observability/supabaseSpanExporter.js +115 -0
- package/dist/lib/observability/tracer.d.ts +44 -0
- package/dist/lib/observability/tracer.js +242 -0
- package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
- package/dist/lib/persistence/supabaseProvider.js +88 -0
- package/dist/lib/session/supabaseSessionService.d.ts +26 -0
- package/dist/lib/session/supabaseSessionService.js +193 -0
- package/dist/lib/session/transcript.d.ts +193 -0
- package/dist/lib/session/transcript.js +309 -0
- package/dist/lib/toolRegistry.d.ts +15 -0
- package/dist/lib/toolRegistry.js +58 -0
- package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
- package/dist/lib/tools/collectionsSearchTool.js +86 -0
- package/dist/lib/tools/generateImageTool.d.ts +16 -0
- package/dist/lib/tools/generateImageTool.js +119 -0
- package/dist/lib/tools/inspectImageTool.d.ts +21 -0
- package/dist/lib/tools/inspectImageTool.js +97 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
- package/dist/lib/tools/mcpToolFactory.js +116 -0
- package/dist/lib/tools/toolContract.d.ts +78 -0
- package/dist/lib/tools/toolContract.js +129 -0
- package/dist/lib/tools/webExtractTool.d.ts +70 -0
- package/dist/lib/tools/webExtractTool.js +461 -0
- package/dist/lib/tools/webSearchTool.d.ts +53 -0
- package/dist/lib/tools/webSearchTool.js +126 -0
- package/dist/lib/tools/wikiTools.d.ts +112 -0
- package/dist/lib/tools/wikiTools.js +697 -0
- package/dist/lib/tools/xSearchTool.d.ts +61 -0
- package/dist/lib/tools/xSearchTool.js +125 -0
- package/dist/lib/wiki/agentRun.d.ts +39 -0
- package/dist/lib/wiki/agentRun.js +79 -0
- package/dist/lib/wiki/builder.d.ts +71 -0
- package/dist/lib/wiki/builder.js +165 -0
- package/dist/lib/wiki/dive.d.ts +56 -0
- package/dist/lib/wiki/dive.js +208 -0
- package/dist/lib/wiki/entities.d.ts +220 -0
- package/dist/lib/wiki/entities.js +661 -0
- package/dist/lib/wiki/extract.d.ts +97 -0
- package/dist/lib/wiki/extract.js +305 -0
- package/dist/lib/wiki/fill.d.ts +37 -0
- package/dist/lib/wiki/fill.js +175 -0
- package/dist/lib/wiki/format.d.ts +132 -0
- package/dist/lib/wiki/format.js +177 -0
- package/dist/lib/wiki/graph.d.ts +80 -0
- package/dist/lib/wiki/graph.js +147 -0
- package/dist/lib/wiki/lint.d.ts +58 -0
- package/dist/lib/wiki/lint.js +267 -0
- package/dist/lib/wiki/markdown.d.ts +127 -0
- package/dist/lib/wiki/markdown.js +336 -0
- package/dist/lib/wiki/pipeline.d.ts +69 -0
- package/dist/lib/wiki/pipeline.js +119 -0
- package/dist/lib/wiki/vault.d.ts +84 -0
- package/dist/lib/wiki/vault.js +194 -0
- package/dist/scripts/a2a_server.d.ts +2 -0
- package/dist/scripts/a2a_server.js +997 -0
- package/dist/scripts/syndicate_chat.d.ts +2 -0
- package/dist/scripts/syndicate_chat.js +485 -0
- package/package.json +96 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/openAiCompatibleLlm.ts — shared base class for every provider
|
|
3
|
+
* that speaks the OpenAI chat-completions wire format.
|
|
4
|
+
*
|
|
5
|
+
* WHY this file exists:
|
|
6
|
+
* Separates the OpenAI chat-completions wire translation (ADK Content →
|
|
7
|
+
* chat messages, tool-definition building, tool_call parsing, usage
|
|
8
|
+
* accounting, reasoning surfacing) from provider specifics, so any
|
|
9
|
+
* /chat/completions-speaking provider is a small subclass supplying only
|
|
10
|
+
* the endpoint, auth headers, wire model name, and extra body fields.
|
|
11
|
+
* Current subclass: Ollama (local, keyless). xAI Grok used to live here
|
|
12
|
+
* too, but xAI's Agent Tools API is Responses-shaped, so GrokLlm now
|
|
13
|
+
* subclasses GptLlm instead (see lib/models/grokLlm.ts).
|
|
14
|
+
*
|
|
15
|
+
* WHAT THE BASE PROVIDES (uniformly, for every subclass):
|
|
16
|
+
* - Tool schemas normalized to lowercase JSON-Schema types
|
|
17
|
+
* (lib/models/schemaNormalize.ts — the ADK/Gemini dialect is uppercase).
|
|
18
|
+
* - usage → LlmResponse.usageMetadata mapping (prompt_tokens →
|
|
19
|
+
* promptTokenCount, completion_tokens → candidatesTokenCount,
|
|
20
|
+
* completion_tokens_details.reasoning_tokens → thoughtsTokenCount),
|
|
21
|
+
* so traceAgentRun / traceLlmGeneration count tokens for every provider.
|
|
22
|
+
* - Reasoning ("<think>…</think>" blocks, reasoning_content fields)
|
|
23
|
+
* surfaced as a { text, thought: true } part in a partial response —
|
|
24
|
+
* printers display it dimmed; it never enters session history.
|
|
25
|
+
* - A per-request `llm.request` OpenTelemetry span (lib/observability).
|
|
26
|
+
* - web_search handling: subclasses with native search return its body
|
|
27
|
+
* fields from webSearchBodyFields(); those without return null and the
|
|
28
|
+
* base omits the tool with a one-time warning + span attribute.
|
|
29
|
+
*/
|
|
30
|
+
import { BaseLlm } from '@google/adk';
|
|
31
|
+
import { traceLlmGeneration, setLlmSpanAttribute, } from "../observability/tracer.js";
|
|
32
|
+
import { wantsWebSearch, isWebSearchSentinel, } from "../tools/webSearchTool.js";
|
|
33
|
+
import { toLowercaseJsonSchema } from "./schemaNormalize.js";
|
|
34
|
+
/** Splits "<think>…</think>" scratchpad from the reply. */
|
|
35
|
+
export function splitThinkBlocks(text) {
|
|
36
|
+
const blocks = [];
|
|
37
|
+
const answer = text
|
|
38
|
+
.replace(/<think>([\s\S]*?)<\/think>/g, (_, inner) => {
|
|
39
|
+
blocks.push(inner.trim());
|
|
40
|
+
return '';
|
|
41
|
+
})
|
|
42
|
+
.trimStart();
|
|
43
|
+
return { reasoning: blocks.join('\n\n'), answer };
|
|
44
|
+
}
|
|
45
|
+
// ── OpenAiCompatibleLlm ──────────────────────────────────────────────────────
|
|
46
|
+
export class OpenAiCompatibleLlm extends BaseLlm {
|
|
47
|
+
webSearchWarned = false;
|
|
48
|
+
/** Model id as the wire expects it (e.g. strip the "ollama/" namespace). */
|
|
49
|
+
wireModelName() {
|
|
50
|
+
return this.model;
|
|
51
|
+
}
|
|
52
|
+
/** Provider-specific request body fields (merged last). */
|
|
53
|
+
extraBodyFields(_llmRequest) {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Body fields enabling the provider's NATIVE web search, or null when the
|
|
58
|
+
* provider has none (the base then omits the tool and warns once).
|
|
59
|
+
*/
|
|
60
|
+
webSearchBodyFields() {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Extracts reasoning from the response message. Default: "<think>" blocks
|
|
65
|
+
* in content plus the reasoning_content / reasoning fields emitted by
|
|
66
|
+
* OpenAI-compatible reasoning models.
|
|
67
|
+
*/
|
|
68
|
+
extractReasoning(message) {
|
|
69
|
+
const { reasoning: thinkReasoning, answer } = splitThinkBlocks(message.content ?? '');
|
|
70
|
+
const fieldReasoning = message.reasoning_content ?? message.reasoning ?? '';
|
|
71
|
+
return {
|
|
72
|
+
reasoning: [fieldReasoning, thinkReasoning].filter(Boolean).join('\n\n'),
|
|
73
|
+
answer,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Yielded before the HTTP call when a precondition is missing (e.g. no
|
|
77
|
+
* API key). Return undefined when ready to call. */
|
|
78
|
+
missingRequirement() {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
/** Error response for a non-2xx HTTP status. */
|
|
82
|
+
httpError(status, detail) {
|
|
83
|
+
return {
|
|
84
|
+
errorCode: `${this.providerId().toUpperCase()}_HTTP_ERROR`,
|
|
85
|
+
errorMessage: `${this.providerId()} returned ${status}: ${detail.slice(0, 400)}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Error response when the endpoint can't be reached at all. */
|
|
89
|
+
unreachable(message) {
|
|
90
|
+
return {
|
|
91
|
+
errorCode: `${this.providerId().toUpperCase()}_UNREACHABLE`,
|
|
92
|
+
errorMessage: `Could not reach ${this.endpointUrl()} (${message}).`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// ── Generation ─────────────────────────────────────────────────────────────
|
|
96
|
+
async *generateContentAsync(llmRequest, stream = false) {
|
|
97
|
+
yield* traceLlmGeneration({ provider: this.providerId(), model: this.model }, this.generateInner(llmRequest, stream));
|
|
98
|
+
}
|
|
99
|
+
async *generateInner(llmRequest, _stream) {
|
|
100
|
+
const missing = this.missingRequirement();
|
|
101
|
+
if (missing) {
|
|
102
|
+
yield missing;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const messages = this.buildMessages(llmRequest);
|
|
106
|
+
const openAiTools = this.buildTools(llmRequest);
|
|
107
|
+
const cfg = llmRequest.config ?? {};
|
|
108
|
+
const body = {
|
|
109
|
+
model: this.wireModelName(),
|
|
110
|
+
messages,
|
|
111
|
+
stream: false,
|
|
112
|
+
...(cfg.temperature !== undefined ? { temperature: cfg.temperature } : {}),
|
|
113
|
+
...(cfg.topP !== undefined ? { top_p: cfg.topP } : {}),
|
|
114
|
+
...(cfg.maxOutputTokens !== undefined
|
|
115
|
+
? { max_tokens: cfg.maxOutputTokens }
|
|
116
|
+
: {}),
|
|
117
|
+
...(openAiTools.length > 0 ? { tools: openAiTools } : {}),
|
|
118
|
+
// critic-style structured output: responseMimeType JSON → json mode
|
|
119
|
+
...(cfg.responseMimeType === 'application/json'
|
|
120
|
+
? { response_format: { type: 'json_object' } }
|
|
121
|
+
: {}),
|
|
122
|
+
...this.extraBodyFields(llmRequest),
|
|
123
|
+
};
|
|
124
|
+
// web_search: enable the provider's native search, or omit with a warning.
|
|
125
|
+
if (wantsWebSearch(llmRequest)) {
|
|
126
|
+
const searchFields = this.webSearchBodyFields();
|
|
127
|
+
if (searchFields) {
|
|
128
|
+
Object.assign(body, searchFields);
|
|
129
|
+
setLlmSpanAttribute('llm.web_search.native', true);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
setLlmSpanAttribute('llm.web_search.omitted', true);
|
|
133
|
+
if (!this.webSearchWarned) {
|
|
134
|
+
this.webSearchWarned = true;
|
|
135
|
+
console.warn(`⚠ web_search requested but ${this.model} has no native web search — ` +
|
|
136
|
+
'tool omitted (the agent runs without search).');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch(this.endpointUrl(), {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: { 'Content-Type': 'application/json', ...this.headers() },
|
|
144
|
+
body: JSON.stringify(body),
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
const detail = await res.text().catch(() => '');
|
|
148
|
+
yield this.httpError(res.status, detail);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const data = await res.json();
|
|
152
|
+
const message = data.choices?.[0]?.message ?? {};
|
|
153
|
+
const usageMetadata = mapUsage(data.usage);
|
|
154
|
+
const { reasoning, answer } = this.extractReasoning(message);
|
|
155
|
+
if (reasoning) {
|
|
156
|
+
// Scratchpad, not reply: shown by printers, kept out of history.
|
|
157
|
+
yield {
|
|
158
|
+
content: {
|
|
159
|
+
role: 'model',
|
|
160
|
+
parts: [{ text: reasoning, thought: true }],
|
|
161
|
+
},
|
|
162
|
+
partial: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const parts = [];
|
|
166
|
+
if (answer)
|
|
167
|
+
parts.push({ text: answer });
|
|
168
|
+
for (const call of message.tool_calls ?? []) {
|
|
169
|
+
let args = {};
|
|
170
|
+
try {
|
|
171
|
+
args = JSON.parse(call.function?.arguments ?? '{}');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
args = { raw: call.function?.arguments };
|
|
175
|
+
}
|
|
176
|
+
parts.push({
|
|
177
|
+
functionCall: {
|
|
178
|
+
name: call.function?.name,
|
|
179
|
+
args,
|
|
180
|
+
id: call.id,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
yield {
|
|
185
|
+
content: { role: 'model', parts },
|
|
186
|
+
turnComplete: true,
|
|
187
|
+
...(usageMetadata ? { usageMetadata } : {}),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
192
|
+
yield this.unreachable(msg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// ── Translation helpers ────────────────────────────────────────────────────
|
|
196
|
+
/** ADK Contents (Google GenAI format) → OpenAI-compatible chat messages. */
|
|
197
|
+
buildMessages(llmRequest) {
|
|
198
|
+
const systemParts = [];
|
|
199
|
+
const messages = [];
|
|
200
|
+
for (const content of llmRequest.contents) {
|
|
201
|
+
// ADK injects the system instruction as a 'system' role content.
|
|
202
|
+
if (content.role === 'system') {
|
|
203
|
+
const text = content.parts
|
|
204
|
+
?.filter((p) => p.text)
|
|
205
|
+
.map((p) => p.text)
|
|
206
|
+
.join('\n');
|
|
207
|
+
if (text)
|
|
208
|
+
systemParts.push(text);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const isModel = content.role === 'model';
|
|
212
|
+
const textChunks = [];
|
|
213
|
+
const imageParts = [];
|
|
214
|
+
const toolCalls = [];
|
|
215
|
+
for (const part of content.parts ?? []) {
|
|
216
|
+
const p = part;
|
|
217
|
+
if (p.thought) {
|
|
218
|
+
// Prior-turn scratchpad is display-only; never replay it.
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (p.text) {
|
|
222
|
+
textChunks.push(p.text);
|
|
223
|
+
}
|
|
224
|
+
else if (p.inlineData?.data) {
|
|
225
|
+
// ADK inline image → OpenAI image_url data URI (vision models)
|
|
226
|
+
const mime = p.inlineData.mimeType ?? 'image/png';
|
|
227
|
+
imageParts.push({
|
|
228
|
+
type: 'image_url',
|
|
229
|
+
image_url: { url: `data:${mime};base64,${p.inlineData.data}` },
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else if (p.functionCall) {
|
|
233
|
+
// ADK function_call → OpenAI assistant tool_calls entry
|
|
234
|
+
toolCalls.push({
|
|
235
|
+
id: p.functionCall.id ?? `call_${toolCalls.length}`,
|
|
236
|
+
type: 'function',
|
|
237
|
+
function: {
|
|
238
|
+
name: p.functionCall.name,
|
|
239
|
+
arguments: JSON.stringify(p.functionCall.args ?? {}),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
else if (p.functionResponse) {
|
|
244
|
+
// ADK function_response → OpenAI 'tool' role message
|
|
245
|
+
messages.push({
|
|
246
|
+
role: 'tool',
|
|
247
|
+
tool_call_id: p.functionResponse.id ?? '',
|
|
248
|
+
content: JSON.stringify(p.functionResponse.response ?? {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const text = textChunks.join('\n');
|
|
253
|
+
if (isModel) {
|
|
254
|
+
if (text || toolCalls.length > 0) {
|
|
255
|
+
messages.push({
|
|
256
|
+
role: 'assistant',
|
|
257
|
+
content: text || null,
|
|
258
|
+
...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
else if (text || imageParts.length > 0) {
|
|
263
|
+
messages.push({
|
|
264
|
+
role: 'user',
|
|
265
|
+
content: imageParts.length > 0
|
|
266
|
+
? [...(text ? [{ type: 'text', text }] : []), ...imageParts]
|
|
267
|
+
: text,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Also pull system instruction from generateContentConfig if present
|
|
272
|
+
const configSystem = llmRequest.config?.systemInstruction;
|
|
273
|
+
if (configSystem) {
|
|
274
|
+
const text = typeof configSystem === 'string'
|
|
275
|
+
? configSystem
|
|
276
|
+
: configSystem.parts?.map((p) => p.text).join('\n') ?? '';
|
|
277
|
+
if (text)
|
|
278
|
+
systemParts.unshift(text);
|
|
279
|
+
}
|
|
280
|
+
if (systemParts.length > 0) {
|
|
281
|
+
messages.unshift({ role: 'system', content: systemParts.join('\n\n') });
|
|
282
|
+
}
|
|
283
|
+
return messages;
|
|
284
|
+
}
|
|
285
|
+
/** ADK toolsDict → OpenAI function-tool definitions (lowercase schemas). */
|
|
286
|
+
buildTools(llmRequest) {
|
|
287
|
+
const openAiTools = [];
|
|
288
|
+
for (const [, tool] of Object.entries(llmRequest.toolsDict ?? {})) {
|
|
289
|
+
if (isWebSearchSentinel(tool))
|
|
290
|
+
continue; // handled via body fields
|
|
291
|
+
const t = tool;
|
|
292
|
+
if (t.name && t.description) {
|
|
293
|
+
openAiTools.push({
|
|
294
|
+
type: 'function',
|
|
295
|
+
function: {
|
|
296
|
+
name: t.name,
|
|
297
|
+
description: t.description,
|
|
298
|
+
parameters: toLowercaseJsonSchema(t.parameters ?? { type: 'object', properties: {} }),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return openAiTools;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Live/bidirectional streaming — not part of the OpenAI-compatible
|
|
307
|
+
* chat-completions surface. Throws to surface this clearly.
|
|
308
|
+
*/
|
|
309
|
+
async connect(_llmRequest) {
|
|
310
|
+
throw new Error(`${this.constructor.name} does not support live bidirectional connections. ` +
|
|
311
|
+
'Use a Gemini model for live/streaming sessions.');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/** OpenAI-style usage → GenAI usageMetadata (undefined when absent). */
|
|
315
|
+
export function mapUsage(usage) {
|
|
316
|
+
if (!usage || typeof usage !== 'object')
|
|
317
|
+
return undefined;
|
|
318
|
+
const reasoning = usage.completion_tokens_details?.reasoning_tokens;
|
|
319
|
+
return {
|
|
320
|
+
...(usage.prompt_tokens !== undefined
|
|
321
|
+
? { promptTokenCount: usage.prompt_tokens }
|
|
322
|
+
: {}),
|
|
323
|
+
...(usage.completion_tokens !== undefined
|
|
324
|
+
? { candidatesTokenCount: usage.completion_tokens }
|
|
325
|
+
: {}),
|
|
326
|
+
...(reasoning !== undefined ? { thoughtsTokenCount: reasoning } : {}),
|
|
327
|
+
...(usage.total_tokens !== undefined
|
|
328
|
+
? { totalTokenCount: usage.total_tokens }
|
|
329
|
+
: {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/providerMap.ts — the single source of truth mapping a model id
|
|
3
|
+
* (as declared in an agent YAML) to its provider.
|
|
4
|
+
*
|
|
5
|
+
* WHY a leaf module:
|
|
6
|
+
* Both the LLM registry (lib/models/registry.ts) and the web-search
|
|
7
|
+
* abstraction (lib/tools/webSearchTool.ts) need this mapping; keeping it
|
|
8
|
+
* dependency-free avoids an import cycle between adapters and tools.
|
|
9
|
+
*
|
|
10
|
+
* Model-name conventions (see DOCUMENTATION.md §7):
|
|
11
|
+
* claude-* → anthropic gpt-* / o<digit>* → openai
|
|
12
|
+
* grok-* → xai ollama/<model> → ollama (local, keyless)
|
|
13
|
+
* everything else → gemini (the ADK-native default)
|
|
14
|
+
*/
|
|
15
|
+
export type ProviderId = 'gemini' | 'anthropic' | 'openai' | 'xai' | 'ollama';
|
|
16
|
+
interface ProviderInfo {
|
|
17
|
+
/** Env var holding the API key; null = keyless (local). */
|
|
18
|
+
keyEnv: string | null;
|
|
19
|
+
/** Human-readable label for logs and demo banners. */
|
|
20
|
+
label: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const PROVIDERS: Record<ProviderId, ProviderInfo>;
|
|
23
|
+
/** Maps a model id to its provider. Unknown ids default to gemini — the
|
|
24
|
+
* ADK-native provider — matching the LLMRegistry's fallback behavior. */
|
|
25
|
+
export declare function providerForModel(model: string): ProviderId;
|
|
26
|
+
/** True when the provider's key is present (always true for local Ollama). */
|
|
27
|
+
export declare function providerKeyPresent(provider: ProviderId): boolean;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/providerMap.ts — the single source of truth mapping a model id
|
|
3
|
+
* (as declared in an agent YAML) to its provider.
|
|
4
|
+
*
|
|
5
|
+
* WHY a leaf module:
|
|
6
|
+
* Both the LLM registry (lib/models/registry.ts) and the web-search
|
|
7
|
+
* abstraction (lib/tools/webSearchTool.ts) need this mapping; keeping it
|
|
8
|
+
* dependency-free avoids an import cycle between adapters and tools.
|
|
9
|
+
*
|
|
10
|
+
* Model-name conventions (see DOCUMENTATION.md §7):
|
|
11
|
+
* claude-* → anthropic gpt-* / o<digit>* → openai
|
|
12
|
+
* grok-* → xai ollama/<model> → ollama (local, keyless)
|
|
13
|
+
* everything else → gemini (the ADK-native default)
|
|
14
|
+
*/
|
|
15
|
+
export const PROVIDERS = {
|
|
16
|
+
gemini: { keyEnv: 'GOOGLE_GENAI_API_KEY', label: 'Google Gemini' },
|
|
17
|
+
anthropic: { keyEnv: 'ANTHROPIC_API_KEY', label: 'Anthropic Claude' },
|
|
18
|
+
openai: { keyEnv: 'OPENAI_API_KEY', label: 'OpenAI GPT' },
|
|
19
|
+
xai: { keyEnv: 'XAI_API_KEY', label: 'xAI Grok' },
|
|
20
|
+
ollama: { keyEnv: null, label: 'Ollama (local)' },
|
|
21
|
+
};
|
|
22
|
+
/** Maps a model id to its provider. Unknown ids default to gemini — the
|
|
23
|
+
* ADK-native provider — matching the LLMRegistry's fallback behavior. */
|
|
24
|
+
export function providerForModel(model) {
|
|
25
|
+
if (/^ollama\//.test(model))
|
|
26
|
+
return 'ollama';
|
|
27
|
+
if (/^claude-/.test(model))
|
|
28
|
+
return 'anthropic';
|
|
29
|
+
if (/^gpt-/.test(model) || /^o[0-9]/.test(model))
|
|
30
|
+
return 'openai';
|
|
31
|
+
if (/^grok-/.test(model))
|
|
32
|
+
return 'xai';
|
|
33
|
+
return 'gemini';
|
|
34
|
+
}
|
|
35
|
+
/** True when the provider's key is present (always true for local Ollama). */
|
|
36
|
+
export function providerKeyPresent(provider) {
|
|
37
|
+
const keyEnv = PROVIDERS[provider].keyEnv;
|
|
38
|
+
if (!keyEnv)
|
|
39
|
+
return true;
|
|
40
|
+
// Gemini historically accepts either env name.
|
|
41
|
+
if (provider === 'gemini') {
|
|
42
|
+
return !!(process.env.GOOGLE_GENAI_API_KEY || process.env.GEMINI_API_KEY);
|
|
43
|
+
}
|
|
44
|
+
return !!process.env[keyEnv];
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/registry.ts — unified model routing. THE seam for model
|
|
3
|
+
* optionality: an agent's YAML declares `model`, and this module makes sure
|
|
4
|
+
* that string reaches the right provider adapter.
|
|
5
|
+
*
|
|
6
|
+
* TWO RESOLUTION PATHS, ONE PREFIX TABLE (lib/models/providerMap.ts):
|
|
7
|
+
*
|
|
8
|
+
* 1. registerAvailableProviders() — for entrypoints that pass `model` as a
|
|
9
|
+
* STRING to LlmAgent (scripts/syndicate_chat.ts). Registers every
|
|
10
|
+
* adapter whose credentials exist into the ADK LLMRegistry; the
|
|
11
|
+
* registry then string-matches supportedModels patterns:
|
|
12
|
+
* claude-* → ClaudeLlm gpt-* / o<digit>* → GptLlm
|
|
13
|
+
* grok-* → GrokLlm ollama/<model> → OllamaLlm
|
|
14
|
+
* gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
|
|
15
|
+
*
|
|
16
|
+
* 2. resolveModel() — instance factory for BYOK paths (scripts/
|
|
17
|
+
* a2a_server.ts), where a per-request API key from an HTTP header must
|
|
18
|
+
* be injected into the adapter. Same prefix table; the YAML model
|
|
19
|
+
* string always wins over the X-Provider header (which only selects a
|
|
20
|
+
* DEFAULT model when the YAML omits `model` — a deprecated affordance).
|
|
21
|
+
*
|
|
22
|
+
* Register BEFORE constructing any agent: the LLMRegistry caches model→class
|
|
23
|
+
* resolution (LRU), so late registration can be masked by stale cache hits.
|
|
24
|
+
*/
|
|
25
|
+
import { Gemini } from '@google/adk';
|
|
26
|
+
import type { BaseLlm, LlmRequest, LlmResponse } from '@google/adk';
|
|
27
|
+
import { PROVIDERS, providerForModel, providerKeyPresent } from './providerMap.ts';
|
|
28
|
+
import type { ProviderId } from './providerMap.ts';
|
|
29
|
+
export { providerForModel, providerKeyPresent, PROVIDERS };
|
|
30
|
+
export type { ProviderId };
|
|
31
|
+
export declare class TracedGemini extends Gemini {
|
|
32
|
+
static readonly supportedModels: Array<string | RegExp>;
|
|
33
|
+
generateContentAsync(llmRequest: LlmRequest, stream?: boolean): AsyncGenerator<LlmResponse, void>;
|
|
34
|
+
/** Tags the llm.request span (web_search = Gemini grounding), then
|
|
35
|
+
* delegates to ADK's Gemini. Runs inside the span context. */
|
|
36
|
+
private tagAndGenerate;
|
|
37
|
+
}
|
|
38
|
+
export interface ProviderStatus {
|
|
39
|
+
provider: ProviderId;
|
|
40
|
+
label: string;
|
|
41
|
+
available: boolean;
|
|
42
|
+
/** Why the provider is unavailable (e.g. which env var is missing). */
|
|
43
|
+
reason?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Availability report without side effects (used by demos and key gates). */
|
|
46
|
+
export declare function providerStatuses(): ProviderStatus[];
|
|
47
|
+
/**
|
|
48
|
+
* Registers every adapter whose credentials exist into the ADK LLMRegistry
|
|
49
|
+
* (Ollama unconditionally — local needs no key; Gemini always, since the
|
|
50
|
+
* registry needs a fallback class, and a missing Gemini key surfaces as a
|
|
51
|
+
* clear API error at call time). Returns the per-provider statuses so
|
|
52
|
+
* entrypoints can log which models are routable.
|
|
53
|
+
*/
|
|
54
|
+
export declare function registerAvailableProviders(log?: (msg: string) => void): ProviderStatus[];
|
|
55
|
+
export interface ResolveModelOptions {
|
|
56
|
+
/** Per-request API key (e.g. the A2A X-Api-Key header). */
|
|
57
|
+
apiKey?: string;
|
|
58
|
+
/**
|
|
59
|
+
* DEPRECATED — the A2A X-Provider header. Only consulted when `model` is
|
|
60
|
+
* undefined, to pick that provider's default model. A model id in the
|
|
61
|
+
* YAML always wins.
|
|
62
|
+
*/
|
|
63
|
+
defaultProvider?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolves a model id (from YAML) to a provider adapter INSTANCE, injecting
|
|
67
|
+
* a per-request apiKey where the provider accepts one. When `model` is
|
|
68
|
+
* undefined, falls back to the default model of `defaultProvider` (or
|
|
69
|
+
* Gemini).
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveModel(model: string | undefined, options?: ResolveModelOptions): BaseLlm;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/registry.ts — unified model routing. THE seam for model
|
|
3
|
+
* optionality: an agent's YAML declares `model`, and this module makes sure
|
|
4
|
+
* that string reaches the right provider adapter.
|
|
5
|
+
*
|
|
6
|
+
* TWO RESOLUTION PATHS, ONE PREFIX TABLE (lib/models/providerMap.ts):
|
|
7
|
+
*
|
|
8
|
+
* 1. registerAvailableProviders() — for entrypoints that pass `model` as a
|
|
9
|
+
* STRING to LlmAgent (scripts/syndicate_chat.ts). Registers every
|
|
10
|
+
* adapter whose credentials exist into the ADK LLMRegistry; the
|
|
11
|
+
* registry then string-matches supportedModels patterns:
|
|
12
|
+
* claude-* → ClaudeLlm gpt-* / o<digit>* → GptLlm
|
|
13
|
+
* grok-* → GrokLlm ollama/<model> → OllamaLlm
|
|
14
|
+
* gemini-* → TracedGemini (ADK's Gemini + llm.request spans)
|
|
15
|
+
*
|
|
16
|
+
* 2. resolveModel() — instance factory for BYOK paths (scripts/
|
|
17
|
+
* a2a_server.ts), where a per-request API key from an HTTP header must
|
|
18
|
+
* be injected into the adapter. Same prefix table; the YAML model
|
|
19
|
+
* string always wins over the X-Provider header (which only selects a
|
|
20
|
+
* DEFAULT model when the YAML omits `model` — a deprecated affordance).
|
|
21
|
+
*
|
|
22
|
+
* Register BEFORE constructing any agent: the LLMRegistry caches model→class
|
|
23
|
+
* resolution (LRU), so late registration can be masked by stale cache hits.
|
|
24
|
+
*/
|
|
25
|
+
import { Gemini, LLMRegistry } from '@google/adk';
|
|
26
|
+
import { DEFAULT_CLAUDE_MODEL, DEFAULT_GEMINI_MODEL, DEFAULT_GPT_MODEL, DEFAULT_GROK_MODEL, DEFAULT_OLLAMA_MODEL, } from "../config.js";
|
|
27
|
+
import { traceLlmGeneration, setLlmSpanAttribute, } from "../observability/tracer.js";
|
|
28
|
+
import { ClaudeLlm, registerClaudeLlm } from "./claudeLlm.js";
|
|
29
|
+
import { GptLlm, registerGptLlm } from "./gptLlm.js";
|
|
30
|
+
import { GrokLlm, registerGrokLlm } from "./grokLlm.js";
|
|
31
|
+
import { OllamaLlm, registerOllamaLlm } from "./ollamaLlm.js";
|
|
32
|
+
import { PROVIDERS, providerForModel, providerKeyPresent, } from "./providerMap.js";
|
|
33
|
+
export { providerForModel, providerKeyPresent, PROVIDERS };
|
|
34
|
+
// ── TracedGemini ─────────────────────────────────────────────────────────────
|
|
35
|
+
// ADK's built-in Gemini adapter, wrapped so Gemini calls emit the same
|
|
36
|
+
// per-request llm.request spans (tokens, latency) as every other provider.
|
|
37
|
+
export class TracedGemini extends Gemini {
|
|
38
|
+
// CRITICAL: reuse Gemini's exact regex instances. The LLMRegistry dict is
|
|
39
|
+
// keyed by the regex OBJECT — registering the same instances REPLACES the
|
|
40
|
+
// built-in Gemini entries instead of adding shadowed duplicates.
|
|
41
|
+
static supportedModels = Gemini.supportedModels;
|
|
42
|
+
async *generateContentAsync(llmRequest, stream) {
|
|
43
|
+
yield* traceLlmGeneration({ provider: 'gemini', model: this.model }, this.tagAndGenerate(llmRequest, stream));
|
|
44
|
+
}
|
|
45
|
+
/** Tags the llm.request span (web_search = Gemini grounding), then
|
|
46
|
+
* delegates to ADK's Gemini. Runs inside the span context. */
|
|
47
|
+
async *tagAndGenerate(llmRequest, stream) {
|
|
48
|
+
const hasGrounding = (llmRequest.config?.tools ?? []).some((t) => t && (t.googleSearch || t.googleSearchRetrieval));
|
|
49
|
+
if (hasGrounding)
|
|
50
|
+
setLlmSpanAttribute('llm.web_search.native', true);
|
|
51
|
+
yield* super.generateContentAsync(llmRequest, stream);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Availability report without side effects (used by demos and key gates). */
|
|
55
|
+
export function providerStatuses() {
|
|
56
|
+
return Object.keys(PROVIDERS).map((provider) => {
|
|
57
|
+
const available = providerKeyPresent(provider);
|
|
58
|
+
return {
|
|
59
|
+
provider,
|
|
60
|
+
label: PROVIDERS[provider].label,
|
|
61
|
+
available,
|
|
62
|
+
...(available
|
|
63
|
+
? {}
|
|
64
|
+
: { reason: `${PROVIDERS[provider].keyEnv} not set` }),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const REGISTRARS = {
|
|
69
|
+
gemini: () => LLMRegistry.register(TracedGemini),
|
|
70
|
+
anthropic: registerClaudeLlm,
|
|
71
|
+
openai: registerGptLlm,
|
|
72
|
+
xai: registerGrokLlm,
|
|
73
|
+
ollama: registerOllamaLlm,
|
|
74
|
+
};
|
|
75
|
+
let providersRegistered = false;
|
|
76
|
+
/**
|
|
77
|
+
* Registers every adapter whose credentials exist into the ADK LLMRegistry
|
|
78
|
+
* (Ollama unconditionally — local needs no key; Gemini always, since the
|
|
79
|
+
* registry needs a fallback class, and a missing Gemini key surfaces as a
|
|
80
|
+
* clear API error at call time). Returns the per-provider statuses so
|
|
81
|
+
* entrypoints can log which models are routable.
|
|
82
|
+
*/
|
|
83
|
+
export function registerAvailableProviders(log) {
|
|
84
|
+
const statuses = providerStatuses();
|
|
85
|
+
if (!providersRegistered) {
|
|
86
|
+
providersRegistered = true;
|
|
87
|
+
for (const status of statuses) {
|
|
88
|
+
if (status.available || status.provider === 'gemini') {
|
|
89
|
+
REGISTRARS[status.provider]();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (log) {
|
|
94
|
+
for (const s of statuses) {
|
|
95
|
+
log(s.available
|
|
96
|
+
? `✓ ${s.label} — active`
|
|
97
|
+
: `⚠ ${s.label} disabled (${s.reason}) — ${modelHint(s.provider)} models unavailable`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return statuses;
|
|
101
|
+
}
|
|
102
|
+
function modelHint(provider) {
|
|
103
|
+
switch (provider) {
|
|
104
|
+
case 'anthropic':
|
|
105
|
+
return 'claude-*';
|
|
106
|
+
case 'openai':
|
|
107
|
+
return 'gpt-*/o*';
|
|
108
|
+
case 'xai':
|
|
109
|
+
return 'grok-*';
|
|
110
|
+
case 'ollama':
|
|
111
|
+
return 'ollama/*';
|
|
112
|
+
case 'gemini':
|
|
113
|
+
return 'gemini-*';
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const DEFAULT_MODEL_FOR = {
|
|
117
|
+
gemini: DEFAULT_GEMINI_MODEL,
|
|
118
|
+
anthropic: DEFAULT_CLAUDE_MODEL,
|
|
119
|
+
openai: DEFAULT_GPT_MODEL,
|
|
120
|
+
xai: DEFAULT_GROK_MODEL,
|
|
121
|
+
ollama: DEFAULT_OLLAMA_MODEL,
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Resolves a model id (from YAML) to a provider adapter INSTANCE, injecting
|
|
125
|
+
* a per-request apiKey where the provider accepts one. When `model` is
|
|
126
|
+
* undefined, falls back to the default model of `defaultProvider` (or
|
|
127
|
+
* Gemini).
|
|
128
|
+
*/
|
|
129
|
+
export function resolveModel(model, options = {}) {
|
|
130
|
+
const resolved = model ??
|
|
131
|
+
DEFAULT_MODEL_FOR[normalizeProvider(options.defaultProvider)];
|
|
132
|
+
// BYOK is scoped to the CALLER'S provider: the X-API-Key a client sends
|
|
133
|
+
// authenticates that client's own provider (typically Gemini). Passing it
|
|
134
|
+
// to a different provider's adapter would override the server's env key
|
|
135
|
+
// (adapters prefer a passed key) and fail auth — e.g. a Gemini key sent to
|
|
136
|
+
// xAI. Cross-provider agents therefore resolve keys from server env.
|
|
137
|
+
const apiKey = providerForModel(model ?? DEFAULT_MODEL_FOR[normalizeProvider(options.defaultProvider)]) === normalizeProvider(options.defaultProvider)
|
|
138
|
+
? options.apiKey
|
|
139
|
+
: undefined;
|
|
140
|
+
switch (providerForModel(resolved)) {
|
|
141
|
+
case 'ollama':
|
|
142
|
+
return new OllamaLlm({ model: resolved });
|
|
143
|
+
case 'anthropic':
|
|
144
|
+
return new ClaudeLlm({ model: resolved, apiKey });
|
|
145
|
+
case 'openai':
|
|
146
|
+
return new GptLlm({ model: resolved, apiKey });
|
|
147
|
+
case 'xai':
|
|
148
|
+
return new GrokLlm({ model: resolved, apiKey });
|
|
149
|
+
case 'gemini':
|
|
150
|
+
return new TracedGemini({ model: resolved, apiKey });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function normalizeProvider(provider) {
|
|
154
|
+
const p = provider?.toLowerCase();
|
|
155
|
+
if (p === 'ollama' || p === 'anthropic' || p === 'openai' || p === 'xai') {
|
|
156
|
+
return p;
|
|
157
|
+
}
|
|
158
|
+
return 'gemini';
|
|
159
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/schemaNormalize.ts — JSON-Schema dialect bridge.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* The ADK is Gemini-native, and Gemini's Schema type spells JSON-Schema
|
|
6
|
+
* types in UPPERCASE ('OBJECT', 'STRING', …). Every other provider this
|
|
7
|
+
* framework routes to (Anthropic, OpenAI, xAI, Ollama's OpenAI-compatible
|
|
8
|
+
* endpoint) requires standard lowercase JSON-Schema types and rejects the
|
|
9
|
+
* Gemini spelling — Anthropic, for example, with:
|
|
10
|
+
* 400 invalid_request_error: tools.N.custom.input_schema.type:
|
|
11
|
+
* Input should be 'object'
|
|
12
|
+
* Tools are declared once (YAML / MCP discovery) in the Gemini dialect, so
|
|
13
|
+
* every non-Gemini adapter runs its tool schemas through this function at
|
|
14
|
+
* request-build time. See DOCUMENTATION.md §7.1.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Deep-clones a Gemini/ADK-style JSON schema, lowercasing every `type` value
|
|
18
|
+
* ('OBJECT' → 'object', ['STRING','NULL'] → ['string','null']) while leaving
|
|
19
|
+
* `description`, `enum`, `required`, `format`, and unknown keywords untouched.
|
|
20
|
+
* Never mutates the input — a Gemini agent may hold the same tool object.
|
|
21
|
+
*/
|
|
22
|
+
export declare function toLowercaseJsonSchema(schema: unknown): Record<string, unknown>;
|