mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
feat(agentswarm): optional c0mpute.com inference backend
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 <noreply@anthropic.com>
This commit is contained in:
parent
dd7fe0b5c4
commit
fbbb323477
4 changed files with 148 additions and 2 deletions
56
packages/agentswarm/src/compute.test.ts
Normal file
56
packages/agentswarm/src/compute.test.ts
Normal file
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
78
packages/agentswarm/src/compute.ts
Normal file
78
packages/agentswarm/src/compute.ts
Normal file
|
|
@ -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<string, string | undefined> = 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<any> {
|
||||
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<any> {
|
||||
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 }
|
||||
});
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue