@kindgi/capabilities
npm install @kindgi/capabilities · source
Capability declarations, a tenant-scoped model-provider registry, and a deterministic router for Kindgi. An agent declares what it needs from a model (features, context window, cost, region, latency, provider or model allow and deny lists) and what it prefers; the router picks a (provider, model) pair from the tenant's registered providers that meets every hard requirement and the tenant's policy, ranked by the preferences. The package also defines ModelProvider, the provider-neutral contract that model adapters implement.
Purpose
Section titled “Purpose”Keep agent definitions independent of any one model vendor. Agents state requirements instead of naming a model; the binding to a concrete model happens at run time, and when nothing qualifies the router returns a structured explanation of which candidate failed which requirement. Providers are registered per tenant, so routing for one tenant never sees another tenant's providers.
Exports
Section titled “Exports”- Declaring requirements
Capability—{ kind?, needs, prefer?, budget? }.kinddefaults toDEFAULT_CAPABILITY_KIND('llm-inference');BUILT_IN_CAPABILITY_KINDS/BuiltInCapabilityKindlist the well-known kinds, andCapabilityKindis an open string.Requirement— hard constraints:{ feature },{ contextWindow: { op, value } },{ costPerCall: { op, usd } }(compared against the model's prompt price per 1K tokens),{ region },{ p95LatencyMs: { op, value } },{ providers: { allow?, deny? } },{ models: { allow?, deny? } }. Operators areComparisonOp(>=,>,=,<=,<) andUpperBoundOp(<=,<) for latency.Preference—{ feature, weight };featureis aFeatureor a provider attribute such as'lower-cost'. Weights add up per candidate and may be negative.FEATURES/Feature— the closed feature set:structured-output,vision,audio-input,audio-output,tool-use,parallel-tool-use,thinking,long-context,code-execution,web-search,file-search,streaming,batch.Budget—maxCostUsd,maxTokens,maxDurationMs.defineCapability(spec)— validates a declaration against the bundledcapability.schema.json(the same file as in@kindgi/specs; its$idis exported asCAPABILITY_SCHEMA_URI) and returnsResult<Capability, InvalidCapabilityError>. The schema acceptsneeds,preferandbudgetwith every requirement exceptmodels, so a declaration that setskindor uses amodelsrequirement is rejected here even though the type and the router accept it.
- Providers
ModelProvider—metadataplusinvoke(input: ModelCallInput): Promise<ModelCallResult>.ProviderMetadata—id,region('unspecified'if not region-scoped),models, and optionalattributes,description,capabilityKind. One provider is one connection that can expose several models.ModelInfo—name,contextWindow,features,cost(promptUsdPer1kTokens,completionUsdPer1kTokens), and optionalp95LatencyMs,maxOutputTokens,description.
- Model call shape —
ModelCallInput(model,messages, optionaltools,structuredOutput,temperature,maxOutputTokens,abortSignal),ModelMessage(rolessystem,user,assistant,tool),ModelToolCall,ModelToolDefinition,StructuredOutputRequest,ModelCallResult(message,finishReason,usage,costUsd,durationMs,provider),UsageCounters. Adapters translate these to and from their vendor SDKs. - Registry
createProviderRegistry(seed?)— in-memory registry. Returns{ registry, register }:register(tenantId, provider)validates the metadata and returns aResult(invalid-provider,duplicate-provider);registryis theProviderRegistryview, whose ownregisterthrows on the same errors.ProviderRegistry—register,get,list,has, all keyed bytenantId, plus optionalhydrate(tenantId)andinvalidate(tenantId)for implementations backed by persistent storage.
- Routing
route(input: RouteInput)— returnsResult<RoutingDecision, CapabilityError>. It expands each provider into one candidate per model, filters bykind,needsand the tenant policy, ranks by summed preference weight, moves candidates matchingpreferredProvider/preferredModelto the front, and breaks ties on(providerId, modelName). The same inputs always produce the same decision.RouteInput—capability,providers(already scoped to one tenant, e.g.registry.list(tenantId)), and optionaltenantPolicy,preferredProvider,preferredModel.RoutingDecision— the chosenproviderandmodel, a human-readablereason, andalternates(ProviderModelPick[]) in rank order.matchTuples(input)— the filtering step on its own: the passing candidates, unranked, orcapability-unsatisfiablewhen none pass.TenantPolicy—tenantId, provider and modelallow/denylists,regionAllow,maxCostPerCallUsd,maxTokensPerCall. An allow list (orregionAllow) that is present restricts even when empty:[]allows nothing.
- Adapter factories —
createAdapterFactoryRegistry(seed?)returns anAdapterFactoryRegistry(register,get,has,list): a deployment-wide map from adapter id to the code that builds providers. AnAdapterFactoryEntryholdsadapterId,capabilityKind, afactory(AdapterFactory:(input: AdapterFactoryInput) => ModelProvider, whereAdapterFactoryInputismetadataplus an optionalresolveApiKey), and an optionalprepare(params)that streams **PrepareEvent**s (progress,ready,error) for downloads or warm-up. Registering the same id twice throws. - Errors —
CapabilityError, a union ofInvalidCapabilityError,InvalidProviderError,DuplicateProviderError,CapabilityUnsatisfiableErrorandBudgetExceededError.CapabilityUnsatisfiableError.reasonslists, for each requirement that rejected at least one candidate, the candidates that satisfied it and a structuredRejectionReasonfor each that did not (missing-feature,context-window-mismatch,region-mismatch,tenant-denied,capability-kind-mismatch, …).
Example
Section titled “Example”import { createProviderRegistry, defineCapability, route } from '@kindgi/capabilities';import type { ModelProvider } from '@kindgi/capabilities';import type { TenantId } from '@kindgi/types';
// One connection to a model vendor, exposing two models.const acmeLlm: ModelProvider = { metadata: { id: 'acme-llm', region: 'eu-west-1', attributes: ['lower-cost'], models: [ { name: 'acme-large', contextWindow: 200_000, features: ['tool-use', 'structured-output', 'streaming'], cost: { promptUsdPer1kTokens: 0.003, completionUsdPer1kTokens: 0.015 }, p95LatencyMs: 4_000, }, { name: 'acme-small', contextWindow: 32_000, features: ['tool-use'], cost: { promptUsdPer1kTokens: 0.0002, completionUsdPer1kTokens: 0.0008 }, }, ], }, invoke: (input) => callAcmeApi(input), // your vendor SDK call};
const tenantId = process.env.TENANT_ID as TenantId;const { registry, register } = createProviderRegistry();const registered = register(tenantId, acmeLlm);if (registered.kind === 'err') throw new Error(registered.error.message);
const capability = defineCapability({ needs: [{ feature: 'tool-use' }, { contextWindow: { op: '>=', value: 100_000 } }], prefer: [{ feature: 'lower-cost', weight: 1 }],});if (capability.kind === 'err') throw new Error(capability.error.message);
const decision = route({ capability: capability.value, providers: registry.list(tenantId), tenantPolicy: { tenantId, regionAllow: ['eu-west-1'] },});if (decision.kind === 'err') { if (decision.error.code === 'capability-unsatisfiable') { for (const r of decision.error.reasons) { console.error(r.requirement, r.rejectingProviders.map((p) => `${p.id}: ${p.reason.code}`)); } } process.exit(1);}
// acme-small fails the context-window requirement, so this is acme-llm / acme-large.const { provider, model } = decision.value;const reply = await provider.invoke({ model: model.name, messages: [{ role: 'user', content: 'Summarize clause 4.2 in one sentence.' }],});console.log(reply.message.content, reply.costUsd);Non-goals
Section titled “Non-goals”- No vendor adapters. Concrete
ModelProviderimplementations live in adapter packages such as those underpackages/adapters. - No cost metering.
BudgetandBudgetExceededErrordescribe limits; nothing in this package tracks spend or enforces them. - No retries or failover.
routereturns rankedalternates; retrying with the next candidate is up to the caller. - No persistence. Both registries are in-memory. Implementations backed by storage plug in through
ProviderRegistry.hydrateandinvalidate.
Related
Section titled “Related”@kindgi/agents— routes an agent's capability declaration withrouteat the start of every turn.@kindgi/specs—capability.schema.json, the wire schemadefineCapabilityvalidates against.@kindgi/dev-echo-provider— aModelProviderfor local development.@kindgi/types—TenantIdandResult.