agent402-openclaw 0.4.5 → 0.5.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 CHANGED
@@ -43,6 +43,16 @@ model listing, gateway boot and one agent turn (`test-real-install.js`).
43
43
  No OpenClaw? `agent402-openclaw proxy` runs the proxy alone; point any OpenAI
44
44
  client at `http://127.0.0.1:8412/v1` with model `auto`.
45
45
 
46
+ **Claude Code and other Anthropic-wire clients** (0.5.0): the proxy also answers
47
+ `POST /v1/messages` and forwards it to the gateway's metered Messages route,
48
+ paid from the same wallet:
49
+
50
+ ```bash
51
+ export ANTHROPIC_BASE_URL=http://127.0.0.1:8412
52
+ export ANTHROPIC_AUTH_TOKEN=unused # the proxy pays; this value is ignored
53
+ claude --model claude-sonnet-5
54
+ ```
55
+
46
56
  ## Pay from a wallet instead
47
57
 
48
58
  ```bash
package/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  // agent402-openclaw <command>
3
3
  // setup [--credits-key a402_...] [--write] store the key, print (or merge) the openclaw.json block
4
- // proxy [--port N] [--upstream URL] run the local proxy on its own (no OpenClaw needed)
4
+ // proxy [--port N] [--upstream URL] run the local proxy on its own (no OpenClaw needed;
5
+ // serves the OpenAI wire and Anthropic POST /v1/messages)
5
6
  // doctor show what is configured and whether the gateway answers
6
7
  import { mkdirSync, writeFileSync, readFileSync, existsSync, chmodSync, statSync, realpathSync, renameSync } from "node:fs";
7
8
  import { pathToFileURL } from "node:url";
package/index.js CHANGED
@@ -37,7 +37,7 @@ export function resolveCreditsKey(pluginConfig = {}) {
37
37
 
38
38
  // x402 wallet payment - optional (peer deps) and, when the wallet has a
39
39
  // Permit2 allowance, METERED: the `upto` scheme authorizes a CEILING (the
40
- // per-request quote) and the gateway settles actual usage x 1.15, the way a
40
+ // per-request quote) and the gateway settles actual usage plus a markup, the way a
41
41
  // per-token router bills. Without the allowance it pays `exact` (the quote).
42
42
  export const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
43
43
  export const BASE_CAIP2 = "eip155:8453";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent402-openclaw",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
4
4
  "description": "OpenClaw model provider for Agent402: an auto-routed tier at a flat per-call price and explicit LLM models priced per request, paid in USDC over x402 or with a prepaid credits key through a local proxy. Zero dependencies; x402 wallet payment is an optional peer.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -44,7 +44,8 @@
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@x402/evm": ">=2.20.0",
47
- "@x402/fetch": ">=2.20.0"
47
+ "@x402/fetch": ">=2.20.0",
48
+ "viem": ">=2.48.11"
48
49
  },
49
50
  "peerDependenciesMeta": {
50
51
  "@x402/evm": {
@@ -52,6 +53,9 @@
52
53
  },
53
54
  "@x402/fetch": {
54
55
  "optional": true
56
+ },
57
+ "viem": {
58
+ "optional": true
55
59
  }
56
60
  },
57
61
  "keywords": [
package/proxy.js CHANGED
@@ -1,5 +1,9 @@
1
- // Local OpenAI-compatible proxy: OpenClaw (or any client) talks to
2
- // http://127.0.0.1:<port>/v1, the proxy pays Agent402 and forwards.
1
+ // Local proxy: OpenClaw (or any client) talks to http://127.0.0.1:<port>, the
2
+ // proxy pays Agent402 and forwards. Two wires:
3
+ // OpenAI POST /v1/chat/completions -> the tier the model id names
4
+ // Anthropic POST /v1/messages -> the gateway's metered Messages
5
+ // route (/v1/metered/messages), so Claude Code and the Anthropic
6
+ // SDK pay from a wallet too (ANTHROPIC_BASE_URL=http://127.0.0.1:<port>).
3
7
  //
4
8
  // Two ways to pay, chosen at start:
5
9
  // creditsKey - a prepaid card-credits key (a402_...) sent as a Bearer; the
@@ -79,6 +83,20 @@ export function refusalAsOpenAIError(status, doc) {
79
83
  };
80
84
  }
81
85
 
86
+ /** The same refusal in the Anthropic error shape ({type:"error", error:{type,
87
+ * message}}), which Anthropic clients read their error text from. Null for any
88
+ * other body, which the caller relays unchanged. */
89
+ export function refusalAsAnthropicError(status, doc) {
90
+ const o = refusalAsOpenAIError(status, doc);
91
+ if (!o) return null;
92
+ const { error, ...fields } = o;
93
+ return { type: "error", error: { type: status === 402 ? "payment_required" : "api_error", message: error.message }, ...fields };
94
+ }
95
+
96
+ // The gateway's metered Anthropic Messages route: quoted per request from the
97
+ // body, any model id the gateway lists (dated Claude ids resolve to the live one).
98
+ export const MESSAGES_UPSTREAM_PATH = "/v1/metered/messages";
99
+
82
100
  const json = (res, status, obj, headers = {}) => {
83
101
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", ...headers });
84
102
  res.end(JSON.stringify(obj));
@@ -105,6 +123,39 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
105
123
  let table = routes || await loadRoutes(upstream, fetchImpl, { pricing });
106
124
  const stats = { requests: 0, forwarded: 0, errors: 0, startedAt: new Date().toISOString() };
107
125
 
126
+ // Pays and forwards one call. Streams pass through byte for byte; a refused
127
+ // paid call's offer-carrying JSON body is answered in the client wire's own
128
+ // error shape (mapRefusal); every other upstream body is relayed as sent.
129
+ async function forward(req, res, endpoint, outbound, extraHeaders, mapRefusal) {
130
+ const clientIdem = typeof req.headers["idempotency-key"] === "string" && /^[\w.:-]{8,128}$/.test(req.headers["idempotency-key"]) ? req.headers["idempotency-key"] : null;
131
+ const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": `agent402-openclaw/${PKG_VERSION}`, ...extraHeaders };
132
+ if (key) headers.authorization = `Bearer ${key}`;
133
+ const up = await paid(`${upstream}${endpoint}`, { method: "POST", headers, body: JSON.stringify(outbound), signal: AbortSignal.timeout(300_000) });
134
+ stats.forwarded++;
135
+ const passthrough = {};
136
+ // payment-response / x-payment-response carry the x402 settlement receipt
137
+ // (v2 / v1); payment-receipt is MPP's. The client sees what it paid.
138
+ for (const h of ["content-type", "x-credits-balance", "payment-response", "x-payment-response", "payment-receipt", "x-cache", "cache-control"]) { const v = up.headers.get(h); if (v) passthrough[h] = v; }
139
+ if (!up.ok && /json/i.test(passthrough["content-type"] || "")) {
140
+ const raw = await up.text();
141
+ let doc = null;
142
+ try { doc = JSON.parse(raw); } catch { /* not JSON after all: relayed as sent */ }
143
+ const mapped = mapRefusal(up.status, doc);
144
+ if (mapped) {
145
+ const { "content-type": _ct, ...rest } = passthrough;
146
+ return json(res, up.status, mapped, rest);
147
+ }
148
+ res.writeHead(up.status, passthrough);
149
+ return res.end(raw);
150
+ }
151
+ res.writeHead(up.status, passthrough);
152
+ if (!up.body) return res.end();
153
+ const reader = up.body.getReader();
154
+ req.on("close", () => { reader.cancel().catch(() => {}); });
155
+ for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }
156
+ return res.end();
157
+ }
158
+
108
159
  const server = createServer(async (req, res) => {
109
160
  stats.requests++;
110
161
  const url = new URL(req.url, "http://localhost");
@@ -136,31 +187,22 @@ export async function startProxy({ upstream = DEFAULT_UPSTREAM, creditsKey = nul
136
187
  }
137
188
  const outbound = { ...body };
138
189
  if (requested === AUTO_ID) delete outbound.model; else outbound.model = route.id;
139
- const clientIdem = typeof req.headers["idempotency-key"] === "string" && /^[\w.:-]{8,128}$/.test(req.headers["idempotency-key"]) ? req.headers["idempotency-key"] : null;
140
- const headers = { "content-type": "application/json", accept: req.headers.accept || "application/json", "idempotency-key": clientIdem || randomUUID(), "user-agent": `agent402-openclaw/${PKG_VERSION}` };
141
- if (key) headers.authorization = `Bearer ${key}`;
142
- const up = await paid(`${upstream}${route.endpoint}`, { method: "POST", headers, body: JSON.stringify(outbound), signal: AbortSignal.timeout(300_000) });
143
- stats.forwarded++;
144
- const passthrough = {};
145
- for (const h of ["content-type", "x-credits-balance", "payment-receipt", "x-cache", "cache-control"]) { const v = up.headers.get(h); if (v) passthrough[h] = v; }
146
- if (!up.ok && /json/i.test(passthrough["content-type"] || "")) {
147
- const raw = await up.text();
148
- let doc = null;
149
- try { doc = JSON.parse(raw); } catch { /* not JSON after all: relayed as sent */ }
150
- const mapped = refusalAsOpenAIError(up.status, doc);
151
- if (mapped) {
152
- const { "content-type": _ct, ...rest } = passthrough;
153
- return json(res, up.status, mapped, rest);
154
- }
155
- res.writeHead(up.status, passthrough);
156
- return res.end(raw);
190
+ return forward(req, res, route.endpoint, outbound, {}, refusalAsOpenAIError);
191
+ }
192
+ if (req.method === "POST" && url.pathname === "/v1/messages") {
193
+ const raw = await readBody(req);
194
+ let body;
195
+ try { body = JSON.parse(raw.toString("utf8") || "{}"); } catch { return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "Request body must be JSON" } }); }
196
+ if (typeof body.model !== "string" || !body.model.trim()) {
197
+ return json(res, 400, { type: "error", error: { type: "invalid_request_error", message: "\"model\" is required (any id from GET /v1/models, or a Claude id such as claude-sonnet-5)" } });
198
+ }
199
+ if (!paid) {
200
+ return json(res, 402, { type: "error", error: { type: "payment_required", message: "No payment method configured. Set AGENT402_WALLET_KEY to an EVM key holding USDC on Base (or run `agent402-openclaw setup`), or set AGENT402_CREDITS_KEY to a credits key already issued." } });
157
201
  }
158
- res.writeHead(up.status, passthrough);
159
- if (!up.body) return res.end();
160
- const reader = up.body.getReader();
161
- req.on("close", () => { reader.cancel().catch(() => {}); });
162
- for (;;) { const { done, value } = await reader.read(); if (done) break; res.write(Buffer.from(value)); }
163
- return res.end();
202
+ const outbound = { ...body, model: body.model.trim().replace(/^agent402\//, "") };
203
+ const extra = {};
204
+ for (const h of ["anthropic-version", "anthropic-beta"]) { const v = req.headers[h]; if (typeof v === "string" && v.length <= 512) extra[h] = v; }
205
+ return forward(req, res, MESSAGES_UPSTREAM_PATH, outbound, extra, refusalAsAnthropicError);
164
206
  }
165
207
  return json(res, 404, { error: { message: `No route for ${req.method} ${url.pathname}`, type: "invalid_request_error" } });
166
208
  } catch (e) {