@ottocode/openclaw-setu 0.1.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/openclaw-setu",
3
- "version": "0.1.2",
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",
package/src/config.ts CHANGED
@@ -1,4 +1,11 @@
1
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
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
- return JSON.parse(readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
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 writeOpenClawConfig(config: Record<string, unknown>): void {
41
- writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
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) 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) 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
- providers[PROVIDER_KEY] = await buildProviderConfigWithCatalog(port);
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
- writeOpenClawConfig(config);
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
- writeOpenClawConfig(config);
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)",