mcp server
nova
TypeScript contracts, provider adapters, execution primitives, and MCP server scaffolding for AI applications.
Set up
Runs on your computer. Pick your agent and operating system.
This project does not publish an install method we can turn into a config. Follow the README instead.
README
synced Oct 29, 2026Nova
One vocabulary for every AI provider. Schemas, adapters, and MCP scaffolding for the gateway you're already building.
The contracts every layer of an AI stack has to speak — chat, embeddings, models, streaming, health, usage, and pricing — written once, as Zod-validated types, plus a battle-tested OpenAI-compatible adapter factory and the MCP operator scaffolding everything else re-derives.
What it is
Nova is the shared contract layer for multi-provider AI systems. It gives you one canonical, Zod-validated vocabulary for the shapes that cross every process boundary in an AI stack — chat requests and responses, streaming events, embeddings, model catalogs, provider health, usage records, and pricing — plus:
- a provider factory that turns any OpenAI-compatible endpoint into a typed
AiProvider(chat, streaming, embeddings, model listing, health probing), and - MCP server scaffolding — audit and usage sinks, a content-envelope helper, a pricing/cost layer, and a unified operator facade with a planner.
Implement AiProvider once; route, bill, observe, and proxy everywhere. Nova has no opinion about how you serve, route, or fail over — it ships the contracts, not the orchestrator.
Four packages, MIT-licensed, ESM ("type": "module"). @novaproto/contracts, @novaproto/mcp-shared and @novaproto/mcp are live on npm at 0.1.0 (the tree carries all three at 0.2.0, not yet released); @novaproto/exec-primitives is live at 0.1.0 and released by its own workflow:
| Package | Role |
|---|---|
@novaproto/contracts | The SDK core. Zod schemas for chat / embeddings / models / health / stream / usage / pricing / retrieval, the AiProvider interface, and createOpenAICompatProvider. Zero runtime deps beyond zod. |
@novaproto/mcp-shared | Cross-cutting helpers for MCP servers — audit sink, content envelope, usage sink + reader, pricing loader + cost estimation. Transport-agnostic; plug into any MCP server. |
@novaproto/mcp | Unified operator MCP facade — roll-up tools over sibling operator YAMLs + usage JSONL, a 1:1 downstream proxy, and nova.operator.plan, an intent-to-plan translator. A reference consumer of the two layers above. |
@novaproto/exec-primitives | Host-configured execution primitives for running agent processes — process-group supervision, stdio ACP transport and permission handling, a warm-process pool, ACP session bootstrap. No runtime dependencies; built to dist/ and published to npm. See its README. |
Why
Pick any AI-gateway-ish problem — routing requests across providers, logging usage, fronting OpenAI-compat to a bespoke backend, writing an MCP tool that queries model health, teaching an LLM to fill out an operator runbook. Each one re-derives the same primitives:
- "What do a chat request and response look like on the wire?"
- "How do I stream content and tool calls?"
- "What shape is usage data in, and how do I price it?"
- "How do I expose this surface through MCP without re-inventing audit and content envelopes?"
Nova answers those once. Adapter fixes land in one place; schema changes propagate on the next install. The design rule is strict: Nova does not know about its consumers — it depends on nothing downstream, so the dependency arrow only ever points toward it.
How it fits together
flowchart TB
subgraph consumers["Reference consumers"]
llamactl["llamactl<br/>(control plane)"]
sirius["sirius-gateway<br/>(multi-provider gateway)"]
ember["embersynth<br/>(capability router)"]
end
subgraph nova["Nova packages"]
mcp["@novaproto/mcp<br/>operator facade + planner"]
shared["@novaproto/mcp-shared<br/>audit + usage + pricing sinks"]
contracts["@novaproto/contracts<br/>schemas + AiProvider + openai-compat factory"]
end
subgraph upstream["OpenAI-compatible endpoints"]
apis["OpenAI / Together / groq /<br/>Mistral / local llama.cpp"]
end
llamactl --> contracts
llamactl --> shared
llamactl --> mcp
sirius --> contracts
ember --> contracts
mcp --> shared
shared --> contracts
contracts -->|"/chat/completions, /embeddings, /models"| apis
classDef harness fill:#1f2937,stroke:#4b5563,color:#f9fafb
classDef pkg fill:#0f3d3e,stroke:#2dd4bf,color:#ecfeff
classDef store fill:#3b2f1e,stroke:#d97706,color:#fffbeb
class llamactl,sirius,ember harness
class mcp,shared,contracts pkg
class apis store
@novaproto/contracts is the leaf — the only package with no internal Nova dependency. Everything else, including the reference consumers, depends inward. For the MCP facade's runtime topology (proxy + downstream boot + storage layout), see the facade deep-dive in AGENTS.md.
@novaproto/contracts — the SDK core
Zero runtime dependencies beyond zod. Every type that crosses a process boundary, as a Zod schema with an inferred TypeScript type:
- Chat wire types —
UnifiedAiRequestSchema/UnifiedAiResponseSchema(+ inferredUnifiedAiRequest/UnifiedAiResponse),ChatMessageSchema,ContentBlockSchema(a discriminated union ofTextBlockSchema,ImageBlockSchema,InputAudioBlockSchema),ToolSchema,ToolCallSchema,ToolCallDeltaSchema,ToolChoiceSchema,ResponseFormatSchema(text/json_object/json_schema),RoleSchema(system | user | assistant | tool | developer),FinishReasonSchema(stop | length | tool_calls | content_filter | error), andUsageSchema. - Streaming events —
UnifiedStreamEventSchema, a discriminated union overchunk/tool_call/error/done, withUnifiedStreamChunkSchema,StreamChoiceSchema, andStreamDeltaSchemapreserving tool-call deltas frame by frame. - Execution events —
ExecutionEventV1Schema: one accepted execution attempt asaccepted→content(canonicalchunk/tool_callpayloads) → exactly oneterminal, each event carryingrequestId,attemptIdand a gap-freesequence. The terminal iscompleted(withcompletion: "upstream"proof),cancelled,failed(a typedExecutionErrorCodeSchemacode) orunknown-outcome, and always carries aUsageObservationV1.validateExecutionEventSequence(orstepExecutionSequence+finishExecutionSequence, incrementally) checks the stream invariants and returns typed violations. Golden valid and invalid streams live inpackages/contracts/test/fixtures/execution-event-v1/. - Embeddings —
UnifiedEmbeddingRequestSchema/UnifiedEmbeddingResponseSchemaandEmbeddingRowSchema. Input is a string | array | token-array union, withencoding_formatanddimensions. - Models catalog —
ModelInfoSchema,ModelListResponseSchema,ModelCapabilitySchema(chat | embeddings | reasoning | vision | audio | tools | json_mode | structured_output | long_context | code), andModelCostSchema. - Health —
ProviderHealthSchemaandProviderHealthStateSchema(healthy | degraded | unhealthy | unknown). - Usage record —
UsageRecordSchema(ts, provider, model, kind, prompt/completion/total tokens, latency, optionalrequest_id/estimated_cost_usd/user/route),UsageKindSchema(chat | embedding | responses), andMinimalUsageInput. Privacy lock by design: it records counts, not content. - Pricing —
ModelPricingSchema,ProviderPricingSchema, andPricingCatalog(aMap<string, ProviderPricing>). - Retrieval (RAG) —
SearchRequest/SearchResponse,StoreRequest/StoreResponse,DeleteRequest/DeleteResponse,ListCollectionsResponse,DocumentSchema,SearchResultSchema,CollectionInfoSchema. Scores are cosine similarity normalized to0..1. - Runtime abstractions (TypeScript interfaces, not Zod) —
AiProvider(createResponse, optionalstreamResponse,createEmbeddings,listModels,healthCheck),RetrievalProvider,ProviderFactory/ProviderFactoryInput, andProviderRegistry.
The OpenAI-compat adapter factory
createOpenAICompatProvider(opts: OpenAICompatOptions): AiProvider turns any endpoint that speaks the OpenAI REST dialect — OpenAI itself, Together, groq, Mistral, a self-hosted llama-server — into a full AiProvider. Options:
| Field | Required | Notes |
|---|---|---|
name | yes | Provider name used in metadata + telemetry labels. |
baseUrl | yes | e.g. https://api.openai.com/v1. Trailing slash tolerated. |
apiKey | yes | Bearer token, sent as Authorization: Bearer <key>. |
displayName | no | Human-friendly label. |
fetch | no | fetch override for tests or runtime-specific TLS pinning. |
extraHeaders | no | Headers merged into every request (e.g. OpenAI-Organization). |
healthPath | no | Endpoint probed by healthCheck. Defaults to /models. |
onUsage | no | Callback fired after each successful call (see usage logging example below). |
The factory covers non-streaming and SSE streaming (content and tool-call deltas), embeddings, listModels, and healthCheck; it fires onUsage after each successful call, and strips Nova-only capabilities / providerOptions before anything goes on the wire. It also exports the helpers mergeRequestHeaders, and the types OpenAICompatUsageSnapshot and OpenAICompatOnUsage. It is kept deliberately thin — no retry loop, no failover, no logging; those belong to the orchestrator that composes providers.
@novaproto/mcp-shared — MCP server scaffolding
Depends on @novaproto/contracts and yaml. Thin utilities every MCP server wants:
- Audit sink —
appendAudit(opts: AuditOptions): AuditRecordwrites one JSONL line per invocation to~/.llamactl/mcp/audit/<server>-<YYYY-MM-DD>.jsonl(override withLLAMACTL_MCP_AUDIT_DIR).defaultAuditDir(env?)resolves the directory; typesAuditRecord/AuditOptions. - Content envelope —
toTextContent(payload): TextContentEnvelopewraps a payload in the MCP{ content: [{ type: 'text', text }] }shape, keeping theJSON.stringifydetail out of each handler. - Usage sink —
appendUsage(opts): stringreturns the file path it wrote (and throws ifrecord.provideris missing);appendUsageBackground(opts): voidis the fire-and-forget variant that swallows errors on hot paths. JSONL lands at~/.llamactl/usage/<provider>-<YYYY-MM-DD>.jsonl(override withLLAMACTL_USAGE_DIRorDEV_STORAGE).defaultUsageDir(env?); typeUsageWriteOptions. - Usage reader —
readUsage(opts?): UsageReadResultreturns{ records, filesScanned, malformedLines }, with a day-boundary pre-filter and torn-write tolerance. TypesUsageReadOptions(dir/since/until/provider) andUsageReadResult. - Pricing + cost —
loadPricing(opts?): LoadPricingResult,estimateCostUsd(record, catalog): number | undefined,computeCost(pricing, promptTokens, completionTokens): number,findModelPricing(provider, model, catalog), anddefaultPricingDir(env?). Pricing YAML lives underLLAMACTL_PRICING_DIR/DEV_STORAGE; malformed YAML is skipped, never thrown. TypesLoadPricingOptions/LoadPricingResult.
Drop into any MCP server — your own, or @novaproto/mcp — without pulling framework dependencies.
@novaproto/mcp — unified operator MCP facade
Optional but useful: a stdio MCP server that rolls up the YAMLs a multi-provider AI deployment typically writes, proxies sibling MCP servers 1:1, and surfaces everything through a single endpoint. Depends on @modelcontextprotocol/sdk (1.29.0), @novaproto/contracts, @novaproto/mcp-shared, yaml, and zod.
Native tools
| Tool | Purpose |
|---|---|
nova.ops.overview | Reads the three operator YAMLs and returns agents + gateways + providers + profiles + synthetic models. Missing files become empty sections. |
nova.ops.healthcheck | GET-probes each gateway and sirius provider baseUrl; fails soft per probe. Input timeoutMs (default 1500, max 30000). |
nova.ops.cost.snapshot | Aggregates usage JSONL over the last days (default 7, max 90) into per-provider and per-(provider, model) roll-ups, joining pricing when present. Inputs disablePricing / pricingDir / dir. |
nova.operator.plan | Translates a natural-language goal (+ optional context) into a PlanSchema-validated tool-call plan, allowlist-filtered. The default executor is a stub — bind your own LLM executor for real planning. |
nova.models.list | Aggregates llamactl.catalog.list + sirius.models.list + embersynth.synthetic.list in parallel, merges by the fixed priority ["llamactl", "sirius", "embersynth"], dedupes by id (recording alsoAvailableIn), and reports partial. |
The planner
Four pure pieces, exported from the package root:
- Schema —
PlanSchemaandPlanStepSchema(withPlan/PlanStep/PlannerToolDescriptor/ToolSafetyTier=read | mutation-dry-run-safe | mutation-destructive). A 20-step hard cap; each step requires anannotation. - Allowlist —
filterTools+DEFAULT_ALLOWLIST(typeAllowlistConfig). Deny wins over allow, the destructive tier needsallowDestructive, and an empty allow list fails closed.DEFAULT_ALLOWLISTallowsllamactl.* | sirius.* | embersynth.* | nova.*and deniessirius.providers.deregister,llamactl.infra.uninstall,llamactl.workload.delete. - Prompt —
buildPlannerPrompt(opts): BuildPlannerPromptResultemits the deterministic system + user messages and thesubmit_planfunction schema. - Executor — the
PlannerExecutorinterface, the cannedstubPlannerExecutor, and therunPlanner(opts): Promise<RunPlannerResult>composer (allowlist → prompt → executor →PlanSchemaparse, returning a discriminated result). To bind a real model,createLlmExecutor(opts: CreateLlmExecutorOptions): PlannerExecutorwraps anyAiProviderand forces thesubmit_plantool viatool_choice.
The package also exports buildNovaMcpServer(opts?: BuildNovaMcpServerOptions): McpServer (opts name / version / plannerExecutor / plannerAllowlist / plannerTools), the cost-snapshot helper computeCostSnapshot(opts?): CostSnapshot (types CostGroup / CostSnapshot / CostSnapshotOptions), and the default-path resolvers defaultKubeconfigPath / defaultSiriusProvidersPath / defaultEmbersynthConfigPath.
Honest boundaries
These are deliberate, not gaps:
- The default planner executor is a stub — it returns a canned plan until you bind an LLM via
createLlmExecutor. - The facade proxy snapshots downstream tools at boot only — there is no hot reload; restart the facade to pick up downstream tool changes.
Install
All three packages are live on npm under the @novaproto scope.
npm install @novaproto/contracts @novaproto/mcp-shared @novaproto/mcp
# or: bun add @novaproto/contracts @novaproto/mcp-shared @novaproto/mcp
{
"dependencies": {
"@novaproto/contracts": "^0.1.0",
"@novaproto/mcp-shared": "^0.1.0",
"@novaproto/mcp": "^0.1.0"
}
}
Node-portable. Each package builds to dist/src/*.js + .d.ts via tsc --build, with main / types pointing at the compiled JS — so consumers run on plain Node. Bun is the development and CI runtime only; the published artifacts run anywhere Node does.
Inside this monorepo the three packages reference each other with workspace:*. The release workflow packs each package with bun pm pack, which rewrites that to the exact version recorded for the dependency under workspaces in bun.lock (for example "@novaproto/contracts": "0.1.0"), and refuses a tarball that still carries a workspace: or other non-registry spec, or whose @novaproto/* version differs from that dependency's package.json, so consumers always pull a real published version, never a workspace or file specifier. npm itself does not rewrite workspace: specs.
Examples
Build a provider adapter
import { createOpenAICompatProvider } from "@novaproto/contracts";
const provider = createOpenAICompatProvider({
name: "together",
baseUrl: "https://api.together.xyz/v1",
apiKey: process.env.TOGETHER_API_KEY!,
});
const res = await provider.createResponse({
model: "meta-llama/Llama-3.3-70B-Instruct",
messages: [{ role: "user", content: "hello" }],
});
console.log(res.choices[0].message.content, res.latencyMs, res.provider);
Log usage via the adapter's onUsage hook
import { createOpenAICompatProvider } from "@novaproto/contracts";
import { appendUsageBackground } from "@novaproto/mcp-shared";
const provider = createOpenAICompatProvider({
name: "openai",
baseUrl: "https://api.openai.com/v1",
apiKey: process.env.OPENAI_API_KEY!,
onUsage: (s) =>
queueMicrotask(() =>
appendUsageBackground({
record: { ...s, ts: new Date().toISOString() },
}),
),
});
OpenAICompatUsageSnapshot carries provider / model / kind / prompt_tokens / completion_tokens / total_tokens / latency_ms — exactly the writable fields of a UsageRecord minus ts, which you stamp on the way to the sink.
MCP server with audit + content envelope
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { appendAudit, toTextContent } from "@novaproto/mcp-shared";
import { z } from "zod";
const server = new McpServer({ name: "my-service", version: "0.1.0" });
server.registerTool(
"my.service.status",
{ title: "Service status", inputSchema: { verbose: z.boolean().default(false) } },
(input) => {
const status = { ok: true, verbose: input.verbose };
appendAudit({ server: "my-service", tool: "my.service.status", input });
return toTextContent(status);
},
);
Planner with a real LLM executor
import { createOpenAICompatProvider } from "@novaproto/contracts";
import { buildNovaMcpServer, createLlmExecutor } from "@novaproto/mcp";
const provider = createOpenAICompatProvider({
name: "openai",
baseUrl: "https://api.openai.com/v1",
apiKey: process.env.OPENAI_API_KEY!,
});
const server = buildNovaMcpServer({
plannerExecutor: createLlmExecutor({ provider, model: "gpt-4o-mini" }),
});
For the pure path without a server, runPlanner({ goal, context, tools, allowlist, executor }) returns a discriminated RunPlannerResult — on failure, reason is one of executor-failed / plan-shape-invalid / empty-goal / disallowed-tool.
Cost snapshot without a server
import { computeCostSnapshot } from "@novaproto/mcp";
const snap = computeCostSnapshot({ days: 7 });
// snap.byProvider, snap.byModel, snap.totalEstimatedCostUsd
Running nova-mcp
The @novaproto/mcp package ships a nova-mcp binary — a stdio MCP facade you wire into Claude Desktop or any MCP client. On boot it runs loadConfig → bootAll → mountProxyTools (1:1 proxy, first-wins on name collisions, native tool names seeded so they always win) → registerUnifiedTools, then serves over a stdio transport with clean SIGINT / SIGTERM shutdown. The facade reads its downstream config from ~/.llamactl/nova-mcp.yaml (override with NOVA_MCP_CONFIG); each downstream is stdio or http, with ${VAR} env interpolation. The full facade reference — config schema, boot/passthrough sequence, the snapshot-at-boot contract, and what to avoid — lives in AGENTS.md.
bun install
bun packages/mcp/bin/nova-mcp.ts
Toolchain
A strict, hard-gated toolchain. Every command runs on Bun (pinned in CI):
bun run build # tsc --build -> dist + .d.ts for every package
bun run typecheck:strict # tsc --noEmit over the whole tree
bun run lint # no-cross-package-relative guard + eslint --max-warnings=0 + strict typecheck
bun test # all packages
bun run lint is zero-tolerance: ESLint runs with --max-warnings=0, a custom no-cross-package-relative guard forbids reaching across package boundaries with relative imports, and the strict typecheck is folded in.
CI (.github/workflows/check.yml) re-runs the full gate as a hard block on every pull request and push to main. The test step routes through scripts/bun-test-gate.ts, which gates on reported pass/fail counts and tolerates a Bun NAPI-teardown panic (exit 133) only when zero tests failed.
Releasing
Releases are tag-driven, one package per run. .github/workflows/release.yml runs only when a tag named contracts-v<version>, mcp-shared-v<version> or mcp-v<version> is pushed, and releases only the package the tag names. Its build job has read-only permissions and no credentials: it refuses a tag that is not one of those three names with a plain X.Y.Z version, checks that package's manifest (name and version must match the tag, private must be absent, publishConfig must be exactly {"access":"public"}) and that bun.lock records, under workspaces, the package.json version of that package and of each @novaproto/* dependency it declares (no other workspace is checked), installs with the frozen lockfile, builds, runs lint with the strict typecheck, runs every test suite through the check.yml test gate, and packs <package>.tgz with bun pm pack. Packing rewrites workspace:* to the version recorded under workspaces in bun.lock (npm itself does not rewrite workspace: specs); the job refuses a tarball that lacks a declared entry point, whose dependencies, optionalDependencies or peerDependencies hold a workspace:, catalog:, file:, link:, git, URL, path or tarball-file (.tgz, .tar, .tar.gz) spec, or whose @novaproto/* dependency is not the exact version in that dependency's package.json. Its publish job waits for approval on the npm-release environment, checks out nothing, re-checks the tarball's structure, name, version, private, publishConfig and dependency-spec kinds (an @novaproto/* one must be an exact X.Y.Z), refuses unless every @novaproto/* dependency version is already live on the public registry and its published dependencies pin each @novaproto/* package the tarball also pins at the same version, then stages that exact tarball with npm stage publish through npm trusted publishing (OIDC). No npm token is used; the job refuses to run if one is configured. A staged version goes live only after a maintainer approves it with two-factor authentication on npmjs.com.
One-time setup (before the first tag push).
- Add
contracts-v*,mcp-shared-v*andmcp-v*to the deployment tags of thenpm-releaseenvironment, which the exec-primitives release shares (required reviewer, admin bypass off). Keepexec-primitives-v*there too: dropping a pattern blocks that package's releases at the environment gate. A workflow that names a missing environment creates it without protection rules. - Cover
contracts-v*,mcp-shared-v*andmcp-v*with a tag ruleset (Restrict creations, Restrict updates and Restrict deletions, with the release maintainer or the Repository admin role on its bypass list), so only they can create release tags. - All three packages already exist on npm, so none needs a manual first publish. On npmjs.com, for each of
@novaproto/contracts,@novaproto/mcp-sharedand@novaproto/mcp, add a trusted publisher: organization or userfrozename, repositorynova, workflow filenamerelease.yml, environmentnpm-release, stage-only. The fields are case-sensitive and not validated on save. Then set "Require two-factor authentication and disallow tokens" on the package. - Revoke every npm token that can publish under
@novaproto, including any used to publish 0.1.0, and delete anyNPM_TOKENsecret (repository or environment). Never add one: a tag push runs the workflow file at the tagged commit, and older commits carry a token-basedrelease.ymlthat av*tag runs with theNPM_TOKENsecret. Addingv*to the tag ruleset above keeps anyone outside its bypass list from pushing such a tag.
Release order. Release in dependency order: contracts → mcp-shared → mcp. bun pm pack pins each @novaproto/* dependency to the version recorded for it under workspaces in bun.lock, and the build job refuses unless that equals the version in the dependency's package.json. The publish job refuses to stage a package while a pinned dependency version is not live (a staged, unapproved version is not), so approve a dependency on npmjs.com before pushing its dependent's tag. If the publish job refused for that reason, approve the dependency, then re-run the failed job. The pins are exact, so the publish job also refuses a package whose pinned dependency pins a different version of a package both depend on: mcp released after contracts, while the live mcp-shared still pins the previous contracts. So after releasing a package, re-release every package that depends on it, in order, before releasing anything further downstream: after contracts, release mcp-shared, then mcp. If a dependency's source changed since its last release, bump and release it first: a dependent released without that bump pins the previously published version.
Every release.
-
In a release commit, set
versionin the package'spackage.json, and set the sameversionfor that package underworkspacesinbun.lockby hand:bun installdoes not update that entry, and regeneratingbun.lockwould re-resolve every dependency. Merge the commit tomain. -
Tag the merged commit and push the tag:
git tag <package>-v<version> <merged-release-commit> git push origin <package>-v<version> -
Approve the
publishjob on thenpm-releaseenvironment only if the run is for the release commit you merged and the tag you pushed. -
Approve the staged version on npmjs.com with two-factor authentication only if its name and version are the ones the tag names, its file list is the one you expect, and each
@novaproto/*dependency version equals the version inpackages/<dependency>/package.jsonat the tagged commit. Reject anything you did not tag.
Pre-releases and backports are not supported as-is. npm stage publish refuses a pre-release, or a version below the highest published version that is neither a pre-release nor deprecated, without an explicit --tag. The workflow passes none and the tarball check rejects publishConfig.tag. A pre-release tag (not plain X.Y.Z) is refused by the build job, before the approval gate; a backport passes the gate and npm normally refuses it at the stage step (a client-side check; reject it on npmjs.com if it is ever staged). Either needs a commit that adds --tag <dist-tag> to the workflow's npm stage publish line (and, for a pre-release, relaxes the X.Y.Z checks in both jobs, including the one on @novaproto/* dependency specs) before that commit is tagged. The trusted publisher binds only the workflow filename, so this edit keeps it valid.
A published version number can never be reused on npm, even after an unpublish.
Reference consumers
Real-world apps built on Nova. Nova depends on none of them — the arrow points inward.
- llamactl — single-operator control plane for llama.cpp fleets. Uses every Nova package.
- sirius-gateway — multi-provider AI gateway with OpenAI-compatible routes. Adapters delegate to
@novaproto/contracts. - embersynth — capability-based distributed AI orchestration runtime. Its OpenAI-compatible adapter delegates to Nova's provider factory.
License
MIT. See LICENSE.