@robot-resources/openclaw-plugin 0.1.0 → 0.3.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/index.js +173 -26
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -1
package/index.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Robot Resources Router plugin for OpenClaw.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* cost-optimized model selection. The Router picks the cheapest
|
|
6
|
-
* capable model per request via keyword/LLM confidence branching.
|
|
4
|
+
* Two routing modes depending on auth:
|
|
7
5
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - before_model_resolve hook overrides provider to "robot-resources"
|
|
11
|
-
* - OpenClaw sends requests to Router at localhost:3838
|
|
12
|
-
* - Router selects optimal model, forwards to upstream provider
|
|
13
|
-
* - Response flows back through OpenClaw transparently
|
|
6
|
+
* 1. API-key users: Registers "robot-resources" as a provider that proxies
|
|
7
|
+
* requests through the Router at localhost:3838. Works end-to-end.
|
|
14
8
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* 2. Subscription (OAuth) users: Uses `message_received` hook to ask the
|
|
10
|
+
* Router's /v1/route endpoint for the cheapest model, then logs the
|
|
11
|
+
* recommendation. Full modelOverride requires a `before_model_select`
|
|
12
|
+
* hook that the OpenClaw plugin SDK does not yet provide.
|
|
13
|
+
*
|
|
14
|
+
* LIMITATION: `message_received` only fires for channel messages
|
|
15
|
+
* (Telegram, Slack, etc.), NOT for `openclaw agent --local` CLI mode.
|
|
17
16
|
*
|
|
18
17
|
* Install: openclaw plugins install @robot-resources/openclaw-plugin
|
|
19
18
|
* Requires: Robot Resources Router running (npx robot-resources)
|
|
@@ -21,24 +20,172 @@
|
|
|
21
20
|
|
|
22
21
|
const DEFAULT_ROUTER_URL = 'http://localhost:3838';
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const ROUTER_MODELS = [
|
|
24
|
+
'claude-sonnet-4-20250514',
|
|
25
|
+
'claude-haiku-4-5-20251001',
|
|
26
|
+
'claude-opus-4-20250514',
|
|
27
|
+
];
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Ask the Router which model is cheapest for this prompt.
|
|
31
|
+
* Returns { provider, model, savings } or null on failure.
|
|
32
|
+
*/
|
|
33
|
+
async function askRouter(routerUrl, prompt, providers = null) {
|
|
34
|
+
try {
|
|
35
|
+
const body = { prompt };
|
|
36
|
+
if (providers) body.providers = providers;
|
|
35
37
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
const resp = await fetch(`${routerUrl}/v1/route`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
signal: AbortSignal.timeout(3000),
|
|
43
|
+
});
|
|
44
|
+
if (!resp.ok) return null;
|
|
45
|
+
const data = await resp.json();
|
|
38
46
|
return {
|
|
39
|
-
|
|
47
|
+
provider: data.provider || 'anthropic',
|
|
48
|
+
model: data.model || null,
|
|
49
|
+
savings: data.savings_percent || 0,
|
|
40
50
|
};
|
|
41
|
-
}
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Detect if OpenClaw is using subscription/OAuth auth.
|
|
58
|
+
*/
|
|
59
|
+
function detectSubscriptionMode(config) {
|
|
60
|
+
const profiles = config?.auth?.profiles;
|
|
61
|
+
if (profiles && typeof profiles === 'object') {
|
|
62
|
+
for (const profile of Object.values(profiles)) {
|
|
63
|
+
if (profile?.mode === 'token') return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (config?.gateway?.auth?.mode === 'token') return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function buildModelDefinition(modelId) {
|
|
71
|
+
return {
|
|
72
|
+
id: modelId,
|
|
73
|
+
name: modelId,
|
|
74
|
+
api: 'anthropic-messages',
|
|
75
|
+
reasoning: false,
|
|
76
|
+
input: ['text', 'image'],
|
|
77
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
78
|
+
contextWindow: 200_000,
|
|
79
|
+
maxTokens: 8192,
|
|
80
|
+
};
|
|
42
81
|
}
|
|
43
82
|
|
|
44
|
-
|
|
83
|
+
const robotResourcesPlugin = {
|
|
84
|
+
id: 'openclaw-plugin',
|
|
85
|
+
name: 'Robot Resources Router',
|
|
86
|
+
description: 'Cost-optimized AI model routing — selects the cheapest capable model per request',
|
|
87
|
+
|
|
88
|
+
register(api) {
|
|
89
|
+
const pluginConfig = api.pluginConfig || {};
|
|
90
|
+
const routerUrl = pluginConfig.routerUrl || DEFAULT_ROUTER_URL;
|
|
91
|
+
const isSubscription = detectSubscriptionMode(api.config);
|
|
92
|
+
|
|
93
|
+
if (isSubscription) {
|
|
94
|
+
api.logger.info('[robot-resources] Subscription mode detected — routing restricted to Anthropic models');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Model routing: before_model_resolve hook ──
|
|
98
|
+
// Fires before model selection. Returns { modelOverride, providerOverride }
|
|
99
|
+
// to redirect to the cheapest capable model via the Router.
|
|
100
|
+
// Works in ALL modes: CLI, channels, subagents.
|
|
101
|
+
const providers = isSubscription ? ['anthropic'] : null;
|
|
102
|
+
let lastRouting = null;
|
|
103
|
+
|
|
104
|
+
api.on('before_model_resolve', async (event, _ctx) => {
|
|
105
|
+
const prompt = event.prompt || '';
|
|
106
|
+
if (!prompt) return;
|
|
107
|
+
|
|
108
|
+
const decision = await askRouter(routerUrl, prompt, providers);
|
|
109
|
+
if (!decision?.model) return;
|
|
110
|
+
|
|
111
|
+
lastRouting = decision;
|
|
112
|
+
api.logger.info(
|
|
113
|
+
`[robot-resources] Routing: ${decision.model} (${decision.savings}% savings)`,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
modelOverride: decision.model,
|
|
118
|
+
providerOverride: decision.provider,
|
|
119
|
+
};
|
|
120
|
+
}, { priority: 10 });
|
|
121
|
+
|
|
122
|
+
// ── Append routing tag to outgoing messages ──
|
|
123
|
+
api.on('message_sending', (event, _ctx) => {
|
|
124
|
+
if (!lastRouting) return;
|
|
125
|
+
const tag = `\n\n⚡ _Routed → ${lastRouting.model} (${lastRouting.savings}% savings)_`;
|
|
126
|
+
lastRouting = null;
|
|
127
|
+
return { content: event.content + tag };
|
|
128
|
+
}, { priority: -10 });
|
|
129
|
+
|
|
130
|
+
// ── API-key mode: register provider for HTTP proxy ──
|
|
131
|
+
api.registerProvider({
|
|
132
|
+
id: 'robot-resources',
|
|
133
|
+
label: 'Robot Resources',
|
|
134
|
+
docsPath: '/providers/models',
|
|
135
|
+
auth: [
|
|
136
|
+
{
|
|
137
|
+
id: 'local',
|
|
138
|
+
label: 'Local Router proxy',
|
|
139
|
+
hint: 'Route requests through the Robot Resources Router for cost optimization',
|
|
140
|
+
kind: 'custom',
|
|
141
|
+
async run(ctx) {
|
|
142
|
+
const baseUrlInput = await ctx.prompter.text({
|
|
143
|
+
message: 'Robot Resources Router URL',
|
|
144
|
+
initialValue: routerUrl,
|
|
145
|
+
validate: (value) => {
|
|
146
|
+
try { new URL(value); } catch { return 'Enter a valid URL'; }
|
|
147
|
+
return undefined;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const baseUrl = baseUrlInput.trim().replace(/\/+$/, '');
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
profiles: [
|
|
155
|
+
{
|
|
156
|
+
profileId: 'robot-resources:local',
|
|
157
|
+
credential: {
|
|
158
|
+
type: 'token',
|
|
159
|
+
provider: 'robot-resources',
|
|
160
|
+
token: 'n/a',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
configPatch: {
|
|
165
|
+
models: {
|
|
166
|
+
providers: {
|
|
167
|
+
'robot-resources': {
|
|
168
|
+
baseUrl,
|
|
169
|
+
apiKey: 'n/a',
|
|
170
|
+
api: 'anthropic-messages',
|
|
171
|
+
authHeader: false,
|
|
172
|
+
models: ROUTER_MODELS.map(buildModelDefinition),
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
defaultModel: `robot-resources/${ROUTER_MODELS[0]}`,
|
|
178
|
+
notes: [
|
|
179
|
+
'Robot Resources Router must be running (npx robot-resources).',
|
|
180
|
+
'Requests are routed through localhost:3838 for cost optimization.',
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export default robotResourcesPlugin;
|
|
191
|
+
export { DEFAULT_ROUTER_URL, ROUTER_MODELS, askRouter, detectSubscriptionMode };
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robot-resources/openclaw-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Robot Resources Router plugin for OpenClaw — cost-optimized AI model routing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
"directory": "packages/openclaw-plugin"
|
|
28
28
|
},
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"openclaw": {
|
|
31
|
+
"extensions": [
|
|
32
|
+
"./index.js"
|
|
33
|
+
]
|
|
34
|
+
},
|
|
30
35
|
"devDependencies": {
|
|
31
36
|
"vitest": "^3.0.0"
|
|
32
37
|
}
|