@robot-resources/openclaw-plugin 0.1.0 → 0.4.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 CHANGED
@@ -1,19 +1,15 @@
1
1
  /**
2
- * Robot Resources Router plugin for OpenClaw.
2
+ * Robot Resources plugin for OpenClaw.
3
3
  *
4
- * Routes requests through the local Robot Resources Router for
5
- * cost-optimized model selection. The Router picks the cheapest
6
- * capable model per request via keyword/LLM confidence branching.
4
+ * Two integrations:
7
5
  *
8
- * Architecture:
9
- * - Plugin registers "robot-resources" as a provider (via manifest)
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. Model routing (before_model_resolve): Routes each LLM call through
7
+ * the Router at localhost:3838 to select the cheapest capable model.
14
8
  *
15
- * The plugin survives gateway restarts because it lives in
16
- * ~/.openclaw/extensions/, not in openclaw.json.
9
+ * 2. Web fetch override (before_tool_call): Intercepts web_fetch calls
10
+ * and routes them through the Scraper MCP for token-compressed output.
11
+ * The scraper-mcp must be registered in openclaw.json (done by the
12
+ * unified installer: npx robot-resources).
17
13
  *
18
14
  * Install: openclaw plugins install @robot-resources/openclaw-plugin
19
15
  * Requires: Robot Resources Router running (npx robot-resources)
@@ -21,24 +17,193 @@
21
17
 
22
18
  const DEFAULT_ROUTER_URL = 'http://localhost:3838';
23
19
 
24
- export default function register(api) {
25
- const pluginConfig = api.config || {};
26
- const routerUrl = pluginConfig.routerUrl || DEFAULT_ROUTER_URL;
20
+ const ROUTER_MODELS = [
21
+ 'claude-sonnet-4-20250514',
22
+ 'claude-haiku-4-5-20251001',
23
+ 'claude-opus-4-20250514',
24
+ ];
27
25
 
28
- // Register provider with Router's baseUrl
29
- if (typeof api.registerProvider === 'function') {
30
- api.registerProvider('robot-resources', {
31
- baseUrl: routerUrl,
32
- api: 'anthropic-messages',
33
- });
34
- }
26
+ /**
27
+ * Ask the Router which model is cheapest for this prompt.
28
+ * Returns { provider, model, savings } or null on failure.
29
+ */
30
+ async function askRouter(routerUrl, prompt, providers = null) {
31
+ try {
32
+ const body = { prompt };
33
+ if (providers) body.providers = providers;
35
34
 
36
- // Override model resolution to route through Robot Resources
37
- api.on('before_model_resolve', (_event, _ctx) => {
35
+ const resp = await fetch(`${routerUrl}/v1/route`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify(body),
39
+ signal: AbortSignal.timeout(3000),
40
+ });
41
+ if (!resp.ok) return null;
42
+ const data = await resp.json();
38
43
  return {
39
- providerOverride: 'robot-resources',
44
+ provider: data.provider || 'anthropic',
45
+ model: data.model || null,
46
+ savings: data.savings_percent || 0,
40
47
  };
41
- }, { priority: 10 });
48
+ } catch {
49
+ return null;
50
+ }
42
51
  }
43
52
 
44
- export { DEFAULT_ROUTER_URL };
53
+ /**
54
+ * Detect if OpenClaw is using subscription/OAuth auth.
55
+ */
56
+ function detectSubscriptionMode(config) {
57
+ const profiles = config?.auth?.profiles;
58
+ if (profiles && typeof profiles === 'object') {
59
+ for (const profile of Object.values(profiles)) {
60
+ if (profile?.mode === 'token') return true;
61
+ }
62
+ }
63
+ if (config?.gateway?.auth?.mode === 'token') return true;
64
+ return false;
65
+ }
66
+
67
+ function buildModelDefinition(modelId) {
68
+ return {
69
+ id: modelId,
70
+ name: modelId,
71
+ api: 'anthropic-messages',
72
+ reasoning: false,
73
+ input: ['text', 'image'],
74
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
75
+ contextWindow: 200_000,
76
+ maxTokens: 8192,
77
+ };
78
+ }
79
+
80
+ const robotResourcesPlugin = {
81
+ id: 'openclaw-plugin',
82
+ name: 'Robot Resources',
83
+ description: 'Cost-optimized model routing + token-compressed web fetching',
84
+
85
+ register(api) {
86
+ const pluginConfig = api.pluginConfig || {};
87
+ const routerUrl = pluginConfig.routerUrl || DEFAULT_ROUTER_URL;
88
+ const isSubscription = detectSubscriptionMode(api.config);
89
+
90
+ if (isSubscription) {
91
+ api.logger.info('[robot-resources] Subscription mode detected — routing restricted to Anthropic models');
92
+ }
93
+
94
+ // ── Model routing: before_model_resolve hook ──
95
+ // Fires before model selection. Returns { modelOverride, providerOverride }
96
+ // to redirect to the cheapest capable model via the Router.
97
+ // Works in ALL modes: CLI, channels, subagents.
98
+ const providers = isSubscription ? ['anthropic'] : null;
99
+ let lastRouting = null;
100
+
101
+ api.on('before_model_resolve', async (event, _ctx) => {
102
+ const prompt = event.prompt || '';
103
+ if (!prompt) return;
104
+
105
+ const decision = await askRouter(routerUrl, prompt, providers);
106
+ if (!decision?.model) return;
107
+
108
+ lastRouting = decision;
109
+ api.logger.info(
110
+ `[robot-resources] Routing: ${decision.model} (${decision.savings}% savings)`,
111
+ );
112
+
113
+ return {
114
+ modelOverride: decision.model,
115
+ providerOverride: decision.provider,
116
+ };
117
+ }, { priority: 10 });
118
+
119
+ // ── Append routing tag to outgoing messages ──
120
+ api.on('message_sending', (event, _ctx) => {
121
+ if (!lastRouting) return;
122
+ const tag = `\n\n⚡ _Routed → ${lastRouting.model} (${lastRouting.savings}% savings)_`;
123
+ lastRouting = null;
124
+ return { content: event.content + tag };
125
+ }, { priority: -10 });
126
+
127
+ // ── Scraper override: intercept web_fetch → use scraper_compress_url ──
128
+ // When the agent calls web_fetch, redirect to scraper_compress_url for
129
+ // token-compressed output. Falls through silently if scraper-mcp is not
130
+ // registered (the tool just won't exist).
131
+ api.on('before_tool_call', async (event, _ctx) => {
132
+ if (event.tool !== 'web_fetch') return;
133
+
134
+ const url = event.params?.url;
135
+ if (!url) return;
136
+
137
+ api.logger.info(`[robot-resources] Redirecting web_fetch → scraper_compress_url: ${url}`);
138
+
139
+ return {
140
+ toolOverride: 'scraper_compress_url',
141
+ paramsOverride: {
142
+ url,
143
+ mode: 'auto',
144
+ },
145
+ };
146
+ }, { priority: 10 });
147
+
148
+ // ── API-key mode: register provider for HTTP proxy ──
149
+ api.registerProvider({
150
+ id: 'robot-resources',
151
+ label: 'Robot Resources',
152
+ docsPath: '/providers/models',
153
+ auth: [
154
+ {
155
+ id: 'local',
156
+ label: 'Local Router proxy',
157
+ hint: 'Route requests through the Robot Resources Router for cost optimization',
158
+ kind: 'custom',
159
+ async run(ctx) {
160
+ const baseUrlInput = await ctx.prompter.text({
161
+ message: 'Robot Resources Router URL',
162
+ initialValue: routerUrl,
163
+ validate: (value) => {
164
+ try { new URL(value); } catch { return 'Enter a valid URL'; }
165
+ return undefined;
166
+ },
167
+ });
168
+
169
+ const baseUrl = baseUrlInput.trim().replace(/\/+$/, '');
170
+
171
+ return {
172
+ profiles: [
173
+ {
174
+ profileId: 'robot-resources:local',
175
+ credential: {
176
+ type: 'token',
177
+ provider: 'robot-resources',
178
+ token: 'n/a',
179
+ },
180
+ },
181
+ ],
182
+ configPatch: {
183
+ models: {
184
+ providers: {
185
+ 'robot-resources': {
186
+ baseUrl,
187
+ apiKey: 'n/a',
188
+ api: 'anthropic-messages',
189
+ authHeader: false,
190
+ models: ROUTER_MODELS.map(buildModelDefinition),
191
+ },
192
+ },
193
+ },
194
+ },
195
+ defaultModel: `robot-resources/${ROUTER_MODELS[0]}`,
196
+ notes: [
197
+ 'Robot Resources Router must be running (npx robot-resources).',
198
+ 'Requests are routed through localhost:3838 for cost optimization.',
199
+ ],
200
+ };
201
+ },
202
+ },
203
+ ],
204
+ });
205
+ },
206
+ };
207
+
208
+ export default robotResourcesPlugin;
209
+ export { DEFAULT_ROUTER_URL, ROUTER_MODELS, askRouter, detectSubscriptionMode };
@@ -1,7 +1,7 @@
1
1
  {
2
- "id": "robot-resources-router",
3
- "name": "Robot Resources Router",
4
- "description": "Cost-optimized AI model routing selects the cheapest capable model per request",
2
+ "id": "openclaw-plugin",
3
+ "name": "Robot Resources",
4
+ "description": "Cost-optimized model routing + token-compressed web fetching",
5
5
  "providers": ["robot-resources"],
6
6
  "configSchema": {
7
7
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robot-resources/openclaw-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.4.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
  }