@datalayer/agent-runtimes 1.3.15 → 1.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/chat/base/ChatBase.js +3 -2
- package/lib/config/AgentConfiguration.d.ts +1 -1
- package/lib/loop/embed/LoopEmbed.d.ts +6 -1
- package/lib/loop/embed/LoopEmbed.js +3 -2
- package/lib/loop/plugins/chat/ChatView.js +9 -4
- package/lib/loop/plugins/chat/index.d.ts +10 -0
- package/lib/loop/plugins/chat/index.js +1 -0
- package/lib/loop/presets.d.ts +6 -0
- package/lib/loop/presets.js +2 -1
- package/lib/loop/shell/LoopWorkspace.d.ts +14 -0
- package/lib/loop/shell/LoopWorkspace.js +20 -6
- package/lib/loop/shell/index.d.ts +1 -1
- package/lib/loop/shell/index.js +1 -1
- package/lib/loop/shell/useWorkspaceFullScreen.d.ts +17 -4
- package/lib/loop/shell/useWorkspaceFullScreen.js +23 -6
- package/lib/specs/agents/agents.d.ts +1 -0
- package/lib/specs/agents/agents.js +338 -284
- package/lib/specs/events.d.ts +6 -1
- package/lib/specs/events.js +144 -1
- package/lib/specs/index.d.ts +2 -0
- package/lib/specs/index.js +2 -0
- package/lib/specs/mcpServers.js +2 -2
- package/lib/specs/memory.js +4 -0
- package/lib/specs/modelProviders.d.ts +22 -0
- package/lib/specs/modelProviders.js +102 -0
- package/lib/specs/models.d.ts +31 -0
- package/lib/specs/models.js +215 -0
- package/lib/specs/outputs.js +2 -2
- package/lib/specs/skills.js +1 -1
- package/lib/specs/teams/teams.js +18 -18
- package/lib/specs/uiPlugins.d.ts +16 -0
- package/lib/specs/uiPlugins.js +40 -0
- package/lib/types/agentspecs.d.ts +18 -2
- package/lib/types/memory.d.ts +2 -0
- package/lib/types/models.d.ts +52 -0
- package/package.json +1 -1
- package/scripts/check-cloudflare-models.py +122 -0
- package/scripts/codegen/generate_agents.py +6 -4
- package/scripts/codegen/generate_memory.py +2 -0
- package/scripts/codegen/generate_model_providers.py +246 -0
- package/scripts/codegen/generate_models.py +135 -4
- package/scripts/codegen/generate_sandbox_agents.py +1 -1
- package/scripts/codegen/generate_ui_plugins.py +226 -0
- package/scripts/gen_workers.py +2 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
* Distributed under the terms of the Modified BSD License.
|
|
4
|
+
*/
|
|
5
|
+
export const A2UI_UI_PLUGIN_0_0_1 = {
|
|
6
|
+
id: 'a2ui',
|
|
7
|
+
version: '0.0.1',
|
|
8
|
+
name: 'A2UI',
|
|
9
|
+
description: 'An agent describes an interface — a form, a table, a card — as a tree of components from a catalogue the host allows, and the host renders it; what the user does in it comes back to the agent as an action.',
|
|
10
|
+
docsUrl: 'https://a2ui.org/',
|
|
11
|
+
enabled: true,
|
|
12
|
+
};
|
|
13
|
+
export const MCP_APPS_UI_PLUGIN_0_0_1 = {
|
|
14
|
+
id: 'mcp-apps',
|
|
15
|
+
version: '0.0.1',
|
|
16
|
+
name: 'MCP Apps',
|
|
17
|
+
description: "An MCP server ships an interactive app with a tool: the host renders it in a sandboxed frame beside the conversation, and the app calls the server's tools through the host.",
|
|
18
|
+
docsUrl: 'https://modelcontextprotocol.io/docs/extensions/apps',
|
|
19
|
+
enabled: false,
|
|
20
|
+
};
|
|
21
|
+
export const MCP_UI_UI_PLUGIN_0_0_1 = {
|
|
22
|
+
id: 'mcp-ui',
|
|
23
|
+
version: '0.0.1',
|
|
24
|
+
name: 'MCP UI',
|
|
25
|
+
description: 'An MCP tool answers with a UI resource — HTML, a remote page or a component — that the host renders in place of plain text.',
|
|
26
|
+
docsUrl: 'https://mcpui.dev/',
|
|
27
|
+
enabled: false,
|
|
28
|
+
};
|
|
29
|
+
export const UI_PLUGIN_CATALOGUE = {
|
|
30
|
+
a2ui: A2UI_UI_PLUGIN_0_0_1,
|
|
31
|
+
'mcp-apps': MCP_APPS_UI_PLUGIN_0_0_1,
|
|
32
|
+
'mcp-ui': MCP_UI_UI_PLUGIN_0_0_1,
|
|
33
|
+
};
|
|
34
|
+
/** The plugin an agent spec's `uiPlugin` names, or undefined. */
|
|
35
|
+
export function getUIPlugin(pluginId) {
|
|
36
|
+
return UI_PLUGIN_CATALOGUE[pluginId];
|
|
37
|
+
}
|
|
38
|
+
export function listUIPlugins() {
|
|
39
|
+
return Object.values(UI_PLUGIN_CATALOGUE);
|
|
40
|
+
}
|
|
@@ -74,6 +74,22 @@ export interface AgentCapability {
|
|
|
74
74
|
/** Free text, for humans reading a catalogue; never matched on. */
|
|
75
75
|
tags?: string[];
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* How an agent's answer becomes an interface: a protocol the host renders
|
|
79
|
+
* (`agentspecs/ui-plugins`). An agent spec's `uiPlugin` names one.
|
|
80
|
+
*/
|
|
81
|
+
export interface UIPluginSpec {
|
|
82
|
+
/** What an agent spec's `uiPlugin` names (e.g. 'a2ui'). */
|
|
83
|
+
id: string;
|
|
84
|
+
version: string;
|
|
85
|
+
name: string;
|
|
86
|
+
/** What the plugin renders, and how the user's action comes back. */
|
|
87
|
+
description: string;
|
|
88
|
+
/** The protocol's own documentation. */
|
|
89
|
+
docsUrl: string;
|
|
90
|
+
/** Whether an agent spec may name it today. */
|
|
91
|
+
enabled: boolean;
|
|
92
|
+
}
|
|
77
93
|
export interface Agentspec {
|
|
78
94
|
/** Unique agent identifier */
|
|
79
95
|
id: string;
|
|
@@ -154,8 +170,8 @@ export interface Agentspec {
|
|
|
154
170
|
delegable?: AgentCapability[];
|
|
155
171
|
/** Communication protocol (e.g., 'ag-ui', 'acp', 'a2a', 'vercel-ai') */
|
|
156
172
|
protocol?: string;
|
|
157
|
-
/** UI
|
|
158
|
-
|
|
173
|
+
/** UI plugin (e.g., 'a2ui', 'mcp-apps'), one of the `UI_PLUGIN_CATALOGUE`. */
|
|
174
|
+
uiPlugin?: string;
|
|
159
175
|
/** Trigger configuration (type, cron, event source, prompt) */
|
|
160
176
|
trigger?: AgentTriggerConfig;
|
|
161
177
|
/** Model configuration (temperature, max_tokens) */
|
package/lib/types/memory.d.ts
CHANGED
package/lib/types/models.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface AIModel {
|
|
|
13
13
|
description: string;
|
|
14
14
|
/** Provider name (anthropic, openai, bedrock, azure-openai) */
|
|
15
15
|
provider: string;
|
|
16
|
+
/** The page on the provider's website that describes this model. */
|
|
17
|
+
providerUrl?: string;
|
|
16
18
|
/** Whether this is the default model */
|
|
17
19
|
default: boolean;
|
|
18
20
|
/**
|
|
@@ -46,6 +48,56 @@ export interface AIModel {
|
|
|
46
48
|
* 'thinking'. Empty means unstated rather than incapable.
|
|
47
49
|
*/
|
|
48
50
|
capabilities?: string[];
|
|
51
|
+
/** How the provider bills it, when worth telling: 'standard' or 'credits'. */
|
|
52
|
+
billing?: 'standard' | 'credits';
|
|
53
|
+
/**
|
|
54
|
+
* How a Cloudflare model is reached: 'workers-ai' (a model Cloudflare
|
|
55
|
+
* hosts, ids 'cloudflare:wrk/…') or 'ai-gateway' (a third-party model its
|
|
56
|
+
* gateway fronts, ids 'cloudflare:gtw/…').
|
|
57
|
+
*/
|
|
58
|
+
route?: 'workers-ai' | 'ai-gateway';
|
|
59
|
+
/** The tokens a request may carry, input and output together. */
|
|
60
|
+
contextWindow?: number;
|
|
61
|
+
/** Whether the provider keeps nothing of a request once it is answered. */
|
|
62
|
+
zeroDataRetention?: boolean;
|
|
63
|
+
/** The provider's list price per million tokens, when a service meters by it. */
|
|
64
|
+
pricing?: {
|
|
65
|
+
inputUsdPerMillion: number;
|
|
66
|
+
outputUsdPerMillion: number;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Where the route keeps a log of the requests it carries, apart from what
|
|
70
|
+
* the provider retains: 'none', or 'gateway' (AI Gateway's request logs).
|
|
71
|
+
* Read beside zeroDataRetention.
|
|
72
|
+
*/
|
|
73
|
+
requestLogging?: 'none' | 'gateway';
|
|
74
|
+
/** Older ids this spec answers to, kept when an id had to move. */
|
|
75
|
+
aliases?: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Who serves a model: the vendor's own API, a cloud that hosts it, or the
|
|
79
|
+
* user's machine — with what a person choosing it has to be able to read
|
|
80
|
+
* (`agentspecs/model-providers`).
|
|
81
|
+
*/
|
|
82
|
+
export interface ModelProvider {
|
|
83
|
+
/** What a model spec's `provider` names (e.g. 'anthropic'). */
|
|
84
|
+
id: string;
|
|
85
|
+
version: string;
|
|
86
|
+
name: string;
|
|
87
|
+
/** What the provider is, and what is worth knowing before choosing it. */
|
|
88
|
+
description: string;
|
|
89
|
+
/** The provider's product page. */
|
|
90
|
+
website?: string;
|
|
91
|
+
/** The provider's documentation. */
|
|
92
|
+
docsUrl?: string;
|
|
93
|
+
/** The terms of service a call is made under. */
|
|
94
|
+
termsUrl?: string;
|
|
95
|
+
/** The provider's privacy policy. */
|
|
96
|
+
privacyUrl?: string;
|
|
97
|
+
/** What the provider says about the data a request carries, when it has a page for it. */
|
|
98
|
+
dataUsageUrl?: string;
|
|
99
|
+
/** Where the model runs: the provider's cloud, or the user's machine. */
|
|
100
|
+
hosting: 'cloud' | 'local';
|
|
49
101
|
}
|
|
50
102
|
/**
|
|
51
103
|
* Configuration for an AI model runtime (as returned by the server).
|
package/package.json
CHANGED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
# Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
"""Hold the Cloudflare model specs to what Cloudflare serves.
|
|
5
|
+
|
|
6
|
+
The catalogue moves — a model the specs could have named in September had
|
|
7
|
+
been deprecated in May — so this reads the account's own listing and says
|
|
8
|
+
which spec names a model no longer served, which served model with tool
|
|
9
|
+
calling has no spec, and where a spec's context differs from the listing's.
|
|
10
|
+
Run by hand, with the account's token::
|
|
11
|
+
|
|
12
|
+
CLOUDFLARE_ACCOUNT_ID=… CLOUDFLARE_API_TOKEN=… python scripts/check-cloudflare-models.py
|
|
13
|
+
|
|
14
|
+
The names are the ones datalayer-ai-inference reads (``DATALAYER_CLOUDFLARE_*``
|
|
15
|
+
are read too). Exits 1 when a spec names a model that is not served.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.request
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
|
|
28
|
+
SPECS = Path(__file__).resolve().parent.parent / "agentspecs" / "agentspecs" / "models"
|
|
29
|
+
API = "https://api.cloudflare.com/client/v4/accounts"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def env(*names: str) -> str:
|
|
33
|
+
for name in names:
|
|
34
|
+
value = (os.environ.get(name) or "").strip()
|
|
35
|
+
if value:
|
|
36
|
+
return value
|
|
37
|
+
return ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def served(account: str, token: str) -> dict[str, dict]:
|
|
41
|
+
"""Every text-generation model Cloudflare lists for the account, by name."""
|
|
42
|
+
models: dict[str, dict] = {}
|
|
43
|
+
page = 1
|
|
44
|
+
while True:
|
|
45
|
+
request = urllib.request.Request(
|
|
46
|
+
f"{API}/{account}/ai/models/search?task=Text%20Generation&per_page=100&page={page}",
|
|
47
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
48
|
+
)
|
|
49
|
+
# The address is the constant https `API` above, never caller input.
|
|
50
|
+
with urllib.request.urlopen(request, timeout=60) as response: # nosec B310
|
|
51
|
+
answer = json.load(response)
|
|
52
|
+
if not answer.get("success"):
|
|
53
|
+
raise SystemExit(f"Cloudflare refused the listing: {answer.get('errors')}")
|
|
54
|
+
for model in answer.get("result") or []:
|
|
55
|
+
properties = {
|
|
56
|
+
p.get("property_id"): p.get("value")
|
|
57
|
+
for p in model.get("properties") or []
|
|
58
|
+
}
|
|
59
|
+
models[model["name"]] = {
|
|
60
|
+
# The listing says it as a string: "true".
|
|
61
|
+
"tools": str(properties.get("function_calling")).lower() == "true",
|
|
62
|
+
"context": properties.get("context_window"),
|
|
63
|
+
}
|
|
64
|
+
if len(answer.get("result") or []) < 100:
|
|
65
|
+
return models
|
|
66
|
+
page += 1
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def specs() -> list[dict]:
|
|
70
|
+
"""The Workers AI specs (``cloudflare-wrk-*.yaml``) that name a model Cloudflare
|
|
71
|
+
hosts under ``@cf/``. The gateway flavour (``cloudflare-gtw-*``) and the typed
|
|
72
|
+
judgment models (``typesafe/jev``, with no ``@cf/`` namespace) are not in the
|
|
73
|
+
account's model listing and are left out.
|
|
74
|
+
"""
|
|
75
|
+
found = []
|
|
76
|
+
for path in sorted(SPECS.glob("cloudflare-wrk-*.yaml")):
|
|
77
|
+
with open(path) as handle:
|
|
78
|
+
data = yaml.safe_load(handle) or {}
|
|
79
|
+
if "judgments" in (data.get("capabilities") or []):
|
|
80
|
+
continue
|
|
81
|
+
found.append({"file": path.name, **data})
|
|
82
|
+
return found
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main() -> int:
|
|
86
|
+
account = env("CLOUDFLARE_ACCOUNT_ID", "DATALAYER_CLOUDFLARE_ACCOUNT_ID")
|
|
87
|
+
token = env(
|
|
88
|
+
"CLOUDFLARE_API_TOKEN",
|
|
89
|
+
"DATALAYER_CLOUDFLARE_API_TOKEN",
|
|
90
|
+
"DATALAYER_CLOUDFLARE_ACCOUNT_API_TOKEN",
|
|
91
|
+
)
|
|
92
|
+
if not account or not token:
|
|
93
|
+
print(
|
|
94
|
+
"CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are needed", file=sys.stderr
|
|
95
|
+
)
|
|
96
|
+
return 2
|
|
97
|
+
listing = served(account, token)
|
|
98
|
+
named = {}
|
|
99
|
+
failures = 0
|
|
100
|
+
for spec in specs():
|
|
101
|
+
name = "@cf/" + spec["id"].split(":", 1)[1]
|
|
102
|
+
named[name] = spec
|
|
103
|
+
entry = listing.get(name)
|
|
104
|
+
if entry is None:
|
|
105
|
+
failures += 1
|
|
106
|
+
print(f"NOT SERVED {spec['file']}: Cloudflare no longer lists {name}")
|
|
107
|
+
continue
|
|
108
|
+
if "tools" in (spec.get("capabilities") or []) and not entry["tools"]:
|
|
109
|
+
print(
|
|
110
|
+
f"NO TOOLS {spec['file']}: the listing says {name} has no function calling"
|
|
111
|
+
)
|
|
112
|
+
print(f"ok {spec['file']}: {name} (context {entry['context']})")
|
|
113
|
+
for name, entry in sorted(listing.items()):
|
|
114
|
+
if entry["tools"] and name not in named:
|
|
115
|
+
print(
|
|
116
|
+
f"unlisted {name} has tool calling and no spec (context {entry['context']})"
|
|
117
|
+
)
|
|
118
|
+
return 1 if failures else 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == "__main__":
|
|
122
|
+
sys.exit(main())
|
|
@@ -455,7 +455,7 @@ from agent_runtimes.types import (
|
|
|
455
455
|
protocol_val = spec.get("protocol")
|
|
456
456
|
protocol_str = f'"{protocol_val}"' if protocol_val else "None"
|
|
457
457
|
delegable_str = _fmt_py_delegable(spec.get("delegable", []))
|
|
458
|
-
ui_ext = spec.get("ui_extension")
|
|
458
|
+
ui_ext = spec.get("ui_plugin") or spec.get("ui_extension")
|
|
459
459
|
ui_ext_str = f'"{ui_ext}"' if ui_ext else "None"
|
|
460
460
|
trigger_val = spec.get("trigger")
|
|
461
461
|
model_cfg = spec.get("model_config")
|
|
@@ -567,7 +567,7 @@ from agent_runtimes.types import (
|
|
|
567
567
|
goal={goal_str},
|
|
568
568
|
delegable={delegable_str},
|
|
569
569
|
protocol={protocol_str},
|
|
570
|
-
|
|
570
|
+
ui_plugin={ui_ext_str},
|
|
571
571
|
trigger={_fmt_py_literal(trigger_val)},
|
|
572
572
|
model_configuration={_fmt_py_literal(model_cfg)},
|
|
573
573
|
mcp_server_tools={_fmt_py_literal(mcp_srv_tools)},
|
|
@@ -1075,7 +1075,7 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
|
|
|
1075
1075
|
protocol_val = spec.get("protocol")
|
|
1076
1076
|
protocol_ts = f"'{protocol_val}'" if protocol_val else "undefined"
|
|
1077
1077
|
delegable_ts = _fmt_ts_delegable(spec.get("delegable", []))
|
|
1078
|
-
ui_ext = spec.get("ui_extension")
|
|
1078
|
+
ui_ext = spec.get("ui_plugin") or spec.get("ui_extension")
|
|
1079
1079
|
ui_ext_ts = f"'{ui_ext}'" if ui_ext else "undefined"
|
|
1080
1080
|
trigger_val = spec.get("trigger")
|
|
1081
1081
|
model_cfg = spec.get("model_config")
|
|
@@ -1135,7 +1135,7 @@ const FRONTEND_TOOL_MAP: Record<string, any> = {
|
|
|
1135
1135
|
goal: {goal_ts},
|
|
1136
1136
|
delegable: {delegable_ts},
|
|
1137
1137
|
protocol: {protocol_ts},
|
|
1138
|
-
|
|
1138
|
+
uiPlugin: {ui_ext_ts},
|
|
1139
1139
|
trigger: {_fmt_ts_literal(trigger_val)},
|
|
1140
1140
|
modelConfig: {_fmt_ts_literal(model_cfg)},
|
|
1141
1141
|
mcpServerTools: {_fmt_ts_literal(mcp_srv_tools)},
|
|
@@ -1573,11 +1573,13 @@ export * from './loops';
|
|
|
1573
1573
|
export * from './mcpServers';
|
|
1574
1574
|
export * from './memory';
|
|
1575
1575
|
export * from './models';
|
|
1576
|
+
export * from './modelProviders';
|
|
1576
1577
|
export * from './notifications';
|
|
1577
1578
|
export * from './outputs';
|
|
1578
1579
|
export * from './skills';
|
|
1579
1580
|
export * from './tools';
|
|
1580
1581
|
export * from './triggers';
|
|
1582
|
+
export * from './uiPlugins';
|
|
1581
1583
|
"""
|
|
1582
1584
|
)
|
|
1583
1585
|
|
|
@@ -119,6 +119,7 @@ def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
|
119
119
|
f' persistence="{spec.get("persistence", "none")}",',
|
|
120
120
|
f' scope="{spec.get("scope", "agent")}",',
|
|
121
121
|
f' backend="{spec.get("backend", "in-memory")}",',
|
|
122
|
+
f" enabled={bool(spec.get('enabled', False))},",
|
|
122
123
|
f" icon={icon},",
|
|
123
124
|
f" emoji={emoji},",
|
|
124
125
|
")",
|
|
@@ -268,6 +269,7 @@ def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
|
268
269
|
f" persistence: '{spec.get('persistence', 'none')}',",
|
|
269
270
|
f" scope: '{spec.get('scope', 'agent')}',",
|
|
270
271
|
f" backend: '{spec.get('backend', 'in-memory')}',",
|
|
272
|
+
f" enabled: {'true' if spec.get('enabled', False) else 'false'},",
|
|
271
273
|
f" icon: '{spec.get('icon', 'database')}',",
|
|
272
274
|
f" emoji: '{spec.get('emoji', '🧠')}',",
|
|
273
275
|
"};",
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
|
+
# Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Generate Python and TypeScript code from YAML model-provider specifications.
|
|
7
|
+
|
|
8
|
+
A model provider is who serves a model (Anthropic, Amazon Bedrock, Cloudflare,
|
|
9
|
+
Ollama…): its site, its documentation, its terms of service, its privacy
|
|
10
|
+
policy, what it says about the data a request carries, and whether it runs
|
|
11
|
+
the model or the user's machine does. One YAML per provider under
|
|
12
|
+
``agentspecs/model-providers``; a model spec's ``provider`` names one.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
python generate_model_providers.py \\
|
|
16
|
+
--specs-dir agentspecs/agentspecs/model-providers \\
|
|
17
|
+
--python-output agent_runtimes/specs/model_providers.py \\
|
|
18
|
+
--typescript-output src/specs/modelProviders.ts
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import sys
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
import yaml
|
|
27
|
+
from versioning import ensure_spec_version, version_suffix
|
|
28
|
+
|
|
29
|
+
FIELDS = ("website", "docs_url", "terms_url", "privacy_url", "data_usage_url")
|
|
30
|
+
CAMEL = {
|
|
31
|
+
"website": "website",
|
|
32
|
+
"docs_url": "docsUrl",
|
|
33
|
+
"terms_url": "termsUrl",
|
|
34
|
+
"privacy_url": "privacyUrl",
|
|
35
|
+
"data_usage_url": "dataUsageUrl",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_provider_specs(specs_dir: Path) -> list[dict[str, Any]]:
|
|
40
|
+
"""Load all model-provider YAML specifications from a directory."""
|
|
41
|
+
specs = []
|
|
42
|
+
for yaml_file in sorted(specs_dir.glob("*.yaml")):
|
|
43
|
+
with open(yaml_file) as f:
|
|
44
|
+
spec = yaml.safe_load(f)
|
|
45
|
+
ensure_spec_version(spec)
|
|
46
|
+
if spec["id"] != yaml_file.stem:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"{yaml_file.name}: the file is named for id {yaml_file.stem!r}, the spec says {spec['id']!r}"
|
|
49
|
+
)
|
|
50
|
+
specs.append(spec)
|
|
51
|
+
return specs
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _const_name(provider_id: str) -> str:
|
|
55
|
+
return provider_id.replace("-", "_").replace(".", "_").upper()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _py(value: str) -> str:
|
|
59
|
+
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _ts(value: str) -> str:
|
|
63
|
+
return "'" + value.replace("\\", "\\\\").replace("'", "\\'") + "'"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def generate_python_code(specs: list[dict[str, Any]]) -> str:
|
|
67
|
+
"""Generate Python code from model-provider specifications."""
|
|
68
|
+
lines = [
|
|
69
|
+
"# Copyright (c) 2025-2026 Datalayer, Inc.",
|
|
70
|
+
"# Distributed under the terms of the Modified BSD License.",
|
|
71
|
+
'"""',
|
|
72
|
+
"Model Provider Catalog.",
|
|
73
|
+
"",
|
|
74
|
+
"Who serves a model: the vendor's own API, a cloud that hosts it, or the",
|
|
75
|
+
"user's machine — with its site, documentation, terms, privacy policy and",
|
|
76
|
+
"what it says about the data a request carries.",
|
|
77
|
+
"",
|
|
78
|
+
"This file is AUTO-GENERATED from YAML specifications.",
|
|
79
|
+
"DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
|
|
80
|
+
'"""',
|
|
81
|
+
"",
|
|
82
|
+
"from typing import Dict",
|
|
83
|
+
"",
|
|
84
|
+
"from agent_runtimes.types import ModelProvider",
|
|
85
|
+
"",
|
|
86
|
+
"",
|
|
87
|
+
"# " + "=" * 76,
|
|
88
|
+
"# Model Provider Definitions",
|
|
89
|
+
"# " + "=" * 76,
|
|
90
|
+
"",
|
|
91
|
+
]
|
|
92
|
+
for spec in specs:
|
|
93
|
+
const = _const_name(spec["id"]) + version_suffix(spec["version"])
|
|
94
|
+
lines.extend(
|
|
95
|
+
[
|
|
96
|
+
f"{const} = ModelProvider(",
|
|
97
|
+
f" id={_py(spec['id'])},",
|
|
98
|
+
f" version={_py(str(spec['version']))},",
|
|
99
|
+
f" name={_py(spec['name'])},",
|
|
100
|
+
f" description={_py(str(spec.get('description', '')).strip())},",
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
for field in FIELDS:
|
|
104
|
+
value = spec.get(field)
|
|
105
|
+
if value:
|
|
106
|
+
lines.append(f" {field}={_py(str(value))},")
|
|
107
|
+
lines.append(f" hosting={_py(spec.get('hosting', 'cloud'))},")
|
|
108
|
+
lines.extend([")", ""])
|
|
109
|
+
lines.extend(
|
|
110
|
+
[
|
|
111
|
+
"",
|
|
112
|
+
"# " + "=" * 76,
|
|
113
|
+
"# Model Provider Catalog",
|
|
114
|
+
"# " + "=" * 76,
|
|
115
|
+
"",
|
|
116
|
+
"MODEL_PROVIDER_CATALOGUE: Dict[str, ModelProvider] = {",
|
|
117
|
+
]
|
|
118
|
+
)
|
|
119
|
+
for spec in specs:
|
|
120
|
+
lines.append(
|
|
121
|
+
f" {_py(spec['id'])}: {_const_name(spec['id']) + version_suffix(spec['version'])},"
|
|
122
|
+
)
|
|
123
|
+
lines.extend(
|
|
124
|
+
[
|
|
125
|
+
"}",
|
|
126
|
+
"",
|
|
127
|
+
"",
|
|
128
|
+
"def get_model_provider(provider_id: str) -> ModelProvider | None:",
|
|
129
|
+
' """The provider a model spec\'s `provider` names, or None."""',
|
|
130
|
+
" return MODEL_PROVIDER_CATALOGUE.get(provider_id)",
|
|
131
|
+
"",
|
|
132
|
+
"",
|
|
133
|
+
"def list_model_providers() -> list[ModelProvider]:",
|
|
134
|
+
" return list(MODEL_PROVIDER_CATALOGUE.values())",
|
|
135
|
+
"",
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
return "\n".join(lines)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def generate_typescript_code(specs: list[dict[str, Any]]) -> str:
|
|
142
|
+
"""Generate TypeScript code from model-provider specifications."""
|
|
143
|
+
lines = [
|
|
144
|
+
"/*",
|
|
145
|
+
" * Copyright (c) 2025-2026 Datalayer, Inc.",
|
|
146
|
+
" * Distributed under the terms of the Modified BSD License.",
|
|
147
|
+
" */",
|
|
148
|
+
"",
|
|
149
|
+
"/**",
|
|
150
|
+
" * Model Provider Catalog.",
|
|
151
|
+
" *",
|
|
152
|
+
" * Who serves a model: the vendor's own API, a cloud that hosts it, or the",
|
|
153
|
+
" * user's machine — with its site, documentation, terms, privacy policy and",
|
|
154
|
+
" * what it says about the data a request carries.",
|
|
155
|
+
" *",
|
|
156
|
+
" * This file is AUTO-GENERATED from YAML specifications.",
|
|
157
|
+
" * DO NOT EDIT MANUALLY - run 'make specs' to regenerate.",
|
|
158
|
+
" */",
|
|
159
|
+
"",
|
|
160
|
+
"import type { ModelProvider } from '../types/models';",
|
|
161
|
+
"",
|
|
162
|
+
]
|
|
163
|
+
for spec in specs:
|
|
164
|
+
const = _const_name(spec["id"]) + version_suffix(spec["version"])
|
|
165
|
+
lines.extend(
|
|
166
|
+
[
|
|
167
|
+
f"export const {const}: ModelProvider = {{",
|
|
168
|
+
f" id: {_ts(spec['id'])},",
|
|
169
|
+
f" version: {_ts(str(spec['version']))},",
|
|
170
|
+
f" name: {_ts(spec['name'])},",
|
|
171
|
+
f" description: {_ts(str(spec.get('description', '')).strip())},",
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
for field in FIELDS:
|
|
175
|
+
value = spec.get(field)
|
|
176
|
+
if value:
|
|
177
|
+
lines.append(f" {CAMEL[field]}: {_ts(str(value))},")
|
|
178
|
+
lines.append(f" hosting: {_ts(spec.get('hosting', 'cloud'))},")
|
|
179
|
+
lines.extend(["};", ""])
|
|
180
|
+
lines.append(
|
|
181
|
+
"export const MODEL_PROVIDER_CATALOGUE: Record<string, ModelProvider> = {"
|
|
182
|
+
)
|
|
183
|
+
for spec in specs:
|
|
184
|
+
lines.append(
|
|
185
|
+
f" {_ts(spec['id'])}: {_const_name(spec['id']) + version_suffix(spec['version'])},"
|
|
186
|
+
)
|
|
187
|
+
lines.extend(
|
|
188
|
+
[
|
|
189
|
+
"};",
|
|
190
|
+
"",
|
|
191
|
+
"/** The provider a model spec's `provider` names, or undefined. */",
|
|
192
|
+
"export function getModelProvider(providerId: string): ModelProvider | undefined {",
|
|
193
|
+
" return MODEL_PROVIDER_CATALOGUE[providerId];",
|
|
194
|
+
"}",
|
|
195
|
+
"",
|
|
196
|
+
"export function listModelProviders(): ModelProvider[] {",
|
|
197
|
+
" return Object.values(MODEL_PROVIDER_CATALOGUE);",
|
|
198
|
+
"}",
|
|
199
|
+
"",
|
|
200
|
+
]
|
|
201
|
+
)
|
|
202
|
+
return "\n".join(lines)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
parser = argparse.ArgumentParser(
|
|
207
|
+
description="Generate Python and TypeScript code from YAML model-provider specifications"
|
|
208
|
+
)
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
"--specs-dir",
|
|
211
|
+
type=Path,
|
|
212
|
+
required=True,
|
|
213
|
+
help="Directory containing model-provider YAML files",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"--python-output", type=Path, required=True, help="Output path for Python file"
|
|
217
|
+
)
|
|
218
|
+
parser.add_argument(
|
|
219
|
+
"--typescript-output",
|
|
220
|
+
type=Path,
|
|
221
|
+
required=True,
|
|
222
|
+
help="Output path for TypeScript file",
|
|
223
|
+
)
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
if not args.specs_dir.exists():
|
|
227
|
+
print(f"Error: Specs directory not found: {args.specs_dir}", file=sys.stderr)
|
|
228
|
+
sys.exit(1)
|
|
229
|
+
specs = load_provider_specs(args.specs_dir)
|
|
230
|
+
if not specs:
|
|
231
|
+
print(
|
|
232
|
+
f"Warning: No model-provider specifications found in {args.specs_dir}",
|
|
233
|
+
file=sys.stderr,
|
|
234
|
+
)
|
|
235
|
+
return
|
|
236
|
+
args.python_output.parent.mkdir(parents=True, exist_ok=True)
|
|
237
|
+
args.python_output.write_text(generate_python_code(specs))
|
|
238
|
+
print(f"Generated Python code: {args.python_output}")
|
|
239
|
+
args.typescript_output.parent.mkdir(parents=True, exist_ok=True)
|
|
240
|
+
args.typescript_output.write_text(generate_typescript_code(specs))
|
|
241
|
+
print(f"Generated TypeScript code: {args.typescript_output}")
|
|
242
|
+
print(f"\n✓ Successfully generated code from {len(specs)} model-provider specs")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
main()
|