From fbbb32347712d243a024fa26e7c86edbb582cd80 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 25 Jun 2026 17:47:00 +0000 Subject: [PATCH] feat(agentswarm): optional c0mpute.com inference backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit createC0mputeModel() turns a connected c0mpute.com account (apiKey + OpenAI- compatible base URL) into a LangChain chat model, usable as the model for createDeepAgentRunner or the chatModel for the judge/router — so agent inference can run on community-shared GPUs. Opt-in; default stays the hosted provider. agentswarm performs no OAuth: the host's c0mpute connector supplies credentials (resolveC0mputeConnector / c0mputeConnectorFromEnv normalize them). DeepAgent runner now accepts a model instance, not just a provider string. 7 new tests (defaults, env mapping, not-connected + missing-peer errors); 34/34 pass. Co-Authored-By: Claude Opus 4.8 --- packages/agentswarm/src/compute.test.ts | 56 ++++++++++++++++++ packages/agentswarm/src/compute.ts | 78 +++++++++++++++++++++++++ packages/agentswarm/src/index.ts | 8 +++ packages/agentswarm/src/runner.ts | 8 ++- 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 packages/agentswarm/src/compute.test.ts create mode 100644 packages/agentswarm/src/compute.ts diff --git a/packages/agentswarm/src/compute.test.ts b/packages/agentswarm/src/compute.test.ts new file mode 100644 index 0000000..7995b12 --- /dev/null +++ b/packages/agentswarm/src/compute.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { + C0MPUTE_DEFAULT_BASE_URL, + C0MPUTE_DEFAULT_MODEL, + c0mputeConnectorFromEnv, + createC0mputeModel, + resolveC0mputeConnector +} from "./index.js"; + +describe("resolveC0mputeConnector", () => { + it("applies default base URL and model", () => { + const resolved = resolveC0mputeConnector({ apiKey: "key" }); + expect(resolved.baseUrl).toBe(C0MPUTE_DEFAULT_BASE_URL); + expect(resolved.model).toBe(C0MPUTE_DEFAULT_MODEL); + expect(resolved.apiKey).toBe("key"); + }); + + it("keeps explicit base URL and model", () => { + const resolved = resolveC0mputeConnector({ + apiKey: "key", + baseUrl: "https://gpu.example/v1", + model: "llama-3.1-70b" + }); + expect(resolved.baseUrl).toBe("https://gpu.example/v1"); + expect(resolved.model).toBe("llama-3.1-70b"); + }); + + it("throws when not connected (no apiKey)", () => { + expect(() => resolveC0mputeConnector({ apiKey: "" })).toThrow(/connect a c0mpute\.com account/); + }); +}); + +describe("c0mputeConnectorFromEnv", () => { + it("maps C0MPUTE_* env vars", () => { + const connector = c0mputeConnectorFromEnv({ + C0MPUTE_API_KEY: "k", + C0MPUTE_API_URL: "https://gpu.example/v1", + C0MPUTE_MODEL: "m" + }); + expect(connector).toEqual({ apiKey: "k", baseUrl: "https://gpu.example/v1", model: "m" }); + }); + + it("yields an empty apiKey when unset (so resolve will reject)", () => { + expect(c0mputeConnectorFromEnv({}).apiKey).toBe(""); + }); +}); + +describe("createC0mputeModel", () => { + it("rejects when the account is not connected", async () => { + await expect(createC0mputeModel({ apiKey: "" })).rejects.toThrow(/apiKey/); + }); + + it("explains the optional @langchain/openai peer when it is missing", async () => { + await expect(createC0mputeModel({ apiKey: "key" })).rejects.toThrow(/@langchain\/openai/); + }); +}); diff --git a/packages/agentswarm/src/compute.ts b/packages/agentswarm/src/compute.ts new file mode 100644 index 0000000..2c3f045 --- /dev/null +++ b/packages/agentswarm/src/compute.ts @@ -0,0 +1,78 @@ +/** + * Optional c0mpute.com backend — run agent/judge/router inference on + * community-shared GPUs instead of (or alongside) a hosted provider. + * + * The host app connects a user's c0mpute.com account via the c0mpute OAuth + * connector and obtains an API key + inference base URL; this module turns that + * into a LangChain chat model (c0mpute exposes an OpenAI-compatible endpoint). + * agentswarm itself performs no OAuth — it only consumes the resolved credentials. + */ + +export const C0MPUTE_DEFAULT_BASE_URL = "https://api.c0mpute.com/v1"; +export const C0MPUTE_DEFAULT_MODEL = "default"; + +/** Result of a user connecting their c0mpute.com account. */ +export interface C0mputeConnector { + /** API key issued after the user OAuths/connects their c0mpute.com account. */ + apiKey: string; + /** OpenAI-compatible inference base URL. Defaults to the c0mpute public endpoint. */ + baseUrl?: string; + /** Model id to request from the worker pool. */ + model?: string; +} + +export interface ResolvedC0mputeConnector { + apiKey: string; + baseUrl: string; + model: string; +} + +/** Apply defaults and validate a connector; throws if it has not been connected. */ +export function resolveC0mputeConnector(connector: C0mputeConnector): ResolvedC0mputeConnector { + if (!connector.apiKey) { + throw new Error("c0mpute connector requires an apiKey — connect a c0mpute.com account first"); + } + return { + apiKey: connector.apiKey, + baseUrl: connector.baseUrl ?? C0MPUTE_DEFAULT_BASE_URL, + model: connector.model ?? C0MPUTE_DEFAULT_MODEL + }; +} + +/** Build a connector from env vars (shares names with the c0mpute plugin). */ +export function c0mputeConnectorFromEnv( + env: Record = process.env +): C0mputeConnector { + return { + apiKey: env.C0MPUTE_API_KEY ?? "", + baseUrl: env.C0MPUTE_API_URL, + model: env.C0MPUTE_MODEL + }; +} + +/** Indirection so TS does not statically resolve the optional `@langchain/openai` peer. */ +async function loadOptionalModule(specifier: string): Promise { + return import(specifier); +} + +/** + * Build a LangChain chat model pointed at c0mpute.com. Pass the result as the + * `model` of {@link createDeepAgentRunner}, or as the `chatModel` of + * `createLLMJudge` / `createLLMRouter`, to run that inference on c0mpute GPUs. + * + * Optional peer: requires `@langchain/openai` in the host app. + */ +export async function createC0mputeModel(connector: C0mputeConnector): Promise { + const resolved = resolveC0mputeConnector(connector); + let openai: any; + try { + openai = await loadOptionalModule("@langchain/openai"); + } catch { + throw new Error("createC0mputeModel requires '@langchain/openai'. Install it: npm i @langchain/openai"); + } + return new openai.ChatOpenAI({ + apiKey: resolved.apiKey, + model: resolved.model, + configuration: { baseURL: resolved.baseUrl } + }); +} diff --git a/packages/agentswarm/src/index.ts b/packages/agentswarm/src/index.ts index 699031f..2c311cc 100644 --- a/packages/agentswarm/src/index.ts +++ b/packages/agentswarm/src/index.ts @@ -17,6 +17,14 @@ export type { SwarmOptions, LLMRouterOptions } from "./swarm.js"; +export { + createC0mputeModel, + resolveC0mputeConnector, + c0mputeConnectorFromEnv, + C0MPUTE_DEFAULT_BASE_URL, + C0MPUTE_DEFAULT_MODEL +} from "./compute.js"; +export type { C0mputeConnector, ResolvedC0mputeConnector } from "./compute.js"; export { InMemoryBudgetLedger, withBudget } from "./budget.js"; export type { AgentIdentity, BudgetLedger, BudgetRunnerOptions } from "./budget.js"; export { createRubricRunner, createLLMJudge } from "./rubric.js"; diff --git a/packages/agentswarm/src/runner.ts b/packages/agentswarm/src/runner.ts index 7851a40..2af2844 100644 --- a/packages/agentswarm/src/runner.ts +++ b/packages/agentswarm/src/runner.ts @@ -1,8 +1,12 @@ import type { SwarmMessage, SwarmRunInput, SwarmRunResult, SwarmRunner } from "./types.js"; export interface DeepAgentRunnerOptions { - /** Provider-prefixed model id, e.g. "anthropic:claude-sonnet-4-6". */ - model: string; + /** + * The model to run on: a provider-prefixed id (e.g. "anthropic:claude-sonnet-4-6") + * or a pre-built LangChain chat model instance — e.g. from {@link createC0mputeModel} + * to run inference on community-shared GPUs. + */ + model: string | object; /** System instructions for the lead agent. */ instructions?: string; /**