@ottocode/openclaw-setu 0.1.1 → 0.1.3
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 +12 -5
- package/package.json +2 -2
- package/src/config.ts +237 -9
- package/src/index.ts +28 -0
package/README.md
CHANGED
|
@@ -17,17 +17,22 @@ OpenClaw → localhost:8403 (Setu proxy) → api.setu.ottocode.io → LLM provid
|
|
|
17
17
|
|
|
18
18
|
## Quick Start
|
|
19
19
|
|
|
20
|
+
No project installation needed — this is a global tool.
|
|
21
|
+
|
|
20
22
|
```bash
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
+
# Option 1: Zero-install with bunx (recommended)
|
|
24
|
+
bunx @ottocode/openclaw-setu setup
|
|
23
25
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
+
# Option 2: Global install
|
|
27
|
+
bun install -g @ottocode/openclaw-setu
|
|
28
|
+
openclaw-setu setup
|
|
29
|
+
```
|
|
26
30
|
|
|
31
|
+
```bash
|
|
27
32
|
# Fund your wallet with USDC on Solana (address shown during setup)
|
|
28
33
|
|
|
29
34
|
# Start the proxy
|
|
30
|
-
bunx openclaw-setu start
|
|
35
|
+
bunx @ottocode/openclaw-setu start
|
|
31
36
|
|
|
32
37
|
# Restart OpenClaw
|
|
33
38
|
openclaw gateway restart
|
|
@@ -49,6 +54,8 @@ openclaw-setu config remove Remove Setu provider from openclaw.json
|
|
|
49
54
|
openclaw-setu config status Check if Setu is configured
|
|
50
55
|
```
|
|
51
56
|
|
|
57
|
+
All commands work with `bunx @ottocode/openclaw-setu <command>` (no install required).
|
|
58
|
+
|
|
52
59
|
## As an OpenClaw Plugin
|
|
53
60
|
|
|
54
61
|
If OpenClaw loads the plugin automatically (via `openclaw.extensions` in package.json), Setu registers:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ottocode/openclaw-setu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Setu provider for OpenClaw — pay for AI with Solana USDC. No API keys, just a wallet.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"directory": "packages/openclaw-setu"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ottocode/ai-sdk": "
|
|
40
|
+
"@ottocode/ai-sdk": "^0.1.4"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"openclaw": ">=2025.1.0"
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
writeFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
} from "node:fs";
|
|
2
9
|
import { join } from "node:path";
|
|
3
10
|
import { homedir } from "node:os";
|
|
4
11
|
import type { ModelApi } from "./types.ts";
|
|
@@ -9,6 +16,7 @@ const OPENCLAW_CONFIG_PATH = join(OPENCLAW_DIR, "openclaw.json");
|
|
|
9
16
|
const PROVIDER_KEY = "setu";
|
|
10
17
|
const DEFAULT_PROXY_PORT = 8403;
|
|
11
18
|
const DEFAULT_BASE_URL = "https://api.setu.ottocode.io";
|
|
19
|
+
const SETU_PROXY_PORT_PATTERN = /[:\/]8403/;
|
|
12
20
|
|
|
13
21
|
export interface SetuModelConfig {
|
|
14
22
|
id: string;
|
|
@@ -28,17 +36,42 @@ export interface SetuProviderConfig {
|
|
|
28
36
|
models: SetuModelConfig[];
|
|
29
37
|
}
|
|
30
38
|
|
|
39
|
+
const DUMMY_API_KEY = "setu-proxy-handles-auth";
|
|
40
|
+
|
|
41
|
+
const MODEL_ALIASES: Array<{ id: string; alias: string }> = [
|
|
42
|
+
{ id: "claude-sonnet-4-6", alias: "sonnet-4.6" },
|
|
43
|
+
{ id: "claude-sonnet-4-5", alias: "sonnet-4.5" },
|
|
44
|
+
{ id: "claude-opus-4-6", alias: "opus" },
|
|
45
|
+
{ id: "claude-3-5-haiku-20241022", alias: "haiku" },
|
|
46
|
+
{ id: "gpt-5.1-codex", alias: "codex" },
|
|
47
|
+
{ id: "gpt-5", alias: "gpt5" },
|
|
48
|
+
{ id: "gpt-5-mini", alias: "gpt5-mini" },
|
|
49
|
+
{ id: "codex-mini-latest", alias: "codex-mini" },
|
|
50
|
+
{ id: "gemini-3-pro-preview", alias: "gemini-pro" },
|
|
51
|
+
{ id: "gemini-3-flash-preview", alias: "gemini-flash" },
|
|
52
|
+
{ id: "kimi-k2.5", alias: "kimi" },
|
|
53
|
+
{ id: "glm-5", alias: "glm" },
|
|
54
|
+
{ id: "MiniMax-M2.5", alias: "minimax" },
|
|
55
|
+
];
|
|
56
|
+
|
|
31
57
|
function readOpenClawConfig(): Record<string, unknown> {
|
|
32
58
|
if (!existsSync(OPENCLAW_CONFIG_PATH)) return {};
|
|
33
59
|
try {
|
|
34
|
-
|
|
60
|
+
const content = readFileSync(OPENCLAW_CONFIG_PATH, "utf-8").trim();
|
|
61
|
+
if (!content) return {};
|
|
62
|
+
return JSON.parse(content);
|
|
35
63
|
} catch {
|
|
36
64
|
return {};
|
|
37
65
|
}
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
function
|
|
41
|
-
|
|
68
|
+
function writeOpenClawConfigAtomic(config: Record<string, unknown>): void {
|
|
69
|
+
if (!existsSync(OPENCLAW_DIR)) {
|
|
70
|
+
mkdirSync(OPENCLAW_DIR, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
const tmpPath = `${OPENCLAW_CONFIG_PATH}.tmp.${process.pid}`;
|
|
73
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n");
|
|
74
|
+
renameSync(tmpPath, OPENCLAW_CONFIG_PATH);
|
|
42
75
|
}
|
|
43
76
|
|
|
44
77
|
interface CatalogModel {
|
|
@@ -233,17 +266,212 @@ export async function buildProviderConfigWithCatalog(
|
|
|
233
266
|
};
|
|
234
267
|
}
|
|
235
268
|
|
|
269
|
+
function removeConflictingCustomProviders(
|
|
270
|
+
providers: Record<string, unknown>,
|
|
271
|
+
port: number,
|
|
272
|
+
): string[] {
|
|
273
|
+
const removed: string[] = [];
|
|
274
|
+
for (const key of Object.keys(providers)) {
|
|
275
|
+
if (key === PROVIDER_KEY) continue;
|
|
276
|
+
if (!key.startsWith("custom-")) continue;
|
|
277
|
+
const p = providers[key] as Record<string, unknown> | undefined;
|
|
278
|
+
if (!p?.baseUrl) continue;
|
|
279
|
+
const baseUrl = String(p.baseUrl);
|
|
280
|
+
const pointsToSetuProxy =
|
|
281
|
+
baseUrl.includes(`:${port}`) || SETU_PROXY_PORT_PATTERN.test(baseUrl);
|
|
282
|
+
if (pointsToSetuProxy) {
|
|
283
|
+
delete providers[key];
|
|
284
|
+
removed.push(key);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return removed;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function migrateDefaultModel(
|
|
291
|
+
config: Record<string, unknown>,
|
|
292
|
+
removedKeys: string[],
|
|
293
|
+
): void {
|
|
294
|
+
if (removedKeys.length === 0) return;
|
|
295
|
+
const agents = config.agents as Record<string, unknown> | undefined;
|
|
296
|
+
const defaults = agents?.defaults as Record<string, unknown> | undefined;
|
|
297
|
+
const model = defaults?.model as Record<string, unknown> | undefined;
|
|
298
|
+
if (!model?.primary) return;
|
|
299
|
+
const primary = String(model.primary);
|
|
300
|
+
for (const oldKey of removedKeys) {
|
|
301
|
+
if (primary.startsWith(`${oldKey}/`)) {
|
|
302
|
+
const modelId = primary.slice(oldKey.length + 1);
|
|
303
|
+
model.primary = `setu/${modelId}`;
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
236
309
|
export async function injectConfig(port: number = DEFAULT_PROXY_PORT): Promise<void> {
|
|
237
310
|
const config = readOpenClawConfig();
|
|
311
|
+
let needsWrite = false;
|
|
238
312
|
|
|
239
|
-
if (!config.models)
|
|
313
|
+
if (!config.models) {
|
|
314
|
+
config.models = {};
|
|
315
|
+
needsWrite = true;
|
|
316
|
+
}
|
|
240
317
|
const models = config.models as Record<string, unknown>;
|
|
241
|
-
if (!models.providers)
|
|
318
|
+
if (!models.providers) {
|
|
319
|
+
models.providers = {};
|
|
320
|
+
needsWrite = true;
|
|
321
|
+
}
|
|
242
322
|
const providers = models.providers as Record<string, unknown>;
|
|
243
323
|
|
|
244
|
-
|
|
324
|
+
const removed = removeConflictingCustomProviders(providers, port);
|
|
325
|
+
if (removed.length > 0) {
|
|
326
|
+
migrateDefaultModel(config, removed);
|
|
327
|
+
needsWrite = true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const expectedBaseUrl = `http://localhost:${port}/v1`;
|
|
331
|
+
const existing = providers[PROVIDER_KEY] as Record<string, unknown> | undefined;
|
|
332
|
+
|
|
333
|
+
if (!existing) {
|
|
334
|
+
providers[PROVIDER_KEY] = await buildProviderConfigWithCatalog(port);
|
|
335
|
+
needsWrite = true;
|
|
336
|
+
} else {
|
|
337
|
+
if (!existing.baseUrl || existing.baseUrl !== expectedBaseUrl) {
|
|
338
|
+
existing.baseUrl = expectedBaseUrl;
|
|
339
|
+
needsWrite = true;
|
|
340
|
+
}
|
|
341
|
+
if (!existing.apiKey) {
|
|
342
|
+
existing.apiKey = DUMMY_API_KEY;
|
|
343
|
+
needsWrite = true;
|
|
344
|
+
}
|
|
345
|
+
if (!existing.api) {
|
|
346
|
+
existing.api = "openai-completions";
|
|
347
|
+
needsWrite = true;
|
|
348
|
+
}
|
|
349
|
+
const currentModels = existing.models as Array<{ id?: string }> | undefined;
|
|
350
|
+
const defaultModels = getDefaultModels();
|
|
351
|
+
const currentIds = new Set(
|
|
352
|
+
Array.isArray(currentModels) ? currentModels.map((m) => m?.id).filter(Boolean) : [],
|
|
353
|
+
);
|
|
354
|
+
const expectedIds = defaultModels.map((m) => m.id);
|
|
355
|
+
if (
|
|
356
|
+
!currentModels ||
|
|
357
|
+
!Array.isArray(currentModels) ||
|
|
358
|
+
currentModels.length !== defaultModels.length ||
|
|
359
|
+
expectedIds.some((id) => !currentIds.has(id))
|
|
360
|
+
) {
|
|
361
|
+
existing.models = await fetchModelsFromCatalog();
|
|
362
|
+
needsWrite = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!config.agents) {
|
|
367
|
+
config.agents = {};
|
|
368
|
+
needsWrite = true;
|
|
369
|
+
}
|
|
370
|
+
const agents = config.agents as Record<string, unknown>;
|
|
371
|
+
if (!agents.defaults) {
|
|
372
|
+
agents.defaults = {};
|
|
373
|
+
needsWrite = true;
|
|
374
|
+
}
|
|
375
|
+
const defaults = agents.defaults as Record<string, unknown>;
|
|
376
|
+
if (!defaults.model) {
|
|
377
|
+
defaults.model = {};
|
|
378
|
+
needsWrite = true;
|
|
379
|
+
}
|
|
380
|
+
const model = defaults.model as Record<string, unknown>;
|
|
381
|
+
|
|
382
|
+
if (!model.primary) {
|
|
383
|
+
model.primary = "setu/claude-sonnet-4-6";
|
|
384
|
+
needsWrite = true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!defaults.models) {
|
|
388
|
+
defaults.models = {};
|
|
389
|
+
needsWrite = true;
|
|
390
|
+
}
|
|
391
|
+
const allowlist = defaults.models as Record<string, unknown>;
|
|
392
|
+
for (const m of MODEL_ALIASES) {
|
|
393
|
+
const fullId = `setu/${m.id}`;
|
|
394
|
+
const entry = allowlist[fullId] as Record<string, unknown> | undefined;
|
|
395
|
+
if (!entry) {
|
|
396
|
+
allowlist[fullId] = { alias: m.alias };
|
|
397
|
+
needsWrite = true;
|
|
398
|
+
} else if (entry.alias !== m.alias) {
|
|
399
|
+
entry.alias = m.alias;
|
|
400
|
+
needsWrite = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (needsWrite) {
|
|
405
|
+
writeOpenClawConfigAtomic(config);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
245
408
|
|
|
246
|
-
|
|
409
|
+
export function injectAuthProfile(): void {
|
|
410
|
+
const agentsDir = join(OPENCLAW_DIR, "agents");
|
|
411
|
+
|
|
412
|
+
if (!existsSync(agentsDir)) {
|
|
413
|
+
try {
|
|
414
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
415
|
+
} catch {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
let agents: string[];
|
|
421
|
+
try {
|
|
422
|
+
agents = readdirSync(agentsDir, { withFileTypes: true })
|
|
423
|
+
.filter((d) => d.isDirectory())
|
|
424
|
+
.map((d) => d.name);
|
|
425
|
+
} catch {
|
|
426
|
+
agents = [];
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (!agents.includes("main")) {
|
|
430
|
+
agents = ["main", ...agents];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
for (const agentId of agents) {
|
|
434
|
+
const authDir = join(agentsDir, agentId, "agent");
|
|
435
|
+
const authPath = join(authDir, "auth-profiles.json");
|
|
436
|
+
|
|
437
|
+
if (!existsSync(authDir)) {
|
|
438
|
+
try {
|
|
439
|
+
mkdirSync(authDir, { recursive: true });
|
|
440
|
+
} catch {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
let store: { version: number; profiles: Record<string, unknown> } = {
|
|
446
|
+
version: 1,
|
|
447
|
+
profiles: {},
|
|
448
|
+
};
|
|
449
|
+
if (existsSync(authPath)) {
|
|
450
|
+
try {
|
|
451
|
+
const existing = JSON.parse(readFileSync(authPath, "utf-8"));
|
|
452
|
+
if (existing.version && existing.profiles) {
|
|
453
|
+
store = existing;
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
// use fresh store
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const profileKey = "setu:default";
|
|
461
|
+
if (store.profiles[profileKey]) continue;
|
|
462
|
+
|
|
463
|
+
store.profiles[profileKey] = {
|
|
464
|
+
type: "api_key",
|
|
465
|
+
provider: "setu",
|
|
466
|
+
key: DUMMY_API_KEY,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
writeFileSync(authPath, JSON.stringify(store, null, 2));
|
|
471
|
+
} catch {
|
|
472
|
+
// skip
|
|
473
|
+
}
|
|
474
|
+
}
|
|
247
475
|
}
|
|
248
476
|
|
|
249
477
|
export function removeConfig(): void {
|
|
@@ -254,7 +482,7 @@ export function removeConfig(): void {
|
|
|
254
482
|
const providers = models.providers as Record<string, unknown>;
|
|
255
483
|
delete providers[PROVIDER_KEY];
|
|
256
484
|
|
|
257
|
-
|
|
485
|
+
writeOpenClawConfigAtomic(config);
|
|
258
486
|
}
|
|
259
487
|
|
|
260
488
|
export function isConfigured(): boolean {
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
buildProviderConfig,
|
|
14
14
|
injectConfig,
|
|
15
|
+
injectAuthProfile,
|
|
15
16
|
isConfigured,
|
|
16
17
|
} from "./config.ts";
|
|
17
18
|
import { isValidPrivateKey } from "@ottocode/ai-sdk";
|
|
@@ -32,6 +33,33 @@ const plugin: OpenClawPluginDefinition = {
|
|
|
32
33
|
async register(api: OpenClawPluginApi) {
|
|
33
34
|
const port = getPort(api);
|
|
34
35
|
|
|
36
|
+
await injectConfig(port).catch(() => {});
|
|
37
|
+
try { injectAuthProfile(); } catch {}
|
|
38
|
+
|
|
39
|
+
if (!api.config.models) {
|
|
40
|
+
api.config.models = { providers: {} };
|
|
41
|
+
}
|
|
42
|
+
if (!api.config.models.providers) {
|
|
43
|
+
api.config.models.providers = {};
|
|
44
|
+
}
|
|
45
|
+
const providerConfig = buildProviderConfig(port);
|
|
46
|
+
api.config.models.providers.setu = {
|
|
47
|
+
baseUrl: providerConfig.baseUrl,
|
|
48
|
+
api: providerConfig.api,
|
|
49
|
+
apiKey: providerConfig.apiKey,
|
|
50
|
+
models: providerConfig.models,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (!api.config.agents) api.config.agents = {};
|
|
54
|
+
const agents = api.config.agents as Record<string, unknown>;
|
|
55
|
+
if (!agents.defaults) agents.defaults = {};
|
|
56
|
+
const defaults = agents.defaults as Record<string, unknown>;
|
|
57
|
+
if (!defaults.model) defaults.model = {};
|
|
58
|
+
const model = defaults.model as Record<string, unknown>;
|
|
59
|
+
if (!model.primary) {
|
|
60
|
+
model.primary = "setu/claude-sonnet-4-6";
|
|
61
|
+
}
|
|
62
|
+
|
|
35
63
|
api.registerProvider({
|
|
36
64
|
id: "setu",
|
|
37
65
|
label: "Setu (Solana USDC)",
|