agent402-openclaw 0.2.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/README.md +27 -5
- package/cli.js +50 -7
- package/index.js +45 -8
- package/models.js +36 -2
- package/openclaw.plugin.json +44 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,15 +11,24 @@ Guide: https://agent402.tools/guides/openclaw-model-provider
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
openclaw plugins install agent402-openclaw
|
|
14
|
-
AGENT402_CREDITS_KEY=a402_... agent402-openclaw setup --write # key by env (or `--credits-key -` on stdin); buy one by card at https://agent402.tools/credits
|
|
14
|
+
AGENT402_CREDITS_KEY=a402_... npx agent402-openclaw setup --write # key by env (or `--credits-key -` on stdin); buy one by card at https://agent402.tools/credits
|
|
15
15
|
openclaw gateway restart
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
(`openclaw plugins install` copies the plugin into `~/.openclaw/extensions` and
|
|
19
|
+
does not link its CLI, hence `npx`.) `setup` stores the key under
|
|
20
|
+
`~/.openclaw/agent402/credits.key` (0600) and prints the `openclaw.json` block;
|
|
21
|
+
add `--write` to merge it in. The primary model it writes is the cheapest
|
|
22
|
+
metered model that can hold OpenClaw's own prompt: OpenClaw sends roughly 70k
|
|
23
|
+
characters of system prompt and tool schemas before your first word, and the
|
|
24
|
+
routed `auto` tier caps input at 16k, so `auto` stays listed for short one-off
|
|
25
|
+
prompts but is never made primary (OpenClaw would refuse every turn as a
|
|
26
|
+
context overflow). The plugin starts a loopback proxy (`127.0.0.1:8412`) that
|
|
21
27
|
pays Agent402 and forwards; OpenClaw only ever sees a local OpenAI-compatible URL.
|
|
22
28
|
|
|
29
|
+
Tested in CI against a real `openclaw@latest` install: plugin install, setup,
|
|
30
|
+
model listing, gateway boot and one agent turn (`test-real-install.js`).
|
|
31
|
+
|
|
23
32
|
No OpenClaw? `agent402-openclaw proxy` runs the proxy alone; point any OpenAI
|
|
24
33
|
client at `http://127.0.0.1:8412/v1` with model `auto`.
|
|
25
34
|
|
|
@@ -33,6 +42,18 @@ export AGENT402_WALLET_KEY=0x... # an EVM key holding USDC on Base
|
|
|
33
42
|
With no credits key present the proxy signs an x402 payment per call from that
|
|
34
43
|
wallet. The key never leaves the machine.
|
|
35
44
|
|
|
45
|
+
Two ways the wallet can pay a metered call:
|
|
46
|
+
|
|
47
|
+
- **exact** (default): the 402 quotes the call from its body (input + your
|
|
48
|
+
`max_tokens` at the model's list price, x1.15) and the wallet pays that quote.
|
|
49
|
+
- **upto, actual usage**: run `agent402-openclaw permit2-approve` once (one USDC
|
|
50
|
+
approval transaction on Base; the wallet needs a little ETH for gas). From
|
|
51
|
+
then on the proxy authorizes the quote as a CEILING and the gateway settles
|
|
52
|
+
what the call actually cost x1.15, so a short answer costs a fraction of the
|
|
53
|
+
quote. `agent402-openclaw doctor` says which mode the wallet is in.
|
|
54
|
+
|
|
55
|
+
Card / credits buyers already pay actual usage on metered calls.
|
|
56
|
+
|
|
36
57
|
## Models and pricing
|
|
37
58
|
|
|
38
59
|
`auto` (routed per prompt, flat $0.01/call) plus every id from
|
|
@@ -53,8 +74,9 @@ the key.
|
|
|
53
74
|
|
|
54
75
|
## Commands
|
|
55
76
|
|
|
56
|
-
- `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--write] [--port N]`
|
|
77
|
+
- `agent402-openclaw setup [--credits-key K | --credits-key - (stdin) | AGENT402_CREDITS_KEY env] [--write] [--port N] [--flat]`
|
|
57
78
|
- `agent402-openclaw proxy [--port N] [--upstream URL]`
|
|
58
79
|
- `agent402-openclaw doctor`
|
|
80
|
+
- `agent402-openclaw permit2-approve [--rpc URL]` (one-time USDC approval so the wallet pays actual usage over upto)
|
|
59
81
|
|
|
60
82
|
Zero dependencies. MIT. Maintained by Havok Holdings LLC.
|
package/cli.js
CHANGED
|
@@ -7,9 +7,9 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, statSync
|
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { STATE_DIR, CREDITS_KEY_FILE, resolveCreditsKey, resolvePayFetch } from "./index.js";
|
|
10
|
+
import { STATE_DIR, CREDITS_KEY_FILE, resolveCreditsKey, resolvePayFetch, USDC_BASE, DEFAULT_BASE_RPC, uptoReady } from "./index.js";
|
|
11
11
|
import { startProxy, loadRoutes, DEFAULT_UPSTREAM } from "./proxy.js";
|
|
12
|
-
import { providerModelsConfig, stripTrailingSlashes } from "./models.js";
|
|
12
|
+
import { providerModelsConfig, stripTrailingSlashes, defaultPrimary, AUTO_ID, OPENCLAW_MIN_INPUT_CHARS } from "./models.js";
|
|
13
13
|
import { DEFAULT_PORT, PROVIDER_ID } from "./provider.js";
|
|
14
14
|
|
|
15
15
|
const args = process.argv.slice(2);
|
|
@@ -20,24 +20,32 @@ const out = (s) => process.stdout.write(s + "\n");
|
|
|
20
20
|
|
|
21
21
|
/** The provider block for openclaw.json (JSON, so it can be merged; OpenClaw reads JSON5 which is a superset). */
|
|
22
22
|
export function configBlock({ port = DEFAULT_PORT, routes }) {
|
|
23
|
+
const primary = defaultPrimary(routes);
|
|
23
24
|
return {
|
|
24
|
-
agents: { defaults: { model: { primary: `${PROVIDER_ID}
|
|
25
|
+
agents: { defaults: { model: { primary: `${PROVIDER_ID}/${primary ? primary.id : AUTO_ID}` } } },
|
|
25
26
|
models: { providers: { [PROVIDER_ID]: { ...providerModelsConfig(`http://127.0.0.1:${port}`, routes), timeoutSeconds: 120 } } },
|
|
26
27
|
};
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function mergeInto(target, block) {
|
|
30
|
+
function mergeInto(target, block, { port = DEFAULT_PORT } = {}) {
|
|
30
31
|
const t = target && typeof target === "object" ? target : {};
|
|
31
32
|
t.agents = t.agents || {}; t.agents.defaults = t.agents.defaults || {};
|
|
32
33
|
t.agents.defaults.model = { ...(t.agents.defaults.model || {}), primary: block.agents.defaults.model.primary };
|
|
33
34
|
t.models = t.models || {}; t.models.providers = t.models.providers || {};
|
|
34
35
|
t.models.providers[PROVIDER_ID] = block.models.providers[PROVIDER_ID];
|
|
36
|
+
// The plugin service reads ITS port from plugins.entries.<id>.config, not from
|
|
37
|
+
// the provider baseUrl - a --port that only moved the baseUrl left the proxy
|
|
38
|
+
// on 8412 and OpenClaw dialing a port nobody listened on (real-install test).
|
|
39
|
+
if (port !== DEFAULT_PORT) {
|
|
40
|
+
t.plugins = t.plugins || {}; t.plugins.entries = t.plugins.entries || {};
|
|
41
|
+
t.plugins.entries[PROVIDER_ID] = { ...(t.plugins.entries[PROVIDER_ID] || {}), config: { ...(t.plugins.entries[PROVIDER_ID]?.config || {}), port } };
|
|
42
|
+
}
|
|
35
43
|
return t;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
async function main() {
|
|
39
47
|
if (cmd === "help" || has("--help")) {
|
|
40
|
-
out("agent402-openclaw setup [--credits-key a402_...|-] [--write] [--flat] | proxy [--port N] [--upstream URL] [--flat] | doctor");
|
|
48
|
+
out("agent402-openclaw setup [--credits-key a402_...|-] [--write] [--flat] | proxy [--port N] [--upstream URL] [--flat] | doctor | permit2-approve [--rpc URL]");
|
|
41
49
|
return 0;
|
|
42
50
|
}
|
|
43
51
|
const upstream = stripTrailingSlashes(opt("--upstream") || process.env.AGENT402_UPSTREAM || DEFAULT_UPSTREAM);
|
|
@@ -64,6 +72,10 @@ async function main() {
|
|
|
64
72
|
let routes;
|
|
65
73
|
try { routes = await loadRoutes(upstream, fetch, { pricing }); } catch (e) { console.error(`could not read ${upstream}/v1/models: ${e?.message || e}`); return 2; }
|
|
66
74
|
const block = configBlock({ port, routes });
|
|
75
|
+
const primary = block.agents.defaults.model.primary;
|
|
76
|
+
if (primary === `${PROVIDER_ID}/${AUTO_ID}`) {
|
|
77
|
+
console.error(`WARNING: no model in this catalog (${pricing} pricing) accepts the ~${Math.round(OPENCLAW_MIN_INPUT_CHARS / 1000)}k chars OpenClaw sends before your first word; primary falls back to ${primary}, which will answer "Context overflow" on every turn. Rerun without --flat to use the metered route.`);
|
|
78
|
+
}
|
|
67
79
|
const cfgPath = join(process.env.AGENT402_OPENCLAW_HOME || join(homedir(), ".openclaw"), "openclaw.json");
|
|
68
80
|
if (has("--write")) {
|
|
69
81
|
let existing = {};
|
|
@@ -77,10 +89,10 @@ async function main() {
|
|
|
77
89
|
let mode = 0o600;
|
|
78
90
|
try { if (existsSync(cfgPath)) mode = statSync(cfgPath).mode & 0o777; } catch { /* keep 0600 */ }
|
|
79
91
|
const tmp = `${cfgPath}.${process.pid}.tmp`;
|
|
80
|
-
writeFileSync(tmp, JSON.stringify(mergeInto(existing, block), null, 2) + "\n", { mode });
|
|
92
|
+
writeFileSync(tmp, JSON.stringify(mergeInto(existing, block, { port }), null, 2) + "\n", { mode });
|
|
81
93
|
try { chmodSync(tmp, mode); } catch { /* best effort */ }
|
|
82
94
|
renameSync(tmp, cfgPath);
|
|
83
|
-
out(`wrote provider "${PROVIDER_ID}" + primary model ${
|
|
95
|
+
out(`wrote provider "${PROVIDER_ID}" + primary model ${primary} into ${cfgPath}; run: openclaw gateway restart`);
|
|
84
96
|
} else {
|
|
85
97
|
out(`add to ${cfgPath} (or rerun with --write):`);
|
|
86
98
|
out(JSON.stringify(block, null, 2));
|
|
@@ -96,11 +108,42 @@ async function main() {
|
|
|
96
108
|
await new Promise(() => {}); // run until killed
|
|
97
109
|
}
|
|
98
110
|
|
|
111
|
+
// One-time USDC -> Permit2 approval on Base so the wallet can pay ACTUAL
|
|
112
|
+
// usage over `upto` instead of the per-request quote over `exact`.
|
|
113
|
+
if (cmd === "permit2-approve") {
|
|
114
|
+
const pk = (process.env.AGENT402_WALLET_KEY || "").trim();
|
|
115
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(pk)) { console.error("AGENT402_WALLET_KEY (0x + 64 hex) is required"); return 2; }
|
|
116
|
+
try {
|
|
117
|
+
const [{ createPermit2ApprovalTx, getPermit2AllowanceReadParams }, { createWalletClient, createPublicClient, http }, { privateKeyToAccount }, { base }] = await Promise.all([import("@x402/evm/upto/client"), import("viem"), import("viem/accounts"), import("viem/chains")]);
|
|
118
|
+
const account = privateKeyToAccount(pk);
|
|
119
|
+
const rpc = (opt("--rpc") || process.env.AGENT402_BASE_RPC || DEFAULT_BASE_RPC).trim();
|
|
120
|
+
const pub = createPublicClient({ chain: base, transport: http(rpc) });
|
|
121
|
+
const read = () => pub.readContract(getPermit2AllowanceReadParams({ tokenAddress: USDC_BASE, ownerAddress: account.address }));
|
|
122
|
+
if (uptoReady(await read())) { out(`already approved: ${account.address} has a Permit2 allowance on USDC (Base); the proxy pays actual usage (upto)`); return 0; }
|
|
123
|
+
const tx = createPermit2ApprovalTx(USDC_BASE);
|
|
124
|
+
const wallet = createWalletClient({ account, chain: base, transport: http(rpc) });
|
|
125
|
+
const hash = await wallet.sendTransaction({ to: tx.to, data: tx.data });
|
|
126
|
+
out(`approval sent: ${hash} (waiting for confirmation)`);
|
|
127
|
+
await pub.waitForTransactionReceipt({ hash, timeout: 120_000 });
|
|
128
|
+
out(uptoReady(await read()) ? `approved: ${account.address} can now pay actual usage over upto. Restart the gateway.` : "transaction confirmed but the allowance still reads low - check the wallet on basescan");
|
|
129
|
+
return 0;
|
|
130
|
+
} catch (e) { console.error(`permit2-approve failed: ${String(e?.message || e).slice(0, 300)}`); return 1; }
|
|
131
|
+
}
|
|
132
|
+
|
|
99
133
|
if (cmd === "doctor") {
|
|
100
134
|
const key = resolveCreditsKey();
|
|
101
135
|
out(`upstream: ${upstream}`);
|
|
102
136
|
out(`credits key: ${key ? `present (${key.slice(0, 8)}…)` : "none"}`);
|
|
103
137
|
out(`wallet key: ${/^0x[0-9a-fA-F]{64}$/.test(process.env.AGENT402_WALLET_KEY || "") ? "present" : "none"}`);
|
|
138
|
+
if (/^0x[0-9a-fA-F]{64}$/.test(process.env.AGENT402_WALLET_KEY || "")) {
|
|
139
|
+
try {
|
|
140
|
+
const [{ getPermit2AllowanceReadParams }, { createPublicClient, http }, { privateKeyToAccount }, { base }] = await Promise.all([import("@x402/evm/upto/client"), import("viem"), import("viem/accounts"), import("viem/chains")]);
|
|
141
|
+
const account = privateKeyToAccount(process.env.AGENT402_WALLET_KEY.trim());
|
|
142
|
+
const pub = createPublicClient({ chain: base, transport: http((process.env.AGENT402_BASE_RPC || DEFAULT_BASE_RPC).trim()) });
|
|
143
|
+
const allowance = await pub.readContract(getPermit2AllowanceReadParams({ tokenAddress: USDC_BASE, ownerAddress: account.address }));
|
|
144
|
+
out(`wallet ${account.address}: ${uptoReady(allowance) ? "pays actual usage (upto; Permit2 approved)" : "pays the per-request quote (exact) - run permit2-approve once to pay actual usage"}`);
|
|
145
|
+
} catch (e) { out(`wallet: allowance unreadable (${String(e?.message || e).slice(0, 80)})`); }
|
|
146
|
+
}
|
|
104
147
|
try { const r = await loadRoutes(upstream, fetch, { pricing }); out(`gateway: ok, ${r.size} models (${pricing} pricing: ${[...r.values()].filter((x) => x.metered).length} metered)`); } catch (e) { out(`gateway: unreachable (${e?.message || e})`); return 1; }
|
|
105
148
|
if (key) {
|
|
106
149
|
try {
|
package/index.js
CHANGED
|
@@ -22,18 +22,55 @@ export function resolveCreditsKey(pluginConfig = {}) {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// x402 wallet payment - optional (peer deps) and, when the wallet has a
|
|
26
|
+
// Permit2 allowance, METERED: the `upto` scheme authorizes a CEILING (the
|
|
27
|
+
// per-request quote) and the gateway settles actual usage x 1.15, the way a
|
|
28
|
+
// per-token router bills. Without the allowance it pays `exact` (the quote).
|
|
29
|
+
export const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
30
|
+
export const BASE_CAIP2 = "eip155:8453";
|
|
31
|
+
export const DEFAULT_BASE_RPC = "https://mainnet.base.org";
|
|
32
|
+
// Permit2 approvals are max-uint by convention; anything this large is "approved".
|
|
33
|
+
export const PERMIT2_READY_MIN = 10n ** 12n; // 1,000,000 USDC (6 decimals): a real approval, not dust
|
|
34
|
+
|
|
35
|
+
/** Which 402 accept to pay: upto on Base when the client can (settle-actual),
|
|
36
|
+
* else exact on Base, else the first the client supports. Pure. */
|
|
37
|
+
export function selectAccept(accepts, { preferUpto = true } = {}) {
|
|
38
|
+
const list = Array.isArray(accepts) ? accepts : [];
|
|
39
|
+
if (preferUpto) { const u = list.find((a) => a?.scheme === "upto" && a?.network === BASE_CAIP2); if (u) return u; }
|
|
40
|
+
return list.find((a) => a?.scheme === "exact" && a?.network === BASE_CAIP2) || list[0];
|
|
41
|
+
}
|
|
42
|
+
export const uptoReady = (allowance) => { try { return BigInt(allowance ?? 0) >= PERMIT2_READY_MIN; } catch { return false; } };
|
|
43
|
+
|
|
26
44
|
export async function resolvePayFetch(pluginConfig = {}, log = () => {}) {
|
|
27
45
|
const pk = (pluginConfig.walletKey || process.env.AGENT402_WALLET_KEY || "").trim();
|
|
28
46
|
if (!/^0x[0-9a-fA-F]{64}$/.test(pk)) return null;
|
|
29
47
|
try {
|
|
30
|
-
const [{ wrapFetchWithPayment }, { privateKeyToAccount }] = await Promise.all([
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
48
|
+
const [{ wrapFetchWithPayment, x402Client }, { privateKeyToAccount }, { toClientEvmSigner }, { registerExactEvmScheme }] = await Promise.all([
|
|
49
|
+
import("@x402/fetch"), import("viem/accounts"), import("@x402/evm"), import("@x402/evm/exact/client"),
|
|
50
|
+
]);
|
|
51
|
+
const account = privateKeyToAccount(pk);
|
|
52
|
+
const signer = toClientEvmSigner(account);
|
|
53
|
+
// Upto needs a Permit2 allowance on USDC (one approval transaction, ever).
|
|
54
|
+
let upto = false;
|
|
55
|
+
try {
|
|
56
|
+
const [{ UptoEvmScheme, getPermit2AllowanceReadParams }, { createPublicClient, http }, { base }] = await Promise.all([import("@x402/evm/upto/client"), import("viem"), import("viem/chains")]);
|
|
57
|
+
const pub = createPublicClient({ chain: base, transport: http((pluginConfig.baseRpc || process.env.AGENT402_BASE_RPC || DEFAULT_BASE_RPC).trim()) });
|
|
58
|
+
const allowance = await pub.readContract(getPermit2AllowanceReadParams({ tokenAddress: USDC_BASE, ownerAddress: account.address }));
|
|
59
|
+
upto = uptoReady(allowance);
|
|
60
|
+
const client = new x402Client((v, accepts) => selectAccept(accepts, { preferUpto: upto }));
|
|
61
|
+
registerExactEvmScheme(client, { signer });
|
|
62
|
+
if (upto) client.register(BASE_CAIP2, new UptoEvmScheme(signer));
|
|
63
|
+
log(upto
|
|
64
|
+
? `[agent402-openclaw] x402 wallet ${account.address}: paying actual usage (upto, Permit2 allowance present) on Base; exact elsewhere`
|
|
65
|
+
: `[agent402-openclaw] x402 wallet ${account.address}: paying the per-request quote (exact). To pay actual usage instead, run \`agent402-openclaw permit2-approve\` once (one USDC approval transaction on Base; needs a little ETH for gas)`);
|
|
66
|
+
return wrapFetchWithPayment(fetch, client);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
// The allowance read or the upto scheme failed: exact still works.
|
|
69
|
+
log(`[agent402-openclaw] upto unavailable (${String(e?.message || e).slice(0, 120)}) - paying exact`);
|
|
70
|
+
const client = new x402Client((v, accepts) => selectAccept(accepts, { preferUpto: false }));
|
|
71
|
+
registerExactEvmScheme(client, { signer });
|
|
72
|
+
return wrapFetchWithPayment(fetch, client);
|
|
73
|
+
}
|
|
37
74
|
} catch (e) {
|
|
38
75
|
log(`[agent402-openclaw] x402 wallet payment unavailable (${e?.message || e}); install @x402/fetch @x402/evm viem, or use a credits key`);
|
|
39
76
|
return null;
|
package/models.js
CHANGED
|
@@ -8,6 +8,23 @@
|
|
|
8
8
|
|
|
9
9
|
export const AUTO_ID = "auto";
|
|
10
10
|
|
|
11
|
+
/** The metered route's own caps (llm-gateway-kit TIERS["v1-chat-metered"]),
|
|
12
|
+
* used when the catalog entry does not carry `meteredMaxInputChars` /
|
|
13
|
+
* `meteredMaxTokens` (gateways older than 2026-08-26). A metered call is
|
|
14
|
+
* validated against the METERED tier's limits, never the model's flat home
|
|
15
|
+
* tier - so a nano model that takes 12k chars flat takes 200k chars metered. */
|
|
16
|
+
export const METERED_MAX_INPUT_CHARS = 200_000;
|
|
17
|
+
export const METERED_MAX_TOKENS = 8192;
|
|
18
|
+
|
|
19
|
+
/** What a model must accept to be OpenClaw's primary. Measured on OpenClaw
|
|
20
|
+
* 2026.7.1 with the stock agent: ~33k chars of system prompt plus ~37k chars
|
|
21
|
+
* of tool schemas BEFORE the user's first word, and OpenClaw's own precheck
|
|
22
|
+
* refuses the turn ("Context overflow") when that exceeds the model's
|
|
23
|
+
* contextWindow. The routed `auto` tier caps input at 16k chars, so a primary
|
|
24
|
+
* of `agent402/auto` never completes a single turn - found by the real-install
|
|
25
|
+
* test, invisible to every stub. */
|
|
26
|
+
export const OPENCLAW_MIN_INPUT_CHARS = 80_000;
|
|
27
|
+
|
|
11
28
|
/** Linear trailing-slash strip (a regex like /\/+$/ is polynomial on long runs of "/"). */
|
|
12
29
|
export function stripTrailingSlashes(s) {
|
|
13
30
|
let str = String(s ?? ""); let end = str.length;
|
|
@@ -40,8 +57,8 @@ export function routesFromCatalog(catalog, { pricing = "metered" } = {}) {
|
|
|
40
57
|
tier: useMetered ? "v1-chat-metered" : (x.tier || null),
|
|
41
58
|
priceUsd: useMetered ? (Number(x.meteredFromUsd) || x.priceUsd) : x.priceUsd,
|
|
42
59
|
metered: useMetered,
|
|
43
|
-
maxTokens: Number(x.maxTokens) || 1024,
|
|
44
|
-
maxInputChars: Number(x.maxInputChars) || 32_000,
|
|
60
|
+
maxTokens: useMetered ? (Number(x.meteredMaxTokens) || METERED_MAX_TOKENS) : (Number(x.maxTokens) || 1024),
|
|
61
|
+
maxInputChars: useMetered ? (Number(x.meteredMaxInputChars) || METERED_MAX_INPUT_CHARS) : (Number(x.maxInputChars) || 32_000),
|
|
45
62
|
stealth: !!x.stealth,
|
|
46
63
|
});
|
|
47
64
|
}
|
|
@@ -58,6 +75,23 @@ export function routesFromCatalog(catalog, { pricing = "metered" } = {}) {
|
|
|
58
75
|
return routes;
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
/** Preferred primaries, in order: capable tool-callers that are cheap when
|
|
79
|
+
* metered. Only consulted among routes that can hold OpenClaw's prompt. */
|
|
80
|
+
const PRIMARY_PREFERENCE = ["anthropic/claude-haiku-4.5", "openai/gpt-4.1-mini", "google/gemini-2.5-flash", "openai/gpt-4o-mini"];
|
|
81
|
+
|
|
82
|
+
/** The route OpenClaw should make its primary model: the first preferred id
|
|
83
|
+
* that fits OPENCLAW_MIN_INPUT_CHARS, else the cheapest fitting one, else
|
|
84
|
+
* null (nothing in this catalog can hold OpenClaw's prompt - the caller says
|
|
85
|
+
* so instead of writing a primary that fails every turn). `auto` is never
|
|
86
|
+
* chosen: its input cap is the routed tier's. */
|
|
87
|
+
export function defaultPrimary(routes, { minInputChars = OPENCLAW_MIN_INPUT_CHARS } = {}) {
|
|
88
|
+
const fits = [...routes.values()].filter((r) => r.id !== AUTO_ID && !r.stealth && r.maxInputChars >= minInputChars);
|
|
89
|
+
if (!fits.length) return null;
|
|
90
|
+
for (const id of PRIMARY_PREFERENCE) { const r = fits.find((x) => x.id === id); if (r) return r; }
|
|
91
|
+
fits.sort((a, b) => a.priceUsd - b.priceUsd || a.id.localeCompare(b.id));
|
|
92
|
+
return fits[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
const priceLabel = (usd) => (usd < 0.01 ? `$${usd.toFixed(3)}` : `$${usd.toFixed(2)}`);
|
|
62
96
|
|
|
63
97
|
/** OpenClaw `models[]` entries for a provider block. `auto` first. */
|
package/openclaw.plugin.json
CHANGED
|
@@ -6,5 +6,48 @@
|
|
|
6
6
|
"onStartup": true
|
|
7
7
|
},
|
|
8
8
|
"enabledByDefault": true,
|
|
9
|
-
"providers": [
|
|
9
|
+
"providers": [
|
|
10
|
+
"agent402"
|
|
11
|
+
],
|
|
12
|
+
"configSchema": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"additionalProperties": false,
|
|
15
|
+
"properties": {
|
|
16
|
+
"creditsKey": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"description": "Agent402 prepaid credits key (a402_...). Precedence: this, then AGENT402_CREDITS_KEY, then ~/.openclaw/agent402/credits.key."
|
|
19
|
+
},
|
|
20
|
+
"walletKey": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Optional EVM private key for x402 USDC payment (needs @x402/fetch + @x402/evm + viem installed)."
|
|
23
|
+
},
|
|
24
|
+
"port": {
|
|
25
|
+
"type": "number",
|
|
26
|
+
"description": "Loopback proxy port (default 8412)."
|
|
27
|
+
},
|
|
28
|
+
"upstream": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"description": "Agent402 gateway base URL (default https://agent402.tools)."
|
|
31
|
+
},
|
|
32
|
+
"pricing": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"enum": [
|
|
35
|
+
"metered",
|
|
36
|
+
"flat"
|
|
37
|
+
],
|
|
38
|
+
"description": "metered (default): explicit models pay a per-request quote; flat: every model on its flat tier."
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"uiHints": {
|
|
43
|
+
"creditsKey": {
|
|
44
|
+
"label": "Credits key",
|
|
45
|
+
"help": "Buy by card at https://agent402.tools/credits",
|
|
46
|
+
"sensitive": true
|
|
47
|
+
},
|
|
48
|
+
"walletKey": {
|
|
49
|
+
"label": "Wallet key",
|
|
50
|
+
"sensitive": true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
10
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent402-openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "OpenClaw model provider for Agent402: auto-routed and explicit LLM models at a flat per-call price, paid by card (prepaid credits key) or in USDC over x402 through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|