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.
Files changed (111) hide show
  1. package/AGENT_SETUP.md +182 -0
  2. package/DOCUMENTATION.md +614 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +164 -0
  5. package/README.md +151 -0
  6. package/config/agents/README.md +27 -0
  7. package/config/agents/examples/ares.yaml +74 -0
  8. package/config/agents/examples/cartographers.yaml +115 -0
  9. package/config/agents/examples/claude.yaml +17 -0
  10. package/config/agents/examples/council.yaml +65 -0
  11. package/config/agents/examples/critic.yaml +74 -0
  12. package/config/agents/examples/delegation.yaml +29 -0
  13. package/config/agents/examples/hierarchical.yaml +26 -0
  14. package/config/agents/examples/image_production.yaml +112 -0
  15. package/config/agents/examples/librarian.yaml +75 -0
  16. package/config/agents/examples/model_zoo.yaml +81 -0
  17. package/config/agents/examples/patient_advocate.yaml +107 -0
  18. package/config/agents/examples/scriptorium.yaml +96 -0
  19. package/config/agents/examples/style_council.yaml +83 -0
  20. package/config/agents/examples/syndicate.yaml +34 -0
  21. package/config/agents/examples/tutor.yaml +66 -0
  22. package/config/agents/syndicateSchema.yaml +291 -0
  23. package/dist/lib/config.d.ts +108 -0
  24. package/dist/lib/config.js +110 -0
  25. package/dist/lib/dispatch.d.ts +133 -0
  26. package/dist/lib/dispatch.js +182 -0
  27. package/dist/lib/grounding.d.ts +28 -0
  28. package/dist/lib/grounding.js +61 -0
  29. package/dist/lib/index.d.ts +14 -0
  30. package/dist/lib/index.js +13 -0
  31. package/dist/lib/loadEnv.d.ts +21 -0
  32. package/dist/lib/loadEnv.js +58 -0
  33. package/dist/lib/loadSyndicate.d.ts +177 -0
  34. package/dist/lib/loadSyndicate.js +264 -0
  35. package/dist/lib/memory/supabaseMemoryService.d.ts +118 -0
  36. package/dist/lib/memory/supabaseMemoryService.js +499 -0
  37. package/dist/lib/models/claudeLlm.d.ts +93 -0
  38. package/dist/lib/models/claudeLlm.js +332 -0
  39. package/dist/lib/models/gptLlm.d.ts +110 -0
  40. package/dist/lib/models/gptLlm.js +497 -0
  41. package/dist/lib/models/grokLlm.d.ts +64 -0
  42. package/dist/lib/models/grokLlm.js +86 -0
  43. package/dist/lib/models/ollamaLlm.d.ts +105 -0
  44. package/dist/lib/models/ollamaLlm.js +134 -0
  45. package/dist/lib/models/openAiCompatibleLlm.d.ts +125 -0
  46. package/dist/lib/models/openAiCompatibleLlm.js +331 -0
  47. package/dist/lib/models/providerMap.d.ts +28 -0
  48. package/dist/lib/models/providerMap.js +45 -0
  49. package/dist/lib/models/registry.d.ts +71 -0
  50. package/dist/lib/models/registry.js +159 -0
  51. package/dist/lib/models/schemaNormalize.d.ts +22 -0
  52. package/dist/lib/models/schemaNormalize.js +58 -0
  53. package/dist/lib/observability/supabaseSpanExporter.d.ts +35 -0
  54. package/dist/lib/observability/supabaseSpanExporter.js +115 -0
  55. package/dist/lib/observability/tracer.d.ts +44 -0
  56. package/dist/lib/observability/tracer.js +242 -0
  57. package/dist/lib/persistence/supabaseProvider.d.ts +44 -0
  58. package/dist/lib/persistence/supabaseProvider.js +88 -0
  59. package/dist/lib/session/supabaseSessionService.d.ts +26 -0
  60. package/dist/lib/session/supabaseSessionService.js +193 -0
  61. package/dist/lib/session/transcript.d.ts +193 -0
  62. package/dist/lib/session/transcript.js +309 -0
  63. package/dist/lib/toolRegistry.d.ts +15 -0
  64. package/dist/lib/toolRegistry.js +58 -0
  65. package/dist/lib/tools/collectionsSearchTool.d.ts +55 -0
  66. package/dist/lib/tools/collectionsSearchTool.js +86 -0
  67. package/dist/lib/tools/generateImageTool.d.ts +16 -0
  68. package/dist/lib/tools/generateImageTool.js +119 -0
  69. package/dist/lib/tools/inspectImageTool.d.ts +21 -0
  70. package/dist/lib/tools/inspectImageTool.js +97 -0
  71. package/dist/lib/tools/mcpToolFactory.d.ts +2 -0
  72. package/dist/lib/tools/mcpToolFactory.js +116 -0
  73. package/dist/lib/tools/toolContract.d.ts +78 -0
  74. package/dist/lib/tools/toolContract.js +129 -0
  75. package/dist/lib/tools/webExtractTool.d.ts +70 -0
  76. package/dist/lib/tools/webExtractTool.js +461 -0
  77. package/dist/lib/tools/webSearchTool.d.ts +53 -0
  78. package/dist/lib/tools/webSearchTool.js +126 -0
  79. package/dist/lib/tools/wikiTools.d.ts +112 -0
  80. package/dist/lib/tools/wikiTools.js +697 -0
  81. package/dist/lib/tools/xSearchTool.d.ts +61 -0
  82. package/dist/lib/tools/xSearchTool.js +125 -0
  83. package/dist/lib/wiki/agentRun.d.ts +39 -0
  84. package/dist/lib/wiki/agentRun.js +79 -0
  85. package/dist/lib/wiki/builder.d.ts +71 -0
  86. package/dist/lib/wiki/builder.js +165 -0
  87. package/dist/lib/wiki/dive.d.ts +56 -0
  88. package/dist/lib/wiki/dive.js +208 -0
  89. package/dist/lib/wiki/entities.d.ts +220 -0
  90. package/dist/lib/wiki/entities.js +661 -0
  91. package/dist/lib/wiki/extract.d.ts +97 -0
  92. package/dist/lib/wiki/extract.js +305 -0
  93. package/dist/lib/wiki/fill.d.ts +37 -0
  94. package/dist/lib/wiki/fill.js +175 -0
  95. package/dist/lib/wiki/format.d.ts +132 -0
  96. package/dist/lib/wiki/format.js +177 -0
  97. package/dist/lib/wiki/graph.d.ts +80 -0
  98. package/dist/lib/wiki/graph.js +147 -0
  99. package/dist/lib/wiki/lint.d.ts +58 -0
  100. package/dist/lib/wiki/lint.js +267 -0
  101. package/dist/lib/wiki/markdown.d.ts +127 -0
  102. package/dist/lib/wiki/markdown.js +336 -0
  103. package/dist/lib/wiki/pipeline.d.ts +69 -0
  104. package/dist/lib/wiki/pipeline.js +119 -0
  105. package/dist/lib/wiki/vault.d.ts +84 -0
  106. package/dist/lib/wiki/vault.js +194 -0
  107. package/dist/scripts/a2a_server.d.ts +2 -0
  108. package/dist/scripts/a2a_server.js +997 -0
  109. package/dist/scripts/syndicate_chat.d.ts +2 -0
  110. package/dist/scripts/syndicate_chat.js +485 -0
  111. 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>;