mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
feat(agentswarm): M5 — x402 billing wrapper for the swarm route
withX402() wraps the Web handler so unpaid calls get a standard HTTP 402 challenge (x402Version + accepts[]) and paid calls fall through to the agent. Payment verification is injectable (host wires CoinPay/x402); CORS preflight passes through untouched; custom challenge responses supported. README documents the optional c0mpute.com GPU backend. 4 new tests; 38/38 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
fbbb323477
commit
0e5a13f5fb
4 changed files with 137 additions and 0 deletions
|
|
@ -89,6 +89,23 @@ const runner = createRubricRunner({
|
||||||
|
|
||||||
Pass the rubric per request: `{ "messages": [...], "rubric": "- one sentence\n- mentions chlorophyll" }`.
|
Pass the rubric per request: `{ "messages": [...], "rubric": "- one sentence\n- mentions chlorophyll" }`.
|
||||||
|
|
||||||
|
## Optional: run on community GPUs (c0mpute.com)
|
||||||
|
|
||||||
|
Inference is swappable. If a user connects their [c0mpute.com](https://c0mpute.com)
|
||||||
|
account (peer-shared GPUs), run agents/judges/routers on it instead of a hosted
|
||||||
|
provider. agentswarm does no OAuth — the host's c0mpute connector supplies the
|
||||||
|
credentials; this just consumes them:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createC0mputeModel, createDeepAgentRunner } from "@logicsrc/agentswarm";
|
||||||
|
|
||||||
|
// `connector` comes from the user connecting their c0mpute.com account
|
||||||
|
const model = await createC0mputeModel({ apiKey: connector.apiKey, model: "llama-3.1-70b" });
|
||||||
|
const runner = await createDeepAgentRunner({ model });
|
||||||
|
```
|
||||||
|
|
||||||
|
Opt-in: omit it and the default hosted provider is used. Requires `@langchain/openai`.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
- **M1** ✓ core handler + injectable runner + deepagents adapter + x402 gate hook.
|
- **M1** ✓ core handler + injectable runner + deepagents adapter + x402 gate hook.
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@
|
||||||
*/
|
*/
|
||||||
export { createSwarmHandler } from "./handler.js";
|
export { createSwarmHandler } from "./handler.js";
|
||||||
export type { SwarmHandlerOptions } from "./handler.js";
|
export type { SwarmHandlerOptions } from "./handler.js";
|
||||||
|
export { withX402 } from "./x402.js";
|
||||||
|
export type { X402Options, X402Accept } from "./x402.js";
|
||||||
export { createDeepAgentRunner } from "./runner.js";
|
export { createDeepAgentRunner } from "./runner.js";
|
||||||
export type { DeepAgentRunnerOptions } from "./runner.js";
|
export type { DeepAgentRunnerOptions } from "./runner.js";
|
||||||
export { createSwarm, createLLMRouter } from "./swarm.js";
|
export { createSwarm, createLLMRouter } from "./swarm.js";
|
||||||
|
|
|
||||||
61
packages/agentswarm/src/x402.test.ts
Normal file
61
packages/agentswarm/src/x402.test.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { createSwarmHandler, withX402 } from "./index.js";
|
||||||
|
import type { SwarmRunner } from "./index.js";
|
||||||
|
|
||||||
|
function okRunner(): SwarmRunner {
|
||||||
|
return {
|
||||||
|
run: async (input) => ({
|
||||||
|
threadId: "t",
|
||||||
|
messages: [...input.messages, { role: "assistant", content: "ok" }],
|
||||||
|
output: "ok"
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function post(): Request {
|
||||||
|
return new Request("http://localhost/swarm", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ messages: [{ role: "user", content: "hi" }] })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = createSwarmHandler({ runner: okRunner() });
|
||||||
|
const accepts = [{ scheme: "exact", network: "base", asset: "USDC", amount: 1000, payTo: "0xabc" }];
|
||||||
|
|
||||||
|
describe("withX402", () => {
|
||||||
|
it("passes paid requests through to the handler", async () => {
|
||||||
|
const paid = withX402(handler, { verify: () => true, accepts });
|
||||||
|
const res = await paid(post());
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect((await res.json()).output).toBe("ok");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns a 402 x402 challenge when unpaid, without running the agent", async () => {
|
||||||
|
const verify = vi.fn(() => false);
|
||||||
|
const paid = withX402(handler, { verify, accepts });
|
||||||
|
const res = await paid(post());
|
||||||
|
expect(res.status).toBe(402);
|
||||||
|
const body = await res.json();
|
||||||
|
expect(body.x402Version).toBe(1);
|
||||||
|
expect(body.accepts).toEqual(accepts);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets CORS preflight through without payment", async () => {
|
||||||
|
const verify = vi.fn(() => false);
|
||||||
|
const paid = withX402(handler, { verify, accepts });
|
||||||
|
const res = await paid(new Request("http://localhost/swarm", { method: "OPTIONS" }));
|
||||||
|
expect(res.status).toBe(204);
|
||||||
|
expect(verify).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses a custom challenge when provided", async () => {
|
||||||
|
const paid = withX402(handler, {
|
||||||
|
verify: () => false,
|
||||||
|
challenge: () => new Response("pay up", { status: 402, headers: { "x-pay": "url" } })
|
||||||
|
});
|
||||||
|
const res = await paid(post());
|
||||||
|
expect(res.status).toBe(402);
|
||||||
|
expect(res.headers.get("x-pay")).toBe("url");
|
||||||
|
});
|
||||||
|
});
|
||||||
57
packages/agentswarm/src/x402.ts
Normal file
57
packages/agentswarm/src/x402.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* x402 billing wrapper — gate a swarm route behind payment. Wraps the Web
|
||||||
|
* handler so unpaid requests get a standard HTTP 402 challenge (the x402
|
||||||
|
* "accepts" shape) and paid requests fall through to the agent. The actual
|
||||||
|
* payment verification is injectable so a host wires it to CoinPay/x402.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface X402Accept {
|
||||||
|
scheme: string;
|
||||||
|
network: string;
|
||||||
|
asset: string;
|
||||||
|
/** Price in the asset's minor units. */
|
||||||
|
amount: number;
|
||||||
|
/** Address / CoinPay account that receives payment. */
|
||||||
|
payTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface X402Options {
|
||||||
|
/** Returns true when the request carries valid payment/authorization. */
|
||||||
|
verify: (request: Request) => boolean | Promise<boolean>;
|
||||||
|
/** Payment options advertised in the 402 challenge. */
|
||||||
|
accepts?: X402Accept[];
|
||||||
|
/** Fully custom 402 response; overrides `accepts`. */
|
||||||
|
challenge?: (request: Request) => Response | Promise<Response>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CORS_ORIGIN = { "access-control-allow-origin": "*" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap a `(Request) => Response` swarm handler so each call must be paid for.
|
||||||
|
* CORS preflights pass through untouched; everything else must satisfy `verify`
|
||||||
|
* or receives a 402 with the advertised payment options.
|
||||||
|
*/
|
||||||
|
export function withX402(
|
||||||
|
handler: (request: Request) => Promise<Response>,
|
||||||
|
options: X402Options
|
||||||
|
): (request: Request) => Promise<Response> {
|
||||||
|
return async function paid(request: Request): Promise<Response> {
|
||||||
|
if (request.method === "OPTIONS") {
|
||||||
|
return handler(request);
|
||||||
|
}
|
||||||
|
if (await options.verify(request)) {
|
||||||
|
return handler(request);
|
||||||
|
}
|
||||||
|
if (options.challenge) {
|
||||||
|
return options.challenge(request);
|
||||||
|
}
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
x402Version: 1,
|
||||||
|
error: "payment_required",
|
||||||
|
accepts: options.accepts ?? []
|
||||||
|
}),
|
||||||
|
{ status: 402, headers: { "content-type": "application/json", ...CORS_ORIGIN } }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue