Library API reference#
@kontourai/datum has one entry point, import { … } from "@kontourai/datum".
This page lists what it exports, grouped by job. The CLI (datum, from
bin/datum.mjs) is a thin front-end over these functions; its commands are
listed in the README. The config file format is defined by
datum.schema.json.
Only the functions marked network make network calls, and none of them
runs unless you call it: runDoctor with probe: true,
probeAnthropicCompatible, probeOpenaiCompatible, discoverModels,
fetchOpenaiCompatibleModels, testConnection, refreshCapabilityCatalog,
and safeFetch.
Resolve options#
Most functions take a ResolveOptions object. Every field is optional.
| Field | Meaning |
|---|---|
config |
A pre-loaded DatumConfig. When set, no file is read; the object is validated directly. |
cwd |
Directory used to find the repo-level .datum/config.json. Default: process.cwd(). |
home |
Directory used to find the user-level ~/.config/kontour/datum.json. Default: os.homedir(). |
userConfigPath |
Explicit user config path. Overrides home. |
repoConfigPath |
Explicit repo config path. Overrides cwd. |
env |
Environment overrides, merged over process.env. They win for both the escape hatches and { env } key lookups. (Standalone materializeAuthRef treats its own env option differently; see Auth references.) |
secretRunner |
A SecretRunner for keychain and 1Password reads. Default: defaultSecretRunner. |
The CLI flags --cwd, --repo-config-path, and --user-config-path map to
cwd, repoConfigPath, and userConfigPath.
Resolving a ref#
import { resolve, resolveRef } from "@kontourai/datum";
const routing = resolveRef("extraction-default"); // no secret read
const target = resolve("extraction-default"); // reads the key
resolve(ref, opts?) → ResolvedTargetreturns{ provider, kind, baseUrl?, apiKey, model }and reads the key from the provider's auth backend.resolveRef(ref, opts?) → ResolvedRefreturns{ provider, kind, baseUrl?, model, auth, apiKeyEnv?, apiKeySet }without reading the key.authis anAuthStatus:{ kind, ref, envVar?, available, tool? }. Forenv,availablemeans the variable is set; forkeychainandop, it means the backing tool is present.envKey(name)returns the escape-hatch suffix for a name: upper-cased, with every run of other characters replaced by_.
How a ref is resolved:
- A ref containing
@is a model ref,model@provider. It is never a role. An unknown provider throwsUNKNOWN_PROVIDER. - A bare ref is looked up as a role first.
DATUM_ROLE_<NAME>(fromopts.envorprocess.env) replaces the role's target, or defines the role when the config has no role of that name. A role whose value is a string resolves as that model ref. A role whose value is a capability policy throwsINVALID_CONFIG; useresolveCapabilityRole()for those. - A bare ref that is not a role is a bare model. Exactly one provider must list
it in
models: none throwsUNKNOWN_ROLE, more than one throwsAMBIGUOUS_MODEL. DATUM_BASEURL_<PROVIDER>replaces the provider'sbaseUrl. Datum does not read a downstream SDK's own variables, such asANTHROPIC_BASE_URL.
Loading config#
loadConfig(opts?) → { config, sources }reads the user file, then the repo file, deep-merges them, and validates the result.sourceslists the paths actually read, user first. A missing file is skipped; a file that cannot be read or is not valid JSON throwsINVALID_CONFIGnaming the path.deepMerge(base, overlay)merges objects per key, recursively. Arrays and scalars fromoverlayreplace those inbase, so a repo file'smodelslist replaces the user file's list.userConfigPath(opts)andrepoConfigPath(opts)return the pathsloadConfigwould read.validateConfig(value) → DatumConfigvalidates a config object against the schema rules and the secret-literal rule, and throws on the first problem.
For capabilityCatalog, a repo-level remoteUrl or localPath replaces the
user-level source rather than merging into a mixed source; maxAgeSeconds
still inherits unless the repo sets it.
Auth references#
parseAuthRef(value) → AuthRefaccepts exactly one of{ env },{ keychain: { service, account? } }, or{ op }. It rejects unknown keys and values that look like a pasted secret. It reads nothing.materializeAuthRef(ref, { env?, secretRunner? }?) → stringparses the reference, then reads that one secret. A missing or empty variable throwsMISSING_ENV. Hereenvreplacesprocess.envrather than merging over it: when you passenv, an{ env }reference is looked up only in that object.describeAuth(auth, env, runner) → AuthStatus,authKind(auth), andauthRefString(auth)describe a reference without reading the secret.looksLikeSecretLiteral(value)is the check the validator uses to reject key-looking literals.defaultSecretRunnerreads keychain items with/usr/bin/security find-generic-password -w(macOS only) and 1Password references withop read. Implement theSecretRunnerinterface (keychainAvailable,opAvailable,readKeychain,readOp) to replace it, for example in tests.
Capability roles#
resolveCapabilityRole(role, request, opts?) → CapabilityRoleResult resolves a
role against a caller-supplied runtime inventory. It is offline: it reads the
catalog from opts.catalog or from the local cache, and never fetches one.
requestmust haveschemaVersion: "datum.capability-role.request/v1", a Bearingtask, and aninventoryof candidates. It may addrequirements,preferences,advisories, and afixedOverridemodel ref.optsextendsResolveOptionswithcatalog,providerBindings,cacheRoot, andnow.opts.cataloginjects a catalog instead of reading the cache. It has theCapabilityCatalogResultshape thatloadCapabilityCatalogandrefreshCapabilityCatalogreturn:{ catalog, metadata }. It is validated strictly: unknown keys are refused, the snapshot is re-parsed,metadata'sdigestandasOfmust match the snapshot, and the config'scapabilityCatalog.maxAgeSecondsfreshness bound still applies.- The result reports
mode(fixedorpolicy),posture(override,durable,fallback, orunavailable), the selectedtarget,alternatives,exclusions, catalog provenance,overrideandfallbackstate, anddiagnostics.
The README section Capability roles explains
the precedence, locality, and providerBindings rules. The CLI equivalent is
datum resolve-policy <role> --request <json-file>.
Capability catalog#
loadCapabilityCatalog(opts?)is offline. AlocalPathsource is read and validated directly; aremoteUrlsource is read only from the cache under<cwd>/.kontourai/datum/bearing(override withcacheRoot).refreshCapabilityCatalog(opts?)(network) fetches aremoteUrlsource, validates it with Bearing, and saves it to the cache. If the fetch fails and a cached catalog that is not stale exists, it returns that catalog with a fallback diagnostic; otherwise it throws. For alocalPathsource it behaves likeloadCapabilityCatalog. Extra options:transport,allowInsecure,requestTimeoutMs,maxResponseBytes, andresolveHost.- The
DEFAULT_CATALOG_*constants are the default size, count, and timeout limits.
The CLI equivalents are datum catalog status and datum catalog refresh.
Config generators#
Generators emit references, never secret values.
generateOpencodeProviderBlock(config) → { block, warnings }builds opencode'sproviderblock. Providers whose kind has no known opencode package, or whose auth is not{ env }, are skipped with a warning.npmForKind(kind)returns the package used for a kind.OPENCODE_FORMAT_VERSIONnames the opencode config format the generator was written against.mergeIntoOpencodeConfig(existing, block)returns a new config with Datum's provider ids replaced and everything else kept.generateClaudeCodeEnv(role, resolvedRef)returns{ role, provider, kind, env, ownedKeys, apiKeyInstruction }.envis Claude Code's settingsenvblock (ANTHROPIC_MODEL, plusANTHROPIC_BASE_URLwhen the provider has a base URL);ownedKeyslists the keys present inenv;apiKeyInstructionis text naming where the key lives, never the key. It throwsINVALID_CONFIGunless the provider kind isanthropic-compatible.CLAUDE_CODE_FORMAT_VERSIONnames the Claude Code settings format the generator was written against.mergeIntoClaudeCodeSettings(existing, env)returns new settings with onlyCLAUDE_CODE_MANAGED_ENV_KEYSreplaced.
The CLI equivalents are datum sync opencode and datum sync claude-code.
Diagnostics#
runDoctor({ probe?, fetchImpl?, allowInsecure?, …ResolveOptions })returns{ ok, checks, warnings }. It checks that config parses, that every fixed role resolves, and that every provider's key backend is available, without reading a key. Policy roles are reported asskip. Withprobe: true(network) it also sends one request per provider throughprobeAnthropicCompatibleorprobeOpenaiCompatible.discoverModels(providerId, opts?)(network) lists the model ids from anopenai-compatibleprovider'sGET {baseUrl}/models. It reports, rather than throws, an unknown provider or an unsupported kind.fetchOpenaiCompatibleModelsis the underlying request.testConnection(providerId, opts?)(network) checks one provider's auth and reachability. Failures are classified asauth,unreachable,incompatible, orinsecure. Kinds other than the two implemented ones are reported asskip.enforceHttpsPolicy(url, { allowInsecure? })applies the HTTPS policy these functions share:https://and loopbackhttp://are allowed, and otherhttp://is blocked unlessallowInsecureis set.safeFetch(url, init, fetchImpl, { allowInsecure?, maxRedirects? }?)(network) returnsPromise<{ blocked, detail?, warning?, response? }>. It callsfetchImplwithredirect: "manual"and follows redirects itself, re-checking the policy before each hop, up tomaxRedirects(default 5), after which it throws. A blocked URL returnsblocked: truewith adetail, andfetchImplis never called for it.
Errors#
Every expected library failure is a DatumError with a stable code (type
DatumErrorCode). DatumError is exported, so callers can test
err instanceof DatumError and branch on err.code.
When a CLI command throws a DatumError, the CLI prints CODE: message to
stderr and exits 1, with these exceptions:
- With
--json,datum cataloganddatum resolve-policyinstead print a JSON envelope to stdout,{ schemaVersion, ok: false, error: { code, message } }, and exit1. - Any other thrown error is reported as
INTERNAL_ERROR: Unexpected internal failure. datum doctor,datum test-connection, anddatum discoverreturn failed checks as part of their report. When a check fails, they print the report (or, fordiscover, the failure class and detail) and exit1without an error code.- Usage errors, such as a missing argument, print a message and the usage
text and exit
1without an error code.
| Code | When |
|---|---|
UNKNOWN_ROLE |
A bare ref is neither a role nor a model any provider offers. |
UNKNOWN_PROVIDER |
A model@provider ref names a provider that is not configured. |
UNKNOWN_MODEL |
A role points at a model no provider offers. |
AMBIGUOUS_MODEL |
A bare model is offered by more than one provider. Use model@provider. |
MISSING_ENV |
An { env } key is materialized but the variable is unset or empty. |
INVALID_CONFIG |
A config file cannot be read, is not valid JSON, or fails validation. Also raised by resolve() on a policy role. |
SECRET_LITERAL |
An auth field holds what looks like a pasted secret, or an auth object has a key other than env, keychain, or op. |
SECRET_BACKEND_UNAVAILABLE |
A keychain read off macOS, or a 1Password read without op installed. |
SECRET_LOOKUP_FAILED |
The backend ran but returned no secret (item missing, empty, or a non-zero exit). |
CAPABILITY_CATALOG_* |
A catalog load or refresh failed. The suffix names why: UNAVAILABLE, MALFORMED, UNSUPPORTED_SCHEMA, DIGEST_MISMATCH, CONFLICT, STALE, RESPONSE_TOO_LARGE, LIMIT_EXCEEDED, INSECURE_URL, PATH_OUTSIDE_ROOT, or CACHE_CORRUPT. |
INTERNAL_ERROR |
The CLI caught an unexpected, non-Datum error. |