melchizedek-agents 0.9.6 → 0.15.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 +20 -9
- package/DOCUMENTATION.md +56 -2
- package/QUICKSTART.md +37 -8
- package/README.md +23 -3
- package/config/agents/README.md +12 -1
- package/config/agents/examples/ares.yaml +1 -0
- package/config/agents/examples/assistant.yaml +109 -0
- package/config/agents/examples/augustin.yaml +62 -20
- package/config/agents/examples/cartographers.yaml +1 -0
- package/config/agents/examples/claude.yaml +1 -0
- package/config/agents/examples/council.yaml +1 -0
- package/config/agents/examples/critic.yaml +1 -0
- package/config/agents/examples/delegation.yaml +1 -0
- package/config/agents/examples/hierarchical.yaml +1 -0
- package/config/agents/examples/image_production.yaml +1 -0
- package/config/agents/examples/librarian.yaml +1 -0
- package/config/agents/examples/model_zoo.yaml +4 -3
- package/config/agents/examples/patient_advocate.yaml +1 -0
- package/config/agents/examples/research.yaml +209 -0
- package/config/agents/examples/scribe.yaml +210 -0
- package/config/agents/examples/scriptorium.yaml +1 -0
- package/config/agents/examples/style_council.yaml +1 -0
- package/config/agents/examples/syndicate.yaml +1 -0
- package/config/agents/examples/tutor.yaml +1 -0
- package/config/agents/syndicateSchema.yaml +5 -0
- package/config/agents/templates/README.md +53 -0
- package/config/agents/templates/account_memory.yaml +95 -0
- package/config/agents/templates/case_desk.yaml +117 -0
- package/config/agents/templates/conversational.yaml +81 -0
- package/config/agents/templates/draft_review.yaml +103 -0
- package/config/agents/templates/intake_extractor.yaml +135 -0
- package/config/agents/templates/research_brief.yaml +123 -0
- package/config/agents/templates/research_desk.yaml +108 -0
- package/config/agents/templates/review_panel.yaml +153 -0
- package/config/agents/templates/support_triage.yaml +206 -0
- package/config/agents/templates/systems_operator.yaml +106 -0
- package/dist/lib/compile.js +18 -0
- package/dist/lib/config.d.ts +5 -5
- package/dist/lib/config.js +5 -5
- package/dist/lib/doctor.d.ts +98 -0
- package/dist/lib/doctor.js +370 -0
- package/dist/lib/guards/index.d.ts +28 -0
- package/dist/lib/guards/index.js +51 -0
- package/dist/lib/guards/science.d.ts +117 -0
- package/dist/lib/guards/science.js +444 -0
- package/dist/lib/loadSyndicate.d.ts +30 -2
- package/dist/lib/loadSyndicate.js +52 -8
- package/dist/lib/models/capabilities.d.ts +53 -0
- package/dist/lib/models/capabilities.js +81 -0
- package/dist/lib/models/gateway.d.ts +107 -0
- package/dist/lib/models/gateway.js +156 -0
- package/dist/lib/models/gatewayLlm.d.ts +57 -0
- package/dist/lib/models/gatewayLlm.js +105 -0
- package/dist/lib/models/gptLlm.d.ts +29 -1
- package/dist/lib/models/gptLlm.js +92 -3
- package/dist/lib/models/grokLlm.d.ts +6 -6
- package/dist/lib/models/grokLlm.js +7 -7
- package/dist/lib/models/openAiCompatibleLlm.d.ts +7 -0
- package/dist/lib/models/openAiCompatibleLlm.js +15 -1
- package/dist/lib/models/registry.d.ts +18 -2
- package/dist/lib/models/registry.js +63 -12
- package/dist/lib/observability/tracer.d.ts +10 -0
- package/dist/lib/observability/tracer.js +37 -0
- package/dist/lib/skills.d.ts +50 -0
- package/dist/lib/skills.js +138 -0
- package/dist/lib/toolRegistry.js +16 -0
- package/dist/lib/tools/mcpServe.d.ts +46 -0
- package/dist/lib/tools/mcpServe.js +89 -0
- package/dist/lib/tools/mcpToolFactory.d.ts +1 -0
- package/dist/lib/tools/mcpToolFactory.js +29 -1
- package/dist/lib/tools/science/corrections.d.ts +42 -0
- package/dist/lib/tools/science/corrections.js +115 -0
- package/dist/lib/tools/science/europepmc.d.ts +51 -0
- package/dist/lib/tools/science/europepmc.js +127 -0
- package/dist/lib/tools/science/http.d.ts +31 -0
- package/dist/lib/tools/science/http.js +104 -0
- package/dist/lib/tools/science/openalex.d.ts +30 -0
- package/dist/lib/tools/science/openalex.js +81 -0
- package/dist/lib/tools/science/trials.d.ts +58 -0
- package/dist/lib/tools/science/trials.js +203 -0
- package/dist/lib/tools/science/types.d.ts +69 -0
- package/dist/lib/tools/science/types.js +48 -0
- package/dist/lib/tools/scienceTools.d.ts +100 -0
- package/dist/lib/tools/scienceTools.js +277 -0
- package/dist/lib/tools/taskTools.d.ts +155 -0
- package/dist/lib/tools/taskTools.js +342 -0
- package/dist/lib/tools/webExtractTool.d.ts +6 -0
- package/dist/lib/tools/webExtractTool.js +8 -4
- package/dist/lib/tools/xApiSearchTool.d.ts +194 -0
- package/dist/lib/tools/xApiSearchTool.js +583 -0
- package/dist/lib/tools/xPacket.d.ts +102 -0
- package/dist/lib/tools/xPacket.js +226 -0
- package/dist/scripts/a2a_server.js +67 -1
- package/dist/scripts/assistant_worker.d.ts +31 -0
- package/dist/scripts/assistant_worker.js +163 -0
- package/dist/scripts/doctor.d.ts +21 -0
- package/dist/scripts/doctor.js +47 -0
- package/dist/scripts/skills_install.d.ts +7 -0
- package/dist/scripts/skills_install.js +137 -0
- package/package.json +17 -5
- package/skills/README.md +74 -0
- package/skills/briefs/README.md +16 -0
- package/skills/briefs/_shared.md +16 -0
- package/skills/briefs/melchizedek-author.md +60 -0
- package/skills/briefs/melchizedek-memory.md +45 -0
- package/skills/briefs/melchizedek-models.md +47 -0
- package/skills/briefs/melchizedek-scribe.md +37 -0
- package/skills/briefs/melchizedek-serve.md +52 -0
- package/skills/briefs/melchizedek.md +69 -0
- package/skills/briefs/skills-readme.md +33 -0
- package/skills/melchizedek/SKILL.md +159 -0
- package/skills/melchizedek-author/SKILL.md +99 -0
- package/skills/melchizedek-author/templates/minimal.yaml +60 -0
- package/skills/melchizedek-memory/SKILL.md +136 -0
- package/skills/melchizedek-models/SKILL.md +137 -0
- package/skills/melchizedek-scribe/SKILL.md +72 -0
- package/skills/melchizedek-scribe/templates/brief.md +27 -0
- package/skills/melchizedek-serve/SKILL.md +110 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/capabilities.ts — what an agent keeps and loses on the path
|
|
3
|
+
* its model will actually take.
|
|
4
|
+
*
|
|
5
|
+
* WHY this file exists:
|
|
6
|
+
* A model id is the whole routing decision, and some of what an agent
|
|
7
|
+
* declares only exists on one provider: Gemini grounding, xAI's x_search
|
|
8
|
+
* and Collections, the server-side web_search sentinel that every cloud
|
|
9
|
+
* adapter turns into its provider's native search. Until now the only
|
|
10
|
+
* defence against losing one was a one-time console.warn inside an
|
|
11
|
+
* adapter. This module states the loss BEFORE a request is made, per
|
|
12
|
+
* agent, on the RESOLVED transport (direct or gateway), so the doctor
|
|
13
|
+
* (lib/doctor.ts), the A2A startup log (lib/compile.ts) and the ledger
|
|
14
|
+
* (llm.capability.dropped) all say the same thing.
|
|
15
|
+
*
|
|
16
|
+
* It knows nothing about gateways beyond planTransport(); a future
|
|
17
|
+
* transport that restores a native feature changes the table here, not
|
|
18
|
+
* the callers.
|
|
19
|
+
*/
|
|
20
|
+
import { PROVIDERS } from "./providerMap.js";
|
|
21
|
+
import { planTransport } from "./gateway.js";
|
|
22
|
+
/**
|
|
23
|
+
* Server-side tool sentinels and the providers whose DIRECT adapter
|
|
24
|
+
* honours them natively. Anything not listed is a client-side function
|
|
25
|
+
* tool and travels on every path.
|
|
26
|
+
*/
|
|
27
|
+
export const SERVER_SIDE_TOOLS = {
|
|
28
|
+
web_search: ['gemini', 'anthropic', 'openai', 'xai'],
|
|
29
|
+
google_search: ['gemini'],
|
|
30
|
+
x_search: ['xai'],
|
|
31
|
+
collections_search: ['xai'],
|
|
32
|
+
};
|
|
33
|
+
export function describeCapabilities(model, tools = [], opts = {}) {
|
|
34
|
+
const plan = planTransport(model, opts);
|
|
35
|
+
const native = [];
|
|
36
|
+
const dropped = [];
|
|
37
|
+
const portable = [];
|
|
38
|
+
for (const name of tools) {
|
|
39
|
+
const providers = SERVER_SIDE_TOOLS[name];
|
|
40
|
+
if (!providers) {
|
|
41
|
+
portable.push(name);
|
|
42
|
+
}
|
|
43
|
+
else if (plan.transport === 'direct' && providers.includes(plan.provider)) {
|
|
44
|
+
native.push(name);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
dropped.push(name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
model,
|
|
52
|
+
provider: plan.provider,
|
|
53
|
+
providerLabel: PROVIDERS[plan.provider].label,
|
|
54
|
+
transport: plan.transport,
|
|
55
|
+
...(plan.gateway ? { gateway: plan.gateway.id } : {}),
|
|
56
|
+
funded: plan.funded,
|
|
57
|
+
keyEnv: plan.keyEnv,
|
|
58
|
+
native,
|
|
59
|
+
dropped,
|
|
60
|
+
portable,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* One line for a startup log, or undefined when there is nothing to say —
|
|
65
|
+
* a funded direct path with no dropped tool is the quiet default.
|
|
66
|
+
*/
|
|
67
|
+
export function capabilitySummary(agentName, r) {
|
|
68
|
+
if (!r.funded) {
|
|
69
|
+
return `${agentName}: ${r.model} has no route — set ${r.keyEnv ?? 'a provider key'} (or a MODEL_GATEWAY).`;
|
|
70
|
+
}
|
|
71
|
+
const via = r.transport === 'gateway' ? ` via gateway:${r.gateway}` : '';
|
|
72
|
+
if (r.dropped.length === 0) {
|
|
73
|
+
return via ? `${agentName}: ${r.model}${via}.` : undefined;
|
|
74
|
+
}
|
|
75
|
+
const why = r.transport === 'gateway'
|
|
76
|
+
? 'a gateway cannot enable upstream native search'
|
|
77
|
+
: r.provider === 'ollama'
|
|
78
|
+
? 'a local model has no native search'
|
|
79
|
+
: `${r.providerLabel} has no native ${r.dropped.join('/')}`;
|
|
80
|
+
return `${agentName}: ${r.model}${via} — dropped ${r.dropped.join(', ')} (${why}).`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/gateway.ts — the optional "one key" transport.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* A newcomer should be able to run every starter-pack syndicate with ONE
|
|
6
|
+
* credential when they choose to. A hosted gateway (Vercel AI Gateway,
|
|
7
|
+
* OpenRouter) fronts OpenAI, Anthropic, Google and xAI behind a single
|
|
8
|
+
* OpenAI-compatible endpoint, so a model id whose direct key is absent can
|
|
9
|
+
* still be served. This module owns that decision and nothing else.
|
|
10
|
+
*
|
|
11
|
+
* THE RULE (plans/model-access-tiers.md, ADR 0012):
|
|
12
|
+
* Direct adapters are canonical. A model id routes to its provider's own
|
|
13
|
+
* adapter whenever that provider's key is present. Only when the key is
|
|
14
|
+
* ABSENT and a gateway is configured does the id route through the
|
|
15
|
+
* gateway. Ollama never does. So the gateway is a fallback, never a
|
|
16
|
+
* preempt: adding a direct key beside the gateway key silently upgrades
|
|
17
|
+
* that provider's agents back to native fidelity (Gemini grounding, xAI
|
|
18
|
+
* search, Anthropic's server-side web_search) with no YAML change.
|
|
19
|
+
*
|
|
20
|
+
* WHAT A GATEWAY LOSES — reported, never silent:
|
|
21
|
+
* Every server-side tool sentinel (web_search, google_search, x_search,
|
|
22
|
+
* collections_search) is dropped on the gateway path, because a
|
|
23
|
+
* chat-completions endpoint has no uniform way to enable a provider's
|
|
24
|
+
* native search. lib/models/capabilities.ts turns that into a per-agent
|
|
25
|
+
* report the doctor, the A2A startup log and the ledger all read.
|
|
26
|
+
*
|
|
27
|
+
* WHY a leaf (imports providerMap only):
|
|
28
|
+
* The registry, the gateway adapter, the capability report and the
|
|
29
|
+
* doctor all need this decision; keeping it free of adapter imports
|
|
30
|
+
* avoids the cycle model-routing warns about.
|
|
31
|
+
*
|
|
32
|
+
* Env (deployment config, never YAML):
|
|
33
|
+
* MODEL_GATEWAY vercel | openrouter (unset = off, the default)
|
|
34
|
+
* MODEL_GATEWAY_API_KEY the gateway's key
|
|
35
|
+
* MODEL_GATEWAY_BASE_URL optional endpoint override (self-hosted proxies
|
|
36
|
+
* that speak the same dialect, e.g. LiteLLM)
|
|
37
|
+
* MODEL_GATEWAY_MODEL_MAP optional "from=to,from=to" wire-name overrides
|
|
38
|
+
* for ids the default mapper gets wrong
|
|
39
|
+
*/
|
|
40
|
+
import type { ProviderId } from './providerMap.ts';
|
|
41
|
+
export type GatewayId = 'vercel' | 'openrouter';
|
|
42
|
+
/** Providers a gateway can front (everything cloud; never Ollama). */
|
|
43
|
+
export type CloudProviderId = Exclude<ProviderId, 'ollama'>;
|
|
44
|
+
export interface GatewayInfo {
|
|
45
|
+
id: GatewayId;
|
|
46
|
+
label: string;
|
|
47
|
+
/** OpenAI-compatible base URL (chat/completions is appended). */
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
/** Where the operator creates the key. */
|
|
50
|
+
consoleUrl: string;
|
|
51
|
+
/** The gateway's slug for each upstream provider, e.g. "x-ai" vs "xai". */
|
|
52
|
+
slugs: Record<CloudProviderId, string>;
|
|
53
|
+
}
|
|
54
|
+
export declare const GATEWAY_ENV = "MODEL_GATEWAY";
|
|
55
|
+
export declare const GATEWAY_KEY_ENV = "MODEL_GATEWAY_API_KEY";
|
|
56
|
+
export declare const GATEWAY_BASE_URL_ENV = "MODEL_GATEWAY_BASE_URL";
|
|
57
|
+
export declare const GATEWAY_MODEL_MAP_ENV = "MODEL_GATEWAY_MODEL_MAP";
|
|
58
|
+
export declare const GATEWAYS: Record<GatewayId, GatewayInfo>;
|
|
59
|
+
export interface GatewayConfig {
|
|
60
|
+
gateway: GatewayInfo;
|
|
61
|
+
/** Effective base URL (env override or the gateway's default). */
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
keyPresent: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The configured gateway, or null when MODEL_GATEWAY is unset. A set but
|
|
67
|
+
* unknown value also yields null — `gatewayProblem()` says why, so the
|
|
68
|
+
* doctor and the registry log can name the mistake instead of routing
|
|
69
|
+
* silently to nothing.
|
|
70
|
+
*/
|
|
71
|
+
export declare function gatewayConfig(): GatewayConfig | null;
|
|
72
|
+
/** A human-readable misconfiguration, or undefined when the gateway is
|
|
73
|
+
* either off or usable. */
|
|
74
|
+
export declare function gatewayProblem(): string | undefined;
|
|
75
|
+
/** True when a gateway is configured well enough to serve requests. */
|
|
76
|
+
export declare function gatewayUsable(): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The id a gateway expects for a YAML model id.
|
|
79
|
+
*
|
|
80
|
+
* Gateways prefix every id with the upstream provider's slug and spell
|
|
81
|
+
* Anthropic's version segment with a dot ("claude-sonnet-4-6" is
|
|
82
|
+
* "anthropic/claude-sonnet-4.6" on both Vercel and OpenRouter). Gemini,
|
|
83
|
+
* GPT and Grok ids already carry dots, so only the prefix is added. An id
|
|
84
|
+
* the mapper gets wrong is fixed in MODEL_GATEWAY_MODEL_MAP, not in code.
|
|
85
|
+
*/
|
|
86
|
+
export declare function gatewayWireModel(model: string, gateway: GatewayInfo): string;
|
|
87
|
+
export type Transport = 'direct' | 'gateway';
|
|
88
|
+
export interface TransportPlan {
|
|
89
|
+
provider: ProviderId;
|
|
90
|
+
transport: Transport;
|
|
91
|
+
/** Set when transport is 'gateway'. */
|
|
92
|
+
gateway?: GatewayInfo;
|
|
93
|
+
/** True when this transport has a credential (or needs none). */
|
|
94
|
+
funded: boolean;
|
|
95
|
+
/** The direct key env, for the "set X to unlock" message. */
|
|
96
|
+
keyEnv: string | null;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Where a model id will be served from under the current environment.
|
|
100
|
+
*
|
|
101
|
+
* `callerKey` is the A2A BYOK case: a per-request key the caller sent for
|
|
102
|
+
* its own provider counts as that provider's key being present, so a BYOK
|
|
103
|
+
* request never falls through to the operator's gateway.
|
|
104
|
+
*/
|
|
105
|
+
export declare function planTransport(model: string, opts?: {
|
|
106
|
+
callerKey?: boolean;
|
|
107
|
+
}): TransportPlan;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/gateway.ts — the optional "one key" transport.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* A newcomer should be able to run every starter-pack syndicate with ONE
|
|
6
|
+
* credential when they choose to. A hosted gateway (Vercel AI Gateway,
|
|
7
|
+
* OpenRouter) fronts OpenAI, Anthropic, Google and xAI behind a single
|
|
8
|
+
* OpenAI-compatible endpoint, so a model id whose direct key is absent can
|
|
9
|
+
* still be served. This module owns that decision and nothing else.
|
|
10
|
+
*
|
|
11
|
+
* THE RULE (plans/model-access-tiers.md, ADR 0012):
|
|
12
|
+
* Direct adapters are canonical. A model id routes to its provider's own
|
|
13
|
+
* adapter whenever that provider's key is present. Only when the key is
|
|
14
|
+
* ABSENT and a gateway is configured does the id route through the
|
|
15
|
+
* gateway. Ollama never does. So the gateway is a fallback, never a
|
|
16
|
+
* preempt: adding a direct key beside the gateway key silently upgrades
|
|
17
|
+
* that provider's agents back to native fidelity (Gemini grounding, xAI
|
|
18
|
+
* search, Anthropic's server-side web_search) with no YAML change.
|
|
19
|
+
*
|
|
20
|
+
* WHAT A GATEWAY LOSES — reported, never silent:
|
|
21
|
+
* Every server-side tool sentinel (web_search, google_search, x_search,
|
|
22
|
+
* collections_search) is dropped on the gateway path, because a
|
|
23
|
+
* chat-completions endpoint has no uniform way to enable a provider's
|
|
24
|
+
* native search. lib/models/capabilities.ts turns that into a per-agent
|
|
25
|
+
* report the doctor, the A2A startup log and the ledger all read.
|
|
26
|
+
*
|
|
27
|
+
* WHY a leaf (imports providerMap only):
|
|
28
|
+
* The registry, the gateway adapter, the capability report and the
|
|
29
|
+
* doctor all need this decision; keeping it free of adapter imports
|
|
30
|
+
* avoids the cycle model-routing warns about.
|
|
31
|
+
*
|
|
32
|
+
* Env (deployment config, never YAML):
|
|
33
|
+
* MODEL_GATEWAY vercel | openrouter (unset = off, the default)
|
|
34
|
+
* MODEL_GATEWAY_API_KEY the gateway's key
|
|
35
|
+
* MODEL_GATEWAY_BASE_URL optional endpoint override (self-hosted proxies
|
|
36
|
+
* that speak the same dialect, e.g. LiteLLM)
|
|
37
|
+
* MODEL_GATEWAY_MODEL_MAP optional "from=to,from=to" wire-name overrides
|
|
38
|
+
* for ids the default mapper gets wrong
|
|
39
|
+
*/
|
|
40
|
+
import { PROVIDERS, providerForModel, providerKeyPresent } from "./providerMap.js";
|
|
41
|
+
export const GATEWAY_ENV = 'MODEL_GATEWAY';
|
|
42
|
+
export const GATEWAY_KEY_ENV = 'MODEL_GATEWAY_API_KEY';
|
|
43
|
+
export const GATEWAY_BASE_URL_ENV = 'MODEL_GATEWAY_BASE_URL';
|
|
44
|
+
export const GATEWAY_MODEL_MAP_ENV = 'MODEL_GATEWAY_MODEL_MAP';
|
|
45
|
+
export const GATEWAYS = {
|
|
46
|
+
vercel: {
|
|
47
|
+
id: 'vercel',
|
|
48
|
+
label: 'Vercel AI Gateway',
|
|
49
|
+
baseUrl: 'https://ai-gateway.vercel.sh/v1',
|
|
50
|
+
consoleUrl: 'https://vercel.com/ai-gateway',
|
|
51
|
+
slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'xai' },
|
|
52
|
+
},
|
|
53
|
+
openrouter: {
|
|
54
|
+
id: 'openrouter',
|
|
55
|
+
label: 'OpenRouter',
|
|
56
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
57
|
+
consoleUrl: 'https://openrouter.ai/keys',
|
|
58
|
+
slugs: { gemini: 'google', anthropic: 'anthropic', openai: 'openai', xai: 'x-ai' },
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The configured gateway, or null when MODEL_GATEWAY is unset. A set but
|
|
63
|
+
* unknown value also yields null — `gatewayProblem()` says why, so the
|
|
64
|
+
* doctor and the registry log can name the mistake instead of routing
|
|
65
|
+
* silently to nothing.
|
|
66
|
+
*/
|
|
67
|
+
export function gatewayConfig() {
|
|
68
|
+
const id = (process.env[GATEWAY_ENV] ?? '').trim().toLowerCase();
|
|
69
|
+
if (!id)
|
|
70
|
+
return null;
|
|
71
|
+
const gateway = GATEWAYS[id];
|
|
72
|
+
if (!gateway)
|
|
73
|
+
return null;
|
|
74
|
+
const override = (process.env[GATEWAY_BASE_URL_ENV] ?? '').trim();
|
|
75
|
+
return {
|
|
76
|
+
gateway,
|
|
77
|
+
baseUrl: (override || gateway.baseUrl).replace(/\/+$/, ''),
|
|
78
|
+
keyPresent: !!process.env[GATEWAY_KEY_ENV],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** A human-readable misconfiguration, or undefined when the gateway is
|
|
82
|
+
* either off or usable. */
|
|
83
|
+
export function gatewayProblem() {
|
|
84
|
+
const id = (process.env[GATEWAY_ENV] ?? '').trim();
|
|
85
|
+
if (!id)
|
|
86
|
+
return undefined;
|
|
87
|
+
const cfg = gatewayConfig();
|
|
88
|
+
if (!cfg) {
|
|
89
|
+
return `${GATEWAY_ENV}=${id} is not a known gateway (expected one of: ${Object.keys(GATEWAYS).join(', ')})`;
|
|
90
|
+
}
|
|
91
|
+
if (!cfg.keyPresent)
|
|
92
|
+
return `${GATEWAY_ENV}=${cfg.gateway.id} is set but ${GATEWAY_KEY_ENV} is not`;
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
/** True when a gateway is configured well enough to serve requests. */
|
|
96
|
+
export function gatewayUsable() {
|
|
97
|
+
const cfg = gatewayConfig();
|
|
98
|
+
return !!cfg && cfg.keyPresent;
|
|
99
|
+
}
|
|
100
|
+
/** Parses "a=b,c=d" into a map; malformed pairs are skipped. */
|
|
101
|
+
function modelMapFromEnv() {
|
|
102
|
+
const raw = process.env[GATEWAY_MODEL_MAP_ENV] ?? '';
|
|
103
|
+
const map = {};
|
|
104
|
+
for (const pair of raw.split(',')) {
|
|
105
|
+
const eq = pair.indexOf('=');
|
|
106
|
+
if (eq <= 0)
|
|
107
|
+
continue;
|
|
108
|
+
const from = pair.slice(0, eq).trim();
|
|
109
|
+
const to = pair.slice(eq + 1).trim();
|
|
110
|
+
if (from && to)
|
|
111
|
+
map[from] = to;
|
|
112
|
+
}
|
|
113
|
+
return map;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The id a gateway expects for a YAML model id.
|
|
117
|
+
*
|
|
118
|
+
* Gateways prefix every id with the upstream provider's slug and spell
|
|
119
|
+
* Anthropic's version segment with a dot ("claude-sonnet-4-6" is
|
|
120
|
+
* "anthropic/claude-sonnet-4.6" on both Vercel and OpenRouter). Gemini,
|
|
121
|
+
* GPT and Grok ids already carry dots, so only the prefix is added. An id
|
|
122
|
+
* the mapper gets wrong is fixed in MODEL_GATEWAY_MODEL_MAP, not in code.
|
|
123
|
+
*/
|
|
124
|
+
export function gatewayWireModel(model, gateway) {
|
|
125
|
+
const override = modelMapFromEnv()[model];
|
|
126
|
+
if (override)
|
|
127
|
+
return override;
|
|
128
|
+
if (model.includes('/'))
|
|
129
|
+
return model; // already provider-qualified
|
|
130
|
+
const provider = providerForModel(model);
|
|
131
|
+
if (provider === 'ollama')
|
|
132
|
+
return model;
|
|
133
|
+
const slug = gateway.slugs[provider];
|
|
134
|
+
const wire = provider === 'anthropic' ? model.replace(/-(\d+)-(\d+)/, '-$1.$2') : model;
|
|
135
|
+
return `${slug}/${wire}`;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Where a model id will be served from under the current environment.
|
|
139
|
+
*
|
|
140
|
+
* `callerKey` is the A2A BYOK case: a per-request key the caller sent for
|
|
141
|
+
* its own provider counts as that provider's key being present, so a BYOK
|
|
142
|
+
* request never falls through to the operator's gateway.
|
|
143
|
+
*/
|
|
144
|
+
export function planTransport(model, opts = {}) {
|
|
145
|
+
const provider = providerForModel(model);
|
|
146
|
+
const keyEnv = PROVIDERS[provider].keyEnv;
|
|
147
|
+
const direct = providerKeyPresent(provider) || !!opts.callerKey;
|
|
148
|
+
if (direct || provider === 'ollama') {
|
|
149
|
+
return { provider, transport: 'direct', funded: true, keyEnv };
|
|
150
|
+
}
|
|
151
|
+
const cfg = gatewayConfig();
|
|
152
|
+
if (cfg && cfg.keyPresent) {
|
|
153
|
+
return { provider, transport: 'gateway', gateway: cfg.gateway, funded: true, keyEnv };
|
|
154
|
+
}
|
|
155
|
+
return { provider, transport: 'direct', funded: false, keyEnv };
|
|
156
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/gatewayLlm.ts — the one-key fallback adapter.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Serves any cloud model id through a hosted gateway's OpenAI-compatible
|
|
6
|
+
* chat-completions endpoint when the provider's direct key is absent
|
|
7
|
+
* (lib/models/gateway.ts owns that decision). It subclasses the
|
|
8
|
+
* chat-completions base the way Ollama does — not GptLlm, which speaks
|
|
9
|
+
* OpenAI's Responses API — because chat completions is the one dialect
|
|
10
|
+
* every gateway serves.
|
|
11
|
+
*
|
|
12
|
+
* WHAT STAYS TRUE THROUGH THE GATEWAY:
|
|
13
|
+
* - Attribution. providerId() is the YAML id's provider (anthropic,
|
|
14
|
+
* openai, gemini, xai), never "gateway", so the ledger and ymir's
|
|
15
|
+
* per-agent cost trends keep the same provider column (ADR 0009). The
|
|
16
|
+
* transport is a separate span attribute: llm.transport = gateway:<id>.
|
|
17
|
+
* - Tool calling, structured output, reasoning_effort, streaming and
|
|
18
|
+
* token accounting — all provided by the base class.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS LOST — and reported:
|
|
21
|
+
* Every server-side tool sentinel (web_search, google_search, x_search,
|
|
22
|
+
* collections_search). The base omits web_search with a warning and marks
|
|
23
|
+
* the span; lib/models/capabilities.ts names the loss per agent for the
|
|
24
|
+
* doctor and the A2A startup log.
|
|
25
|
+
*
|
|
26
|
+
* Registration: LLMRegistry keys on regex OBJECTS, so a gateway class must
|
|
27
|
+
* carry the SAME supportedModels instances as the direct adapter it stands
|
|
28
|
+
* in for (see gatewayClassFor in registry.ts).
|
|
29
|
+
*/
|
|
30
|
+
import type { LlmRequest, LlmResponse } from '@google/adk';
|
|
31
|
+
import { OpenAiCompatibleLlm } from './openAiCompatibleLlm.ts';
|
|
32
|
+
export declare class GatewayLlm extends OpenAiCompatibleLlm {
|
|
33
|
+
/** Never registered directly — see gatewayClassFor(). */
|
|
34
|
+
static readonly supportedModels: Array<string | RegExp>;
|
|
35
|
+
private readonly cfg;
|
|
36
|
+
constructor({ model }: {
|
|
37
|
+
model: string;
|
|
38
|
+
});
|
|
39
|
+
/** Attribution stays with the upstream provider named by the YAML id. */
|
|
40
|
+
protected providerId(): string;
|
|
41
|
+
protected transport(): string;
|
|
42
|
+
protected endpointUrl(): string;
|
|
43
|
+
protected headers(): Record<string, string>;
|
|
44
|
+
protected wireModelName(): string;
|
|
45
|
+
protected missingRequirement(): LlmResponse | undefined;
|
|
46
|
+
protected httpError(status: number, detail: string): LlmResponse;
|
|
47
|
+
protected extraBodyFields(_llmRequest: LlmRequest): Record<string, unknown>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A GatewayLlm subclass that answers for the given model patterns. Pass the
|
|
51
|
+
* direct adapter's OWN `supportedModels` array so the registry entry
|
|
52
|
+
* REPLACES that adapter's (the dict is keyed by regex object) rather than
|
|
53
|
+
* adding a second, shadowed match.
|
|
54
|
+
*/
|
|
55
|
+
export declare function gatewayClassFor(patterns: Array<string | RegExp>): typeof GatewayLlm;
|
|
56
|
+
/** Registers a gateway stand-in for the given patterns. */
|
|
57
|
+
export declare function registerGatewayLlm(patterns: Array<string | RegExp>): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/models/gatewayLlm.ts — the one-key fallback adapter.
|
|
3
|
+
*
|
|
4
|
+
* WHY this file exists:
|
|
5
|
+
* Serves any cloud model id through a hosted gateway's OpenAI-compatible
|
|
6
|
+
* chat-completions endpoint when the provider's direct key is absent
|
|
7
|
+
* (lib/models/gateway.ts owns that decision). It subclasses the
|
|
8
|
+
* chat-completions base the way Ollama does — not GptLlm, which speaks
|
|
9
|
+
* OpenAI's Responses API — because chat completions is the one dialect
|
|
10
|
+
* every gateway serves.
|
|
11
|
+
*
|
|
12
|
+
* WHAT STAYS TRUE THROUGH THE GATEWAY:
|
|
13
|
+
* - Attribution. providerId() is the YAML id's provider (anthropic,
|
|
14
|
+
* openai, gemini, xai), never "gateway", so the ledger and ymir's
|
|
15
|
+
* per-agent cost trends keep the same provider column (ADR 0009). The
|
|
16
|
+
* transport is a separate span attribute: llm.transport = gateway:<id>.
|
|
17
|
+
* - Tool calling, structured output, reasoning_effort, streaming and
|
|
18
|
+
* token accounting — all provided by the base class.
|
|
19
|
+
*
|
|
20
|
+
* WHAT IS LOST — and reported:
|
|
21
|
+
* Every server-side tool sentinel (web_search, google_search, x_search,
|
|
22
|
+
* collections_search). The base omits web_search with a warning and marks
|
|
23
|
+
* the span; lib/models/capabilities.ts names the loss per agent for the
|
|
24
|
+
* doctor and the A2A startup log.
|
|
25
|
+
*
|
|
26
|
+
* Registration: LLMRegistry keys on regex OBJECTS, so a gateway class must
|
|
27
|
+
* carry the SAME supportedModels instances as the direct adapter it stands
|
|
28
|
+
* in for (see gatewayClassFor in registry.ts).
|
|
29
|
+
*/
|
|
30
|
+
import { LLMRegistry } from '@google/adk';
|
|
31
|
+
import { OpenAiCompatibleLlm } from "./openAiCompatibleLlm.js";
|
|
32
|
+
import { GATEWAY_ENV, GATEWAY_KEY_ENV, GATEWAY_MODEL_MAP_ENV, gatewayConfig, gatewayWireModel, } from "./gateway.js";
|
|
33
|
+
import { providerForModel } from "./providerMap.js";
|
|
34
|
+
export class GatewayLlm extends OpenAiCompatibleLlm {
|
|
35
|
+
/** Never registered directly — see gatewayClassFor(). */
|
|
36
|
+
static supportedModels = [];
|
|
37
|
+
cfg;
|
|
38
|
+
constructor({ model }) {
|
|
39
|
+
super({ model });
|
|
40
|
+
this.cfg = gatewayConfig();
|
|
41
|
+
}
|
|
42
|
+
/** Attribution stays with the upstream provider named by the YAML id. */
|
|
43
|
+
providerId() {
|
|
44
|
+
return providerForModel(this.model);
|
|
45
|
+
}
|
|
46
|
+
transport() {
|
|
47
|
+
return this.cfg ? `gateway:${this.cfg.gateway.id}` : 'gateway';
|
|
48
|
+
}
|
|
49
|
+
endpointUrl() {
|
|
50
|
+
const base = this.cfg?.baseUrl ?? '';
|
|
51
|
+
return `${base}/chat/completions`;
|
|
52
|
+
}
|
|
53
|
+
headers() {
|
|
54
|
+
return { Authorization: `Bearer ${process.env[GATEWAY_KEY_ENV] ?? ''}` };
|
|
55
|
+
}
|
|
56
|
+
wireModelName() {
|
|
57
|
+
return this.cfg ? gatewayWireModel(this.model, this.cfg.gateway) : this.model;
|
|
58
|
+
}
|
|
59
|
+
missingRequirement() {
|
|
60
|
+
if (!this.cfg) {
|
|
61
|
+
return {
|
|
62
|
+
errorCode: 'GATEWAY_NOT_CONFIGURED',
|
|
63
|
+
errorMessage: `${GATEWAY_ENV} is not set to a known gateway, so ${this.model} has no route.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (!this.cfg.keyPresent) {
|
|
67
|
+
return {
|
|
68
|
+
errorCode: 'GATEWAY_KEY_MISSING',
|
|
69
|
+
errorMessage: `${GATEWAY_ENV}=${this.cfg.gateway.id} but ${GATEWAY_KEY_ENV} is not set.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
// webSearchBodyFields() stays at the base default (null): a gateway has no
|
|
75
|
+
// uniform switch for upstream native search, so the tool is dropped and
|
|
76
|
+
// the loss is reported (llm.capability.dropped, the doctor, the startup log).
|
|
77
|
+
httpError(status, detail) {
|
|
78
|
+
const label = this.cfg?.gateway.label ?? 'gateway';
|
|
79
|
+
const hint = status === 404 || status === 400
|
|
80
|
+
? ` If the model id is the problem, map it with ${GATEWAY_MODEL_MAP_ENV}=${this.model}=<gateway id>.`
|
|
81
|
+
: '';
|
|
82
|
+
return {
|
|
83
|
+
errorCode: 'GATEWAY_HTTP_ERROR',
|
|
84
|
+
errorMessage: `${label} returned ${status} for ${this.wireModelName()}: ${detail.slice(0, 4000)}.${hint}`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
extraBodyFields(_llmRequest) {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A GatewayLlm subclass that answers for the given model patterns. Pass the
|
|
93
|
+
* direct adapter's OWN `supportedModels` array so the registry entry
|
|
94
|
+
* REPLACES that adapter's (the dict is keyed by regex object) rather than
|
|
95
|
+
* adding a second, shadowed match.
|
|
96
|
+
*/
|
|
97
|
+
export function gatewayClassFor(patterns) {
|
|
98
|
+
return class GatewayLlmFor extends GatewayLlm {
|
|
99
|
+
static supportedModels = patterns;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Registers a gateway stand-in for the given patterns. */
|
|
103
|
+
export function registerGatewayLlm(patterns) {
|
|
104
|
+
LLMRegistry.register(gatewayClassFor(patterns));
|
|
105
|
+
}
|
|
@@ -27,10 +27,15 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Response `output` items map back:
|
|
29
29
|
* 'reasoning' summary → { text, thought: true } partial (display-only)
|
|
30
|
-
* 'message' output_text → text part
|
|
30
|
+
* 'message' output_text → text part (2nd+ message item opens a new paragraph)
|
|
31
31
|
* 'function_call' → functionCall part
|
|
32
|
+
* '*_call' / 'custom_tool_call' (server-side: web_search, x_search…)
|
|
33
|
+
* → customMetadata['responses.server_tool_calls'],
|
|
34
|
+
* never a functionCall (ADK must not run them)
|
|
32
35
|
* usage.input_tokens / output_tokens / output_tokens_details.reasoning_tokens
|
|
33
36
|
* → LlmResponse.usageMetadata, so token telemetry works like every provider.
|
|
37
|
+
* xAI's server-side tool counters → customMetadata['responses.server_tool_usage']
|
|
38
|
+
* and llm.server_tools.* span attributes.
|
|
34
39
|
*
|
|
35
40
|
* STREAMING (ADK stream=true, i.e. RunConfig streamingMode: SSE):
|
|
36
41
|
* The request is sent with { stream: true }; SSE deltas
|
|
@@ -59,6 +64,29 @@ export declare function streamEventDelta(ev: any): {
|
|
|
59
64
|
thought: boolean;
|
|
60
65
|
text: string;
|
|
61
66
|
} | null;
|
|
67
|
+
/** One tool call the vendor ran on its own side inside a single Responses
|
|
68
|
+
* call. It never comes back to ADK as a functionCall, so without this
|
|
69
|
+
* record a searched answer and a recalled one look identical in a trace. */
|
|
70
|
+
export interface ServerToolCall {
|
|
71
|
+
name: string;
|
|
72
|
+
args: Record<string, unknown>;
|
|
73
|
+
status?: string;
|
|
74
|
+
/** URLs the vendor reports the call returned (web search only). */
|
|
75
|
+
sources?: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The server-side tool calls in a Responses `output` array. Two shapes,
|
|
79
|
+
* both verified against a live xAI response (2026-09-25):
|
|
80
|
+
* web_search_call → { status, action: { type:'search', query, sources:[{url}] } }
|
|
81
|
+
* custom_tool_call → { name:'x_keyword_search'|'x_semantic_search', input:'<json>', status }
|
|
82
|
+
* Any other `*_call` item (code_interpreter_call, file_search_call…) is
|
|
83
|
+
* recorded by its type with its `action` as args. `custom_tool_call` is
|
|
84
|
+
* server-side here because this adapter never declares a client custom tool.
|
|
85
|
+
*/
|
|
86
|
+
export declare function extractServerToolCalls(output: unknown): ServerToolCall[];
|
|
87
|
+
/** xAI's server-side tool counters off `usage` (num_server_side_tools_used +
|
|
88
|
+
* server_side_tool_usage_details), non-zero entries only; {} for OpenAI. */
|
|
89
|
+
export declare function serverToolUsage(usage: any): Record<string, number>;
|
|
62
90
|
export declare class GptLlm extends BaseLlm {
|
|
63
91
|
/** gpt-* and o-series ids route here after registration. */
|
|
64
92
|
static readonly supportedModels: Array<string | RegExp>;
|