@ottocode/openclaw-setu 0.1.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/README.md +79 -0
- package/openclaw.plugin.json +25 -0
- package/package.json +53 -0
- package/src/cli.ts +272 -0
- package/src/config.ts +270 -0
- package/src/index.ts +197 -0
- package/src/proxy.ts +149 -0
- package/src/types.ts +154 -0
- package/src/wallet.ts +79 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# @ottocode/openclaw-setu
|
|
2
|
+
|
|
3
|
+
Setu provider plugin for [OpenClaw](https://github.com/openclaw/openclaw) — pay for AI with Solana USDC.
|
|
4
|
+
|
|
5
|
+
No API keys. No accounts. Just a Solana wallet with USDC.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
OpenClaw → localhost:8403 (Setu proxy) → api.setu.ottocode.io → LLM provider
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
1. Auto-generates a Solana wallet (or import your own)
|
|
14
|
+
2. Fund the wallet with USDC on Solana
|
|
15
|
+
3. Each LLM request is signed with your wallet — payment IS authentication
|
|
16
|
+
4. Access 30+ models across Anthropic, OpenAI, Google, DeepSeek, and more
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Install
|
|
22
|
+
bun add @ottocode/openclaw-setu
|
|
23
|
+
|
|
24
|
+
# Interactive setup (generates wallet, injects config)
|
|
25
|
+
bunx openclaw-setu setup
|
|
26
|
+
|
|
27
|
+
# Fund your wallet with USDC on Solana (address shown during setup)
|
|
28
|
+
|
|
29
|
+
# Start the proxy
|
|
30
|
+
bunx openclaw-setu start
|
|
31
|
+
|
|
32
|
+
# Restart OpenClaw
|
|
33
|
+
openclaw gateway restart
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CLI Commands
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
openclaw-setu setup Interactive setup (wallet + config)
|
|
40
|
+
openclaw-setu start Start the local proxy server
|
|
41
|
+
|
|
42
|
+
openclaw-setu wallet generate Generate a new Solana wallet
|
|
43
|
+
openclaw-setu wallet import Import an existing private key
|
|
44
|
+
openclaw-setu wallet export Export your private key
|
|
45
|
+
openclaw-setu wallet info Show wallet address and balances
|
|
46
|
+
|
|
47
|
+
openclaw-setu config inject Inject Setu provider into openclaw.json
|
|
48
|
+
openclaw-setu config remove Remove Setu provider from openclaw.json
|
|
49
|
+
openclaw-setu config status Check if Setu is configured
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## As an OpenClaw Plugin
|
|
53
|
+
|
|
54
|
+
If OpenClaw loads the plugin automatically (via `openclaw.extensions` in package.json), Setu registers:
|
|
55
|
+
|
|
56
|
+
- **Provider**: `setu` — appears in OpenClaw's auth wizard
|
|
57
|
+
- **Service**: `setu-proxy` — auto-starts the local proxy with the gateway
|
|
58
|
+
- **Commands**: `/wallet` (show balances), `/setu-status` (check config)
|
|
59
|
+
|
|
60
|
+
## Wallet Storage
|
|
61
|
+
|
|
62
|
+
- Private key: `~/.openclaw/setu/wallet.key` (mode 0600)
|
|
63
|
+
- OpenClaw config: `~/.openclaw/openclaw.json`
|
|
64
|
+
|
|
65
|
+
## Environment Variables
|
|
66
|
+
|
|
67
|
+
- `SETU_PROXY_PORT` — Proxy port (default: 8403)
|
|
68
|
+
- `SETU_PRIVATE_KEY` — Alternative to wallet file
|
|
69
|
+
|
|
70
|
+
## How is this different from ClawRouter?
|
|
71
|
+
|
|
72
|
+
| | Setu | ClawRouter |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| Chain | Solana | Base (EVM) |
|
|
75
|
+
| Token | USDC (SPL) | USDC (ERC-20) |
|
|
76
|
+
| Protocol | Solana wallet signatures | x402 / EIP-712 |
|
|
77
|
+
| Proxy port | 8403 | 8402 |
|
|
78
|
+
|
|
79
|
+
Both achieve the same goal: pay-per-token AI with no API keys. Choose based on which chain you prefer.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "setu",
|
|
3
|
+
"name": "Setu",
|
|
4
|
+
"description": "Pay for AI with Solana USDC — 30+ models, no API keys, just a wallet.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"walletKey": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Solana wallet private key (base58). Optional — auto-generated if not set."
|
|
11
|
+
},
|
|
12
|
+
"baseURL": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Setu API base URL (default: https://api.setu.ottocode.io)"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"uiHints": {
|
|
19
|
+
"walletKey": {
|
|
20
|
+
"label": "Solana Wallet Private Key",
|
|
21
|
+
"sensitive": true,
|
|
22
|
+
"placeholder": "base58... (optional — auto-generated)"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ottocode/openclaw-setu",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Setu provider for OpenClaw — pay for AI with Solana USDC. No API keys, just a wallet.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"openclaw-setu": "./src/cli.ts"
|
|
12
|
+
},
|
|
13
|
+
"openclaw": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"./src/index.ts"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"openclaw.plugin.json"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"openclaw",
|
|
24
|
+
"setu",
|
|
25
|
+
"solana",
|
|
26
|
+
"usdc",
|
|
27
|
+
"ai",
|
|
28
|
+
"llm",
|
|
29
|
+
"pay-per-use",
|
|
30
|
+
"crypto"
|
|
31
|
+
],
|
|
32
|
+
"author": "nitishxyz",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/nitishxyz/otto.git",
|
|
37
|
+
"directory": "packages/openclaw-setu"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@ottocode/ai-sdk": "workspace:*"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"openclaw": ">=2025.1.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"openclaw": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"typescript": "^5.0.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
loadWallet,
|
|
5
|
+
saveWallet,
|
|
6
|
+
ensureWallet,
|
|
7
|
+
exportWalletKey,
|
|
8
|
+
getWalletKeyPath,
|
|
9
|
+
getSetuBalance,
|
|
10
|
+
} from "./wallet.ts";
|
|
11
|
+
import { injectConfig, removeConfig, isConfigured, getConfigPath } from "./config.ts";
|
|
12
|
+
import { createProxy } from "./proxy.ts";
|
|
13
|
+
import { isValidPrivateKey } from "@ottocode/ai-sdk";
|
|
14
|
+
import * as readline from "node:readline";
|
|
15
|
+
|
|
16
|
+
function prompt(question: string): Promise<string> {
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout,
|
|
20
|
+
});
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
rl.question(question, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.trim());
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function printHelp() {
|
|
30
|
+
console.log(`
|
|
31
|
+
openclaw-setu — Pay for AI with Solana USDC
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
openclaw-setu setup Interactive setup (wallet + config)
|
|
35
|
+
openclaw-setu start Start the local proxy server
|
|
36
|
+
openclaw-setu stop Stop (placeholder — use Ctrl+C)
|
|
37
|
+
|
|
38
|
+
openclaw-setu wallet generate Generate a new Solana wallet
|
|
39
|
+
openclaw-setu wallet import Import an existing private key
|
|
40
|
+
openclaw-setu wallet export Export your private key
|
|
41
|
+
openclaw-setu wallet info Show wallet address and balances
|
|
42
|
+
|
|
43
|
+
openclaw-setu config inject Inject Setu provider into openclaw.json
|
|
44
|
+
openclaw-setu config remove Remove Setu provider from openclaw.json
|
|
45
|
+
openclaw-setu config status Check if Setu is configured
|
|
46
|
+
|
|
47
|
+
openclaw-setu help Show this help
|
|
48
|
+
`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function cmdSetup() {
|
|
52
|
+
console.log("\n Setu — Pay for AI with Solana USDC\n");
|
|
53
|
+
|
|
54
|
+
const existing = loadWallet();
|
|
55
|
+
let wallet = existing;
|
|
56
|
+
|
|
57
|
+
if (existing) {
|
|
58
|
+
console.log(` Existing wallet found: ${existing.publicKey}`);
|
|
59
|
+
const choice = await prompt(" Use existing wallet? (Y/n): ");
|
|
60
|
+
if (choice.toLowerCase() === "n") {
|
|
61
|
+
const action = await prompt(" (g)enerate new or (i)mport existing? ");
|
|
62
|
+
if (action.toLowerCase() === "i") {
|
|
63
|
+
const key = await prompt(" Enter Solana private key (base58): ");
|
|
64
|
+
if (!isValidPrivateKey(key)) {
|
|
65
|
+
console.error(" Invalid private key.");
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
wallet = saveWallet(key);
|
|
69
|
+
console.log(` Wallet imported: ${wallet.publicKey}`);
|
|
70
|
+
} else {
|
|
71
|
+
wallet = ensureWallet();
|
|
72
|
+
console.log(` New wallet generated: ${wallet.publicKey}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
const action = await prompt(
|
|
77
|
+
" No wallet found. (g)enerate new or (i)mport existing? ",
|
|
78
|
+
);
|
|
79
|
+
if (action.toLowerCase() === "i") {
|
|
80
|
+
const key = await prompt(" Enter Solana private key (base58): ");
|
|
81
|
+
if (!isValidPrivateKey(key)) {
|
|
82
|
+
console.error(" Invalid private key.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
wallet = saveWallet(key);
|
|
86
|
+
console.log(` Wallet imported: ${wallet.publicKey}`);
|
|
87
|
+
} else {
|
|
88
|
+
wallet = ensureWallet();
|
|
89
|
+
console.log(` New wallet generated: ${wallet.publicKey}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!wallet) {
|
|
94
|
+
console.error(" Failed to set up wallet.");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`\n Wallet: ${wallet.publicKey}`);
|
|
99
|
+
console.log(` Key stored at: ${getWalletKeyPath()}`);
|
|
100
|
+
|
|
101
|
+
await injectConfig();
|
|
102
|
+
console.log(` OpenClaw config updated: ${getConfigPath()}`);
|
|
103
|
+
|
|
104
|
+
console.log(`
|
|
105
|
+
Setup complete!
|
|
106
|
+
|
|
107
|
+
Next steps:
|
|
108
|
+
1. Fund your wallet with USDC on Solana:
|
|
109
|
+
${wallet.publicKey}
|
|
110
|
+
|
|
111
|
+
2. Start the proxy:
|
|
112
|
+
openclaw-setu start
|
|
113
|
+
|
|
114
|
+
3. Restart OpenClaw:
|
|
115
|
+
openclaw gateway restart
|
|
116
|
+
|
|
117
|
+
Your wallet address is your identity — no API keys needed.
|
|
118
|
+
`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function cmdWalletGenerate() {
|
|
122
|
+
const existing = loadWallet();
|
|
123
|
+
if (existing) {
|
|
124
|
+
const choice = await prompt(
|
|
125
|
+
`Wallet exists (${existing.publicKey}). Overwrite? (y/N): `,
|
|
126
|
+
);
|
|
127
|
+
if (choice.toLowerCase() !== "y") {
|
|
128
|
+
console.log("Cancelled.");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const wallet = ensureWallet();
|
|
133
|
+
console.log(`Wallet generated: ${wallet.publicKey}`);
|
|
134
|
+
console.log(`Key stored at: ${getWalletKeyPath()}`);
|
|
135
|
+
console.log(`\nFund with USDC on Solana: ${wallet.publicKey}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function cmdWalletImport() {
|
|
139
|
+
const key = await prompt("Enter Solana private key (base58): ");
|
|
140
|
+
if (!isValidPrivateKey(key)) {
|
|
141
|
+
console.error("Invalid private key.");
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const wallet = saveWallet(key);
|
|
145
|
+
console.log(`Wallet imported: ${wallet.publicKey}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function cmdWalletExport() {
|
|
149
|
+
const key = exportWalletKey();
|
|
150
|
+
if (!key) {
|
|
151
|
+
console.error("No wallet found. Run `openclaw-setu setup` first.");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
console.log(key);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function cmdWalletInfo() {
|
|
158
|
+
const wallet = loadWallet();
|
|
159
|
+
if (!wallet) {
|
|
160
|
+
console.error("No wallet found. Run `openclaw-setu setup` first.");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`\nWallet: ${wallet.publicKey}`);
|
|
165
|
+
console.log(`Key path: ${getWalletKeyPath()}`);
|
|
166
|
+
|
|
167
|
+
console.log("\nFetching balances...");
|
|
168
|
+
const balances = await getSetuBalance(wallet.privateKey);
|
|
169
|
+
|
|
170
|
+
if (balances.setu) {
|
|
171
|
+
console.log(`\nSetu Balance: $${balances.setu.balance.toFixed(4)}`);
|
|
172
|
+
console.log(`Total Spent: $${balances.setu.totalSpent.toFixed(4)}`);
|
|
173
|
+
console.log(`Requests: ${balances.setu.requestCount}`);
|
|
174
|
+
} else {
|
|
175
|
+
console.log("\nSetu Balance: (not available — wallet may not be registered yet)");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (balances.wallet) {
|
|
179
|
+
console.log(
|
|
180
|
+
`\nOn-chain USDC: $${balances.wallet.usdcBalance.toFixed(4)} (${balances.wallet.network})`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function cmdConfigInject() {
|
|
186
|
+
await injectConfig();
|
|
187
|
+
console.log(`Setu provider injected into ${getConfigPath()}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cmdConfigRemove() {
|
|
191
|
+
removeConfig();
|
|
192
|
+
console.log(`Setu provider removed from ${getConfigPath()}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function cmdConfigStatus() {
|
|
196
|
+
if (isConfigured()) {
|
|
197
|
+
console.log(`Setu is configured in ${getConfigPath()}`);
|
|
198
|
+
} else {
|
|
199
|
+
console.log("Setu is not configured. Run `openclaw-setu setup`.");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cmdStart() {
|
|
204
|
+
const port = parseInt(process.env.SETU_PROXY_PORT ?? "8403", 10);
|
|
205
|
+
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const { wallet } = createProxy({ port, verbose });
|
|
209
|
+
console.log(`\nSetu proxy running on http://localhost:${port}`);
|
|
210
|
+
console.log(`Wallet: ${wallet.publicKey}`);
|
|
211
|
+
console.log(`\nPress Ctrl+C to stop.\n`);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error((err as Error).message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const [cmd, sub] = process.argv.slice(2);
|
|
219
|
+
|
|
220
|
+
switch (cmd) {
|
|
221
|
+
case "setup":
|
|
222
|
+
cmdSetup();
|
|
223
|
+
break;
|
|
224
|
+
case "start":
|
|
225
|
+
cmdStart();
|
|
226
|
+
break;
|
|
227
|
+
case "wallet":
|
|
228
|
+
switch (sub) {
|
|
229
|
+
case "generate":
|
|
230
|
+
cmdWalletGenerate();
|
|
231
|
+
break;
|
|
232
|
+
case "import":
|
|
233
|
+
cmdWalletImport();
|
|
234
|
+
break;
|
|
235
|
+
case "export":
|
|
236
|
+
cmdWalletExport();
|
|
237
|
+
break;
|
|
238
|
+
case "info":
|
|
239
|
+
cmdWalletInfo();
|
|
240
|
+
break;
|
|
241
|
+
default:
|
|
242
|
+
console.error('Unknown wallet command. Use: generate, import, export, info');
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case "config":
|
|
247
|
+
switch (sub) {
|
|
248
|
+
case "inject":
|
|
249
|
+
cmdConfigInject();
|
|
250
|
+
break;
|
|
251
|
+
case "remove":
|
|
252
|
+
cmdConfigRemove();
|
|
253
|
+
break;
|
|
254
|
+
case "status":
|
|
255
|
+
cmdConfigStatus();
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
console.error("Unknown config command. Use: inject, remove, status");
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
break;
|
|
262
|
+
case "help":
|
|
263
|
+
case "--help":
|
|
264
|
+
case "-h":
|
|
265
|
+
case undefined:
|
|
266
|
+
printHelp();
|
|
267
|
+
break;
|
|
268
|
+
default:
|
|
269
|
+
console.error(`Unknown command: ${cmd}`);
|
|
270
|
+
printHelp();
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import type { ModelApi } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const OPENCLAW_DIR = join(homedir(), ".openclaw");
|
|
7
|
+
const OPENCLAW_CONFIG_PATH = join(OPENCLAW_DIR, "openclaw.json");
|
|
8
|
+
|
|
9
|
+
const PROVIDER_KEY = "setu";
|
|
10
|
+
const DEFAULT_PROXY_PORT = 8403;
|
|
11
|
+
const DEFAULT_BASE_URL = "https://api.setu.ottocode.io";
|
|
12
|
+
|
|
13
|
+
export interface SetuModelConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
api?: ModelApi;
|
|
17
|
+
reasoning?: boolean;
|
|
18
|
+
input?: Array<"text" | "image">;
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SetuProviderConfig {
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
apiKey: string;
|
|
26
|
+
api: ModelApi;
|
|
27
|
+
authHeader: boolean;
|
|
28
|
+
models: SetuModelConfig[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readOpenClawConfig(): Record<string, unknown> {
|
|
32
|
+
if (!existsSync(OPENCLAW_CONFIG_PATH)) return {};
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(readFileSync(OPENCLAW_CONFIG_PATH, "utf-8"));
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeOpenClawConfig(config: Record<string, unknown>): void {
|
|
41
|
+
writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CatalogModel {
|
|
45
|
+
id: string;
|
|
46
|
+
owned_by: string;
|
|
47
|
+
context_length: number;
|
|
48
|
+
max_output: number;
|
|
49
|
+
capabilities?: { tool_call?: boolean; reasoning?: boolean };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function apiForOwner(owner: string): ModelApi {
|
|
53
|
+
switch (owner) {
|
|
54
|
+
case "anthropic":
|
|
55
|
+
return "anthropic-messages";
|
|
56
|
+
case "google":
|
|
57
|
+
return "google-generative-ai";
|
|
58
|
+
default:
|
|
59
|
+
return "openai-completions";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function displayName(id: string, owner: string): string {
|
|
64
|
+
return `${id} (${owner}, via Setu)`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fetchModelsFromCatalog(
|
|
68
|
+
baseURL: string = DEFAULT_BASE_URL,
|
|
69
|
+
): Promise<SetuModelConfig[]> {
|
|
70
|
+
try {
|
|
71
|
+
const resp = await fetch(`${baseURL}/v1/models`);
|
|
72
|
+
if (!resp.ok) return getDefaultModels();
|
|
73
|
+
const data = (await resp.json()) as { data: CatalogModel[] };
|
|
74
|
+
return data.data.map((m) => ({
|
|
75
|
+
id: m.id,
|
|
76
|
+
name: displayName(m.id, m.owned_by),
|
|
77
|
+
api: apiForOwner(m.owned_by),
|
|
78
|
+
reasoning: m.capabilities?.reasoning ?? false,
|
|
79
|
+
input: ["text"] as Array<"text" | "image">,
|
|
80
|
+
contextWindow: m.context_length,
|
|
81
|
+
maxTokens: m.max_output,
|
|
82
|
+
}));
|
|
83
|
+
} catch {
|
|
84
|
+
return getDefaultModels();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getDefaultModels(): SetuModelConfig[] {
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
id: "claude-sonnet-4-6",
|
|
92
|
+
name: "Claude Sonnet 4.6 (anthropic, via Setu)",
|
|
93
|
+
api: "anthropic-messages",
|
|
94
|
+
reasoning: true,
|
|
95
|
+
input: ["text", "image"],
|
|
96
|
+
contextWindow: 200000,
|
|
97
|
+
maxTokens: 64000,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "claude-sonnet-4-5",
|
|
101
|
+
name: "Claude Sonnet 4.5 (anthropic, via Setu)",
|
|
102
|
+
api: "anthropic-messages",
|
|
103
|
+
reasoning: true,
|
|
104
|
+
input: ["text", "image"],
|
|
105
|
+
contextWindow: 200000,
|
|
106
|
+
maxTokens: 64000,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "claude-opus-4-6",
|
|
110
|
+
name: "Claude Opus 4.6 (anthropic, via Setu)",
|
|
111
|
+
api: "anthropic-messages",
|
|
112
|
+
reasoning: true,
|
|
113
|
+
input: ["text", "image"],
|
|
114
|
+
contextWindow: 200000,
|
|
115
|
+
maxTokens: 128000,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
id: "claude-3-5-haiku-20241022",
|
|
119
|
+
name: "Claude 3.5 Haiku (anthropic, via Setu)",
|
|
120
|
+
api: "anthropic-messages",
|
|
121
|
+
reasoning: false,
|
|
122
|
+
input: ["text", "image"],
|
|
123
|
+
contextWindow: 200000,
|
|
124
|
+
maxTokens: 8192,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "gpt-5.1-codex",
|
|
128
|
+
name: "GPT-5.1 Codex (openai, via Setu)",
|
|
129
|
+
api: "openai-completions",
|
|
130
|
+
reasoning: true,
|
|
131
|
+
input: ["text", "image"],
|
|
132
|
+
contextWindow: 400000,
|
|
133
|
+
maxTokens: 128000,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
id: "gpt-5",
|
|
137
|
+
name: "GPT-5 (openai, via Setu)",
|
|
138
|
+
api: "openai-completions",
|
|
139
|
+
reasoning: true,
|
|
140
|
+
input: ["text", "image"],
|
|
141
|
+
contextWindow: 400000,
|
|
142
|
+
maxTokens: 128000,
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "gpt-5-mini",
|
|
146
|
+
name: "GPT-5 Mini (openai, via Setu)",
|
|
147
|
+
api: "openai-completions",
|
|
148
|
+
reasoning: true,
|
|
149
|
+
input: ["text", "image"],
|
|
150
|
+
contextWindow: 400000,
|
|
151
|
+
maxTokens: 128000,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "codex-mini-latest",
|
|
155
|
+
name: "Codex Mini (openai, via Setu)",
|
|
156
|
+
api: "openai-completions",
|
|
157
|
+
reasoning: true,
|
|
158
|
+
input: ["text"],
|
|
159
|
+
contextWindow: 200000,
|
|
160
|
+
maxTokens: 100000,
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "gemini-3-pro-preview",
|
|
164
|
+
name: "Gemini 3 Pro (google, via Setu)",
|
|
165
|
+
api: "openai-completions",
|
|
166
|
+
reasoning: true,
|
|
167
|
+
input: ["text", "image"],
|
|
168
|
+
contextWindow: 1000000,
|
|
169
|
+
maxTokens: 64000,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "gemini-3-flash-preview",
|
|
173
|
+
name: "Gemini 3 Flash (google, via Setu)",
|
|
174
|
+
api: "openai-completions",
|
|
175
|
+
reasoning: true,
|
|
176
|
+
input: ["text", "image"],
|
|
177
|
+
contextWindow: 1048576,
|
|
178
|
+
maxTokens: 65536,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "kimi-k2.5",
|
|
182
|
+
name: "Kimi K2.5 (moonshot, via Setu)",
|
|
183
|
+
api: "openai-completions",
|
|
184
|
+
reasoning: true,
|
|
185
|
+
input: ["text"],
|
|
186
|
+
contextWindow: 262144,
|
|
187
|
+
maxTokens: 262144,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "glm-5",
|
|
191
|
+
name: "GLM-5 (zai, via Setu)",
|
|
192
|
+
api: "openai-completions",
|
|
193
|
+
reasoning: true,
|
|
194
|
+
input: ["text"],
|
|
195
|
+
contextWindow: 204800,
|
|
196
|
+
maxTokens: 131072,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
id: "MiniMax-M2.5",
|
|
200
|
+
name: "MiniMax M2.5 (minimax, via Setu)",
|
|
201
|
+
api: "openai-completions",
|
|
202
|
+
reasoning: true,
|
|
203
|
+
input: ["text"],
|
|
204
|
+
contextWindow: 204800,
|
|
205
|
+
maxTokens: 131072,
|
|
206
|
+
},
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function buildProviderConfig(
|
|
211
|
+
port: number = DEFAULT_PROXY_PORT,
|
|
212
|
+
): SetuProviderConfig {
|
|
213
|
+
return {
|
|
214
|
+
baseUrl: `http://localhost:${port}/v1`,
|
|
215
|
+
apiKey: "setu-proxy-handles-auth",
|
|
216
|
+
api: "openai-completions",
|
|
217
|
+
authHeader: false,
|
|
218
|
+
models: getDefaultModels(),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function buildProviderConfigWithCatalog(
|
|
223
|
+
port: number = DEFAULT_PROXY_PORT,
|
|
224
|
+
baseURL: string = DEFAULT_BASE_URL,
|
|
225
|
+
): Promise<SetuProviderConfig> {
|
|
226
|
+
const models = await fetchModelsFromCatalog(baseURL);
|
|
227
|
+
return {
|
|
228
|
+
baseUrl: `http://localhost:${port}/v1`,
|
|
229
|
+
apiKey: "setu-proxy-handles-auth",
|
|
230
|
+
api: "openai-completions",
|
|
231
|
+
authHeader: false,
|
|
232
|
+
models,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function injectConfig(port: number = DEFAULT_PROXY_PORT): Promise<void> {
|
|
237
|
+
const config = readOpenClawConfig();
|
|
238
|
+
|
|
239
|
+
if (!config.models) config.models = {};
|
|
240
|
+
const models = config.models as Record<string, unknown>;
|
|
241
|
+
if (!models.providers) models.providers = {};
|
|
242
|
+
const providers = models.providers as Record<string, unknown>;
|
|
243
|
+
|
|
244
|
+
providers[PROVIDER_KEY] = await buildProviderConfigWithCatalog(port);
|
|
245
|
+
|
|
246
|
+
writeOpenClawConfig(config);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function removeConfig(): void {
|
|
250
|
+
const config = readOpenClawConfig();
|
|
251
|
+
|
|
252
|
+
const models = config.models as Record<string, unknown> | undefined;
|
|
253
|
+
if (!models?.providers) return;
|
|
254
|
+
const providers = models.providers as Record<string, unknown>;
|
|
255
|
+
delete providers[PROVIDER_KEY];
|
|
256
|
+
|
|
257
|
+
writeOpenClawConfig(config);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function isConfigured(): boolean {
|
|
261
|
+
const config = readOpenClawConfig();
|
|
262
|
+
const models = config.models as Record<string, unknown> | undefined;
|
|
263
|
+
if (!models?.providers) return false;
|
|
264
|
+
const providers = models.providers as Record<string, unknown>;
|
|
265
|
+
return PROVIDER_KEY in providers;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function getConfigPath(): string {
|
|
269
|
+
return OPENCLAW_CONFIG_PATH;
|
|
270
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OpenClawPluginDefinition,
|
|
3
|
+
OpenClawPluginApi,
|
|
4
|
+
OpenClawPluginCommandDefinition,
|
|
5
|
+
} from "./types.ts";
|
|
6
|
+
import {
|
|
7
|
+
loadWallet,
|
|
8
|
+
ensureWallet,
|
|
9
|
+
getSetuBalance,
|
|
10
|
+
getWalletKeyPath,
|
|
11
|
+
} from "./wallet.ts";
|
|
12
|
+
import {
|
|
13
|
+
buildProviderConfig,
|
|
14
|
+
injectConfig,
|
|
15
|
+
isConfigured,
|
|
16
|
+
} from "./config.ts";
|
|
17
|
+
import { isValidPrivateKey } from "@ottocode/ai-sdk";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_PORT = 8403;
|
|
20
|
+
|
|
21
|
+
function getPort(api: OpenClawPluginApi): number {
|
|
22
|
+
const cfg = api.pluginConfig as Record<string, unknown> | undefined;
|
|
23
|
+
return (cfg?.port as number) ?? DEFAULT_PORT;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const plugin: OpenClawPluginDefinition = {
|
|
27
|
+
id: "setu",
|
|
28
|
+
name: "Setu",
|
|
29
|
+
description: "Pay for AI with Solana USDC — no API keys, just a wallet.",
|
|
30
|
+
version: "0.1.0",
|
|
31
|
+
|
|
32
|
+
async register(api: OpenClawPluginApi) {
|
|
33
|
+
const port = getPort(api);
|
|
34
|
+
|
|
35
|
+
api.registerProvider({
|
|
36
|
+
id: "setu",
|
|
37
|
+
label: "Setu (Solana USDC)",
|
|
38
|
+
aliases: ["setu-solana"],
|
|
39
|
+
envVars: ["SETU_PRIVATE_KEY"],
|
|
40
|
+
models: buildProviderConfig(port),
|
|
41
|
+
auth: [
|
|
42
|
+
{
|
|
43
|
+
id: "setu-wallet",
|
|
44
|
+
label: "Solana Wallet",
|
|
45
|
+
hint: "Generate or import a Solana wallet — pay per token with USDC",
|
|
46
|
+
kind: "custom",
|
|
47
|
+
async run(ctx) {
|
|
48
|
+
const existing = loadWallet();
|
|
49
|
+
|
|
50
|
+
if (existing) {
|
|
51
|
+
ctx.prompter.note(
|
|
52
|
+
`Existing Setu wallet found: ${existing.publicKey}`,
|
|
53
|
+
);
|
|
54
|
+
return {
|
|
55
|
+
profiles: [
|
|
56
|
+
{
|
|
57
|
+
profileId: "setu-wallet",
|
|
58
|
+
credential: {
|
|
59
|
+
apiKey: "setu-proxy-handles-auth",
|
|
60
|
+
type: "wallet",
|
|
61
|
+
walletAddress: existing.publicKey,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
configPatch: {
|
|
66
|
+
models: { providers: { setu: buildProviderConfig(port) } },
|
|
67
|
+
},
|
|
68
|
+
defaultModel: `setu/claude-sonnet-4-6`,
|
|
69
|
+
notes: [
|
|
70
|
+
`Wallet: ${existing.publicKey}`,
|
|
71
|
+
`Fund with USDC on Solana to start using.`,
|
|
72
|
+
`Run \`openclaw-setu start\` to start the proxy.`,
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const keyInput = await ctx.prompter.text({
|
|
78
|
+
message:
|
|
79
|
+
"Enter Solana private key (base58) or press Enter to generate a new one:",
|
|
80
|
+
validate: (value: string) => {
|
|
81
|
+
if (value && !isValidPrivateKey(value)) {
|
|
82
|
+
return "Invalid Solana private key";
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const key = typeof keyInput === "string" ? keyInput : "";
|
|
89
|
+
const wallet = key ? ensureWallet() : ensureWallet();
|
|
90
|
+
if (key && isValidPrivateKey(key)) {
|
|
91
|
+
const { saveWallet } = await import("./wallet.ts");
|
|
92
|
+
saveWallet(key);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const finalWallet = loadWallet()!;
|
|
96
|
+
|
|
97
|
+
await injectConfig(port);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
profiles: [
|
|
101
|
+
{
|
|
102
|
+
profileId: "setu-wallet",
|
|
103
|
+
credential: {
|
|
104
|
+
apiKey: "setu-proxy-handles-auth",
|
|
105
|
+
type: "wallet",
|
|
106
|
+
walletAddress: finalWallet.publicKey,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
configPatch: {
|
|
111
|
+
models: { providers: { setu: buildProviderConfig(port) } },
|
|
112
|
+
},
|
|
113
|
+
defaultModel: `setu/claude-sonnet-4-6`,
|
|
114
|
+
notes: [
|
|
115
|
+
`Wallet generated: ${finalWallet.publicKey}`,
|
|
116
|
+
`Key stored at: ${getWalletKeyPath()}`,
|
|
117
|
+
`Fund with USDC on Solana: ${finalWallet.publicKey}`,
|
|
118
|
+
`Run \`openclaw-setu start\` to start the proxy.`,
|
|
119
|
+
],
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const walletCmd: OpenClawPluginCommandDefinition = {
|
|
127
|
+
name: "wallet",
|
|
128
|
+
description: "Show your Setu wallet address and balances",
|
|
129
|
+
requireAuth: true,
|
|
130
|
+
async handler() {
|
|
131
|
+
const wallet = loadWallet();
|
|
132
|
+
if (!wallet) {
|
|
133
|
+
return { text: "No Setu wallet found. Run `openclaw-setu setup`." };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const balances = await getSetuBalance(wallet.privateKey);
|
|
137
|
+
const lines = [`Wallet: ${wallet.publicKey}`];
|
|
138
|
+
|
|
139
|
+
if (balances.setu) {
|
|
140
|
+
lines.push(`Setu Balance: $${balances.setu.balance.toFixed(4)}`);
|
|
141
|
+
lines.push(`Total Spent: $${balances.setu.totalSpent.toFixed(4)}`);
|
|
142
|
+
lines.push(`Requests: ${balances.setu.requestCount}`);
|
|
143
|
+
}
|
|
144
|
+
if (balances.wallet) {
|
|
145
|
+
lines.push(
|
|
146
|
+
`On-chain USDC: $${balances.wallet.usdcBalance.toFixed(4)} (${balances.wallet.network})`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { text: lines.join("\n") };
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
api.registerCommand(walletCmd);
|
|
155
|
+
|
|
156
|
+
const statusCmd: OpenClawPluginCommandDefinition = {
|
|
157
|
+
name: "setu-status",
|
|
158
|
+
description: "Check Setu plugin configuration status",
|
|
159
|
+
async handler() {
|
|
160
|
+
const wallet = loadWallet();
|
|
161
|
+
const configured = isConfigured();
|
|
162
|
+
const lines = [
|
|
163
|
+
`Wallet: ${wallet ? wallet.publicKey : "not set up"}`,
|
|
164
|
+
`OpenClaw config: ${configured ? "injected" : "not configured"}`,
|
|
165
|
+
`Proxy port: ${port}`,
|
|
166
|
+
];
|
|
167
|
+
return { text: lines.join("\n") };
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
api.registerCommand(statusCmd);
|
|
172
|
+
|
|
173
|
+
api.registerService({
|
|
174
|
+
id: "setu-proxy",
|
|
175
|
+
async start() {
|
|
176
|
+
const wallet = loadWallet();
|
|
177
|
+
if (!wallet) {
|
|
178
|
+
api.logger.warn(
|
|
179
|
+
"Setu: No wallet found. Run `openclaw-setu setup` first.",
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const { createProxy } = await import("./proxy.ts");
|
|
185
|
+
createProxy({ port, verbose: false });
|
|
186
|
+
api.logger.info(
|
|
187
|
+
`Setu proxy running on http://localhost:${port}`,
|
|
188
|
+
);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
api.logger.error(`Setu proxy failed: ${(err as Error).message}`);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export default plugin;
|
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createSetuFetch, createWalletContext } from "@ottocode/ai-sdk";
|
|
2
|
+
import { loadWallet } from "./wallet.ts";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_PORT = 8403;
|
|
5
|
+
const DEFAULT_BASE_URL = "https://api.setu.ottocode.io";
|
|
6
|
+
|
|
7
|
+
export interface ProxyOptions {
|
|
8
|
+
port?: number;
|
|
9
|
+
baseURL?: string;
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createProxy(options: ProxyOptions = {}) {
|
|
14
|
+
const port = options.port ?? DEFAULT_PORT;
|
|
15
|
+
const baseURL = options.baseURL ?? DEFAULT_BASE_URL;
|
|
16
|
+
const verbose = options.verbose ?? false;
|
|
17
|
+
|
|
18
|
+
const wallet = loadWallet();
|
|
19
|
+
if (!wallet) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"No wallet found. Run `openclaw-setu setup` or `openclaw-setu wallet generate` first.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const log = verbose
|
|
26
|
+
? (msg: string) => console.log(`[setu-proxy] ${msg}`)
|
|
27
|
+
: (_msg: string) => {};
|
|
28
|
+
|
|
29
|
+
const walletCtx = createWalletContext({ privateKey: wallet.privateKey });
|
|
30
|
+
|
|
31
|
+
const setuFetch = createSetuFetch({
|
|
32
|
+
wallet: walletCtx,
|
|
33
|
+
baseURL,
|
|
34
|
+
callbacks: {
|
|
35
|
+
onPaymentRequired: (amountUsd) => {
|
|
36
|
+
log(`Payment required: $${amountUsd.toFixed(4)}`);
|
|
37
|
+
},
|
|
38
|
+
onPaymentComplete: (data) => {
|
|
39
|
+
log(
|
|
40
|
+
`Payment complete: $${data.amountUsd.toFixed(4)} | balance: $${data.newBalance.toFixed(4)}`,
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
onPaymentError: (error) => {
|
|
44
|
+
console.error(`[setu-proxy] Payment error: ${error}`);
|
|
45
|
+
},
|
|
46
|
+
onBalanceUpdate: (update) => {
|
|
47
|
+
log(
|
|
48
|
+
`Cost: $${update.costUsd.toFixed(4)} | remaining: $${update.balanceRemaining.toFixed(4)}`,
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const proxyBaseURL = baseURL;
|
|
55
|
+
|
|
56
|
+
const server = Bun.serve({
|
|
57
|
+
port,
|
|
58
|
+
async fetch(req: Request): Promise<Response> {
|
|
59
|
+
const url = new URL(req.url);
|
|
60
|
+
|
|
61
|
+
if (url.pathname === "/health") {
|
|
62
|
+
return Response.json({
|
|
63
|
+
status: "ok",
|
|
64
|
+
wallet: wallet.publicKey,
|
|
65
|
+
provider: "setu",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (url.pathname === "/v1/models") {
|
|
70
|
+
log("GET /v1/models");
|
|
71
|
+
const resp = await setuFetch(`${proxyBaseURL}/v1/models`, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
});
|
|
75
|
+
return new Response(resp.body, {
|
|
76
|
+
status: resp.status,
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type":
|
|
79
|
+
resp.headers.get("Content-Type") ?? "application/json",
|
|
80
|
+
"Access-Control-Allow-Origin": "*",
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isCompletions = url.pathname === "/v1/chat/completions";
|
|
86
|
+
const isResponses = url.pathname === "/v1/responses";
|
|
87
|
+
if (!isCompletions && !isResponses) {
|
|
88
|
+
const targetURL = `${proxyBaseURL}${url.pathname}`;
|
|
89
|
+
log(`Proxying ${req.method} ${url.pathname}`);
|
|
90
|
+
const resp = await setuFetch(targetURL, {
|
|
91
|
+
method: req.method,
|
|
92
|
+
headers: { "Content-Type": "application/json" },
|
|
93
|
+
body: req.method !== "GET" ? await req.text() : undefined,
|
|
94
|
+
});
|
|
95
|
+
return new Response(resp.body, {
|
|
96
|
+
status: resp.status,
|
|
97
|
+
headers: {
|
|
98
|
+
"Content-Type":
|
|
99
|
+
resp.headers.get("Content-Type") ?? "application/json",
|
|
100
|
+
"Access-Control-Allow-Origin": "*",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const body = await req.text();
|
|
106
|
+
let parsed: Record<string, unknown> = {};
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(body);
|
|
109
|
+
} catch {
|
|
110
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const model = parsed.model as string;
|
|
114
|
+
const stream = parsed.stream as boolean;
|
|
115
|
+
log(
|
|
116
|
+
`${isCompletions ? "POST /v1/chat/completions" : "POST /v1/responses"} model=${model} stream=${stream}`,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const targetURL = `${proxyBaseURL}${url.pathname}`;
|
|
120
|
+
const resp = await setuFetch(targetURL, {
|
|
121
|
+
method: "POST",
|
|
122
|
+
headers: { "Content-Type": "application/json" },
|
|
123
|
+
body,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (stream && resp.body) {
|
|
127
|
+
return new Response(resp.body, {
|
|
128
|
+
status: resp.status,
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "text/event-stream",
|
|
131
|
+
"Cache-Control": "no-cache",
|
|
132
|
+
Connection: "keep-alive",
|
|
133
|
+
"Access-Control-Allow-Origin": "*",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return new Response(resp.body, {
|
|
139
|
+
status: resp.status,
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"Access-Control-Allow-Origin": "*",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return { server, port, wallet };
|
|
149
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export type ModelApi =
|
|
2
|
+
| "openai-completions"
|
|
3
|
+
| "openai-responses"
|
|
4
|
+
| "anthropic-messages"
|
|
5
|
+
| "google-generative-ai"
|
|
6
|
+
| "github-copilot"
|
|
7
|
+
| "bedrock-converse-stream";
|
|
8
|
+
|
|
9
|
+
export type ModelDefinitionConfig = {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
api?: ModelApi;
|
|
13
|
+
reasoning?: boolean;
|
|
14
|
+
input?: Array<"text" | "image">;
|
|
15
|
+
cost?: {
|
|
16
|
+
input: number;
|
|
17
|
+
output: number;
|
|
18
|
+
cacheRead?: number;
|
|
19
|
+
cacheWrite?: number;
|
|
20
|
+
};
|
|
21
|
+
contextWindow?: number;
|
|
22
|
+
maxTokens?: number;
|
|
23
|
+
headers?: Record<string, string>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ModelProviderConfig = {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
api?: ModelApi;
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
authHeader?: boolean;
|
|
32
|
+
models: ModelDefinitionConfig[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type AuthProfileCredential = {
|
|
36
|
+
apiKey?: string;
|
|
37
|
+
type?: string;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ProviderAuthResult = {
|
|
42
|
+
profiles: Array<{ profileId: string; credential: AuthProfileCredential }>;
|
|
43
|
+
configPatch?: Record<string, unknown>;
|
|
44
|
+
defaultModel?: string;
|
|
45
|
+
notes?: string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type WizardPrompter = {
|
|
49
|
+
text: (opts: {
|
|
50
|
+
message: string;
|
|
51
|
+
validate?: (value: string) => string | undefined;
|
|
52
|
+
}) => Promise<string | symbol>;
|
|
53
|
+
note: (message: string) => void;
|
|
54
|
+
progress: (message: string) => { stop: (message?: string) => void };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ProviderAuthContext = {
|
|
58
|
+
config: Record<string, unknown>;
|
|
59
|
+
agentDir?: string;
|
|
60
|
+
workspaceDir?: string;
|
|
61
|
+
prompter: WizardPrompter;
|
|
62
|
+
runtime: { log: (message: string) => void };
|
|
63
|
+
isRemote: boolean;
|
|
64
|
+
openUrl: (url: string) => Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export type ProviderAuthMethod = {
|
|
68
|
+
id: string;
|
|
69
|
+
label: string;
|
|
70
|
+
hint?: string;
|
|
71
|
+
kind: "oauth" | "api_key" | "token" | "device_code" | "custom";
|
|
72
|
+
run: (ctx: ProviderAuthContext) => Promise<ProviderAuthResult>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type ProviderPlugin = {
|
|
76
|
+
id: string;
|
|
77
|
+
label: string;
|
|
78
|
+
docsPath?: string;
|
|
79
|
+
aliases?: string[];
|
|
80
|
+
envVars?: string[];
|
|
81
|
+
models?: ModelProviderConfig;
|
|
82
|
+
auth: ProviderAuthMethod[];
|
|
83
|
+
formatApiKey?: (cred: AuthProfileCredential) => string;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type PluginLogger = {
|
|
87
|
+
debug?: (message: string) => void;
|
|
88
|
+
info: (message: string) => void;
|
|
89
|
+
warn: (message: string) => void;
|
|
90
|
+
error: (message: string) => void;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type OpenClawPluginService = {
|
|
94
|
+
id: string;
|
|
95
|
+
start: () => void | Promise<void>;
|
|
96
|
+
stop?: () => void | Promise<void>;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type OpenClawPluginApi = {
|
|
100
|
+
id: string;
|
|
101
|
+
name: string;
|
|
102
|
+
version?: string;
|
|
103
|
+
description?: string;
|
|
104
|
+
source: string;
|
|
105
|
+
config: Record<string, unknown> & {
|
|
106
|
+
models?: { providers?: Record<string, ModelProviderConfig> };
|
|
107
|
+
agents?: Record<string, unknown>;
|
|
108
|
+
};
|
|
109
|
+
pluginConfig?: Record<string, unknown>;
|
|
110
|
+
logger: PluginLogger;
|
|
111
|
+
registerProvider: (provider: ProviderPlugin) => void;
|
|
112
|
+
registerTool: (tool: unknown, opts?: unknown) => void;
|
|
113
|
+
registerHook: (events: string | string[], handler: unknown, opts?: unknown) => void;
|
|
114
|
+
registerHttpRoute: (params: { path: string; handler: unknown }) => void;
|
|
115
|
+
registerService: (service: OpenClawPluginService) => void;
|
|
116
|
+
registerCommand: (command: unknown) => void;
|
|
117
|
+
resolvePath: (input: string) => string;
|
|
118
|
+
on: (hookName: string, handler: unknown, opts?: unknown) => void;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type OpenClawPluginDefinition = {
|
|
122
|
+
id?: string;
|
|
123
|
+
name?: string;
|
|
124
|
+
description?: string;
|
|
125
|
+
version?: string;
|
|
126
|
+
register?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
127
|
+
activate?: (api: OpenClawPluginApi) => void | Promise<void>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type PluginCommandContext = {
|
|
131
|
+
senderId?: string;
|
|
132
|
+
channel: string;
|
|
133
|
+
isAuthorizedSender: boolean;
|
|
134
|
+
args?: string;
|
|
135
|
+
commandBody: string;
|
|
136
|
+
config: Record<string, unknown>;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type PluginCommandResult = {
|
|
140
|
+
text?: string;
|
|
141
|
+
isError?: boolean;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type PluginCommandHandler = (
|
|
145
|
+
ctx: PluginCommandContext,
|
|
146
|
+
) => PluginCommandResult | Promise<PluginCommandResult>;
|
|
147
|
+
|
|
148
|
+
export type OpenClawPluginCommandDefinition = {
|
|
149
|
+
name: string;
|
|
150
|
+
description: string;
|
|
151
|
+
acceptsArgs?: boolean;
|
|
152
|
+
requireAuth?: boolean;
|
|
153
|
+
handler: PluginCommandHandler;
|
|
154
|
+
};
|
package/src/wallet.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import {
|
|
5
|
+
generateWallet,
|
|
6
|
+
importWallet,
|
|
7
|
+
isValidPrivateKey,
|
|
8
|
+
fetchBalance,
|
|
9
|
+
fetchWalletUsdcBalance,
|
|
10
|
+
} from "@ottocode/ai-sdk";
|
|
11
|
+
import type { WalletInfo } from "@ottocode/ai-sdk";
|
|
12
|
+
|
|
13
|
+
const WALLET_DIR = join(homedir(), ".openclaw", "setu");
|
|
14
|
+
const WALLET_KEY_PATH = join(WALLET_DIR, "wallet.key");
|
|
15
|
+
|
|
16
|
+
export function getWalletKeyPath(): string {
|
|
17
|
+
return WALLET_KEY_PATH;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function loadWallet(): WalletInfo | null {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(WALLET_KEY_PATH)) return null;
|
|
23
|
+
const privateKey = readFileSync(WALLET_KEY_PATH, "utf-8").trim();
|
|
24
|
+
if (!isValidPrivateKey(privateKey)) return null;
|
|
25
|
+
return importWallet(privateKey);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveWallet(privateKey: string): WalletInfo {
|
|
32
|
+
if (!isValidPrivateKey(privateKey)) {
|
|
33
|
+
throw new Error("Invalid Solana private key");
|
|
34
|
+
}
|
|
35
|
+
const wallet = importWallet(privateKey);
|
|
36
|
+
mkdirSync(WALLET_DIR, { recursive: true });
|
|
37
|
+
writeFileSync(WALLET_KEY_PATH, privateKey, { mode: 0o600 });
|
|
38
|
+
return wallet;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ensureWallet(): WalletInfo {
|
|
42
|
+
const existing = loadWallet();
|
|
43
|
+
if (existing) return existing;
|
|
44
|
+
const wallet = generateWallet();
|
|
45
|
+
saveWallet(wallet.privateKey);
|
|
46
|
+
return wallet;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function exportWalletKey(): string | null {
|
|
50
|
+
try {
|
|
51
|
+
if (!existsSync(WALLET_KEY_PATH)) return null;
|
|
52
|
+
return readFileSync(WALLET_KEY_PATH, "utf-8").trim();
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function getSetuBalance(privateKey: string): Promise<{
|
|
59
|
+
setu: { balance: number; totalSpent: number; requestCount: number } | null;
|
|
60
|
+
wallet: { usdcBalance: number; network: string } | null;
|
|
61
|
+
}> {
|
|
62
|
+
const [setu, wallet] = await Promise.all([
|
|
63
|
+
fetchBalance({ privateKey }),
|
|
64
|
+
fetchWalletUsdcBalance({ privateKey }),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
setu: setu
|
|
69
|
+
? {
|
|
70
|
+
balance: setu.balance,
|
|
71
|
+
totalSpent: setu.totalSpent,
|
|
72
|
+
requestCount: setu.requestCount,
|
|
73
|
+
}
|
|
74
|
+
: null,
|
|
75
|
+
wallet: wallet
|
|
76
|
+
? { usdcBalance: wallet.usdcBalance, network: wallet.network }
|
|
77
|
+
: null,
|
|
78
|
+
};
|
|
79
|
+
}
|