diff --git a/README.md b/README.md index 1faab07..017914e 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # clawrouter-codex -**Run [OpenAI Codex](https://github.com/openai/codex) on any [ClawRouter](https://github.com/BlockRunAI/ClawRouter) / BlockRun model — Claude, Gemini, DeepSeek, Kimi, GLM, Qwen, Grok, GPT and more — paid per request from a wallet (x402 USDC), no API keys.** +**Run [OpenAI Codex](https://github.com/openai/codex) on any [ClawRouter](https://github.com/BlockRunAI/ClawRouter) / BlockRun model — Claude, Gemini, DeepSeek, Kimi, GLM, Qwen, Grok, GPT and more — using an account API key or x402 USDC on Solana or Base.** [![npm](https://img.shields.io/npm/v/@blockrun/clawrouter-codex?color=cb3837&logo=npm)](https://www.npmjs.com/package/@blockrun/clawrouter-codex) Codex only speaks the OpenAI **Responses API** (`/v1/responses`). BlockRun speaks **Chat Completions**. This is a local bridge that translates between them and pays per request, so Codex (CLI, IDE, and Desktop) can use BlockRun's models: ``` -Codex ──/v1/responses──▶ clawrouter-codex ──@blockrun/llm──▶ BlockRun (x402 USDC, default) +Codex ──/v1/responses──▶ clawrouter-codex ──@blockrun/llm──▶ BlockRun (account API or x402) ``` By default the bridge pays BlockRun **directly** via the official [`@blockrun/llm`](https://www.npmjs.com/package/@blockrun/llm) SDK (plain per-request x402 on Base) — one process, no proxy, and the model list comes live from the source. It's wire-format translation plus a few conveniences (model picker, web search, a dashboard). @@ -18,9 +18,15 @@ By default the bridge pays BlockRun **directly** via the official [`@blockrun/ll --- +## Account API setup + +Register at [user.blockrun.ai](https://user.blockrun.ai), create an [API key](https://user.blockrun.ai/dashboard/keys), and add [credits](https://user.blockrun.ai/dashboard/credits). Export `BLOCKRUN_API_KEY` before `up`; direct mode then skips wallet discovery and sends authenticated chat, catalog and web-search requests to `https://api.blockrun.ai` (`BLOCKRUN_API_BASE_URL` overrides staging). Health and stats report account mode and link to the credits portal instead of showing wallet balance or x402 spend. + +This branch requires the TypeScript SDK release containing [PR #36](https://github.com/BlockRunAI/blockrun-llm-ts/pull/36); update `@blockrun/llm` from `^3.5.0` before publishing. Proxy mode requires ClawRouter PR #338 or later. Responses SSE production acceptance also needs Enterprise PR #10 deployed. New wallet users should select Solana before Base; the legacy direct wallet bridge remains Base-only and says so. + ## Quick start -You need [Node ≥ 20](https://nodejs.org) and a funded BlockRun wallet (`~/.blockrun/.session`, or set `BLOCKRUN_WALLET_KEY`). Then: +You need [Node ≥ 20](https://nodejs.org) and either `BLOCKRUN_API_KEY` or a funded BlockRun wallet (`~/.blockrun/.session`, or set `BLOCKRUN_WALLET_KEY`). Then: ```bash npx @blockrun/clawrouter-codex up # start the bridge + write the Codex profile + build the catalog diff --git a/package.json b/package.json index c16ec3e..cbd03b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@blockrun/clawrouter-codex", "version": "0.4.0", - "description": "Front-adapter that lets OpenAI Codex (Responses API) talk to a ClawRouter proxy — wallet-signed, x402-paid, zero API keys.", + "description": "Responses bridge from Codex to BlockRun via account API keys or x402 wallets on Solana/Base.", "type": "module", "license": "MIT", "bin": { diff --git a/scripts/start.mjs b/scripts/start.mjs index 63c4290..34747be 100644 --- a/scripts/start.mjs +++ b/scripts/start.mjs @@ -96,9 +96,10 @@ const PROXY_MODE = process.env.BRIDGE_MODE === "proxy" || Boolean(process.env.CL async function startDirect() { if (await healthy(PORT)) { log(`bridge already up on :${PORT}`); return; } - const { key, auto } = resolveWallet(); + const account = process.env.BLOCKRUN_API_KEY; + const { key, auto } = account ? { key: undefined, auto: false } : resolveWallet(); if (auto) log(`auto-detected BlockRun wallet at ${BLOCKRUN_WALLET}`); - log(`starting bridge on :${PORT} — direct mode (pays BlockRun via @blockrun/llm, no proxy)`); + log(`starting bridge on :${PORT} — direct mode (${account ? "account API" : "x402 wallet"})`); supervise("bridge", process.execPath, [join(ROOT, "src", "server.js")], { PORT: String(PORT), BLOCKRUN_DIRECT: "1", diff --git a/src/direct.js b/src/direct.js index 40c4be5..e320a51 100644 --- a/src/direct.js +++ b/src/direct.js @@ -13,7 +13,9 @@ import { readFileSync } from "node:fs"; import { LLMClient, SearchClient } from "@blockrun/llm"; import { resolvePrivateKey, paths } from "@blockrun/core"; -const DEFAULT_API = process.env.BLOCKRUN_API_URL ?? "https://blockrun.ai/api"; +const DEFAULT_WALLET_API = process.env.BLOCKRUN_API_URL ?? "https://blockrun.ai/api"; +const DEFAULT_ACCOUNT_API = process.env.BLOCKRUN_API_BASE_URL ?? "https://api.blockrun.ai"; +const ACCOUNT_PORTAL = "https://user.blockrun.ai/dashboard/credits"; // Fallback model when smart routing is unavailable. Override w/ BLOCKRUN_DEFAULT_MODEL. const DEFAULT_MODEL = process.env.BLOCKRUN_DEFAULT_MODEL ?? "anthropic/claude-opus-4.5"; const AUTO = new Set(["blockrun/auto", "auto", ""]); @@ -140,10 +142,13 @@ function buildStats() { * `http://direct/v1`. */ export function createDirectFetch(opts = {}) { - const privateKey = opts.privateKey ?? resolveWalletKey(); - const apiUrl = opts.apiUrl ?? DEFAULT_API; - const llm = new LLMClient({ privateKey, apiUrl }); - const search = new SearchClient({ privateKey, apiUrl }); + const apiKey = opts.apiKey ?? process.env.BLOCKRUN_API_KEY; + const privateKey = apiKey ? undefined : (opts.privateKey ?? resolveWalletKey()); + const apiUrl = opts.apiUrl ?? (apiKey ? DEFAULT_ACCOUNT_API : DEFAULT_WALLET_API); + if (apiKey && !Object.getOwnPropertyDescriptor(LLMClient.prototype, "authMode")) throw new Error("Account mode requires the @blockrun/llm release containing PR #36."); + const auth = apiKey ? { apiKey, apiUrl } : { privateKey, apiUrl }; + const llm = new LLMClient(auth); + const search = new SearchClient(auth); return async function directFetch(url, init = {}) { const path = new URL(url, "http://direct").pathname; @@ -200,25 +205,18 @@ export function createDirectFetch(opts = {}) { // Health + wallet/balance for the dashboard. if (path.endsWith("/health")) { if (!String(url).includes("full=true")) return json({ status: "ok", mode: "direct" }); - let balance = 0; - let address = ""; - try { balance = await llm.getBalance(); } catch { /* leave 0 */ } - try { address = search.getWalletAddress(); } catch { /* leave "" */ } - return json({ - status: "ok", - mode: "direct", - paymentChain: "base", - wallet: address, - address, - balance: `$${balance.toFixed(2)}`, - isEmpty: balance <= 0, - }); + if (llm.authMode === "api-key") return json({ status:"ok", mode:"direct", authMode:"api-key", account:ACCOUNT_PORTAL }); + let balance = 0; let address = ""; + try { balance = await llm.getBalance(); } catch {} + try { address = search.getWalletAddress(); } catch {} + return json({ status:"ok", mode:"direct", paymentChain:"base", wallet:address, address, balance:`$${balance.toFixed(2)}`, isEmpty:balance<=0 }); } // Spend stats: build a 7-day window from the SDK's local cost log // (~/.blockrun/cost_log.jsonl) — direct mode has no server-side ledger, // but the SDK records every settled payment with a timestamp. if (path.endsWith("/stats")) { + if (llm.authMode === "api-key") return json({ authMode:"api-key", costSource:"account_portal", url:ACCOUNT_PORTAL }); return json(buildStats()); } diff --git a/test/account-api.test.js b/test/account-api.test.js new file mode 100644 index 0000000..34f959d --- /dev/null +++ b/test/account-api.test.js @@ -0,0 +1,3 @@ +import test from "node:test";import assert from "node:assert/strict";import {createDirectFetch} from "../src/direct.js"; +const CHAT={id:"x",object:"chat.completion",created:1,model:"openai/gpt-4o-mini",choices:[{index:0,message:{role:"assistant",content:"OK"},finish_reason:"stop"}],usage:{prompt_tokens:1,completion_tokens:1,total_tokens:2}}; +test("direct account mode uses Bearer without wallet and reports portal billing",async()=>{const original=globalThis.fetch;let calls=0;globalThis.fetch=async (input,init)=>{calls++;assert.equal(new Headers(init?.headers).get("authorization"),"Bearer brk_live_bridge_test");assert.equal(init?.redirect,"error");return new Response(JSON.stringify(CHAT),{status:200,headers:{"content-type":"application/json"}})};try{const f=createDirectFetch({apiKey:"brk_live_bridge_test",apiUrl:"http://localhost:44123"});const r=await f("http://direct/v1/chat/completions",{method:"POST",body:JSON.stringify({model:"openai/gpt-4o-mini",messages:[{role:"user",content:"hi"}]})});assert.equal(r.status,200);assert.equal(calls,1);const health=await (await f("http://direct/v1/health?full=true")).json();assert.equal(health.authMode,"api-key");const stats=await (await f("http://direct/v1/stats")).json();assert.equal(stats.costSource,"account_portal");}finally{globalThis.fetch=original;}});