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,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/gptLlm.ts — OpenAI GPT provider for the ADK LLMRegistry.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Model optionality: any agent YAML with model: "gpt-*" (or an o-series
|
|
6
|
+
* id like "o4-mini") routes here after registration. Unlike Grok/Ollama
|
|
7
|
+
* this adapter does NOT reuse the chat-completions base class — it speaks
|
|
8
|
+
* OpenAI's Responses API through the official `openai` SDK, because the
|
|
9
|
+
* Responses API is where OpenAI exposes reasoning summaries (the model's
|
|
10
|
+
* thinking, surfaced as { thought: true } parts here) and the first-class
|
|
11
|
+
* `web_search` tool this framework's web_search abstraction maps to.
|
|
12
|
+
*
|
|
13
|
+
* HOW TO ENABLE:
|
|
14
|
+
* 1. Install the SDK (already in package.json): npm install
|
|
15
|
+
* 2. Add your API key to .env: OPENAI_API_KEY=sk-...
|
|
16
|
+
* 3. Set model: "gpt-5-mini" (or any gpt-* / o-series id) in your YAML.
|
|
17
|
+
* registerAvailableProviders() registers this adapter when the key is set.
|
|
18
|
+
*
|
|
19
|
+
* CONTENT FORMAT TRANSLATION:
|
|
20
|
+
* ADK Content/Part objects → Responses API `input` items:
|
|
21
|
+
* user/model text → { role, content } message items
|
|
22
|
+
* functionCall part → { type:'function_call', call_id, name, arguments }
|
|
23
|
+
* functionResponse part → { type:'function_call_output', call_id, output }
|
|
24
|
+
* The call_id round-trip matters: this adapter emits functionCall.id so
|
|
25
|
+
* ADK echoes it back on the tool response, and the Responses API requires
|
|
26
|
+
* function_call_output.call_id to match the originating call.
|
|
27
|
+
*
|
|
28
|
+
* Response `output` items map back:
|
|
29
|
+
* 'reasoning' summary → { text, thought: true } partial (display-only)
|
|
30
|
+
* 'message' output_text → text part
|
|
31
|
+
* 'function_call' → functionCall part
|
|
32
|
+
* usage.input_tokens / output_tokens / output_tokens_details.reasoning_tokens
|
|
33
|
+
* → LlmResponse.usageMetadata, so token telemetry works like every provider.
|
|
34
|
+
*
|
|
35
|
+
* STREAMING (ADK stream=true, i.e. RunConfig streamingMode: SSE):
|
|
36
|
+
* The request is sent with { stream: true }; SSE deltas
|
|
37
|
+
* (response.output_text.delta / response.reasoning_summary_text.delta)
|
|
38
|
+
* are yielded as { partial: true } responses — the ADK Runner displays
|
|
39
|
+
* but does NOT persist partial events — and the terminal
|
|
40
|
+
* response.completed payload is mapped through the same final-response
|
|
41
|
+
* translator as the non-streaming path, carrying full content + usage
|
|
42
|
+
* as the ONE event that lands in the session. Function calls are never
|
|
43
|
+
* delta-streamed (both OpenAI and xAI deliver them whole).
|
|
44
|
+
*/
|
|
45
|
+
import { BaseLlm, LLMRegistry } from '@google/adk';
|
|
46
|
+
import { traceLlmGeneration, setLlmSpanAttribute, } from "../observability/tracer.js";
|
|
47
|
+
import { wantsWebSearch, isWebSearchSentinel, xaiWebSearchParamsFromEnv, } from "../tools/webSearchTool.js";
|
|
48
|
+
import { wantsXSearch, isXSearchSentinel, xSearchParamsFromEnv, } from "../tools/xSearchTool.js";
|
|
49
|
+
import { wantsCollectionsSearch, isCollectionsSearchSentinel, collectionIdsFromEnv, collectionsMaxResultsFromEnv, } from "../tools/collectionsSearchTool.js";
|
|
50
|
+
import { providerForModel } from "./providerMap.js";
|
|
51
|
+
import { toLowercaseJsonSchema } from "./schemaNormalize.js";
|
|
52
|
+
/** Reasoning-capable ids: o-series and the gpt-5 family. The reasoning
|
|
53
|
+
* param is also dropped and retried once on a 400, so a miss here only
|
|
54
|
+
* costs one extra round trip. */
|
|
55
|
+
function isReasoningModel(model) {
|
|
56
|
+
return /^o[0-9]/.test(model) || /^gpt-5/.test(model);
|
|
57
|
+
}
|
|
58
|
+
// ── Request building (exported for offline tests) ────────────────────────────
|
|
59
|
+
/** ADK Contents → Responses API `input` items + `instructions` string. */
|
|
60
|
+
export function buildResponsesInput(llmRequest) {
|
|
61
|
+
const systemParts = [];
|
|
62
|
+
const input = [];
|
|
63
|
+
for (const content of llmRequest.contents) {
|
|
64
|
+
if (content.role === 'system') {
|
|
65
|
+
const text = content.parts
|
|
66
|
+
?.filter((p) => p.text)
|
|
67
|
+
.map((p) => p.text)
|
|
68
|
+
.join('\n');
|
|
69
|
+
if (text)
|
|
70
|
+
systemParts.push(text);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const role = content.role === 'model' ? 'assistant' : 'user';
|
|
74
|
+
const contentParts = [];
|
|
75
|
+
for (const part of content.parts ?? []) {
|
|
76
|
+
const p = part;
|
|
77
|
+
if (p.thought) {
|
|
78
|
+
// Prior-turn scratchpad is display-only; never replay it.
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (p.text) {
|
|
82
|
+
contentParts.push({
|
|
83
|
+
type: role === 'assistant' ? 'output_text' : 'input_text',
|
|
84
|
+
text: p.text,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else if (p.inlineData?.data && role === 'user') {
|
|
88
|
+
const mime = p.inlineData.mimeType ?? 'image/png';
|
|
89
|
+
contentParts.push({
|
|
90
|
+
type: 'input_image',
|
|
91
|
+
image_url: `data:${mime};base64,${p.inlineData.data}`,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
else if (p.functionCall) {
|
|
95
|
+
input.push({
|
|
96
|
+
type: 'function_call',
|
|
97
|
+
call_id: p.functionCall.id ?? `call_${input.length}`,
|
|
98
|
+
name: p.functionCall.name,
|
|
99
|
+
arguments: JSON.stringify(p.functionCall.args ?? {}),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
else if (p.functionResponse) {
|
|
103
|
+
input.push({
|
|
104
|
+
type: 'function_call_output',
|
|
105
|
+
call_id: p.functionResponse.id ?? '',
|
|
106
|
+
output: JSON.stringify(p.functionResponse.response ?? {}),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (contentParts.length > 0) {
|
|
111
|
+
input.push({ role, content: contentParts });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const configSystem = llmRequest.config?.systemInstruction;
|
|
115
|
+
if (configSystem) {
|
|
116
|
+
const text = typeof configSystem === 'string'
|
|
117
|
+
? configSystem
|
|
118
|
+
: configSystem.parts?.map((p) => p.text).join('\n') ?? '';
|
|
119
|
+
if (text)
|
|
120
|
+
systemParts.unshift(text);
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
instructions: systemParts.length > 0 ? systemParts.join('\n\n') : undefined,
|
|
124
|
+
input,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** ADK toolsDict (+ web_search sentinel) → Responses API tool definitions. */
|
|
128
|
+
export function buildResponsesTools(llmRequest) {
|
|
129
|
+
const tools = [];
|
|
130
|
+
for (const [, tool] of Object.entries(llmRequest.toolsDict ?? {})) {
|
|
131
|
+
if (isWebSearchSentinel(tool))
|
|
132
|
+
continue; // added as a native tool below
|
|
133
|
+
if (isXSearchSentinel(tool))
|
|
134
|
+
continue; // added as a native tool below
|
|
135
|
+
if (isCollectionsSearchSentinel(tool))
|
|
136
|
+
continue; // added as a native tool below
|
|
137
|
+
const t = tool;
|
|
138
|
+
if (t.name && t.description) {
|
|
139
|
+
tools.push({
|
|
140
|
+
type: 'function',
|
|
141
|
+
name: t.name,
|
|
142
|
+
description: t.description,
|
|
143
|
+
parameters: toLowercaseJsonSchema(t.parameters ?? { type: 'object', properties: {} }),
|
|
144
|
+
strict: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (wantsWebSearch(llmRequest)) {
|
|
149
|
+
// OpenAI-native, runs server-side. On the xAI path only, optional
|
|
150
|
+
// domain filters ride the tool object (XAI_WEB_SEARCH_* env vars —
|
|
151
|
+
// deployment config, the XAI_COLLECTION_IDS doctrine); xAI's web_search
|
|
152
|
+
// accepts no date bounds (docs.x.ai, 2026-08-08 — from_date/to_date are
|
|
153
|
+
// x_search-only). OpenAI's web_search takes no params and stays bare.
|
|
154
|
+
const xaiParams = llmRequest.model && providerForModel(llmRequest.model) === 'xai'
|
|
155
|
+
? xaiWebSearchParamsFromEnv()
|
|
156
|
+
: {};
|
|
157
|
+
tools.push({ type: 'web_search', ...xaiParams });
|
|
158
|
+
}
|
|
159
|
+
if (wantsXSearch(llmRequest)) {
|
|
160
|
+
// xAI Agent Tools only (sentinel is xai-gated). Optional server-side
|
|
161
|
+
// constraints (date bounds, handle lists) are deployment config from
|
|
162
|
+
// XAI_X_SEARCH_* env vars — the XAI_COLLECTION_IDS doctrine; with none
|
|
163
|
+
// set this is the bare tool it always was.
|
|
164
|
+
tools.push({ type: 'x_search', ...xSearchParamsFromEnv() });
|
|
165
|
+
}
|
|
166
|
+
if (wantsCollectionsSearch(llmRequest)) {
|
|
167
|
+
// xAI Collections ride the OpenAI-compatible `file_search` wire shape;
|
|
168
|
+
// ids are deployment config (XAI_COLLECTION_IDS), never YAML.
|
|
169
|
+
const ids = collectionIdsFromEnv();
|
|
170
|
+
if (ids.length > 0) {
|
|
171
|
+
const max = collectionsMaxResultsFromEnv();
|
|
172
|
+
tools.push({
|
|
173
|
+
type: 'file_search',
|
|
174
|
+
vector_store_ids: ids,
|
|
175
|
+
...(max !== undefined ? { max_num_results: max } : {}),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.warn('[collections_search] declared but XAI_COLLECTION_IDS is empty — tool omitted for this request.');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return tools;
|
|
183
|
+
}
|
|
184
|
+
// ── Stream-event mapping (exported for offline tests) ────────────────────────
|
|
185
|
+
/** Maps one Responses-API SSE event to a displayable delta, or null for
|
|
186
|
+
* event types that carry none. Function calls are NOT delta-streamed —
|
|
187
|
+
* both OpenAI and xAI deliver them whole in the final response. */
|
|
188
|
+
export function streamEventDelta(ev) {
|
|
189
|
+
if (!ev || typeof ev !== 'object')
|
|
190
|
+
return null;
|
|
191
|
+
if (ev.type === 'response.output_text.delta' && typeof ev.delta === 'string') {
|
|
192
|
+
return { thought: false, text: ev.delta };
|
|
193
|
+
}
|
|
194
|
+
if (ev.type === 'response.reasoning_summary_text.delta' &&
|
|
195
|
+
typeof ev.delta === 'string') {
|
|
196
|
+
return { thought: true, text: ev.delta };
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// ── GptLlm ───────────────────────────────────────────────────────────────────
|
|
201
|
+
export class GptLlm extends BaseLlm {
|
|
202
|
+
/** gpt-* and o-series ids route here after registration. */
|
|
203
|
+
static supportedModels = [
|
|
204
|
+
/^gpt-.+/,
|
|
205
|
+
/^o[0-9].*/,
|
|
206
|
+
];
|
|
207
|
+
apiKey;
|
|
208
|
+
constructor({ model, apiKey }) {
|
|
209
|
+
super({ model });
|
|
210
|
+
this.apiKey = apiKey;
|
|
211
|
+
}
|
|
212
|
+
// ── Provider hooks ─────────────────────────────────────────────────────────
|
|
213
|
+
// The Responses API surface is spoken by more than one vendor: xAI's Agent
|
|
214
|
+
// Tools API (lib/models/grokLlm.ts) is wire-compatible, so GrokLlm
|
|
215
|
+
// subclasses this adapter and overrides only these hooks.
|
|
216
|
+
/** Provider id for telemetry and error codes. */
|
|
217
|
+
providerId() {
|
|
218
|
+
return 'openai';
|
|
219
|
+
}
|
|
220
|
+
/** SDK baseURL override; undefined = api.openai.com. */
|
|
221
|
+
baseURL() {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
apiKeyFromEnv() {
|
|
225
|
+
return process.env.OPENAI_API_KEY;
|
|
226
|
+
}
|
|
227
|
+
missingKeyMessage() {
|
|
228
|
+
return 'OPENAI_API_KEY is not set in environment.';
|
|
229
|
+
}
|
|
230
|
+
/** Responses API `reasoning` request param, or undefined to omit it.
|
|
231
|
+
* Base: reasoning summaries for OpenAI's reasoning-capable ids.
|
|
232
|
+
* Subclasses override per vendor (GrokLlm pins grok-4.5 to a reasoning
|
|
233
|
+
* effort). A 400 from a model that rejects the param is retried once
|
|
234
|
+
* without it — see createWithRetry. */
|
|
235
|
+
reasoningParam() {
|
|
236
|
+
return isReasoningModel(this.model) ? { summary: 'auto' } : undefined;
|
|
237
|
+
}
|
|
238
|
+
/** Extra options for the OpenAI SDK client constructor. Subclasses
|
|
239
|
+
* override per vendor (GrokLlm sets a long request timeout, per xAI's
|
|
240
|
+
* streaming guidance for reasoning models). */
|
|
241
|
+
clientOptions() {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
// ── Generation ─────────────────────────────────────────────────────────────
|
|
245
|
+
async *generateContentAsync(llmRequest, stream = false) {
|
|
246
|
+
yield* traceLlmGeneration({ provider: this.providerId(), model: this.model }, this.generateInner(llmRequest, stream));
|
|
247
|
+
}
|
|
248
|
+
async *generateInner(llmRequest, stream) {
|
|
249
|
+
const apiKey = this.apiKey || this.apiKeyFromEnv();
|
|
250
|
+
if (!apiKey) {
|
|
251
|
+
yield {
|
|
252
|
+
errorCode: 'MISSING_API_KEY',
|
|
253
|
+
errorMessage: this.missingKeyMessage(),
|
|
254
|
+
};
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Dynamic import — mirrors claudeLlm.ts, so the framework boots without
|
|
258
|
+
// the openai SDK installed for users of other providers.
|
|
259
|
+
let OpenAI;
|
|
260
|
+
try {
|
|
261
|
+
const mod = await import('openai');
|
|
262
|
+
OpenAI = mod.default ?? mod.OpenAI;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
yield {
|
|
266
|
+
errorCode: 'SDK_NOT_INSTALLED',
|
|
267
|
+
errorMessage: 'The openai package is not installed. Run: npm install openai',
|
|
268
|
+
};
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const client = new OpenAI({
|
|
272
|
+
apiKey,
|
|
273
|
+
...(this.baseURL() ? { baseURL: this.baseURL() } : {}),
|
|
274
|
+
...this.clientOptions(),
|
|
275
|
+
});
|
|
276
|
+
const { instructions, input } = buildResponsesInput(llmRequest);
|
|
277
|
+
const tools = buildResponsesTools(llmRequest);
|
|
278
|
+
if (wantsWebSearch(llmRequest)) {
|
|
279
|
+
setLlmSpanAttribute('llm.web_search.native', true);
|
|
280
|
+
}
|
|
281
|
+
if (wantsCollectionsSearch(llmRequest)) {
|
|
282
|
+
setLlmSpanAttribute(collectionIdsFromEnv().length > 0
|
|
283
|
+
? 'llm.collections_search.native'
|
|
284
|
+
: 'llm.collections_search.omitted', true);
|
|
285
|
+
}
|
|
286
|
+
const cfg = llmRequest.config ?? {};
|
|
287
|
+
const reasoning = this.reasoningParam();
|
|
288
|
+
const request = {
|
|
289
|
+
model: this.model,
|
|
290
|
+
input,
|
|
291
|
+
...(instructions ? { instructions } : {}),
|
|
292
|
+
...(tools.length > 0 ? { tools } : {}),
|
|
293
|
+
...(cfg.maxOutputTokens !== undefined
|
|
294
|
+
? { max_output_tokens: cfg.maxOutputTokens }
|
|
295
|
+
: {}),
|
|
296
|
+
...(cfg.temperature !== undefined && !isReasoningModel(this.model)
|
|
297
|
+
? { temperature: cfg.temperature }
|
|
298
|
+
: {}),
|
|
299
|
+
// Structured output: outputSchema wins over bare JSON mode.
|
|
300
|
+
...(cfg.responseSchema
|
|
301
|
+
? {
|
|
302
|
+
text: {
|
|
303
|
+
format: {
|
|
304
|
+
type: 'json_schema',
|
|
305
|
+
name: 'response',
|
|
306
|
+
schema: toLowercaseJsonSchema(cfg.responseSchema),
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
: cfg.responseMimeType === 'application/json'
|
|
311
|
+
? { text: { format: { type: 'json_object' } } }
|
|
312
|
+
: {}),
|
|
313
|
+
// Reasoning param — summaries and/or vendor effort control (see
|
|
314
|
+
// reasoningParam hook; provider subclasses shape it).
|
|
315
|
+
...(reasoning ? { reasoning } : {}),
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
if (stream) {
|
|
319
|
+
yield* this.streamResponses(client, request);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const response = await this.createWithRetry(client, request);
|
|
323
|
+
yield* this.mapFinalResponse(response);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
327
|
+
yield {
|
|
328
|
+
errorCode: `${this.providerId().toUpperCase()}_ERROR`,
|
|
329
|
+
errorMessage: msg,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/** responses.create with the guarded reasoning retry: if the model
|
|
334
|
+
* rejects the reasoning param with a 400 (a non-reasoning model matched
|
|
335
|
+
* the pattern), drop it and try once more. */
|
|
336
|
+
async createWithRetry(client, request) {
|
|
337
|
+
try {
|
|
338
|
+
return await client.responses.create(request);
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
if (err?.status === 400 && request.reasoning) {
|
|
342
|
+
delete request.reasoning;
|
|
343
|
+
return await client.responses.create(request);
|
|
344
|
+
}
|
|
345
|
+
throw err;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Final (non-delta) Response → LlmResponse yields: reasoning summaries
|
|
349
|
+
* as a display-only thought part, then text/functionCall parts with
|
|
350
|
+
* usage. Shared by the non-streaming path and the stream finalizer
|
|
351
|
+
* (which sets skipThoughts — its summaries already streamed as deltas). */
|
|
352
|
+
*mapFinalResponse(response, opts = {}) {
|
|
353
|
+
if (!opts.skipThoughts) {
|
|
354
|
+
const reasoningTexts = [];
|
|
355
|
+
for (const item of response.output ?? []) {
|
|
356
|
+
if (item.type === 'reasoning') {
|
|
357
|
+
for (const s of item.summary ?? []) {
|
|
358
|
+
if (s?.text)
|
|
359
|
+
reasoningTexts.push(s.text);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (reasoningTexts.length > 0) {
|
|
364
|
+
yield {
|
|
365
|
+
content: {
|
|
366
|
+
role: 'model',
|
|
367
|
+
parts: [{ text: reasoningTexts.join('\n\n'), thought: true }],
|
|
368
|
+
},
|
|
369
|
+
partial: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const parts = [];
|
|
374
|
+
for (const item of response.output ?? []) {
|
|
375
|
+
if (item.type === 'message') {
|
|
376
|
+
for (const c of item.content ?? []) {
|
|
377
|
+
if (c.type === 'output_text' && c.text)
|
|
378
|
+
parts.push({ text: c.text });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
else if (item.type === 'function_call') {
|
|
382
|
+
let args = {};
|
|
383
|
+
try {
|
|
384
|
+
args = JSON.parse(item.arguments ?? '{}');
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
args = { raw: item.arguments };
|
|
388
|
+
}
|
|
389
|
+
parts.push({
|
|
390
|
+
functionCall: { name: item.name, args, id: item.call_id },
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const usage = response.usage;
|
|
395
|
+
yield {
|
|
396
|
+
content: { role: 'model', parts },
|
|
397
|
+
turnComplete: true,
|
|
398
|
+
...(usage
|
|
399
|
+
? {
|
|
400
|
+
usageMetadata: {
|
|
401
|
+
...(usage.input_tokens !== undefined
|
|
402
|
+
? { promptTokenCount: usage.input_tokens }
|
|
403
|
+
: {}),
|
|
404
|
+
...(usage.output_tokens !== undefined
|
|
405
|
+
? { candidatesTokenCount: usage.output_tokens }
|
|
406
|
+
: {}),
|
|
407
|
+
...(usage.output_tokens_details?.reasoning_tokens !== undefined
|
|
408
|
+
? {
|
|
409
|
+
thoughtsTokenCount: usage.output_tokens_details.reasoning_tokens,
|
|
410
|
+
}
|
|
411
|
+
: {}),
|
|
412
|
+
...(usage.total_tokens !== undefined
|
|
413
|
+
? { totalTokenCount: usage.total_tokens }
|
|
414
|
+
: {}),
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
: {}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/** SSE streaming: yield displayable DELTAS as { partial: true } responses
|
|
421
|
+
* (the ADK Runner shows but never persists partials), then map the
|
|
422
|
+
* terminal response.completed payload through mapFinalResponse — full
|
|
423
|
+
* content + usage, the ONE event that lands in the session. Usage rides
|
|
424
|
+
* only the final response so the tracer never double-counts tokens. */
|
|
425
|
+
async *streamResponses(client, request) {
|
|
426
|
+
const events = await this.createWithRetry(client, {
|
|
427
|
+
...request,
|
|
428
|
+
stream: true,
|
|
429
|
+
});
|
|
430
|
+
let finalResponse;
|
|
431
|
+
let streamedThoughts = false;
|
|
432
|
+
const textBuf = [];
|
|
433
|
+
const thoughtBuf = [];
|
|
434
|
+
for await (const ev of events) {
|
|
435
|
+
const delta = streamEventDelta(ev);
|
|
436
|
+
if (delta) {
|
|
437
|
+
if (delta.thought) {
|
|
438
|
+
streamedThoughts = true;
|
|
439
|
+
thoughtBuf.push(delta.text);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
textBuf.push(delta.text);
|
|
443
|
+
}
|
|
444
|
+
yield {
|
|
445
|
+
content: {
|
|
446
|
+
role: 'model',
|
|
447
|
+
parts: [
|
|
448
|
+
delta.thought
|
|
449
|
+
? { text: delta.text, thought: true }
|
|
450
|
+
: { text: delta.text },
|
|
451
|
+
],
|
|
452
|
+
},
|
|
453
|
+
partial: true,
|
|
454
|
+
};
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (ev?.type === 'response.completed' && ev.response) {
|
|
458
|
+
finalResponse = ev.response;
|
|
459
|
+
}
|
|
460
|
+
else if (ev?.type === 'response.failed' || ev?.type === 'error') {
|
|
461
|
+
const msg = ev?.response?.error?.message ?? ev?.message ?? 'response stream failed';
|
|
462
|
+
yield {
|
|
463
|
+
errorCode: `${this.providerId().toUpperCase()}_STREAM_ERROR`,
|
|
464
|
+
errorMessage: String(msg),
|
|
465
|
+
};
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (finalResponse) {
|
|
470
|
+
yield* this.mapFinalResponse(finalResponse, {
|
|
471
|
+
skipThoughts: streamedThoughts,
|
|
472
|
+
});
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
// Defensive: the stream ended without response.completed — aggregate
|
|
476
|
+
// the buffered deltas so the turn still persists a complete event.
|
|
477
|
+
const parts = [];
|
|
478
|
+
if (thoughtBuf.length > 0)
|
|
479
|
+
parts.push({ text: thoughtBuf.join(''), thought: true });
|
|
480
|
+
if (textBuf.length > 0)
|
|
481
|
+
parts.push({ text: textBuf.join('') });
|
|
482
|
+
yield { content: { role: 'model', parts }, turnComplete: true };
|
|
483
|
+
}
|
|
484
|
+
/** Live/bidirectional streaming is not wired for this adapter. */
|
|
485
|
+
async connect(_llmRequest) {
|
|
486
|
+
throw new Error('GptLlm does not support live bidirectional connections. ' +
|
|
487
|
+
'Use a Gemini model for live/streaming sessions.');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
// ── Registration helper ───────────────────────────────────────────────────────
|
|
491
|
+
/**
|
|
492
|
+
* Registers GptLlm with the ADK LLMRegistry. Called by
|
|
493
|
+
* registerAvailableProviders() when OPENAI_API_KEY is present.
|
|
494
|
+
*/
|
|
495
|
+
export function registerGptLlm() {
|
|
496
|
+
LLMRegistry.register(GptLlm);
|
|
497
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/grokLlm.ts — xAI Grok provider for the ADK LLMRegistry.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Model optionality is a primary driver of this framework: the agent YAML
|
|
6
|
+
* declares `model`, and the registry routes it to the right provider.
|
|
7
|
+
*
|
|
8
|
+
* WHY it subclasses GptLlm (Responses API) and not the chat-completions base:
|
|
9
|
+
* xAI retired Live Search on chat completions (the API now returns
|
|
10
|
+
* 410 "Live search is deprecated. Please switch to the Agent Tools API").
|
|
11
|
+
* The Agent Tools API lives at `https://api.x.ai/v1/responses` and is
|
|
12
|
+
* wire-compatible with OpenAI's Responses API — same `input` items, same
|
|
13
|
+
* `{ type: 'function' }` / `{ type: 'web_search' }` tools, same
|
|
14
|
+
* `reasoning` summary / `message` / `function_call` output items (verified
|
|
15
|
+
* live 2026-07-19). So Grok reuses the GptLlm translator via the openai
|
|
16
|
+
* SDK's baseURL override, and gets for free:
|
|
17
|
+
* - native web_search (server-side, with source citations)
|
|
18
|
+
* - x_search (live X posts) and collections_search (hosted document
|
|
19
|
+
* stores via the file_search wire shape — see collectionsSearchTool.ts)
|
|
20
|
+
* - reasoning summaries surfaced as { thought: true } THINKING output
|
|
21
|
+
* - SSE streaming (ADK RunConfig streamingMode: SSE → partial deltas)
|
|
22
|
+
* - structured outputs (YAML outputSchema → text.format json_schema)
|
|
23
|
+
* - function tools with call_id round-tripping, lowercased schemas
|
|
24
|
+
* - usage → usageMetadata mapping for llm.request token spans
|
|
25
|
+
*
|
|
26
|
+
* HOW TO ENABLE:
|
|
27
|
+
* 1. Add your API key to .env: XAI_API_KEY=xai-... (console.x.ai — paid)
|
|
28
|
+
* 2. Set model: "grok-4.5" (or any grok-* id) in your YAML.
|
|
29
|
+
* registerAvailableProviders() registers this adapter when the key is set.
|
|
30
|
+
* grok-4.5 requests carry reasoning effort 'medium' by default — see
|
|
31
|
+
* reasoningParam() below and DEFAULT_GROK_REASONING_EFFORT (lib/config.ts).
|
|
32
|
+
*
|
|
33
|
+
* API-DRIFT NOTE: every xAI-specific choice (endpoint, key env) is confined
|
|
34
|
+
* to the overrides below — upstream drift stays a one-file fix. Note xAI's
|
|
35
|
+
* Responses endpoint reports the serving backend in the response `model`
|
|
36
|
+
* field (e.g. "grok-4.3"), which may differ from the requested id; invalid
|
|
37
|
+
* ids are properly rejected with "Model not found".
|
|
38
|
+
*/
|
|
39
|
+
import { GptLlm } from './gptLlm.ts';
|
|
40
|
+
export declare class GrokLlm extends GptLlm {
|
|
41
|
+
/** Any model: "grok-*" in a YAML config routes here after registration. */
|
|
42
|
+
static readonly supportedModels: Array<string | RegExp>;
|
|
43
|
+
protected providerId(): string;
|
|
44
|
+
protected baseURL(): string;
|
|
45
|
+
protected apiKeyFromEnv(): string | undefined;
|
|
46
|
+
protected missingKeyMessage(): string;
|
|
47
|
+
/** xAI's streaming docs advise a long request timeout for reasoning
|
|
48
|
+
* models (their examples use 3600s) so slow thinking can't trip the
|
|
49
|
+
* SDK's default mid-stream. Applied to all Grok requests. */
|
|
50
|
+
protected clientOptions(): Record<string, unknown>;
|
|
51
|
+
/** grok-4.5 exposes reasoning-effort control ('low' | 'medium' | 'high';
|
|
52
|
+
* xAI defaults to 'high', and reasoning cannot be disabled) via the
|
|
53
|
+
* Responses API `reasoning.effort` field — docs.x.ai › Model
|
|
54
|
+
* capabilities › Text › Reasoning › Effort levels. We pin
|
|
55
|
+
* DEFAULT_GROK_REASONING_EFFORT (medium). Other grok ids don't accept
|
|
56
|
+
* the param and get none (their reasoning summaries arrive unrequested);
|
|
57
|
+
* if xAI ever rejects it, GptLlm's guarded 400 retry drops it. */
|
|
58
|
+
protected reasoningParam(): Record<string, unknown> | undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Registers GrokLlm with the ADK LLMRegistry. Called by
|
|
62
|
+
* registerAvailableProviders() when XAI_API_KEY is present.
|
|
63
|
+
*/
|
|
64
|
+
export declare function registerGrokLlm(): void;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/grokLlm.ts — xAI Grok provider for the ADK LLMRegistry.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Model optionality is a primary driver of this framework: the agent YAML
|
|
6
|
+
* declares `model`, and the registry routes it to the right provider.
|
|
7
|
+
*
|
|
8
|
+
* WHY it subclasses GptLlm (Responses API) and not the chat-completions base:
|
|
9
|
+
* xAI retired Live Search on chat completions (the API now returns
|
|
10
|
+
* 410 "Live search is deprecated. Please switch to the Agent Tools API").
|
|
11
|
+
* The Agent Tools API lives at `https://api.x.ai/v1/responses` and is
|
|
12
|
+
* wire-compatible with OpenAI's Responses API — same `input` items, same
|
|
13
|
+
* `{ type: 'function' }` / `{ type: 'web_search' }` tools, same
|
|
14
|
+
* `reasoning` summary / `message` / `function_call` output items (verified
|
|
15
|
+
* live 2026-07-19). So Grok reuses the GptLlm translator via the openai
|
|
16
|
+
* SDK's baseURL override, and gets for free:
|
|
17
|
+
* - native web_search (server-side, with source citations)
|
|
18
|
+
* - x_search (live X posts) and collections_search (hosted document
|
|
19
|
+
* stores via the file_search wire shape — see collectionsSearchTool.ts)
|
|
20
|
+
* - reasoning summaries surfaced as { thought: true } THINKING output
|
|
21
|
+
* - SSE streaming (ADK RunConfig streamingMode: SSE → partial deltas)
|
|
22
|
+
* - structured outputs (YAML outputSchema → text.format json_schema)
|
|
23
|
+
* - function tools with call_id round-tripping, lowercased schemas
|
|
24
|
+
* - usage → usageMetadata mapping for llm.request token spans
|
|
25
|
+
*
|
|
26
|
+
* HOW TO ENABLE:
|
|
27
|
+
* 1. Add your API key to .env: XAI_API_KEY=xai-... (console.x.ai — paid)
|
|
28
|
+
* 2. Set model: "grok-4.5" (or any grok-* id) in your YAML.
|
|
29
|
+
* registerAvailableProviders() registers this adapter when the key is set.
|
|
30
|
+
* grok-4.5 requests carry reasoning effort 'medium' by default — see
|
|
31
|
+
* reasoningParam() below and DEFAULT_GROK_REASONING_EFFORT (lib/config.ts).
|
|
32
|
+
*
|
|
33
|
+
* API-DRIFT NOTE: every xAI-specific choice (endpoint, key env) is confined
|
|
34
|
+
* to the overrides below — upstream drift stays a one-file fix. Note xAI's
|
|
35
|
+
* Responses endpoint reports the serving backend in the response `model`
|
|
36
|
+
* field (e.g. "grok-4.3"), which may differ from the requested id; invalid
|
|
37
|
+
* ids are properly rejected with "Model not found".
|
|
38
|
+
*/
|
|
39
|
+
import { LLMRegistry } from '@google/adk';
|
|
40
|
+
import { DEFAULT_GROK_REASONING_EFFORT } from "../config.js";
|
|
41
|
+
import { GptLlm } from "./gptLlm.js";
|
|
42
|
+
const XAI_BASE_URL = 'https://api.x.ai/v1';
|
|
43
|
+
// ── GrokLlm ───────────────────────────────────────────────────────────────────
|
|
44
|
+
export class GrokLlm extends GptLlm {
|
|
45
|
+
/** Any model: "grok-*" in a YAML config routes here after registration. */
|
|
46
|
+
static supportedModels = [/^grok-.+/];
|
|
47
|
+
providerId() {
|
|
48
|
+
return 'xai';
|
|
49
|
+
}
|
|
50
|
+
baseURL() {
|
|
51
|
+
return XAI_BASE_URL;
|
|
52
|
+
}
|
|
53
|
+
apiKeyFromEnv() {
|
|
54
|
+
return process.env.XAI_API_KEY;
|
|
55
|
+
}
|
|
56
|
+
missingKeyMessage() {
|
|
57
|
+
return 'XAI_API_KEY is not set in environment.';
|
|
58
|
+
}
|
|
59
|
+
/** xAI's streaming docs advise a long request timeout for reasoning
|
|
60
|
+
* models (their examples use 3600s) so slow thinking can't trip the
|
|
61
|
+
* SDK's default mid-stream. Applied to all Grok requests. */
|
|
62
|
+
clientOptions() {
|
|
63
|
+
return { timeout: 3_600_000 };
|
|
64
|
+
}
|
|
65
|
+
/** grok-4.5 exposes reasoning-effort control ('low' | 'medium' | 'high';
|
|
66
|
+
* xAI defaults to 'high', and reasoning cannot be disabled) via the
|
|
67
|
+
* Responses API `reasoning.effort` field — docs.x.ai › Model
|
|
68
|
+
* capabilities › Text › Reasoning › Effort levels. We pin
|
|
69
|
+
* DEFAULT_GROK_REASONING_EFFORT (medium). Other grok ids don't accept
|
|
70
|
+
* the param and get none (their reasoning summaries arrive unrequested);
|
|
71
|
+
* if xAI ever rejects it, GptLlm's guarded 400 retry drops it. */
|
|
72
|
+
reasoningParam() {
|
|
73
|
+
if (/^grok-4\.5/.test(this.model)) {
|
|
74
|
+
return { effort: DEFAULT_GROK_REASONING_EFFORT };
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── Registration helper ───────────────────────────────────────────────────────
|
|
80
|
+
/**
|
|
81
|
+
* Registers GrokLlm with the ADK LLMRegistry. Called by
|
|
82
|
+
* registerAvailableProviders() when XAI_API_KEY is present.
|
|
83
|
+
*/
|
|
84
|
+
export function registerGrokLlm() {
|
|
85
|
+
LLMRegistry.register(GrokLlm);
|
|
86
|
+
}
|