@openclaw/radius-provider 2026.9.5
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 +16 -0
- package/assets/activity.svg +4 -0
- package/assets/icon.png +0 -0
- package/dist/catalog.js +111 -0
- package/dist/index.js +170 -0
- package/dist/oauth.js +162 -0
- package/dist/stream.js +291 -0
- package/openclaw.plugin.json +40 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# OpenClaw Radius Provider
|
|
2
|
+
|
|
3
|
+
Connect OpenClaw to Earendil's Radius gateway with browser sign-in or an
|
|
4
|
+
organization API key. The plugin discovers account-visible models and supports
|
|
5
|
+
native Pi message streaming, reasoning, images on supported models, and tool calls.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
openclaw plugins install @openclaw/radius-provider
|
|
9
|
+
openclaw models auth login --provider radius --method oauth --set-default
|
|
10
|
+
openclaw models list --provider radius --refresh
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For API-key setups, use `--method api-key` or `RADIUS_API_KEY`.
|
|
14
|
+
Requests use the selected organization's credits and policies.
|
|
15
|
+
|
|
16
|
+
See [Radius setup and configuration](https://docs.openclaw.ai/providers/radius).
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
2
|
+
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M3 12a9 9 0 1 0 18 0a9 9 0 1 0 -18 0Z M12 12 19 5" />
|
|
3
|
+
<path fill="currentColor" stroke="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M10.5 12a1.5 1.5 0 1 0 3 0a1.5 1.5 0 1 0 -3 0Z" />
|
|
4
|
+
</svg>
|
package/assets/icon.png
ADDED
|
Binary file
|
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fetchLiveProviderModelRows } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
|
|
2
|
+
import { asFiniteNumberInRange, asOptionalRecord, asPositiveSafeInteger, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
|
+
//#region extensions/radius/catalog.ts
|
|
4
|
+
const RADIUS_BASE_URL = "https://radius.pi.dev/v1";
|
|
5
|
+
const THINKING_LEVELS = [
|
|
6
|
+
"off",
|
|
7
|
+
"minimal",
|
|
8
|
+
"low",
|
|
9
|
+
"medium",
|
|
10
|
+
"high",
|
|
11
|
+
"xhigh",
|
|
12
|
+
"max"
|
|
13
|
+
];
|
|
14
|
+
function readRates(value) {
|
|
15
|
+
const row = asOptionalRecord(value);
|
|
16
|
+
const input = asFiniteNumberInRange(row?.input, { min: 0 });
|
|
17
|
+
const output = asFiniteNumberInRange(row?.output, { min: 0 });
|
|
18
|
+
const cacheRead = asFiniteNumberInRange(row?.cacheRead, { min: 0 });
|
|
19
|
+
const cacheWrite = asFiniteNumberInRange(row?.cacheWrite, { min: 0 });
|
|
20
|
+
return input !== void 0 && output !== void 0 && cacheRead !== void 0 && cacheWrite !== void 0 ? {
|
|
21
|
+
input,
|
|
22
|
+
output,
|
|
23
|
+
cacheRead,
|
|
24
|
+
cacheWrite
|
|
25
|
+
} : void 0;
|
|
26
|
+
}
|
|
27
|
+
function readCost(value) {
|
|
28
|
+
const rates = readRates(value);
|
|
29
|
+
if (!rates) return;
|
|
30
|
+
const tiers = asOptionalRecord(value)?.tiers;
|
|
31
|
+
if (tiers === void 0) return rates;
|
|
32
|
+
if (!Array.isArray(tiers)) return;
|
|
33
|
+
const thresholds = [{
|
|
34
|
+
start: 0,
|
|
35
|
+
rates
|
|
36
|
+
}];
|
|
37
|
+
for (const tier of tiers) {
|
|
38
|
+
const tierRates = readRates(tier);
|
|
39
|
+
const threshold = asFiniteNumberInRange(asOptionalRecord(tier)?.inputTokensAbove, { min: 0 });
|
|
40
|
+
if (!tierRates || threshold === void 0 || !Number.isSafeInteger(threshold + 1)) return;
|
|
41
|
+
thresholds.push({
|
|
42
|
+
start: threshold + 1,
|
|
43
|
+
rates: tierRates
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
thresholds.sort((left, right) => left.start - right.start);
|
|
47
|
+
if (thresholds.some((tier, index) => index > 0 && tier.start === thresholds[index - 1]?.start)) return;
|
|
48
|
+
return {
|
|
49
|
+
...rates,
|
|
50
|
+
...tiers.length > 0 ? { tieredPricing: thresholds.map((tier, index) => {
|
|
51
|
+
const next = thresholds[index + 1];
|
|
52
|
+
const range = next ? [tier.start, next.start] : [tier.start];
|
|
53
|
+
return Object.assign({}, tier.rates, { range });
|
|
54
|
+
}) } : {}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function readModel(value) {
|
|
58
|
+
const row = asOptionalRecord(value);
|
|
59
|
+
const id = normalizeOptionalString(row?.id);
|
|
60
|
+
const name = normalizeOptionalString(row?.name);
|
|
61
|
+
const cost = readCost(row?.cost);
|
|
62
|
+
const contextWindow = asPositiveSafeInteger(row?.contextWindow);
|
|
63
|
+
const maxTokens = asPositiveSafeInteger(row?.maxTokens);
|
|
64
|
+
if (!row || !id || !name || !cost || !contextWindow || !maxTokens || row.enabled === false || typeof row.reasoning !== "boolean" || !Array.isArray(row.input) || row.input.length === 0 || row.input.some((input) => input !== "text" && input !== "image")) return;
|
|
65
|
+
const input = row.input.filter((item) => item === "text" || item === "image");
|
|
66
|
+
let thinkingLevelMap;
|
|
67
|
+
if (row.thinkingLevelMap !== void 0) {
|
|
68
|
+
const levels = asOptionalRecord(row.thinkingLevelMap);
|
|
69
|
+
if (!levels) return;
|
|
70
|
+
thinkingLevelMap = {};
|
|
71
|
+
for (const level of THINKING_LEVELS) {
|
|
72
|
+
const mapped = levels[level];
|
|
73
|
+
if (mapped !== void 0) {
|
|
74
|
+
if (mapped !== null && typeof mapped !== "string") return;
|
|
75
|
+
thinkingLevelMap[level] = mapped;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
id,
|
|
81
|
+
name,
|
|
82
|
+
reasoning: row.reasoning,
|
|
83
|
+
input,
|
|
84
|
+
cost,
|
|
85
|
+
contextWindow,
|
|
86
|
+
maxTokens,
|
|
87
|
+
...thinkingLevelMap ? { thinkingLevelMap } : {}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function fetchRadiusCatalog(apiKey, signal) {
|
|
91
|
+
const documents = await fetchLiveProviderModelRows({
|
|
92
|
+
providerId: "radius",
|
|
93
|
+
endpoint: `${RADIUS_BASE_URL}/config`,
|
|
94
|
+
discoveryApiKey: apiKey,
|
|
95
|
+
signal,
|
|
96
|
+
requireHttps: true,
|
|
97
|
+
readRows: (body) => [body]
|
|
98
|
+
});
|
|
99
|
+
const config = asOptionalRecord(documents[0]);
|
|
100
|
+
const baseUrl = normalizeOptionalString(config?.baseUrl);
|
|
101
|
+
if (!baseUrl || !Array.isArray(config?.models)) throw new Error("Invalid Radius catalog: expected baseUrl and models");
|
|
102
|
+
const endpoint = new URL(baseUrl);
|
|
103
|
+
if (endpoint.protocol !== "https:" || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) throw new Error("Invalid Radius catalog base URL");
|
|
104
|
+
return {
|
|
105
|
+
baseUrl: baseUrl.replace(/\/+$/u, ""),
|
|
106
|
+
api: "pi-messages",
|
|
107
|
+
models: config.models.map(readModel).filter((model) => model !== void 0)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { RADIUS_BASE_URL, fetchRadiusCatalog };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
|
|
2
|
+
import { buildOauthProviderAuthResult, resolveOAuthApiKeyMarker } from "openclaw/plugin-sdk/provider-auth";
|
|
3
|
+
import { runLiveProviderCatalog } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
|
|
4
|
+
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
|
|
5
|
+
//#region extensions/radius/openclaw.plugin.json
|
|
6
|
+
var openclaw_plugin_default = {
|
|
7
|
+
id: "radius",
|
|
8
|
+
name: "Radius",
|
|
9
|
+
description: "Radius model gateway provider.",
|
|
10
|
+
categories: ["models"],
|
|
11
|
+
activation: { "onStartup": false },
|
|
12
|
+
enabledByDefault: true,
|
|
13
|
+
providers: ["radius"],
|
|
14
|
+
modelCatalog: { "discovery": { "radius": "refreshable" } },
|
|
15
|
+
setup: { "providers": [{
|
|
16
|
+
"id": "radius",
|
|
17
|
+
"envVars": ["RADIUS_API_KEY"]
|
|
18
|
+
}] },
|
|
19
|
+
providerAuthChoices: [{
|
|
20
|
+
"provider": "radius",
|
|
21
|
+
"method": "oauth",
|
|
22
|
+
"choiceId": "radius",
|
|
23
|
+
"appGuidedAuth": "oauth",
|
|
24
|
+
"credentialOnly": true,
|
|
25
|
+
"channelLogin": {},
|
|
26
|
+
"choiceLabel": "Radius (browser sign-in)",
|
|
27
|
+
"groupId": "radius",
|
|
28
|
+
"groupLabel": "Radius",
|
|
29
|
+
"groupHint": "Browser sign-in or API key"
|
|
30
|
+
}, {
|
|
31
|
+
"provider": "radius",
|
|
32
|
+
"method": "api-key",
|
|
33
|
+
"choiceId": "radius-api-key",
|
|
34
|
+
"appGuidedSecret": true,
|
|
35
|
+
"choiceLabel": "Radius API key",
|
|
36
|
+
"groupId": "radius",
|
|
37
|
+
"groupLabel": "Radius",
|
|
38
|
+
"groupHint": "Browser sign-in or API key",
|
|
39
|
+
"optionKey": "radiusApiKey",
|
|
40
|
+
"cliFlag": "--radius-api-key",
|
|
41
|
+
"cliOption": "--radius-api-key <key>",
|
|
42
|
+
"cliDescription": "Radius organization API key"
|
|
43
|
+
}],
|
|
44
|
+
configSchema: {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"additionalProperties": false,
|
|
47
|
+
"properties": {}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region extensions/radius/index.ts
|
|
52
|
+
const loadCatalog = createLazyRuntimeModule(() => import("./catalog.js"));
|
|
53
|
+
const loadOAuth = createLazyRuntimeModule(() => import("./oauth.js"));
|
|
54
|
+
const loadStream = createLazyRuntimeModule(() => import("./stream.js"));
|
|
55
|
+
function defaultModel(catalog) {
|
|
56
|
+
const model = catalog.models.find((entry) => entry.id === "balanced") ?? catalog.models[0];
|
|
57
|
+
if (!model) throw new Error("Radius returned no available models. Check your organization's model access.");
|
|
58
|
+
return `radius/${model.id}`;
|
|
59
|
+
}
|
|
60
|
+
var radius_default = defineSingleProviderPluginEntry({
|
|
61
|
+
id: "radius",
|
|
62
|
+
name: "Radius Provider",
|
|
63
|
+
description: "Radius model gateway with organization-scoped authentication and native streaming",
|
|
64
|
+
manifest: openclaw_plugin_default,
|
|
65
|
+
provider: {
|
|
66
|
+
label: "Radius",
|
|
67
|
+
docsPath: "/providers/radius",
|
|
68
|
+
manifestAuth: {
|
|
69
|
+
noteTitle: "Radius",
|
|
70
|
+
noteMessage: "Create an organization API key at https://radius.earendil.com. Requests use that organization's credits and policies.",
|
|
71
|
+
resolveDefaultModel: async ({ apiKey, signal }) => defaultModel(await (await loadCatalog()).fetchRadiusCatalog(apiKey, signal))
|
|
72
|
+
},
|
|
73
|
+
extraAuth: [{
|
|
74
|
+
id: "oauth",
|
|
75
|
+
label: "Radius browser sign-in",
|
|
76
|
+
hint: "Pair your browser and choose a Radius organization",
|
|
77
|
+
kind: "device_code",
|
|
78
|
+
wizard: {
|
|
79
|
+
choiceId: "radius",
|
|
80
|
+
choiceLabel: "Radius (browser sign-in)",
|
|
81
|
+
groupId: "radius",
|
|
82
|
+
groupLabel: "Radius",
|
|
83
|
+
groupHint: "Browser sign-in or API key"
|
|
84
|
+
},
|
|
85
|
+
run: async (ctx) => {
|
|
86
|
+
const credentials = await (await loadOAuth()).loginRadiusOAuth(ctx);
|
|
87
|
+
const notes = ["Radius access is scoped to the organization selected in your browser. Tokens refresh automatically."];
|
|
88
|
+
if (ctx.credentialOnly) {
|
|
89
|
+
ctx.assertCurrent?.();
|
|
90
|
+
return {
|
|
91
|
+
profiles: [{
|
|
92
|
+
profileId: "radius:default",
|
|
93
|
+
credential: {
|
|
94
|
+
type: "oauth",
|
|
95
|
+
provider: "radius",
|
|
96
|
+
...credentials
|
|
97
|
+
}
|
|
98
|
+
}],
|
|
99
|
+
notes
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const catalog = await (await loadCatalog()).fetchRadiusCatalog(credentials.access, ctx.signal);
|
|
103
|
+
ctx.assertCurrent?.();
|
|
104
|
+
return buildOauthProviderAuthResult({
|
|
105
|
+
providerId: "radius",
|
|
106
|
+
defaultModel: defaultModel(catalog),
|
|
107
|
+
access: credentials.access,
|
|
108
|
+
refresh: credentials.refresh,
|
|
109
|
+
expires: credentials.expires,
|
|
110
|
+
notes
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}],
|
|
114
|
+
catalog: {
|
|
115
|
+
order: "profile",
|
|
116
|
+
run: async (ctx) => {
|
|
117
|
+
const { apiKey, discoveryApiKey, profileId } = ctx.resolveProviderAuth("radius", { oauthMarker: resolveOAuthApiKeyMarker("radius") });
|
|
118
|
+
if (!discoveryApiKey) return null;
|
|
119
|
+
return await runLiveProviderCatalog({
|
|
120
|
+
providerId: "radius",
|
|
121
|
+
profileId,
|
|
122
|
+
run: async () => ({ provider: {
|
|
123
|
+
...await (await loadCatalog()).fetchRadiusCatalog(discoveryApiKey),
|
|
124
|
+
apiKey
|
|
125
|
+
} })
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
prepareDynamicModel: async (ctx) => {
|
|
130
|
+
const { resolveApiKeyForProvider } = await import("openclaw/plugin-sdk/provider-auth-runtime");
|
|
131
|
+
const { apiKey } = await resolveApiKeyForProvider({
|
|
132
|
+
provider: "radius",
|
|
133
|
+
cfg: ctx.config,
|
|
134
|
+
agentDir: ctx.agentDir,
|
|
135
|
+
workspaceDir: ctx.workspaceDir,
|
|
136
|
+
...ctx.authProfileId ? {
|
|
137
|
+
profileId: ctx.authProfileId,
|
|
138
|
+
lockedProfile: true
|
|
139
|
+
} : {}
|
|
140
|
+
});
|
|
141
|
+
if (!apiKey) return;
|
|
142
|
+
const catalog = await (await loadCatalog()).fetchRadiusCatalog(apiKey);
|
|
143
|
+
const model = catalog.models.find((entry) => entry.id === ctx.modelId);
|
|
144
|
+
return model ? {
|
|
145
|
+
id: model.id,
|
|
146
|
+
name: model.name,
|
|
147
|
+
provider: "radius",
|
|
148
|
+
api: "pi-messages",
|
|
149
|
+
baseUrl: catalog.baseUrl,
|
|
150
|
+
reasoning: model.reasoning,
|
|
151
|
+
input: model.input,
|
|
152
|
+
cost: model.cost,
|
|
153
|
+
contextWindow: model.contextWindow,
|
|
154
|
+
maxTokens: model.maxTokens,
|
|
155
|
+
thinkingLevelMap: model.thinkingLevelMap
|
|
156
|
+
} : void 0;
|
|
157
|
+
},
|
|
158
|
+
createStreamFn: () => async (...args) => (await loadStream()).createRadiusStreamFn()(...args),
|
|
159
|
+
buildReplayPolicy: () => ({
|
|
160
|
+
sanitizeToolCallIds: false,
|
|
161
|
+
preserveSignatures: true,
|
|
162
|
+
appendOnlyRuntimeContext: true,
|
|
163
|
+
dropThinkingBlocks: false,
|
|
164
|
+
dropReasoningFromHistory: false
|
|
165
|
+
}),
|
|
166
|
+
refreshOAuth: async (credential) => (await loadOAuth()).refreshRadiusOAuthCredential(credential)
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
//#endregion
|
|
170
|
+
export { radius_default as default };
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { asFiniteNumberInRange } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
import { MAX_TIMER_TIMEOUT_MS, positiveSecondsToSafeMilliseconds, resolveExpiresAtMsFromDurationMs } from "openclaw/plugin-sdk/number-runtime";
|
|
3
|
+
import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http";
|
|
4
|
+
import { throwIfOAuthLoginAborted } from "openclaw/plugin-sdk/provider-oauth-runtime";
|
|
5
|
+
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
|
6
|
+
import { sleep } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
7
|
+
//#region extensions/radius/oauth.ts
|
|
8
|
+
const OAUTH_BASE_URL = "https://radius.pi.dev/v1/oauth";
|
|
9
|
+
const CLIENT_ID = "pi-gateway";
|
|
10
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
11
|
+
var RadiusOAuthError = class extends Error {
|
|
12
|
+
constructor(code, status) {
|
|
13
|
+
super(`Radius OAuth request failed (HTTP ${status}). Retry sign-in.`);
|
|
14
|
+
this.code = typeof code === "string" && [
|
|
15
|
+
"authorization_pending",
|
|
16
|
+
"slow_down",
|
|
17
|
+
"access_denied",
|
|
18
|
+
"expired_token"
|
|
19
|
+
].includes(code) ? code : void 0;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
async function postOAuthForm(endpoint, fields, signal, beforeRequest) {
|
|
23
|
+
const { response, release } = await fetchWithSsrFGuard({
|
|
24
|
+
url: `${OAUTH_BASE_URL}/${endpoint}`,
|
|
25
|
+
init: {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: {
|
|
28
|
+
Accept: "application/json",
|
|
29
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
30
|
+
},
|
|
31
|
+
body: new URLSearchParams({
|
|
32
|
+
client_id: CLIENT_ID,
|
|
33
|
+
...fields
|
|
34
|
+
})
|
|
35
|
+
},
|
|
36
|
+
signal,
|
|
37
|
+
beforeRequest,
|
|
38
|
+
timeoutMs: REQUEST_TIMEOUT_MS,
|
|
39
|
+
requireHttps: true,
|
|
40
|
+
policy: { hostnameAllowlist: ["radius.pi.dev"] },
|
|
41
|
+
auditContext: `radius.oauth.${endpoint}`
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const payload = await readProviderJsonObjectResponse(response, "Radius OAuth", {
|
|
45
|
+
maxBytes: 65536,
|
|
46
|
+
requestHeaders: {}
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok || payload.error !== void 0) throw new RadiusOAuthError(payload.error, response.status);
|
|
49
|
+
return payload;
|
|
50
|
+
} finally {
|
|
51
|
+
await release();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function parseToken(payload, previousRefresh) {
|
|
55
|
+
const lifetimeMs = positiveSecondsToSafeMilliseconds(payload.expires_in);
|
|
56
|
+
const expires = lifetimeMs === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(lifetimeMs, { bufferMs: Math.min(6e4, Math.floor(lifetimeMs / 2)) });
|
|
57
|
+
const refresh = payload.refresh_token ?? previousRefresh;
|
|
58
|
+
if (typeof payload.access_token !== "string" || !payload.access_token.trim() || typeof refresh !== "string" || !refresh.trim() || expires === void 0 || payload.scope !== void 0 && typeof payload.scope !== "string") throw new Error("Radius OAuth returned invalid credentials. Retry sign-in.");
|
|
59
|
+
return {
|
|
60
|
+
access: payload.access_token,
|
|
61
|
+
refresh,
|
|
62
|
+
expires
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function parseDevice(payload) {
|
|
66
|
+
const lifetimeMs = positiveSecondsToSafeMilliseconds(payload.expires_in);
|
|
67
|
+
const expiresAt = lifetimeMs === void 0 ? void 0 : resolveExpiresAtMsFromDurationMs(lifetimeMs);
|
|
68
|
+
const intervalSeconds = payload.interval === void 0 ? 5 : asFiniteNumberInRange(payload.interval, {
|
|
69
|
+
min: Number.MIN_VALUE,
|
|
70
|
+
max: MAX_TIMER_TIMEOUT_MS / 1e3
|
|
71
|
+
});
|
|
72
|
+
const intervalMs = intervalSeconds === void 0 ? void 0 : Math.ceil(intervalSeconds * 1e3);
|
|
73
|
+
if (typeof payload.device_code !== "string" || !payload.device_code.trim() || typeof payload.user_code !== "string" || !payload.user_code.trim() || typeof payload.verification_uri !== "string" || expiresAt === void 0 || intervalMs === void 0 || intervalMs > MAX_TIMER_TIMEOUT_MS) throw new Error("Radius OAuth returned an invalid device authorization response.");
|
|
74
|
+
let url;
|
|
75
|
+
try {
|
|
76
|
+
url = new URL(payload.verification_uri);
|
|
77
|
+
} catch {
|
|
78
|
+
throw new Error("Radius OAuth returned an invalid verification URL.");
|
|
79
|
+
}
|
|
80
|
+
if (url.origin !== "https://radius.earendil.com" || url.username || url.password) throw new Error("Radius OAuth returned an invalid verification URL.");
|
|
81
|
+
return {
|
|
82
|
+
deviceCode: payload.device_code,
|
|
83
|
+
userCode: payload.user_code,
|
|
84
|
+
verificationUri: url.href,
|
|
85
|
+
expiresAt,
|
|
86
|
+
intervalMs
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function loginRadiusOAuth(ctx) {
|
|
90
|
+
const assertCurrent = () => {
|
|
91
|
+
throwIfOAuthLoginAborted(ctx.signal);
|
|
92
|
+
ctx.assertCurrent?.();
|
|
93
|
+
};
|
|
94
|
+
assertCurrent();
|
|
95
|
+
const payload = await postOAuthForm("device", { scope: "gateway offline_access" }, ctx.signal, assertCurrent);
|
|
96
|
+
assertCurrent();
|
|
97
|
+
const device = parseDevice(payload);
|
|
98
|
+
if (ctx.prompter.deviceCode) {
|
|
99
|
+
await ctx.openUrl(device.verificationUri);
|
|
100
|
+
await ctx.prompter.deviceCode({
|
|
101
|
+
title: "Radius sign-in",
|
|
102
|
+
code: device.userCode,
|
|
103
|
+
expiresInMinutes: Math.ceil((device.expiresAt - Date.now()) / 6e4),
|
|
104
|
+
message: "Enter this one-time code to sign in to Radius."
|
|
105
|
+
});
|
|
106
|
+
} else await ctx.prompter.note(`Open ${device.verificationUri} and enter code ${device.userCode} to sign in to Radius.`, "Radius sign-in");
|
|
107
|
+
assertCurrent();
|
|
108
|
+
if (!ctx.isRemote && !ctx.prompter.deviceCode) {
|
|
109
|
+
try {
|
|
110
|
+
await ctx.openUrl(device.verificationUri);
|
|
111
|
+
} catch {}
|
|
112
|
+
assertCurrent();
|
|
113
|
+
}
|
|
114
|
+
const progress = ctx.prompter.progress("Waiting for Radius approval…");
|
|
115
|
+
let completed = false;
|
|
116
|
+
try {
|
|
117
|
+
let intervalMs = device.intervalMs;
|
|
118
|
+
while (Date.now() < device.expiresAt) {
|
|
119
|
+
await sleep(Math.min(intervalMs, device.expiresAt - Date.now()), ctx.signal);
|
|
120
|
+
assertCurrent();
|
|
121
|
+
if (Date.now() >= device.expiresAt) break;
|
|
122
|
+
try {
|
|
123
|
+
const token = await postOAuthForm("token", {
|
|
124
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
125
|
+
device_code: device.deviceCode
|
|
126
|
+
}, ctx.signal, assertCurrent);
|
|
127
|
+
assertCurrent();
|
|
128
|
+
const credentials = parseToken(token);
|
|
129
|
+
completed = true;
|
|
130
|
+
return credentials;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
assertCurrent();
|
|
133
|
+
if (!(error instanceof RadiusOAuthError)) throw error;
|
|
134
|
+
switch (error.code) {
|
|
135
|
+
case "authorization_pending": break;
|
|
136
|
+
case "slow_down":
|
|
137
|
+
intervalMs = Math.min(intervalMs + 5e3, MAX_TIMER_TIMEOUT_MS);
|
|
138
|
+
break;
|
|
139
|
+
case "access_denied": throw new Error("Radius sign-in was denied. Retry sign-in when ready.", { cause: error });
|
|
140
|
+
case "expired_token": throw new Error("Radius device code expired. Start sign-in again.", { cause: error });
|
|
141
|
+
default: throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw new Error("Radius device code expired. Start sign-in again.");
|
|
146
|
+
} finally {
|
|
147
|
+
progress.stop(completed ? "Radius sign-in complete" : "Radius sign-in stopped");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function refreshRadiusOAuthCredential(credential) {
|
|
151
|
+
if (!credential.refresh.trim()) throw new Error("Radius refresh token is missing. Sign in again.");
|
|
152
|
+
const payload = await postOAuthForm("token", {
|
|
153
|
+
grant_type: "refresh_token",
|
|
154
|
+
refresh_token: credential.refresh
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
...credential,
|
|
158
|
+
...parseToken(payload, credential.refresh)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { loginRadiusOAuth, refreshRadiusOAuthCredential };
|
package/dist/stream.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
import { createProviderHttpError } from "openclaw/plugin-sdk/provider-http";
|
|
3
|
+
import { createAssistantMessageEventStream, createToolArgumentPreviewSchedule, parseStreamingJson } from "openclaw/plugin-sdk/llm";
|
|
4
|
+
import { buildGuardedModelFetch, createEmptyTransportUsage, failTransportStream, notifyProviderHttpResponse, parseTerminalToolCallArguments } from "openclaw/plugin-sdk/provider-transport-runtime";
|
|
5
|
+
//#region extensions/radius/stream.ts
|
|
6
|
+
const MAX_SSE_EVENT_BYTES = 16777216;
|
|
7
|
+
function record(value) {
|
|
8
|
+
const parsed = asOptionalRecord(value);
|
|
9
|
+
if (!parsed) throw new Error("Invalid Radius stream event object");
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
function string(value) {
|
|
13
|
+
if (typeof value !== "string") throw new Error("Invalid Radius stream string");
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function number(value) {
|
|
17
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw new Error("Invalid Radius usage value");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function usage(value) {
|
|
21
|
+
const raw = record(value);
|
|
22
|
+
const cost = record(raw.cost);
|
|
23
|
+
return {
|
|
24
|
+
...raw,
|
|
25
|
+
input: number(raw.input),
|
|
26
|
+
output: number(raw.output),
|
|
27
|
+
cacheRead: number(raw.cacheRead),
|
|
28
|
+
cacheWrite: number(raw.cacheWrite),
|
|
29
|
+
totalTokens: number(raw.totalTokens),
|
|
30
|
+
cost: {
|
|
31
|
+
...cost,
|
|
32
|
+
input: number(cost.input),
|
|
33
|
+
output: number(cost.output),
|
|
34
|
+
cacheRead: number(cost.cacheRead),
|
|
35
|
+
cacheWrite: number(cost.cacheWrite),
|
|
36
|
+
total: number(cost.total)
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createEventConverter(partial) {
|
|
41
|
+
const toolJson = /* @__PURE__ */ new Map();
|
|
42
|
+
return (raw) => {
|
|
43
|
+
const event = record(raw);
|
|
44
|
+
const type = string(event.type);
|
|
45
|
+
if (type === "done" || type === "error") {
|
|
46
|
+
partial.usage = usage(event.usage);
|
|
47
|
+
if (event.responseId !== void 0) partial.responseId = string(event.responseId);
|
|
48
|
+
const reason = event.reason;
|
|
49
|
+
if (type === "done" && (reason === "stop" || reason === "length" || reason === "toolUse")) {
|
|
50
|
+
if (toolJson.size > 0) throw new Error("Radius stream ended with an unfinished tool call");
|
|
51
|
+
partial.stopReason = reason;
|
|
52
|
+
return {
|
|
53
|
+
type,
|
|
54
|
+
reason,
|
|
55
|
+
message: partial
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (type === "error" && (reason === "error" || reason === "aborted")) {
|
|
59
|
+
partial.stopReason = reason;
|
|
60
|
+
partial.errorMessage = event.errorMessage === void 0 ? "Radius request failed" : string(event.errorMessage);
|
|
61
|
+
return {
|
|
62
|
+
type,
|
|
63
|
+
reason,
|
|
64
|
+
error: partial
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
throw new Error("Invalid Radius terminal reason");
|
|
68
|
+
}
|
|
69
|
+
if (type === "start") return {
|
|
70
|
+
type,
|
|
71
|
+
partial
|
|
72
|
+
};
|
|
73
|
+
const contentIndex = event.contentIndex;
|
|
74
|
+
if (typeof contentIndex !== "number" || !Number.isSafeInteger(contentIndex) || contentIndex < 0) throw new Error("Invalid Radius content index");
|
|
75
|
+
if (type === "text_start" || type === "thinking_start" || type === "toolcall_start") {
|
|
76
|
+
if (contentIndex !== partial.content.length) throw new Error("Out-of-order Radius content block");
|
|
77
|
+
if (type === "text_start") partial.content.push({
|
|
78
|
+
type: "text",
|
|
79
|
+
text: ""
|
|
80
|
+
});
|
|
81
|
+
else if (type === "thinking_start") partial.content.push({
|
|
82
|
+
type: "thinking",
|
|
83
|
+
thinking: ""
|
|
84
|
+
});
|
|
85
|
+
else {
|
|
86
|
+
partial.content.push({
|
|
87
|
+
type: "toolCall",
|
|
88
|
+
id: string(event.id),
|
|
89
|
+
name: string(event.toolName),
|
|
90
|
+
arguments: {}
|
|
91
|
+
});
|
|
92
|
+
toolJson.set(contentIndex, {
|
|
93
|
+
json: "",
|
|
94
|
+
shouldPreview: createToolArgumentPreviewSchedule()
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
type,
|
|
99
|
+
contentIndex,
|
|
100
|
+
partial
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const block = partial.content[contentIndex];
|
|
104
|
+
switch (type) {
|
|
105
|
+
case "text_delta": {
|
|
106
|
+
if (block?.type !== "text") break;
|
|
107
|
+
const delta = string(event.delta);
|
|
108
|
+
block.text += delta;
|
|
109
|
+
return {
|
|
110
|
+
type,
|
|
111
|
+
contentIndex,
|
|
112
|
+
delta,
|
|
113
|
+
partial
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
case "text_end":
|
|
117
|
+
if (block?.type !== "text") break;
|
|
118
|
+
block.text = string(event.content);
|
|
119
|
+
if (event.contentSignature !== void 0) block.textSignature = string(event.contentSignature);
|
|
120
|
+
return {
|
|
121
|
+
type,
|
|
122
|
+
contentIndex,
|
|
123
|
+
content: block.text,
|
|
124
|
+
partial
|
|
125
|
+
};
|
|
126
|
+
case "thinking_delta": {
|
|
127
|
+
if (block?.type !== "thinking") break;
|
|
128
|
+
const delta = string(event.delta);
|
|
129
|
+
block.thinking += delta;
|
|
130
|
+
return {
|
|
131
|
+
type,
|
|
132
|
+
contentIndex,
|
|
133
|
+
delta,
|
|
134
|
+
partial
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "thinking_end":
|
|
138
|
+
if (block?.type !== "thinking") break;
|
|
139
|
+
block.thinking = string(event.content);
|
|
140
|
+
if (event.contentSignature !== void 0) block.thinkingSignature = string(event.contentSignature);
|
|
141
|
+
if (event.redacted !== void 0 && typeof event.redacted !== "boolean") throw new Error("Invalid Radius redacted thinking flag");
|
|
142
|
+
block.redacted = event.redacted;
|
|
143
|
+
return {
|
|
144
|
+
type,
|
|
145
|
+
contentIndex,
|
|
146
|
+
content: block.thinking,
|
|
147
|
+
partial
|
|
148
|
+
};
|
|
149
|
+
case "toolcall_delta": {
|
|
150
|
+
const pending = toolJson.get(contentIndex);
|
|
151
|
+
if (block?.type !== "toolCall" || !pending) break;
|
|
152
|
+
const delta = string(event.delta);
|
|
153
|
+
pending.json += delta;
|
|
154
|
+
if (pending.shouldPreview(pending.json.length)) block.arguments = parseStreamingJson(pending.json);
|
|
155
|
+
return {
|
|
156
|
+
type,
|
|
157
|
+
contentIndex,
|
|
158
|
+
delta,
|
|
159
|
+
partial
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
case "toolcall_end": {
|
|
163
|
+
if (block?.type !== "toolCall" || !toolJson.has(contentIndex)) break;
|
|
164
|
+
const call = record(event.toolCall);
|
|
165
|
+
if (call.type !== "toolCall" || call.id !== block.id || call.name !== block.name) throw new Error("Radius terminal tool call does not match its start");
|
|
166
|
+
block.arguments = parseTerminalToolCallArguments(call.arguments);
|
|
167
|
+
if (call.thoughtSignature !== void 0) block.thoughtSignature = string(call.thoughtSignature);
|
|
168
|
+
toolJson.delete(contentIndex);
|
|
169
|
+
return {
|
|
170
|
+
type,
|
|
171
|
+
contentIndex,
|
|
172
|
+
toolCall: block,
|
|
173
|
+
partial
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw new Error("Invalid Radius stream event or content block sequence");
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async function* readEvents(body, signal) {
|
|
181
|
+
const reader = body.getReader();
|
|
182
|
+
const decoder = new TextDecoder();
|
|
183
|
+
let buffer = "";
|
|
184
|
+
const cancel = () => {
|
|
185
|
+
reader.cancel(signal?.reason).catch(() => void 0);
|
|
186
|
+
};
|
|
187
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
188
|
+
try {
|
|
189
|
+
while (true) {
|
|
190
|
+
signal?.throwIfAborted();
|
|
191
|
+
const { done, value } = await reader.read();
|
|
192
|
+
signal?.throwIfAborted();
|
|
193
|
+
buffer += done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
194
|
+
for (;;) {
|
|
195
|
+
const boundary = /\r\n\r\n|\n\n|\r\r/u.exec(buffer);
|
|
196
|
+
if (!boundary && !done) break;
|
|
197
|
+
const frame = boundary ? buffer.slice(0, boundary.index) : buffer;
|
|
198
|
+
if (Buffer.byteLength(frame, "utf8") > MAX_SSE_EVENT_BYTES) throw new Error("Radius SSE event exceeds 16 MiB");
|
|
199
|
+
buffer = boundary ? buffer.slice(boundary.index + boundary[0].length) : "";
|
|
200
|
+
const data = frame.split(/\r\n|\n|\r/u).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
|
|
201
|
+
if (data && data !== "[DONE]") yield JSON.parse(data);
|
|
202
|
+
if (!boundary) break;
|
|
203
|
+
}
|
|
204
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_SSE_EVENT_BYTES) throw new Error("Radius SSE event exceeds 16 MiB");
|
|
205
|
+
if (done) return;
|
|
206
|
+
}
|
|
207
|
+
} finally {
|
|
208
|
+
signal?.removeEventListener("abort", cancel);
|
|
209
|
+
await reader.cancel().catch(() => void 0);
|
|
210
|
+
reader.releaseLock();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Radius speaks Pi's native message protocol, not an OpenAI-compatible API. */
|
|
214
|
+
function createRadiusStreamFn() {
|
|
215
|
+
return (model, context, options) => {
|
|
216
|
+
const stream = createAssistantMessageEventStream();
|
|
217
|
+
const partial = {
|
|
218
|
+
role: "assistant",
|
|
219
|
+
content: [],
|
|
220
|
+
api: model.api,
|
|
221
|
+
provider: model.provider,
|
|
222
|
+
model: model.id,
|
|
223
|
+
usage: createEmptyTransportUsage(),
|
|
224
|
+
stopReason: "stop",
|
|
225
|
+
timestamp: Date.now()
|
|
226
|
+
};
|
|
227
|
+
const convert = createEventConverter(partial);
|
|
228
|
+
(async () => {
|
|
229
|
+
let response;
|
|
230
|
+
try {
|
|
231
|
+
if (!options?.apiKey) throw new Error("No Radius access token configured. Run openclaw models auth login --provider radius.");
|
|
232
|
+
options.signal?.throwIfAborted();
|
|
233
|
+
let payload = {
|
|
234
|
+
model: model.id,
|
|
235
|
+
context,
|
|
236
|
+
options: {
|
|
237
|
+
temperature: options.temperature,
|
|
238
|
+
maxTokens: options.maxTokens,
|
|
239
|
+
reasoning: options.reasoning === "off" ? void 0 : options.reasoning,
|
|
240
|
+
cacheRetention: options.cacheRetention,
|
|
241
|
+
sessionId: options.sessionId,
|
|
242
|
+
toolChoice: options.toolChoice
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const replacement = await options.onPayload?.(payload, model);
|
|
246
|
+
if (replacement !== void 0) payload = replacement;
|
|
247
|
+
options.signal?.throwIfAborted();
|
|
248
|
+
response = await buildGuardedModelFetch(model, options.timeoutMs, { sanitizeSse: false })(`${model.baseUrl.replace(/\/+$/u, "")}/messages`, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: {
|
|
251
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
252
|
+
accept: "text/event-stream",
|
|
253
|
+
"content-type": "application/json",
|
|
254
|
+
...model.headers,
|
|
255
|
+
...options.headers
|
|
256
|
+
},
|
|
257
|
+
body: JSON.stringify(payload),
|
|
258
|
+
signal: options.signal
|
|
259
|
+
});
|
|
260
|
+
await notifyProviderHttpResponse({
|
|
261
|
+
options,
|
|
262
|
+
response,
|
|
263
|
+
model
|
|
264
|
+
});
|
|
265
|
+
if (!response.ok) throw await createProviderHttpError(response, "Radius request failed");
|
|
266
|
+
if (!response.body) throw new Error("Radius response has no body");
|
|
267
|
+
for await (const raw of readEvents(response.body, options.signal)) {
|
|
268
|
+
const event = convert(raw);
|
|
269
|
+
stream.push(event);
|
|
270
|
+
if (event.type === "done" || event.type === "error") {
|
|
271
|
+
stream.end();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
throw new Error("Radius stream ended without a terminal event");
|
|
276
|
+
} catch (error) {
|
|
277
|
+
failTransportStream({
|
|
278
|
+
stream,
|
|
279
|
+
output: partial,
|
|
280
|
+
signal: options?.signal,
|
|
281
|
+
error
|
|
282
|
+
});
|
|
283
|
+
} finally {
|
|
284
|
+
await response?.body?.cancel().catch(() => void 0);
|
|
285
|
+
}
|
|
286
|
+
})();
|
|
287
|
+
return stream;
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
export { createRadiusStreamFn };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "radius",
|
|
3
|
+
"name": "Radius",
|
|
4
|
+
"description": "Radius model gateway provider.",
|
|
5
|
+
"categories": ["models"],
|
|
6
|
+
"activation": { "onStartup": false },
|
|
7
|
+
"enabledByDefault": true,
|
|
8
|
+
"providers": ["radius"],
|
|
9
|
+
"modelCatalog": { "discovery": { "radius": "refreshable" } },
|
|
10
|
+
"setup": { "providers": [{ "id": "radius", "envVars": ["RADIUS_API_KEY"] }] },
|
|
11
|
+
"providerAuthChoices": [
|
|
12
|
+
{
|
|
13
|
+
"provider": "radius",
|
|
14
|
+
"method": "oauth",
|
|
15
|
+
"choiceId": "radius",
|
|
16
|
+
"appGuidedAuth": "oauth",
|
|
17
|
+
"credentialOnly": true,
|
|
18
|
+
"channelLogin": {},
|
|
19
|
+
"choiceLabel": "Radius (browser sign-in)",
|
|
20
|
+
"groupId": "radius",
|
|
21
|
+
"groupLabel": "Radius",
|
|
22
|
+
"groupHint": "Browser sign-in or API key"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"provider": "radius",
|
|
26
|
+
"method": "api-key",
|
|
27
|
+
"choiceId": "radius-api-key",
|
|
28
|
+
"appGuidedSecret": true,
|
|
29
|
+
"choiceLabel": "Radius API key",
|
|
30
|
+
"groupId": "radius",
|
|
31
|
+
"groupLabel": "Radius",
|
|
32
|
+
"groupHint": "Browser sign-in or API key",
|
|
33
|
+
"optionKey": "radiusApiKey",
|
|
34
|
+
"cliFlag": "--radius-api-key",
|
|
35
|
+
"cliOption": "--radius-api-key <key>",
|
|
36
|
+
"cliDescription": "Radius organization API key"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"configSchema": { "type": "object", "additionalProperties": false, "properties": {} }
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openclaw/radius-provider",
|
|
3
|
+
"version": "2026.9.5",
|
|
4
|
+
"description": "OpenClaw Radius model gateway provider plugin.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/openclaw/openclaw"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"openclaw": {
|
|
11
|
+
"extensions": [
|
|
12
|
+
"./index.ts"
|
|
13
|
+
],
|
|
14
|
+
"install": {
|
|
15
|
+
"clawhubSpec": "clawhub:@openclaw/radius-provider",
|
|
16
|
+
"npmSpec": "@openclaw/radius-provider",
|
|
17
|
+
"defaultChoice": "npm",
|
|
18
|
+
"minHostVersion": ">=2026.9.4"
|
|
19
|
+
},
|
|
20
|
+
"compat": {
|
|
21
|
+
"pluginApi": ">=2026.9.5"
|
|
22
|
+
},
|
|
23
|
+
"build": {
|
|
24
|
+
"openclawVersion": "2026.9.5",
|
|
25
|
+
"bundledDist": false
|
|
26
|
+
},
|
|
27
|
+
"release": {
|
|
28
|
+
"publishToClawHub": true,
|
|
29
|
+
"publishToNpm": true
|
|
30
|
+
},
|
|
31
|
+
"runtimeExtensions": [
|
|
32
|
+
"./dist/index.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist/**",
|
|
37
|
+
"openclaw.plugin.json",
|
|
38
|
+
"README.md",
|
|
39
|
+
"assets/icon.png",
|
|
40
|
+
"assets/activity.svg"
|
|
41
|
+
],
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"openclaw": ">=2026.9.5"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"openclaw": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"bundledDependencies": []
|
|
51
|
+
}
|