@hourslabs/domovoi 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tokenizer.ts","../../src/errors.ts","../../src/prompt.ts","../../src/providers/openai/distribution.ts","../../src/providers/openai/adapter.ts","../../src/providers/openai/factory.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tokenizer abstraction (internal — not exported from the public surface).\n *\n * Supports first-token-id resolution for decision-space collision detection\n * and logit_bias construction. Backed by tiktoken (`cl100k_base` for OpenAI\n * models). Adapters that need different tokenizers can build their own\n * implementation of the same internal interface.\n *\n * Contract:\n * - `encode(label)` returns the token ids for the leading whitespace + label\n * (matches OpenAI's tokenization of an emitted output token).\n * - `firstTokenId(label)` returns the first token id of the encoded label.\n *\n * Note: OpenAI's tokenizer often prepends a leading space to emitted tokens\n * (`\" yes\"` vs `\"yes\"`). We detect first-token id with the leading-space\n * variant since that's what the model emits at the first content position.\n */\n\nimport { get_encoding, type Tiktoken } from \"tiktoken\";\n\ntype TokenizerId = \"openai/cl100k_base\";\n\nexport interface Tokenizer {\n readonly id: TokenizerId;\n /** Token ids for `label` as it would appear at a generation boundary. */\n encode(label: string): number[];\n /** First token id of `label` at a generation boundary. */\n firstTokenId(label: string): number;\n}\n\nlet cl100kSingleton: Tokenizer | undefined;\n\n/**\n * cl100k_base tokenizer, used by GPT-4o family + most OpenAI Chat models.\n * Lazy-initialized; the underlying tiktoken native module is heavy.\n */\nexport function cl100kTokenizer(): Tokenizer {\n if (cl100kSingleton !== undefined) return cl100kSingleton;\n const enc: Tiktoken = get_encoding(\"cl100k_base\");\n cl100kSingleton = {\n id: \"openai/cl100k_base\",\n encode(label: string): number[] {\n // Models typically emit labels with a leading space at the first content\n // position (e.g., the model writes \" yes\" not \"yes\"). Encode with that\n // convention so first-token detection matches what we'd see in logprobs.\n const withLeadingSpace = label.startsWith(\" \") ? label : ` ${label}`;\n const ids = enc.encode(withLeadingSpace);\n return Array.from(ids);\n },\n firstTokenId(label: string): number {\n const ids = this.encode(label);\n const first = ids[0];\n if (first === undefined) {\n throw new Error(`Tokenizer produced empty encoding for label ${JSON.stringify(label)}`);\n }\n return first;\n },\n };\n return cl100kSingleton;\n}\n\n/**\n * Detect first-token-id collisions across a decision space. Returns the\n * conflicting label pair if any two labels resolve to the same first token,\n * or `undefined` if the space is collision-free.\n *\n * Used by the OpenAI adapter (and other tokenizer-aware adapters) at\n * construction time to throw `ConfigError({ code: \"decision_space_collision\" })`\n * before any network I/O.\n *\n * Imperative form (rather than `reduce`) chosen for readability: the early\n * return on first collision short-circuits naturally without the bookkeeping\n * that a `reduce` accumulator would require.\n */\nexport function findFirstTokenCollision(\n tokenizer: Tokenizer,\n space: readonly string[],\n): { a: string; b: string; tokenId: number } | undefined {\n const seenByTokenId = new Map<number, string>();\n for (const label of space) {\n const tokenId = tokenizer.firstTokenId(label);\n const priorLabel = seenByTokenId.get(tokenId);\n if (priorLabel !== undefined && priorLabel !== label) {\n return { a: priorLabel, b: label, tokenId };\n }\n seenByTokenId.set(tokenId, label);\n }\n return undefined;\n}\n\n/**\n * Build a logit_bias map for OpenAI Chat Completions: positive bias on each\n * in-space first-token id. Negative biases are deliberately avoided so the\n * coverage signal stays honest — positive bias nudges, doesn't force.\n */\nexport function buildLogitBias(\n tokenizer: Tokenizer,\n space: readonly string[],\n bias = 100,\n): Record<string, number> {\n return Object.fromEntries(\n space.map((label) => [String(tokenizer.firstTokenId(label)), bias] as const),\n );\n}\n","/**\n * Error taxonomy for domovoi.\n *\n * Four classes (`DomovoiError` base + `ProviderError`, `ConfigError`,\n * `BudgetExhaustedError`) with a stable `code` field for fine-grained\n * discrimination. All accept `{ cause }` for ES2022 chaining.\n *\n * The engine canonicalizes anything thrown from `Provider.sample` that isn't\n * already a `DomovoiError` into `ProviderError({ cause })`, so callers always\n * see a known error type. Under the default `onErrorPolicy: \"fallback\"`,\n * runtime errors become `Unknown` Verdict variants rather than throw;\n * `ConfigError` always throws.\n */\n\n/**\n * Stable error codes carried in `error.code`. Discriminate on these rather\n * than on `instanceof` of removed-historical subclasses.\n */\nexport type ErrorCode =\n // ConfigError — construction-time\n | \"decision_space_collision\"\n | \"decision_space_too_large\"\n | \"missing_provider_config\"\n | \"malformed_provider_config\"\n | \"unknown_provider_factory\"\n | \"missing_credential\"\n | \"incompatible_calibrator\"\n | \"invalid_classifier_name\"\n | \"invalid_thresholds\"\n | \"invalid_space\"\n | \"empty_providers\"\n // ProviderError — runtime\n | \"provider_network\"\n | \"provider_rate_limit\"\n | \"provider_timeout\"\n | \"provider_unauthorized\"\n | \"provider_server_error\"\n | \"provider_malformed_response\"\n | \"invalid_distribution\"\n // BudgetExhaustedError — runtime\n | \"per_call_timeout\"\n | \"chain_timeout\"\n | \"max_calls\";\n\nexport class DomovoiError extends Error {\n readonly code: string;\n\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"DomovoiError\";\n this.code = options?.code ?? \"unspecified\";\n }\n}\n\nexport class ProviderError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ProviderError\";\n }\n}\n\nexport class ConfigError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ConfigError\";\n }\n}\n\nexport class BudgetExhaustedError extends DomovoiError {\n readonly attemptedProviders: readonly string[];\n readonly elapsedMs: number;\n readonly scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n\n constructor(\n message: string,\n options: {\n scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n attemptedProviders: readonly string[];\n elapsedMs: number;\n cause?: unknown;\n },\n ) {\n super(message, { code: options.scope, cause: options.cause });\n this.name = \"BudgetExhaustedError\";\n this.scope = options.scope;\n this.attemptedProviders = options.attemptedProviders;\n this.elapsedMs = options.elapsedMs;\n }\n}\n\n/**\n * Wraps any non-DomovoiError thrown value in `ProviderError({ cause })`.\n * `DomovoiError` subtypes — including `ProviderError` subclasses defined by\n * external Provider implementations — pass through unchanged.\n */\nexport function canonicalizeProviderThrow(thrown: unknown): DomovoiError {\n if (thrown instanceof DomovoiError) return thrown;\n if (thrown instanceof Error) {\n return new ProviderError(thrown.message || \"Provider call failed\", {\n code: \"provider_network\",\n cause: thrown,\n });\n }\n return new ProviderError(String(thrown), {\n code: \"provider_network\",\n cause: thrown,\n });\n}\n\n/**\n * Convert an Error to the JSON-safe shape stored in\n * `Verdict.meta.providerErrors`. Cause chains are preserved recursively.\n */\nexport function serializeError(err: unknown): {\n readonly name: string;\n readonly message: string;\n readonly code?: string;\n readonly cause?: ReturnType<typeof serializeError>;\n readonly stack?: string;\n} {\n if (!(err instanceof Error)) {\n return { name: \"Error\", message: String(err) };\n }\n const code = err instanceof DomovoiError ? err.code : undefined;\n const cause = err.cause !== undefined ? serializeError(err.cause) : undefined;\n return {\n name: err.name,\n message: err.message,\n ...(code !== undefined ? { code } : {}),\n ...(cause !== undefined ? { cause } : {}),\n ...(err.stack !== undefined ? { stack: err.stack } : {}),\n };\n}\n","/**\n * Default classifier prompt template and rendering helpers.\n *\n * Labels render in user-given order (never sorted) so prompt-position bias\n * is the caller's choice. Custom templates must supply their own\n * `templateHash` so cache keys stay correct.\n */\n\nimport type { PromptTemplate } from \"./types.js\";\n\nconst DEFAULT_TEMPLATE_HASH = \"domovoi/v0-default\";\n\nexport const defaultTemplate: PromptTemplate = {\n systemPrompt: \"You are a careful classifier. Output exactly one of: {labels_csv}. No other text.\",\n userTemplate: (input: string, _space: readonly string[], question?: string): string =>\n question === undefined ? input : `${question}\\n${input}`,\n templateHash: DEFAULT_TEMPLATE_HASH,\n};\n\n/** Substitute `{labels_csv}` in the system prompt; `undefined` if there isn't one. */\nexport function renderSystemPrompt(\n template: PromptTemplate,\n space: readonly string[],\n): string | undefined {\n if (template.systemPrompt === undefined) return undefined;\n return template.systemPrompt.replace(\"{labels_csv}\", space.join(\", \"));\n}\n\n/** Render the user message via the template's `userTemplate` callback. */\nexport function renderUserPrompt(\n template: PromptTemplate,\n input: string,\n space: readonly string[],\n question?: string,\n): string {\n return template.userTemplate(input, space, question);\n}\n","/**\n * Two paths from OpenAI's `top_logprobs` array to a `Distribution<T>` over\n * the in-space labels:\n *\n * - `buildDistributionByTokenId` — preferred path when a tokenizer is\n * wired up (hosted OpenAI's cl100k_base, or `openaiCompat` with\n * `useCl100kTokenizer: true`). Each top-K entry's surface-form string\n * is re-encoded to its first token id, then matched against the\n * in-space first-token id map. Reliable across SDK versions and\n * handles whitespace-padded variants.\n * - `buildDistributionByStringMatch` — fallback when no tokenizer is\n * available (Ollama with arbitrary models). Matches by trimmed string\n * equality or label-prefix.\n *\n * Both end with `renormalize` over the in-space mass to produce a proper\n * probability distribution; the pre-renormalization in-space mass is\n * preserved as `coverage`.\n */\n\nimport type OpenAI from \"openai\";\nimport type { Tokenizer } from \"../../tokenizer.js\";\nimport type { Distribution } from \"../../types.js\";\n\ntype TopLogprobEntry = OpenAI.Chat.Completions.ChatCompletionTokenLogprob.TopLogprob;\n\nexport function buildDistributionByTokenId<T extends string>(\n space: readonly T[],\n tokenLogprobs: readonly TopLogprobEntry[],\n inSpaceIds: Map<number, T>,\n tokenizer: Tokenizer,\n): Distribution<T> {\n const inSpace = new Map<T, number>();\n let inSpaceMass = 0;\n\n for (const entry of tokenLogprobs) {\n const ids = tokenizer.encode(entry.token);\n const firstId = ids[0];\n if (firstId === undefined) continue;\n const label = inSpaceIds.get(firstId);\n if (label === undefined) continue;\n const prob = Math.exp(entry.logprob);\n const previous = inSpace.get(label) ?? 0;\n if (prob > previous) {\n inSpace.set(label, prob);\n inSpaceMass += prob - previous;\n }\n }\n\n return renormalize(space, inSpace, inSpaceMass);\n}\n\nexport function buildDistributionByStringMatch<T extends string>(\n space: readonly T[],\n tokenLogprobs: readonly TopLogprobEntry[],\n): Distribution<T> {\n const inSpace = new Map<T, number>();\n let inSpaceMass = 0;\n\n for (const label of space) {\n const trimmed = label.trim();\n let bestProb = 0;\n for (const entry of tokenLogprobs) {\n const tok = entry.token.trim();\n // `startsWith(\"\")` is trivially true; require a non-empty token first.\n if (tok === trimmed || (tok && trimmed.startsWith(tok))) {\n const prob = Math.exp(entry.logprob);\n if (prob > bestProb) bestProb = prob;\n }\n }\n if (bestProb > 0) {\n inSpace.set(label, bestProb);\n inSpaceMass += bestProb;\n }\n }\n\n return renormalize(space, inSpace, inSpaceMass);\n}\n\nfunction renormalize<T extends string>(\n space: readonly T[],\n inSpace: Map<T, number>,\n inSpaceMass: number,\n): Distribution<T> {\n const coverage = Math.min(1, inSpaceMass);\n const probs: Record<string, number> = {};\n for (const label of space) {\n const raw = inSpace.get(label) ?? 0;\n probs[label] = inSpaceMass > 0 ? raw / inSpaceMass : 0;\n }\n return {\n probs: probs as Distribution<T>[\"probs\"],\n coverage,\n };\n}\n","/**\n * The internal `buildAdapter` that all three openai-flavored factories\n * (`openai`, `ollama`, `openaiCompat`) share. Wires up the OpenAI Chat\n * Completions request, translates its top-K logprobs into a `Distribution<T>`,\n * and exposes the eager `validate(space)` hook when a tokenizer is available\n * for first-token collision detection.\n */\n\nimport type OpenAI from \"openai\";\nimport { ConfigError, canonicalizeProviderThrow, ProviderError } from \"../../errors.js\";\nimport { renderSystemPrompt, renderUserPrompt } from \"../../prompt.js\";\nimport { buildLogitBias, findFirstTokenCollision, type Tokenizer } from \"../../tokenizer.js\";\nimport type { Distribution, ProviderCapabilities } from \"../../types.js\";\nimport type { Provider, SampleOptions } from \"../provider.js\";\nimport { buildDistributionByStringMatch, buildDistributionByTokenId } from \"./distribution.js\";\n\n// Positive bias on in-space first-tokens only. Nudges the model toward\n// in-space output without forcing — keeps the coverage signal honest.\nconst LOGIT_BIAS_VALUE = 100;\n\nexport type AdapterArgs = {\n readonly id: string;\n readonly modelId: string;\n readonly tokenizerId: string;\n readonly capabilities: ProviderCapabilities;\n readonly client: OpenAI;\n /**\n * Tokenizer for first-token-id resolution and logit_bias construction.\n * When omitted, the adapter falls back to string-based logprob matching.\n */\n readonly tokenizer?: Tokenizer;\n};\n\nexport function buildAdapter(args: AdapterArgs): Provider {\n // Memo of spaces already checked, shared by the eager validate hook and\n // the per-call defense-in-depth check, so repeat passes are zero-cost.\n const collisionMemo = new Set<string>();\n const tokenizer = args.tokenizer;\n\n // The eager `validate` hook is exposed only when a tokenizer is available;\n // backends without tokenizer info (e.g. default Ollama) skip it.\n const eagerValidate =\n tokenizer === undefined\n ? {}\n : {\n validate: (space: readonly string[]): void => {\n ensureNoCollisions(tokenizer, space, collisionMemo);\n },\n };\n\n return {\n id: args.id,\n modelId: args.modelId,\n tokenizerId: args.tokenizerId,\n capabilities: args.capabilities,\n ...eagerValidate,\n\n async sample<T extends string>(\n input: string,\n space: readonly T[],\n opts: SampleOptions,\n ): Promise<Distribution<T>> {\n // Defense-in-depth: catches callers that bypassed `validateClassifierConfig`.\n let logitBias: Record<string, number> | undefined;\n let inSpaceFirstTokenIds: Map<number, T> | undefined;\n if (tokenizer !== undefined) {\n ensureNoCollisions(tokenizer, space, collisionMemo);\n logitBias = buildLogitBias(tokenizer, space, LOGIT_BIAS_VALUE);\n inSpaceFirstTokenIds = mapFirstTokenIds(tokenizer, space);\n }\n\n const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [];\n const system = renderSystemPrompt(opts.template, space);\n if (system !== undefined) {\n messages.push({ role: \"system\", content: system });\n }\n messages.push({ role: \"user\", content: renderUserPrompt(opts.template, input, space) });\n\n let response: OpenAI.Chat.ChatCompletion;\n try {\n const params: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming = {\n model: args.modelId,\n messages,\n temperature: opts.temperature,\n logprobs: true,\n top_logprobs: Math.min(args.capabilities.maxTopLogprobs, Math.max(space.length * 2, 5)),\n // One label is one short word; 16 tokens is enough headroom.\n max_completion_tokens: 16,\n };\n if (opts.seed !== undefined) params.seed = opts.seed;\n if (logitBias !== undefined) params.logit_bias = logitBias;\n\n const requestOpts: { signal?: AbortSignal; timeout?: number } = {\n timeout: opts.timeoutMs,\n };\n if (opts.signal !== undefined) requestOpts.signal = opts.signal;\n response = await args.client.chat.completions.create(params, requestOpts);\n } catch (err) {\n throw canonicalizeProviderThrow(err);\n }\n\n const choice = response.choices[0];\n if (choice === undefined) {\n throw new ProviderError(\"OpenAI response had no choices.\", {\n code: \"provider_malformed_response\",\n });\n }\n const tokenLogprobs = choice.logprobs?.content?.[0]?.top_logprobs;\n if (tokenLogprobs === undefined || tokenLogprobs.length === 0) {\n throw new ProviderError(\"OpenAI response missing first-token logprobs.\", {\n code: \"provider_malformed_response\",\n });\n }\n\n return tokenizer !== undefined && inSpaceFirstTokenIds !== undefined\n ? buildDistributionByTokenId(space, tokenLogprobs, inSpaceFirstTokenIds, tokenizer)\n : buildDistributionByStringMatch(space, tokenLogprobs);\n },\n };\n}\n\nfunction ensureNoCollisions<T extends string>(\n tokenizer: Tokenizer,\n space: readonly T[],\n memo: Set<string>,\n): void {\n const memoKey = JSON.stringify(space);\n if (memo.has(memoKey)) return;\n const collision = findFirstTokenCollision(tokenizer, space);\n if (collision !== undefined) {\n throw new ConfigError(\n `Decision space contains first-token collision: ${JSON.stringify(collision.a)} and ${JSON.stringify(collision.b)} both encode to token id ${collision.tokenId}. Prefix-disambiguate the labels (e.g., 'A_yes' / 'A_no') or pick alternatives.`,\n { code: \"decision_space_collision\" },\n );\n }\n memo.add(memoKey);\n}\n\nfunction mapFirstTokenIds<T extends string>(\n tokenizer: Tokenizer,\n space: readonly T[],\n): Map<number, T> {\n const map = new Map<number, T>();\n for (const label of space) {\n map.set(tokenizer.firstTokenId(label), label);\n }\n return map;\n}\n","/**\n * Three factories for OpenAI Chat Completions backends — hosted, local\n * Ollama, and generic OpenAI-compatible (vLLM, LM Studio, Together,\n * Fireworks, OpenRouter, …). All share the internal adapter; they differ\n * only in default base URL, default API key, and whether a tokenizer is\n * wired up for first-token collision detection and logit_bias construction.\n */\n\nimport OpenAI from \"openai\";\nimport { cl100kTokenizer } from \"../../tokenizer.js\";\nimport type { ProviderCapabilities } from \"../../types.js\";\nimport type { Provider } from \"../provider.js\";\nimport { type AdapterArgs, buildAdapter } from \"./adapter.js\";\n\n/**\n * The known-models list provides autocomplete; the `(string & {})` member is\n * an escape hatch so new models work without a library release.\n */\nexport type OpenAIModel =\n | \"gpt-4o\"\n | \"gpt-4o-mini\"\n | \"gpt-4-turbo\"\n | \"o1\"\n | \"o1-mini\"\n | \"o1-preview\"\n | \"gpt-3.5-turbo\"\n | (string & {});\n\nexport type OpenAIProviderOptions = {\n /** Default: `\"https://api.openai.com/v1\"`. */\n readonly baseURL?: string;\n /**\n * Default: `process.env.OPENAI_API_KEY`. For Ollama / LM Studio /\n * compat backends, pass any non-empty string the SDK will accept.\n */\n readonly apiKey?: string;\n /** SDK-level request timeout. Independent of the engine's own budget. */\n readonly timeout?: number;\n};\n\nconst LOGPROBS_CAPABILITIES: ProviderCapabilities = {\n distributionSource: \"logprobs\",\n coverageMeasurement: \"exact\",\n // OpenAI hosted caps top_logprobs at 20; most OpenAI-compat backends match.\n maxTopLogprobs: 20,\n};\n\n/**\n * Hosted OpenAI provider. Defaults to `process.env.OPENAI_API_KEY` and\n * `https://api.openai.com/v1`. Uses the cl100k_base tokenizer for exact\n * first-token-id resolution + logit_bias on the request.\n *\n * @example\n * const cloud = openai(\"gpt-4o-mini\");\n */\nexport function openai(model: OpenAIModel, opts?: OpenAIProviderOptions): Provider {\n const client = new OpenAI({\n apiKey: opts?.apiKey ?? process.env.OPENAI_API_KEY,\n baseURL: opts?.baseURL,\n timeout: opts?.timeout,\n });\n return buildAdapter({\n id: `openai/${model}`,\n modelId: model,\n tokenizerId: \"openai/cl100k_base\",\n capabilities: LOGPROBS_CAPABILITIES,\n client,\n tokenizer: cl100kTokenizer(),\n });\n}\n\nconst OLLAMA_DEFAULT_BASE_URL = \"http://localhost:11434/v1\";\nconst OLLAMA_DEFAULT_API_KEY = \"ollama\";\n\n/**\n * Local Ollama provider via OpenAI-compatible endpoint.\n * Defaults to `http://localhost:11434/v1` with apiKey `\"ollama\"`.\n *\n * Ollama backends run various tokenizers depending on the model; the\n * adapter falls back to string-based logprob matching by default. Users\n * who need exact collision detection can supply a custom Provider\n * implementing the public Provider interface.\n *\n * @example\n * const local = ollama(\"llama-3.1-70b\");\n */\nexport function ollama(model: string, opts?: OpenAIProviderOptions): Provider {\n const client = new OpenAI({\n apiKey: opts?.apiKey ?? OLLAMA_DEFAULT_API_KEY,\n baseURL: opts?.baseURL ?? OLLAMA_DEFAULT_BASE_URL,\n timeout: opts?.timeout,\n });\n return buildAdapter({\n id: `ollama/${model}`,\n modelId: model,\n // Tokenizer id for cache-key composition; treated opaquely.\n tokenizerId: `ollama/${model}`,\n capabilities: LOGPROBS_CAPABILITIES,\n client,\n // No tokenizer — string-based fallback matches Ollama's varied tokenizers.\n });\n}\n\nexport type OpenAICompatOptions = OpenAIProviderOptions & {\n /** Required for openaiCompat — caller must specify the endpoint. */\n readonly baseURL: string;\n /** Optional override of the tokenizer identifier (used in cache keys). */\n readonly tokenizerId?: string;\n /** Optional override of the provider id (used in cache keys + meta.providerUsed). */\n readonly providerId?: string;\n /** Override capabilities (e.g., higher maxTopLogprobs than 20 if backend supports). */\n readonly capabilities?: Partial<ProviderCapabilities>;\n /**\n * Opt into cl100k_base tokenizer (use only when backend's tokenizer matches\n * OpenAI's cl100k_base — e.g., vLLM running an OpenAI-compatible model).\n * Default: false (string-based fallback).\n */\n readonly useCl100kTokenizer?: boolean;\n};\n\n/**\n * Generic OpenAI-compatible provider. Use for vLLM, LM Studio, Together,\n * Fireworks, OpenRouter, or any backend that speaks the OpenAI Chat\n * Completions wire format.\n *\n * @example\n * const fireworks = openaiCompat(\"accounts/fireworks/models/llama-3\", {\n * baseURL: \"https://api.fireworks.ai/inference/v1\",\n * apiKey: process.env.FIREWORKS_API_KEY,\n * });\n */\nexport function openaiCompat(model: string, opts: OpenAICompatOptions): Provider {\n const client = new OpenAI({\n apiKey: opts.apiKey,\n baseURL: opts.baseURL,\n timeout: opts.timeout,\n });\n const capabilities: ProviderCapabilities = {\n ...LOGPROBS_CAPABILITIES,\n ...opts.capabilities,\n };\n // Derive an id from baseURL host if not explicitly overridden.\n const inferredId =\n opts.providerId ??\n (() => {\n try {\n const host = new URL(opts.baseURL).host;\n return `${host}/${model}`;\n } catch {\n return `compat/${model}`;\n }\n })();\n const args: AdapterArgs = {\n id: inferredId,\n modelId: model,\n tokenizerId: opts.tokenizerId ?? `compat/${model}`,\n capabilities,\n client,\n };\n if (opts.useCl100kTokenizer === true) {\n return buildAdapter({ ...args, tokenizer: cl100kTokenizer() });\n }\n return buildAdapter(args);\n}\n"]}
1
+ {"version":3,"sources":["../../src/tokenizer.ts","../../src/errors.ts","../../src/prompt.ts","../../src/providers/openai/distribution.ts","../../src/providers/openai/adapter.ts","../../src/providers/openai/factory.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Tokenizer abstraction (internal — not exported from the public surface).\n *\n * Supports first-token-id resolution for decision-space collision detection\n * and logit_bias construction. Backed by tiktoken (`cl100k_base` for OpenAI\n * models). Adapters that need different tokenizers can build their own\n * implementation of the same internal interface.\n *\n * Contract:\n * - `encode(label)` returns the token ids for the leading whitespace + label\n * (matches OpenAI's tokenization of an emitted output token).\n * - `firstTokenId(label)` returns the first token id of the encoded label.\n *\n * Note: OpenAI's tokenizer often prepends a leading space to emitted tokens\n * (`\" yes\"` vs `\"yes\"`). We detect first-token id with the leading-space\n * variant since that's what the model emits at the first content position.\n */\n\nimport { get_encoding, type Tiktoken } from \"tiktoken\";\n\ntype TokenizerId = \"openai/cl100k_base\";\n\nexport interface Tokenizer {\n readonly id: TokenizerId;\n /** Token ids for `label` as it would appear at a generation boundary. */\n encode(label: string): number[];\n /** First token id of `label` at a generation boundary. */\n firstTokenId(label: string): number;\n}\n\nlet cl100kSingleton: Tokenizer | undefined;\n\n/**\n * cl100k_base tokenizer, used by GPT-4o family + most OpenAI Chat models.\n * Lazy-initialized; the underlying tiktoken native module is heavy.\n */\nexport function cl100kTokenizer(): Tokenizer {\n if (cl100kSingleton !== undefined) return cl100kSingleton;\n const enc: Tiktoken = get_encoding(\"cl100k_base\");\n cl100kSingleton = {\n id: \"openai/cl100k_base\",\n encode(label: string): number[] {\n // Models typically emit labels with a leading space at the first content\n // position (e.g., the model writes \" yes\" not \"yes\"). Encode with that\n // convention so first-token detection matches what we'd see in logprobs.\n const withLeadingSpace = label.startsWith(\" \") ? label : ` ${label}`;\n const ids = enc.encode(withLeadingSpace);\n return Array.from(ids);\n },\n firstTokenId(label: string): number {\n const ids = this.encode(label);\n const first = ids[0];\n if (first === undefined) {\n throw new Error(`Tokenizer produced empty encoding for label ${JSON.stringify(label)}`);\n }\n return first;\n },\n };\n return cl100kSingleton;\n}\n\n/**\n * Detect first-token-id collisions across a decision space. Returns the\n * conflicting label pair if any two labels resolve to the same first token,\n * or `undefined` if the space is collision-free.\n *\n * Used by the OpenAI adapter (and other tokenizer-aware adapters) at\n * construction time to throw `ConfigError({ code: \"decision_space_collision\" })`\n * before any network I/O.\n *\n * Imperative form (rather than `reduce`) chosen for readability: the early\n * return on first collision short-circuits naturally without the bookkeeping\n * that a `reduce` accumulator would require.\n */\nexport function findFirstTokenCollision(\n tokenizer: Tokenizer,\n space: readonly string[],\n): { a: string; b: string; tokenId: number } | undefined {\n const seenByTokenId = new Map<number, string>();\n for (const label of space) {\n const tokenId = tokenizer.firstTokenId(label);\n const priorLabel = seenByTokenId.get(tokenId);\n if (priorLabel !== undefined && priorLabel !== label) {\n return { a: priorLabel, b: label, tokenId };\n }\n seenByTokenId.set(tokenId, label);\n }\n return undefined;\n}\n\n/**\n * Build a logit_bias map for OpenAI Chat Completions: positive bias on each\n * in-space first-token id. Negative biases are deliberately avoided so the\n * coverage signal stays honest — positive bias nudges, doesn't force.\n */\nexport function buildLogitBias(\n tokenizer: Tokenizer,\n space: readonly string[],\n bias = 100,\n): Record<string, number> {\n return Object.fromEntries(\n space.map((label) => [String(tokenizer.firstTokenId(label)), bias] as const),\n );\n}\n","/**\n * Error taxonomy for domovoi.\n *\n * Four classes (`DomovoiError` base + `ProviderError`, `ConfigError`,\n * `BudgetExhaustedError`) with a stable `code` field for fine-grained\n * discrimination. All accept `{ cause }` for ES2022 chaining.\n *\n * The engine canonicalizes anything thrown from `Provider.sample` that isn't\n * already a `DomovoiError` into `ProviderError({ cause })`, so callers always\n * see a known error type. Under the default `onErrorPolicy: \"fallback\"`,\n * runtime errors become `Unknown` Verdict variants rather than throw;\n * `ConfigError` always throws.\n */\n\n/**\n * Stable error codes carried in `error.code`. Discriminate on these rather\n * than on `instanceof` of removed-historical subclasses.\n */\nexport type ErrorCode =\n // ConfigError — construction-time\n | \"decision_space_collision\"\n | \"decision_space_too_large\"\n | \"missing_provider_config\"\n | \"malformed_provider_config\"\n | \"unknown_provider_factory\"\n | \"missing_credential\"\n | \"incompatible_calibrator\"\n | \"invalid_classifier_name\"\n | \"invalid_thresholds\"\n | \"invalid_space\"\n | \"empty_providers\"\n // ProviderError — runtime\n | \"provider_network\"\n | \"provider_rate_limit\"\n | \"provider_timeout\"\n | \"provider_unauthorized\"\n | \"provider_server_error\"\n | \"provider_malformed_response\"\n | \"invalid_distribution\"\n // BudgetExhaustedError — runtime (operational: time / call-count)\n | \"per_call_timeout\"\n | \"chain_timeout\"\n | \"max_calls\"\n // BudgetExceededError — runtime (scope token budget)\n | \"tokens_exceeded\"\n // ConfigError — scope budget validation\n | \"invalid_scope_budget\";\n\nexport class DomovoiError extends Error {\n readonly code: string;\n\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = \"DomovoiError\";\n this.code = options?.code ?? \"unspecified\";\n }\n}\n\nexport class ProviderError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ProviderError\";\n }\n}\n\nexport class ConfigError extends DomovoiError {\n constructor(message: string, options?: { code?: string; cause?: unknown }) {\n super(message, options);\n this.name = \"ConfigError\";\n }\n}\n\nexport class BudgetExhaustedError extends DomovoiError {\n readonly attemptedProviders: readonly string[];\n readonly elapsedMs: number;\n readonly scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n\n constructor(\n message: string,\n options: {\n scope: \"per_call_timeout\" | \"chain_timeout\" | \"max_calls\";\n attemptedProviders: readonly string[];\n elapsedMs: number;\n cause?: unknown;\n },\n ) {\n super(message, { code: options.scope, cause: options.cause });\n this.name = \"BudgetExhaustedError\";\n this.scope = options.scope;\n this.attemptedProviders = options.attemptedProviders;\n this.elapsedMs = options.elapsedMs;\n }\n}\n\n/**\n * Thrown when scope token budget is exceeded under `onExceeded: \"throw\"` mode.\n * Distinct from `BudgetExhaustedError` (operational time / call-count budgets):\n * this is the cost ceiling for a `domovoi.scope({ budget: { tokens } })` block.\n *\n * Default mode is `\"graceful\"` — classify returns\n * `Unknown { reason: { type: \"budget_exceeded\", spent, limit } }` instead.\n */\nexport class BudgetExceededError extends DomovoiError {\n readonly spent: number;\n readonly limit: number;\n\n constructor(options: { spent: number; limit: number; cause?: unknown }) {\n super(`Scope budget exceeded: ${options.spent} / ${options.limit} tokens`, {\n code: \"tokens_exceeded\",\n cause: options.cause,\n });\n this.name = \"BudgetExceededError\";\n this.spent = options.spent;\n this.limit = options.limit;\n }\n}\n\n/**\n * Wraps any non-DomovoiError thrown value in `ProviderError({ cause })`.\n * `DomovoiError` subtypes — including `ProviderError` subclasses defined by\n * external Provider implementations — pass through unchanged.\n */\nexport function canonicalizeProviderThrow(thrown: unknown): DomovoiError {\n if (thrown instanceof DomovoiError) return thrown;\n if (thrown instanceof Error) {\n return new ProviderError(thrown.message || \"Provider call failed\", {\n code: \"provider_network\",\n cause: thrown,\n });\n }\n return new ProviderError(String(thrown), {\n code: \"provider_network\",\n cause: thrown,\n });\n}\n\n/**\n * Convert an Error to the JSON-safe shape stored in\n * `Verdict.meta.providerErrors`. Cause chains are preserved recursively.\n */\nexport function serializeError(err: unknown): {\n readonly name: string;\n readonly message: string;\n readonly code?: string;\n readonly cause?: ReturnType<typeof serializeError>;\n readonly stack?: string;\n} {\n if (!(err instanceof Error)) {\n return { name: \"Error\", message: String(err) };\n }\n const code = err instanceof DomovoiError ? err.code : undefined;\n const cause = err.cause !== undefined ? serializeError(err.cause) : undefined;\n return {\n name: err.name,\n message: err.message,\n ...(code !== undefined ? { code } : {}),\n ...(cause !== undefined ? { cause } : {}),\n ...(err.stack !== undefined ? { stack: err.stack } : {}),\n };\n}\n","/**\n * Default classifier prompt template and rendering helpers.\n *\n * Labels render in user-given order (never sorted) so prompt-position bias\n * is the caller's choice. Custom templates must supply their own\n * `templateHash` so cache keys stay correct.\n */\n\nimport type { PromptTemplate } from \"./types.js\";\n\nconst DEFAULT_TEMPLATE_HASH = \"domovoi/v0-default\";\n\nexport const defaultTemplate: PromptTemplate = {\n systemPrompt: \"You are a careful classifier. Output exactly one of: {labels_csv}. No other text.\",\n userTemplate: (input: string, _space: readonly string[], question?: string): string =>\n question === undefined ? input : `${question}\\n${input}`,\n templateHash: DEFAULT_TEMPLATE_HASH,\n};\n\n/** Substitute `{labels_csv}` in the system prompt; `undefined` if there isn't one. */\nexport function renderSystemPrompt(\n template: PromptTemplate,\n space: readonly string[],\n): string | undefined {\n if (template.systemPrompt === undefined) return undefined;\n return template.systemPrompt.replace(\"{labels_csv}\", space.join(\", \"));\n}\n\n/** Render the user message via the template's `userTemplate` callback. */\nexport function renderUserPrompt(\n template: PromptTemplate,\n input: string,\n space: readonly string[],\n question?: string,\n): string {\n return template.userTemplate(input, space, question);\n}\n","/**\n * Two paths from OpenAI's `top_logprobs` array to a `Distribution<T>` over\n * the in-space labels:\n *\n * - `buildDistributionByTokenId` — preferred path when a tokenizer is\n * wired up (hosted OpenAI's cl100k_base, or `openaiCompat` with\n * `useCl100kTokenizer: true`). Each top-K entry's surface-form string\n * is re-encoded to its first token id, then matched against the\n * in-space first-token id map. Reliable across SDK versions and\n * handles whitespace-padded variants.\n * - `buildDistributionByStringMatch` — fallback when no tokenizer is\n * available (Ollama with arbitrary models). Matches by trimmed string\n * equality or label-prefix.\n *\n * Both end with `renormalize` over the in-space mass to produce a proper\n * probability distribution; the pre-renormalization in-space mass is\n * preserved as `coverage`.\n */\n\nimport type OpenAI from \"openai\";\nimport type { Tokenizer } from \"../../tokenizer.js\";\nimport type { Distribution } from \"../../types.js\";\n\ntype TopLogprobEntry = OpenAI.Chat.Completions.ChatCompletionTokenLogprob.TopLogprob;\n\nexport function buildDistributionByTokenId<T extends string>(\n space: readonly T[],\n tokenLogprobs: readonly TopLogprobEntry[],\n inSpaceIds: Map<number, T>,\n tokenizer: Tokenizer,\n): Distribution<T> {\n const inSpace = new Map<T, number>();\n let inSpaceMass = 0;\n\n for (const entry of tokenLogprobs) {\n const ids = tokenizer.encode(entry.token);\n const firstId = ids[0];\n if (firstId === undefined) continue;\n const label = inSpaceIds.get(firstId);\n if (label === undefined) continue;\n const prob = Math.exp(entry.logprob);\n const previous = inSpace.get(label) ?? 0;\n if (prob > previous) {\n inSpace.set(label, prob);\n inSpaceMass += prob - previous;\n }\n }\n\n return renormalize(space, inSpace, inSpaceMass);\n}\n\nexport function buildDistributionByStringMatch<T extends string>(\n space: readonly T[],\n tokenLogprobs: readonly TopLogprobEntry[],\n): Distribution<T> {\n const inSpace = new Map<T, number>();\n let inSpaceMass = 0;\n\n for (const label of space) {\n const trimmed = label.trim();\n let bestProb = 0;\n for (const entry of tokenLogprobs) {\n const tok = entry.token.trim();\n // `startsWith(\"\")` is trivially true; require a non-empty token first.\n if (tok === trimmed || (tok && trimmed.startsWith(tok))) {\n const prob = Math.exp(entry.logprob);\n if (prob > bestProb) bestProb = prob;\n }\n }\n if (bestProb > 0) {\n inSpace.set(label, bestProb);\n inSpaceMass += bestProb;\n }\n }\n\n return renormalize(space, inSpace, inSpaceMass);\n}\n\nfunction renormalize<T extends string>(\n space: readonly T[],\n inSpace: Map<T, number>,\n inSpaceMass: number,\n): Distribution<T> {\n const coverage = Math.min(1, inSpaceMass);\n const probs: Record<string, number> = {};\n for (const label of space) {\n const raw = inSpace.get(label) ?? 0;\n probs[label] = inSpaceMass > 0 ? raw / inSpaceMass : 0;\n }\n return {\n probs: probs as Distribution<T>[\"probs\"],\n coverage,\n };\n}\n","/**\n * The internal `buildAdapter` that all three openai-flavored factories\n * (`openai`, `ollama`, `openaiCompat`) share. Wires up the OpenAI Chat\n * Completions request, translates its top-K logprobs into a `Distribution<T>`,\n * and exposes the eager `validate(space)` hook when a tokenizer is available\n * for first-token collision detection.\n */\n\nimport type OpenAI from \"openai\";\nimport { ConfigError, canonicalizeProviderThrow, ProviderError } from \"../../errors.js\";\nimport { renderSystemPrompt, renderUserPrompt } from \"../../prompt.js\";\nimport { buildLogitBias, findFirstTokenCollision, type Tokenizer } from \"../../tokenizer.js\";\nimport type { Distribution, ProviderCapabilities } from \"../../types.js\";\nimport type { Provider, SampleOptions } from \"../provider.js\";\nimport { buildDistributionByStringMatch, buildDistributionByTokenId } from \"./distribution.js\";\n\n// Positive bias on in-space first-tokens only. Nudges the model toward\n// in-space output without forcing — keeps the coverage signal honest.\nconst LOGIT_BIAS_VALUE = 100;\n\nexport type AdapterArgs = {\n readonly id: string;\n readonly modelId: string;\n readonly tokenizerId: string;\n readonly capabilities: ProviderCapabilities;\n readonly client: OpenAI;\n /**\n * Tokenizer for first-token-id resolution and logit_bias construction.\n * When omitted, the adapter falls back to string-based logprob matching.\n */\n readonly tokenizer?: Tokenizer;\n};\n\nexport function buildAdapter(args: AdapterArgs): Provider {\n // Memo of spaces already checked, shared by the eager validate hook and\n // the per-call defense-in-depth check, so repeat passes are zero-cost.\n const collisionMemo = new Set<string>();\n const tokenizer = args.tokenizer;\n\n // The eager `validate` hook is exposed only when a tokenizer is available;\n // backends without tokenizer info (e.g. default Ollama) skip it.\n const eagerValidate =\n tokenizer === undefined\n ? {}\n : {\n validate: (space: readonly string[]): void => {\n ensureNoCollisions(tokenizer, space, collisionMemo);\n },\n };\n\n return {\n id: args.id,\n modelId: args.modelId,\n tokenizerId: args.tokenizerId,\n capabilities: args.capabilities,\n ...eagerValidate,\n\n async sample<T extends string>(\n input: string,\n space: readonly T[],\n opts: SampleOptions,\n ): Promise<Distribution<T>> {\n // Defense-in-depth: catches callers that bypassed `validateClassifierConfig`.\n let logitBias: Record<string, number> | undefined;\n let inSpaceFirstTokenIds: Map<number, T> | undefined;\n if (tokenizer !== undefined) {\n ensureNoCollisions(tokenizer, space, collisionMemo);\n logitBias = buildLogitBias(tokenizer, space, LOGIT_BIAS_VALUE);\n inSpaceFirstTokenIds = mapFirstTokenIds(tokenizer, space);\n }\n\n const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [];\n const system = renderSystemPrompt(opts.template, space);\n if (system !== undefined) {\n messages.push({ role: \"system\", content: system });\n }\n messages.push({ role: \"user\", content: renderUserPrompt(opts.template, input, space) });\n\n let response: OpenAI.Chat.ChatCompletion;\n try {\n const params: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming = {\n model: args.modelId,\n messages,\n temperature: opts.temperature,\n logprobs: true,\n top_logprobs: Math.min(args.capabilities.maxTopLogprobs, Math.max(space.length * 2, 5)),\n // One label is one short word; 16 tokens is enough headroom.\n max_completion_tokens: 16,\n };\n if (opts.seed !== undefined) params.seed = opts.seed;\n if (logitBias !== undefined) params.logit_bias = logitBias;\n\n const requestOpts: { signal?: AbortSignal; timeout?: number } = {\n timeout: opts.timeoutMs,\n };\n if (opts.signal !== undefined) requestOpts.signal = opts.signal;\n response = await args.client.chat.completions.create(params, requestOpts);\n } catch (err) {\n throw canonicalizeProviderThrow(err);\n }\n\n const choice = response.choices[0];\n if (choice === undefined) {\n throw new ProviderError(\"OpenAI response had no choices.\", {\n code: \"provider_malformed_response\",\n });\n }\n const tokenLogprobs = choice.logprobs?.content?.[0]?.top_logprobs;\n if (tokenLogprobs === undefined || tokenLogprobs.length === 0) {\n throw new ProviderError(\"OpenAI response missing first-token logprobs.\", {\n code: \"provider_malformed_response\",\n });\n }\n\n return tokenizer !== undefined && inSpaceFirstTokenIds !== undefined\n ? buildDistributionByTokenId(space, tokenLogprobs, inSpaceFirstTokenIds, tokenizer)\n : buildDistributionByStringMatch(space, tokenLogprobs);\n },\n };\n}\n\nfunction ensureNoCollisions<T extends string>(\n tokenizer: Tokenizer,\n space: readonly T[],\n memo: Set<string>,\n): void {\n const memoKey = JSON.stringify(space);\n if (memo.has(memoKey)) return;\n const collision = findFirstTokenCollision(tokenizer, space);\n if (collision !== undefined) {\n throw new ConfigError(\n `Decision space contains first-token collision: ${JSON.stringify(collision.a)} and ${JSON.stringify(collision.b)} both encode to token id ${collision.tokenId}. Prefix-disambiguate the labels (e.g., 'A_yes' / 'A_no') or pick alternatives.`,\n { code: \"decision_space_collision\" },\n );\n }\n memo.add(memoKey);\n}\n\nfunction mapFirstTokenIds<T extends string>(\n tokenizer: Tokenizer,\n space: readonly T[],\n): Map<number, T> {\n const map = new Map<number, T>();\n for (const label of space) {\n map.set(tokenizer.firstTokenId(label), label);\n }\n return map;\n}\n","/**\n * Three factories for OpenAI Chat Completions backends — hosted, local\n * Ollama, and generic OpenAI-compatible (vLLM, LM Studio, Together,\n * Fireworks, OpenRouter, …). All share the internal adapter; they differ\n * only in default base URL, default API key, and whether a tokenizer is\n * wired up for first-token collision detection and logit_bias construction.\n */\n\nimport OpenAI from \"openai\";\nimport { cl100kTokenizer } from \"../../tokenizer.js\";\nimport type { ProviderCapabilities } from \"../../types.js\";\nimport type { Provider } from \"../provider.js\";\nimport { type AdapterArgs, buildAdapter } from \"./adapter.js\";\n\n/**\n * The known-models list provides autocomplete; the `(string & {})` member is\n * an escape hatch so new models work without a library release.\n */\nexport type OpenAIModel =\n | \"gpt-4o\"\n | \"gpt-4o-mini\"\n | \"gpt-4-turbo\"\n | \"o1\"\n | \"o1-mini\"\n | \"o1-preview\"\n | \"gpt-3.5-turbo\"\n | (string & {});\n\nexport type OpenAIProviderOptions = {\n /** Default: `\"https://api.openai.com/v1\"`. */\n readonly baseURL?: string;\n /**\n * Default: `process.env.OPENAI_API_KEY`. For Ollama / LM Studio /\n * compat backends, pass any non-empty string the SDK will accept.\n */\n readonly apiKey?: string;\n /** SDK-level request timeout. Independent of the engine's own budget. */\n readonly timeout?: number;\n};\n\nconst LOGPROBS_CAPABILITIES: ProviderCapabilities = {\n distributionSource: \"logprobs\",\n coverageMeasurement: \"exact\",\n // OpenAI hosted caps top_logprobs at 20; most OpenAI-compat backends match.\n maxTopLogprobs: 20,\n};\n\n/**\n * Hosted OpenAI provider. Defaults to `process.env.OPENAI_API_KEY` and\n * `https://api.openai.com/v1`. Uses the cl100k_base tokenizer for exact\n * first-token-id resolution + logit_bias on the request.\n *\n * @example\n * const cloud = openai(\"gpt-4o-mini\");\n */\nexport function openai(model: OpenAIModel, opts?: OpenAIProviderOptions): Provider {\n const client = new OpenAI({\n apiKey: opts?.apiKey ?? process.env.OPENAI_API_KEY,\n baseURL: opts?.baseURL,\n timeout: opts?.timeout,\n });\n return buildAdapter({\n id: `openai/${model}`,\n modelId: model,\n tokenizerId: \"openai/cl100k_base\",\n capabilities: LOGPROBS_CAPABILITIES,\n client,\n tokenizer: cl100kTokenizer(),\n });\n}\n\nconst OLLAMA_DEFAULT_BASE_URL = \"http://localhost:11434/v1\";\nconst OLLAMA_DEFAULT_API_KEY = \"ollama\";\n\n/**\n * Local Ollama provider via OpenAI-compatible endpoint.\n * Defaults to `http://localhost:11434/v1` with apiKey `\"ollama\"`.\n *\n * Ollama backends run various tokenizers depending on the model; the\n * adapter falls back to string-based logprob matching by default. Users\n * who need exact collision detection can supply a custom Provider\n * implementing the public Provider interface.\n *\n * @example\n * const local = ollama(\"llama-3.1-70b\");\n */\nexport function ollama(model: string, opts?: OpenAIProviderOptions): Provider {\n const client = new OpenAI({\n apiKey: opts?.apiKey ?? OLLAMA_DEFAULT_API_KEY,\n baseURL: opts?.baseURL ?? OLLAMA_DEFAULT_BASE_URL,\n timeout: opts?.timeout,\n });\n return buildAdapter({\n id: `ollama/${model}`,\n modelId: model,\n // Tokenizer id for cache-key composition; treated opaquely.\n tokenizerId: `ollama/${model}`,\n capabilities: LOGPROBS_CAPABILITIES,\n client,\n // No tokenizer — string-based fallback matches Ollama's varied tokenizers.\n });\n}\n\nexport type OpenAICompatOptions = OpenAIProviderOptions & {\n /** Required for openaiCompat — caller must specify the endpoint. */\n readonly baseURL: string;\n /** Optional override of the tokenizer identifier (used in cache keys). */\n readonly tokenizerId?: string;\n /** Optional override of the provider id (used in cache keys + meta.providerUsed). */\n readonly providerId?: string;\n /** Override capabilities (e.g., higher maxTopLogprobs than 20 if backend supports). */\n readonly capabilities?: Partial<ProviderCapabilities>;\n /**\n * Opt into cl100k_base tokenizer (use only when backend's tokenizer matches\n * OpenAI's cl100k_base — e.g., vLLM running an OpenAI-compatible model).\n * Default: false (string-based fallback).\n */\n readonly useCl100kTokenizer?: boolean;\n};\n\n/**\n * Generic OpenAI-compatible provider. Use for vLLM, LM Studio, Together,\n * Fireworks, OpenRouter, or any backend that speaks the OpenAI Chat\n * Completions wire format.\n *\n * @example\n * const fireworks = openaiCompat(\"accounts/fireworks/models/llama-3\", {\n * baseURL: \"https://api.fireworks.ai/inference/v1\",\n * apiKey: process.env.FIREWORKS_API_KEY,\n * });\n */\nexport function openaiCompat(model: string, opts: OpenAICompatOptions): Provider {\n const client = new OpenAI({\n apiKey: opts.apiKey,\n baseURL: opts.baseURL,\n timeout: opts.timeout,\n });\n const capabilities: ProviderCapabilities = {\n ...LOGPROBS_CAPABILITIES,\n ...opts.capabilities,\n };\n // Derive an id from baseURL host if not explicitly overridden.\n const inferredId =\n opts.providerId ??\n (() => {\n try {\n const host = new URL(opts.baseURL).host;\n return `${host}/${model}`;\n } catch {\n return `compat/${model}`;\n }\n })();\n const args: AdapterArgs = {\n id: inferredId,\n modelId: model,\n tokenizerId: opts.tokenizerId ?? `compat/${model}`,\n capabilities,\n client,\n };\n if (opts.useCl100kTokenizer === true) {\n return buildAdapter({ ...args, tokenizer: cl100kTokenizer() });\n }\n return buildAdapter(args);\n}\n"]}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `domovoi.scope` — ambient context for embedded decisions.
3
+ *
4
+ * await domovoi.scope(
5
+ * { budget: { tokens: 10_000 }, signal, tracer },
6
+ * async () => {
7
+ * // every domovoi.classify(...) inside picks up budget/signal/tracer
8
+ * // ambiently, no prop-drilling
9
+ * await processBatch(items);
10
+ * },
11
+ * );
12
+ *
13
+ * Three primitives:
14
+ * - `scope(opts, fn)` — runs `fn` with merged ambient state
15
+ * - `currentScope()` — reads the active `ResolvedScope`, or `undefined`
16
+ * - `bind(fn)` — captures the current scope for later invocation in
17
+ * a different async context (queue workers, cron jobs)
18
+ *
19
+ * Resolution semantics (per-field, applied at each `domovoi.classify` call):
20
+ * 1. Per-call option — e.g. `domovoi.classify(..., { signal })`
21
+ * 2. Nearest enclosing scope
22
+ * 3. Default — no enforcement, no tracing, no budget
23
+ *
24
+ * `AbortSignal` is the exception: per-call and scope signals combine via
25
+ * `AbortSignal.any([scopeSignal, callSignal])`. Budget and tracer override.
26
+ *
27
+ * Backward compatibility: zero disruption. Calls outside any scope behave
28
+ * identically to v0.1 — `currentScope()` returns `undefined`, the engine
29
+ * falls through to per-call opts, no enforcement applied.
30
+ */
31
+ import { BudgetTracker, type ScopeBudget } from "./budget-tracker.js";
32
+ import type { Tracer } from "./tracer.js";
33
+ export type ScopeOptions = {
34
+ readonly budget?: ScopeBudget;
35
+ readonly signal?: AbortSignal;
36
+ readonly tracer?: Tracer;
37
+ };
38
+ /**
39
+ * Internal post-merge representation held in `ContextStorage`. Public type
40
+ * is `ScopeOptions` (declarative); `ResolvedScope` carries the mutable
41
+ * `BudgetTracker` so nested inheritance shares the running counter.
42
+ */
43
+ export type ResolvedScope = {
44
+ readonly signal?: AbortSignal;
45
+ readonly tracer?: Tracer;
46
+ readonly budgetTracker?: BudgetTracker;
47
+ };
48
+ /**
49
+ * Run `fn` inside a domovoi scope. Returns whatever `fn` returns.
50
+ *
51
+ * Nested scopes inherit unspecified fields from the parent. Specified
52
+ * fields override (except signals, which AND-combine).
53
+ */
54
+ export declare function scope<R>(opts: ScopeOptions, fn: () => R | Promise<R>): R | Promise<R>;
55
+ /** Read the active scope, or `undefined` if not inside one. */
56
+ export declare function currentScope(): ResolvedScope | undefined;
57
+ /**
58
+ * Capture the current scope and re-apply on later invocation. Use for
59
+ * queue workers, cron jobs, deferred callbacks — work that detaches from
60
+ * the calling stack but should keep the same budget / signal / tracer.
61
+ *
62
+ * domovoi.scope({ budget: { tokens: 10_000 }, tracer }, async () => {
63
+ * const job = domovoi.bind(async (item) => {
64
+ * return domovoi.classify(item.text, ["a", "b"]);
65
+ * });
66
+ * await queue.push(job, items); // budget still enforced inside worker
67
+ * });
68
+ *
69
+ * Mirrors Node's `AsyncLocalStorage.bind` and OpenTelemetry's
70
+ * `context.bind` semantics, scoped to domovoi's ambient state.
71
+ *
72
+ * Edge cases:
73
+ * - No enclosing scope at bind time: returns `fn` unchanged (no-op
74
+ * pass-through; avoids `contextStorage.run` overhead).
75
+ * - Captured signal aborted before invocation: classify returns
76
+ * `Unknown { reason: { type: "cancelled" } }` immediately via the
77
+ * existing abort-detection path in the engine.
78
+ * - Captured budget already exhausted: classify returns
79
+ * `Unknown { reason: { type: "budget_exceeded" } }` on first call.
80
+ * - `bind` inside a `bind`: inner captures the resolved scope at its
81
+ * call site, which already includes the outer captured scope —
82
+ * naturally transitive, no special code.
83
+ */
84
+ export declare function bind<F extends (...args: never[]) => unknown>(fn: F): F;
85
+ /**
86
+ * Merge a parent `ResolvedScope` (possibly undefined) with child
87
+ * `ScopeOptions` to produce a new `ResolvedScope`.
88
+ *
89
+ * Semantics per-field:
90
+ * - signal: AND-combine via `AbortSignal.any([parent, child])`
91
+ * - tracer: child overrides parent if specified
92
+ * - budget: if child specifies budget, fresh tracker is created
93
+ * (override semantics — clears parent budget for this and nested
94
+ * scopes). If child omits budget, inherit parent's tracker by
95
+ * reference so the running counter stays shared.
96
+ */
97
+ export declare function mergeScopes(parent: ResolvedScope | undefined, child: ScopeOptions): ResolvedScope;
98
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEtE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC;CACxC,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAIrF;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,IAAI,aAAa,GAAG,SAAS,CAExD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,IAAI,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,CAKtE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,EAAE,KAAK,EAAE,YAAY,GAAG,aAAa,CAUjG"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * `distribution()` — distribution-shaped assertions for AI behavior.
3
+ *
4
+ * import { distribution } from "@hourslabs/domovoi/testing";
5
+ *
6
+ * const dist = await distribution(
7
+ * () => domovoi.classify("hello there", ["greeting", "request"]),
8
+ * { n: 100 },
9
+ * );
10
+ * dist.coverage("greeting"); // 0.94
11
+ * dist.confidenceInterval("greeting"); // [0.88, 0.98] — 95% Wilson CI
12
+ * dist.modeKind(); // "classified"
13
+ * dist.expectStable({ minCoverage: 0.9, maxUncertain: 0.05 });
14
+ *
15
+ * Single-sample assertions on AI behavior are meaningless (the model
16
+ * varies between runs). This primitive turns "the classifier should
17
+ * reliably tag greetings" into a one-liner backed by a Wilson confidence
18
+ * interval and explicit stability thresholds.
19
+ *
20
+ * Cost note: `n=100` against gpt-4o-mini is ~$0.005 per test. Belongs in
21
+ * `test:e2e`, not unit. Document the cost in the test file's comment.
22
+ *
23
+ * Default concurrency: `Math.min(n, 5)` — six seconds for `n=100` against
24
+ * a 300ms p50 provider, well under OpenAI tier 1 RPM limits at the
25
+ * per-test level. Pass `concurrency: 1` to serialize if you run multiple
26
+ * `distribution()` tests in parallel and hit 429s.
27
+ */
28
+ import type { Label, Verdict } from "../types.js";
29
+ export type DistributionOptions = {
30
+ readonly n: number;
31
+ readonly concurrency?: number;
32
+ };
33
+ export type StabilityAssertion = {
34
+ readonly minCoverage?: Readonly<Record<string, number>> | number;
35
+ readonly maxUncertain?: number;
36
+ readonly maxUnknown?: number;
37
+ };
38
+ /**
39
+ * Result of `distribution()`. The user-facing name avoids collision with
40
+ * the internal `Distribution<T>` (probability distribution over labels).
41
+ */
42
+ export interface Samples<T extends Label> {
43
+ /** Fraction of samples that returned `Classified` with the given value. */
44
+ coverage(label: T): number;
45
+ /** Wilson confidence interval for `coverage(label)` at the given level. */
46
+ confidenceInterval(label: T, level?: ConfidenceLevel): readonly [number, number];
47
+ /** Most-frequent verdict kind across samples. */
48
+ modeKind(): "classified" | "uncertain" | "unknown";
49
+ /** Raw samples, in the order they were collected. */
50
+ samples(): readonly Verdict<T>[];
51
+ /**
52
+ * Throw `AssertionError` if any threshold is violated. `minCoverage`
53
+ * accepts a single number (applies to the mode label) or a per-label map.
54
+ */
55
+ expectStable(opts: StabilityAssertion): void;
56
+ }
57
+ export type ConfidenceLevel = 0.9 | 0.95 | 0.99;
58
+ export declare function distribution<T extends Label>(fn: () => Promise<Verdict<T>>, opts: DistributionOptions): Promise<Samples<T>>;
59
+ /**
60
+ * Wilson score interval for a binomial proportion. More robust than the
61
+ * normal approximation at small n or extreme p.
62
+ *
63
+ * @see https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval
64
+ */
65
+ export declare function wilsonInterval(successes: number, trials: number, level?: ConfidenceLevel): readonly [number, number];
66
+ //# sourceMappingURL=distribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distribution.d.ts","sourceRoot":"","sources":["../../src/testing/distribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC;IACjE,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,OAAO,CAAC,CAAC,SAAS,KAAK;IACtC,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,kBAAkB,CAAC,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjF,iDAAiD;IACjD,QAAQ,IAAI,YAAY,GAAG,WAAW,GAAG,SAAS,CAAC;IACnD,qDAAqD;IACrD,OAAO,IAAI,SAAS,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IACjC;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,kBAAkB,GAAG,IAAI,CAAC;CAC9C;AAED,MAAM,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAahD,wBAAsB,YAAY,CAAC,CAAC,SAAS,KAAK,EAChD,EAAE,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAC7B,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAarB;AA+FD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,eAAsB,GAC5B,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAc3B"}
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Public test helpers exposed via `@hourslabs/domovoi/testing` subpath.
3
3
  *
4
- * `mockProvider({ behavior, capabilities?, id? })` builds a Provider for
5
- * tests without hitting a real LLM. Defaults work out-of-the-box for unit
6
- * tests of engine logic, threshold semantics, and fallback chains.
4
+ * Two primitives:
5
+ *
6
+ * - `mockProvider({ behavior })` Provider stub for unit tests of engine
7
+ * logic without hitting a real LLM.
8
+ * - `distribution(fn, { n })` — distribution-shaped assertions on AI
9
+ * behavior, with Wilson confidence intervals.
7
10
  */
8
11
  import type { Provider, SampleOptions } from "../providers/provider.js";
9
12
  import type { Distribution, ProviderCapabilities } from "../types.js";
13
+ export { type ConfidenceLevel, type DistributionOptions, distribution, type Samples, type StabilityAssertion, wilsonInterval, } from "./distribution.js";
10
14
  export type MockProviderOptions<T extends string = string> = {
11
15
  /**
12
16
  * Function that produces the Distribution for a given input + space + opts.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAUtE,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC3D;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,KAChB,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAC7C,yDAAyD;IACzD,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAoCjG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEtE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,YAAY,EACZ,KAAK,OAAO,EACZ,KAAK,kBAAkB,EACvB,cAAc,GACf,MAAM,mBAAmB,CAAC;AAU3B,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI;IAC3D;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,CACjB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,KAChB,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,oBAAoB,CAAC;IAC7C,yDAAyD;IACzD,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,QAAQ,CAoCjG"}
@@ -1,3 +1,138 @@
1
+ // src/testing/distribution.ts
2
+ function zForLevel(level) {
3
+ switch (level) {
4
+ case 0.9:
5
+ return 1.6449;
6
+ case 0.95:
7
+ return 1.96;
8
+ case 0.99:
9
+ return 2.5758;
10
+ }
11
+ }
12
+ async function distribution(fn, opts) {
13
+ if (!Number.isFinite(opts.n) || opts.n <= 0 || !Number.isInteger(opts.n)) {
14
+ throw new RangeError(`distribution(): n must be a positive integer, got ${opts.n}`);
15
+ }
16
+ const concurrency = opts.concurrency ?? Math.min(opts.n, 5);
17
+ if (!Number.isFinite(concurrency) || concurrency <= 0 || !Number.isInteger(concurrency)) {
18
+ throw new RangeError(
19
+ `distribution(): concurrency must be a positive integer, got ${concurrency}`
20
+ );
21
+ }
22
+ const results = await runWithConcurrency(opts.n, concurrency, fn);
23
+ return makeSamples(results);
24
+ }
25
+ function makeSamples(results) {
26
+ return {
27
+ samples: () => results,
28
+ coverage(label) {
29
+ const matches = results.reduce(
30
+ (acc, v) => v.kind === "classified" && v.value === label ? acc + 1 : acc,
31
+ 0
32
+ );
33
+ return matches / results.length;
34
+ },
35
+ confidenceInterval(label, level = 0.95) {
36
+ const matches = results.reduce(
37
+ (acc, v) => v.kind === "classified" && v.value === label ? acc + 1 : acc,
38
+ 0
39
+ );
40
+ return wilsonInterval(matches, results.length, level);
41
+ },
42
+ modeKind() {
43
+ const counts = { classified: 0, uncertain: 0, unknown: 0 };
44
+ for (const v of results) counts[v.kind] += 1;
45
+ let bestKind = "classified";
46
+ let bestCount = counts.classified;
47
+ if (counts.uncertain > bestCount) {
48
+ bestKind = "uncertain";
49
+ bestCount = counts.uncertain;
50
+ }
51
+ if (counts.unknown > bestCount) bestKind = "unknown";
52
+ return bestKind;
53
+ },
54
+ expectStable(spec) {
55
+ const total = results.length;
56
+ const fractionByKind = {
57
+ classified: results.filter((v) => v.kind === "classified").length / total,
58
+ uncertain: results.filter((v) => v.kind === "uncertain").length / total,
59
+ unknown: results.filter((v) => v.kind === "unknown").length / total
60
+ };
61
+ if (spec.maxUncertain !== void 0 && fractionByKind.uncertain > spec.maxUncertain) {
62
+ throw new Error(
63
+ `Uncertain rate ${fractionByKind.uncertain.toFixed(3)} exceeds maxUncertain ${spec.maxUncertain}`
64
+ );
65
+ }
66
+ if (spec.maxUnknown !== void 0 && fractionByKind.unknown > spec.maxUnknown) {
67
+ throw new Error(
68
+ `Unknown rate ${fractionByKind.unknown.toFixed(3)} exceeds maxUnknown ${spec.maxUnknown}`
69
+ );
70
+ }
71
+ if (spec.minCoverage !== void 0) {
72
+ const coverages = computePerLabelCoverage(results);
73
+ if (typeof spec.minCoverage === "number") {
74
+ const best = Math.max(...Object.values(coverages), 0);
75
+ if (best < spec.minCoverage) {
76
+ throw new Error(
77
+ `Best label coverage ${best.toFixed(3)} below minCoverage ${spec.minCoverage}`
78
+ );
79
+ }
80
+ } else {
81
+ for (const [label, threshold] of Object.entries(spec.minCoverage)) {
82
+ const actual = coverages[label] ?? 0;
83
+ if (actual < threshold) {
84
+ throw new Error(
85
+ `Coverage for "${label}" is ${actual.toFixed(3)}, below minCoverage ${threshold}`
86
+ );
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+ };
93
+ }
94
+ function computePerLabelCoverage(results) {
95
+ const counts = {};
96
+ for (const v of results) {
97
+ if (v.kind === "classified") {
98
+ const key = String(v.value);
99
+ counts[key] = (counts[key] ?? 0) + 1;
100
+ }
101
+ }
102
+ const total = results.length;
103
+ const coverage = {};
104
+ for (const [k, c] of Object.entries(counts)) coverage[k] = c / total;
105
+ return coverage;
106
+ }
107
+ function wilsonInterval(successes, trials, level = 0.95) {
108
+ if (trials <= 0) return [0, 0];
109
+ if (level !== 0.9 && level !== 0.95 && level !== 0.99) {
110
+ throw new RangeError(`wilsonInterval: unsupported level ${level} (use 0.9, 0.95, or 0.99)`);
111
+ }
112
+ const z = zForLevel(level);
113
+ const p = successes / trials;
114
+ const z2 = z * z;
115
+ const denominator = 1 + z2 / trials;
116
+ const center = (p + z2 / (2 * trials)) / denominator;
117
+ const margin = z * Math.sqrt(p * (1 - p) / trials + z2 / (4 * trials * trials)) / denominator;
118
+ const lo = Math.max(0, center - margin);
119
+ const hi = Math.min(1, center + margin);
120
+ return [lo, hi];
121
+ }
122
+ async function runWithConcurrency(n, concurrency, fn) {
123
+ const results = new Array(n);
124
+ let nextIdx = 0;
125
+ async function worker() {
126
+ while (true) {
127
+ const idx = nextIdx++;
128
+ if (idx >= n) return;
129
+ results[idx] = await fn();
130
+ }
131
+ }
132
+ await Promise.all(Array.from({ length: Math.min(concurrency, n) }, () => worker()));
133
+ return results;
134
+ }
135
+
1
136
  // src/testing/index.ts
2
137
  var DEFAULT_CAPABILITIES = {
3
138
  distributionSource: "logprobs",
@@ -29,6 +164,6 @@ function mockProvider(options) {
29
164
  };
30
165
  }
31
166
 
32
- export { mockProvider };
167
+ export { distribution, mockProvider, wilsonInterval };
33
168
  //# sourceMappingURL=index.js.map
34
169
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/testing/index.ts"],"names":[],"mappings":";AAWA,IAAM,oBAAA,GAA6C;AAAA,EACjD,kBAAA,EAAoB,UAAA;AAAA,EACpB,mBAAA,EAAqB,OAAA;AAAA;AAAA;AAAA,EAGrB,cAAA,EAAgB;AAClB,CAAA;AAoCO,SAAS,aAAwC,OAAA,EAA2C;AACjG,EAAA,MAAM,EAAA,GAAK,QAAQ,EAAA,IAAM,WAAA;AACzB,EAAA,MAAM,OAAA,GAAU,QAAQ,OAAA,IAAW,MAAA;AACnC,EAAA,MAAM,WAAA,GAAc,QAAQ,WAAA,IAAe,MAAA;AAC3C,EAAA,MAAM,YAAA,GAAe,QAAQ,YAAA,IAAgB,oBAAA;AAU7C,EAAA,MAAM,SAAS,OAAA,CAAQ,QAAA;AAEvB,EAAA,OAAO;AAAA,IACL,EAAA;AAAA,IACA,OAAA;AAAA,IACA,WAAA;AAAA,IACA,YAAA;AAAA,IACA,MAAM,MAAA,CACJ,KAAA,EACA,KAAA,EACA,IAAA,EAC0B;AAE1B,MAAA,IAAI,IAAA,CAAK,QAAQ,OAAA,EAAS;AACxB,QAAA,MAAM,MAAA,GAAS,KAAK,MAAA,CAAO,MAAA;AAC3B,QAAA,IAAI,MAAA,YAAkB,OAAO,MAAM,MAAA;AACnC,QAAA,MAAM,IAAI,KAAA,CAAM,OAAO,MAAA,KAAW,QAAA,GAAW,SAAS,SAAS,CAAA;AAAA,MACjE;AACA,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,KAAA,EAAO,OAA4B,IAAI,CAAA;AACnE,MAAA,OAAO,MAAA;AAAA,IACT;AAAA,GACF;AACF","file":"index.js","sourcesContent":["/**\n * Public test helpers exposed via `@hourslabs/domovoi/testing` subpath.\n *\n * `mockProvider({ behavior, capabilities?, id? })` builds a Provider for\n * tests without hitting a real LLM. Defaults work out-of-the-box for unit\n * tests of engine logic, threshold semantics, and fallback chains.\n */\n\nimport type { Provider, SampleOptions } from \"../providers/provider.js\";\nimport type { Distribution, ProviderCapabilities } from \"../types.js\";\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n distributionSource: \"logprobs\",\n coverageMeasurement: \"exact\",\n // Higher than OpenAI's 20 so users don't trip the chain-min cap unexpectedly\n // in tests of large decision spaces.\n maxTopLogprobs: 100,\n};\n\nexport type MockProviderOptions<T extends string = string> = {\n /**\n * Function that produces the Distribution for a given input + space + opts.\n * May be sync or async; engine awaits the result.\n */\n readonly behavior: (\n input: string,\n space: readonly T[],\n opts: SampleOptions,\n ) => Distribution<T> | Promise<Distribution<T>>;\n /** Override default capabilities (for testing capability-mismatch logic). */\n readonly capabilities?: ProviderCapabilities;\n /** Override the provider id; defaults to \"mock/test\". */\n readonly id?: string;\n /** Override the model id; defaults to \"test\". */\n readonly modelId?: string;\n /** Override the tokenizer id; defaults to \"mock\". */\n readonly tokenizerId?: string;\n};\n\n/**\n * Construct a mock Provider for testing.\n *\n * @example\n * const c = domovoi.classifier({\n * space: [\"a\",\"b\",\"c\"] as const,\n * thresholds: { high: 0.7, coverageMin: 0.5 },\n * providers: [\n * mockProvider({\n * behavior: () => ({ probs: { a: 0.8, b: 0.1, c: 0.1 }, coverage: 0.95 }),\n * }),\n * ],\n * });\n */\nexport function mockProvider<T extends string = string>(options: MockProviderOptions<T>): Provider {\n const id = options.id ?? \"mock/test\";\n const modelId = options.modelId ?? \"test\";\n const tokenizerId = options.tokenizerId ?? \"mock\";\n const capabilities = options.capabilities ?? DEFAULT_CAPABILITIES;\n\n // Cast the behavior to the generic Provider.sample shape. Tests typically\n // pin `T` via the classifier they pass the mock to, so the type erasure here\n // is safe in practice.\n type AnyBehavior = (\n i: string,\n s: readonly string[],\n o: SampleOptions,\n ) => Distribution<string> | Promise<Distribution<string>>;\n const erased = options.behavior as unknown as AnyBehavior;\n\n return {\n id,\n modelId,\n tokenizerId,\n capabilities,\n async sample<U extends string>(\n input: string,\n space: readonly U[],\n opts: SampleOptions,\n ): Promise<Distribution<U>> {\n // Pre-aborted check: producers should respect cancellation.\n if (opts.signal?.aborted) {\n const reason = opts.signal.reason;\n if (reason instanceof Error) throw reason;\n throw new Error(typeof reason === \"string\" ? reason : \"aborted\");\n }\n const result = await erased(input, space as readonly string[], opts);\n return result as Distribution<U>;\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../../src/testing/distribution.ts","../../src/testing/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `distribution()` — distribution-shaped assertions for AI behavior.\n *\n * import { distribution } from \"@hourslabs/domovoi/testing\";\n *\n * const dist = await distribution(\n * () => domovoi.classify(\"hello there\", [\"greeting\", \"request\"]),\n * { n: 100 },\n * );\n * dist.coverage(\"greeting\"); // 0.94\n * dist.confidenceInterval(\"greeting\"); // [0.88, 0.98] — 95% Wilson CI\n * dist.modeKind(); // \"classified\"\n * dist.expectStable({ minCoverage: 0.9, maxUncertain: 0.05 });\n *\n * Single-sample assertions on AI behavior are meaningless (the model\n * varies between runs). This primitive turns \"the classifier should\n * reliably tag greetings\" into a one-liner backed by a Wilson confidence\n * interval and explicit stability thresholds.\n *\n * Cost note: `n=100` against gpt-4o-mini is ~$0.005 per test. Belongs in\n * `test:e2e`, not unit. Document the cost in the test file's comment.\n *\n * Default concurrency: `Math.min(n, 5)` — six seconds for `n=100` against\n * a 300ms p50 provider, well under OpenAI tier 1 RPM limits at the\n * per-test level. Pass `concurrency: 1` to serialize if you run multiple\n * `distribution()` tests in parallel and hit 429s.\n */\n\nimport type { Label, Verdict } from \"../types.js\";\n\nexport type DistributionOptions = {\n readonly n: number;\n readonly concurrency?: number;\n};\n\nexport type StabilityAssertion = {\n readonly minCoverage?: Readonly<Record<string, number>> | number;\n readonly maxUncertain?: number;\n readonly maxUnknown?: number;\n};\n\n/**\n * Result of `distribution()`. The user-facing name avoids collision with\n * the internal `Distribution<T>` (probability distribution over labels).\n */\nexport interface Samples<T extends Label> {\n /** Fraction of samples that returned `Classified` with the given value. */\n coverage(label: T): number;\n /** Wilson confidence interval for `coverage(label)` at the given level. */\n confidenceInterval(label: T, level?: ConfidenceLevel): readonly [number, number];\n /** Most-frequent verdict kind across samples. */\n modeKind(): \"classified\" | \"uncertain\" | \"unknown\";\n /** Raw samples, in the order they were collected. */\n samples(): readonly Verdict<T>[];\n /**\n * Throw `AssertionError` if any threshold is violated. `minCoverage`\n * accepts a single number (applies to the mode label) or a per-label map.\n */\n expectStable(opts: StabilityAssertion): void;\n}\n\nexport type ConfidenceLevel = 0.9 | 0.95 | 0.99;\n\nfunction zForLevel(level: ConfidenceLevel): number {\n switch (level) {\n case 0.9:\n return 1.6449;\n case 0.95:\n return 1.96;\n case 0.99:\n return 2.5758;\n }\n}\n\nexport async function distribution<T extends Label>(\n fn: () => Promise<Verdict<T>>,\n opts: DistributionOptions,\n): Promise<Samples<T>> {\n if (!Number.isFinite(opts.n) || opts.n <= 0 || !Number.isInteger(opts.n)) {\n throw new RangeError(`distribution(): n must be a positive integer, got ${opts.n}`);\n }\n const concurrency = opts.concurrency ?? Math.min(opts.n, 5);\n if (!Number.isFinite(concurrency) || concurrency <= 0 || !Number.isInteger(concurrency)) {\n throw new RangeError(\n `distribution(): concurrency must be a positive integer, got ${concurrency}`,\n );\n }\n\n const results = await runWithConcurrency(opts.n, concurrency, fn);\n return makeSamples(results);\n}\n\nfunction makeSamples<T extends Label>(results: readonly Verdict<T>[]): Samples<T> {\n return {\n samples: () => results,\n\n coverage(label) {\n const matches = results.reduce(\n (acc, v) => (v.kind === \"classified\" && v.value === label ? acc + 1 : acc),\n 0,\n );\n return matches / results.length;\n },\n\n confidenceInterval(label, level = 0.95) {\n const matches = results.reduce(\n (acc, v) => (v.kind === \"classified\" && v.value === label ? acc + 1 : acc),\n 0,\n );\n return wilsonInterval(matches, results.length, level);\n },\n\n modeKind() {\n const counts = { classified: 0, uncertain: 0, unknown: 0 };\n for (const v of results) counts[v.kind] += 1;\n let bestKind: \"classified\" | \"uncertain\" | \"unknown\" = \"classified\";\n let bestCount = counts.classified;\n if (counts.uncertain > bestCount) {\n bestKind = \"uncertain\";\n bestCount = counts.uncertain;\n }\n if (counts.unknown > bestCount) bestKind = \"unknown\";\n return bestKind;\n },\n\n expectStable(spec) {\n const total = results.length;\n const fractionByKind = {\n classified: results.filter((v) => v.kind === \"classified\").length / total,\n uncertain: results.filter((v) => v.kind === \"uncertain\").length / total,\n unknown: results.filter((v) => v.kind === \"unknown\").length / total,\n };\n\n if (spec.maxUncertain !== undefined && fractionByKind.uncertain > spec.maxUncertain) {\n throw new Error(\n `Uncertain rate ${fractionByKind.uncertain.toFixed(3)} exceeds maxUncertain ${spec.maxUncertain}`,\n );\n }\n if (spec.maxUnknown !== undefined && fractionByKind.unknown > spec.maxUnknown) {\n throw new Error(\n `Unknown rate ${fractionByKind.unknown.toFixed(3)} exceeds maxUnknown ${spec.maxUnknown}`,\n );\n }\n\n if (spec.minCoverage !== undefined) {\n const coverages = computePerLabelCoverage(results);\n if (typeof spec.minCoverage === \"number\") {\n // Single threshold: applies to the most-covered label\n const best = Math.max(...Object.values(coverages), 0);\n if (best < spec.minCoverage) {\n throw new Error(\n `Best label coverage ${best.toFixed(3)} below minCoverage ${spec.minCoverage}`,\n );\n }\n } else {\n for (const [label, threshold] of Object.entries(spec.minCoverage)) {\n const actual = coverages[label] ?? 0;\n if (actual < threshold) {\n throw new Error(\n `Coverage for \"${label}\" is ${actual.toFixed(3)}, below minCoverage ${threshold}`,\n );\n }\n }\n }\n }\n },\n };\n}\n\nfunction computePerLabelCoverage<T extends Label>(\n results: readonly Verdict<T>[],\n): Record<string, number> {\n const counts: Record<string, number> = {};\n for (const v of results) {\n if (v.kind === \"classified\") {\n const key = String(v.value);\n counts[key] = (counts[key] ?? 0) + 1;\n }\n }\n const total = results.length;\n const coverage: Record<string, number> = {};\n for (const [k, c] of Object.entries(counts)) coverage[k] = c / total;\n return coverage;\n}\n\n/**\n * Wilson score interval for a binomial proportion. More robust than the\n * normal approximation at small n or extreme p.\n *\n * @see https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval\n */\nexport function wilsonInterval(\n successes: number,\n trials: number,\n level: ConfidenceLevel = 0.95,\n): readonly [number, number] {\n if (trials <= 0) return [0, 0];\n if (level !== 0.9 && level !== 0.95 && level !== 0.99) {\n throw new RangeError(`wilsonInterval: unsupported level ${level} (use 0.9, 0.95, or 0.99)`);\n }\n const z = zForLevel(level);\n const p = successes / trials;\n const z2 = z * z;\n const denominator = 1 + z2 / trials;\n const center = (p + z2 / (2 * trials)) / denominator;\n const margin = (z * Math.sqrt((p * (1 - p)) / trials + z2 / (4 * trials * trials))) / denominator;\n const lo = Math.max(0, center - margin);\n const hi = Math.min(1, center + margin);\n return [lo, hi];\n}\n\n/**\n * Worker-pool concurrency limiter. Each worker pulls from a shared counter,\n * runs `fn`, repeats until exhausted. Optimal scheduling — no batch\n * barriers; a slow call doesn't hold up the whole batch.\n */\nasync function runWithConcurrency<R>(\n n: number,\n concurrency: number,\n fn: () => Promise<R>,\n): Promise<R[]> {\n const results: R[] = new Array(n);\n let nextIdx = 0;\n\n async function worker(): Promise<void> {\n while (true) {\n const idx = nextIdx++;\n if (idx >= n) return;\n results[idx] = await fn();\n }\n }\n\n await Promise.all(Array.from({ length: Math.min(concurrency, n) }, () => worker()));\n return results;\n}\n","/**\n * Public test helpers exposed via `@hourslabs/domovoi/testing` subpath.\n *\n * Two primitives:\n *\n * - `mockProvider({ behavior })` — Provider stub for unit tests of engine\n * logic without hitting a real LLM.\n * - `distribution(fn, { n })` — distribution-shaped assertions on AI\n * behavior, with Wilson confidence intervals.\n */\n\nimport type { Provider, SampleOptions } from \"../providers/provider.js\";\nimport type { Distribution, ProviderCapabilities } from \"../types.js\";\n\nexport {\n type ConfidenceLevel,\n type DistributionOptions,\n distribution,\n type Samples,\n type StabilityAssertion,\n wilsonInterval,\n} from \"./distribution.js\";\n\nconst DEFAULT_CAPABILITIES: ProviderCapabilities = {\n distributionSource: \"logprobs\",\n coverageMeasurement: \"exact\",\n // Higher than OpenAI's 20 so users don't trip the chain-min cap unexpectedly\n // in tests of large decision spaces.\n maxTopLogprobs: 100,\n};\n\nexport type MockProviderOptions<T extends string = string> = {\n /**\n * Function that produces the Distribution for a given input + space + opts.\n * May be sync or async; engine awaits the result.\n */\n readonly behavior: (\n input: string,\n space: readonly T[],\n opts: SampleOptions,\n ) => Distribution<T> | Promise<Distribution<T>>;\n /** Override default capabilities (for testing capability-mismatch logic). */\n readonly capabilities?: ProviderCapabilities;\n /** Override the provider id; defaults to \"mock/test\". */\n readonly id?: string;\n /** Override the model id; defaults to \"test\". */\n readonly modelId?: string;\n /** Override the tokenizer id; defaults to \"mock\". */\n readonly tokenizerId?: string;\n};\n\n/**\n * Construct a mock Provider for testing.\n *\n * @example\n * const c = domovoi.classifier({\n * space: [\"a\",\"b\",\"c\"] as const,\n * thresholds: { high: 0.7, coverageMin: 0.5 },\n * providers: [\n * mockProvider({\n * behavior: () => ({ probs: { a: 0.8, b: 0.1, c: 0.1 }, coverage: 0.95 }),\n * }),\n * ],\n * });\n */\nexport function mockProvider<T extends string = string>(options: MockProviderOptions<T>): Provider {\n const id = options.id ?? \"mock/test\";\n const modelId = options.modelId ?? \"test\";\n const tokenizerId = options.tokenizerId ?? \"mock\";\n const capabilities = options.capabilities ?? DEFAULT_CAPABILITIES;\n\n // Cast the behavior to the generic Provider.sample shape. Tests typically\n // pin `T` via the classifier they pass the mock to, so the type erasure here\n // is safe in practice.\n type AnyBehavior = (\n i: string,\n s: readonly string[],\n o: SampleOptions,\n ) => Distribution<string> | Promise<Distribution<string>>;\n const erased = options.behavior as unknown as AnyBehavior;\n\n return {\n id,\n modelId,\n tokenizerId,\n capabilities,\n async sample<U extends string>(\n input: string,\n space: readonly U[],\n opts: SampleOptions,\n ): Promise<Distribution<U>> {\n // Pre-aborted check: producers should respect cancellation.\n if (opts.signal?.aborted) {\n const reason = opts.signal.reason;\n if (reason instanceof Error) throw reason;\n throw new Error(typeof reason === \"string\" ? reason : \"aborted\");\n }\n const result = await erased(input, space as readonly string[], opts);\n return result as Distribution<U>;\n },\n };\n}\n"]}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Minimal, OpenTelemetry-compatible Tracer/Span interfaces.
3
+ *
4
+ * domovoi emits one span per provider call when a Tracer is present in
5
+ * scope. Attributes follow the OTel GenAI semantic conventions (v1.40+
6
+ * shape, `Development` status as of May 2026) for `gen_ai.*` fields,
7
+ * with domovoi-specific concepts under `domovoi.*`.
8
+ *
9
+ * Users adapt their existing OpenTelemetry tracer with a ~10-line wrapper:
10
+ *
11
+ * import { trace } from "@opentelemetry/api";
12
+ * const otelTracer = trace.getTracer("my-app");
13
+ * const domovoiTracer: Tracer = {
14
+ * startSpan: (name, attrs) =>
15
+ * otelTracer.startSpan(name, { attributes: attrs }),
16
+ * };
17
+ *
18
+ * The interfaces are deliberately minimal — no SpanContext, no propagation
19
+ * primitives, no events. domovoi-the-library does not depend on
20
+ * `@opentelemetry/api`; consumers wire it in.
21
+ */
22
+ export type AttributeValue = string | number | boolean | readonly string[] | readonly number[] | readonly boolean[];
23
+ export interface Span {
24
+ setAttribute(key: string, value: AttributeValue): void;
25
+ recordException(err: unknown): void;
26
+ setStatus(status: "ok" | "error", message?: string): void;
27
+ end(): void;
28
+ }
29
+ export interface Tracer {
30
+ startSpan(name: string, attrs?: Record<string, AttributeValue>): Span;
31
+ }
32
+ /**
33
+ * Used by the engine when no tracer is in scope. Lets engine code call
34
+ * tracer methods unconditionally without null-checks at every site.
35
+ */
36
+ export declare const noopTracer: Tracer;
37
+ //# sourceMappingURL=tracer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracer.d.ts","sourceRoot":"","sources":["../src/tracer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,cAAc,GACtB,MAAM,GACN,MAAM,GACN,OAAO,GACP,SAAS,MAAM,EAAE,GACjB,SAAS,MAAM,EAAE,GACjB,SAAS,OAAO,EAAE,CAAC;AAEvB,MAAM,WAAW,IAAI;IACnB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAAC;IACvD,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,SAAS,CAAC,MAAM,EAAE,IAAI,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1D,GAAG,IAAI,IAAI,CAAC;CACb;AAED,MAAM,WAAW,MAAM;IACrB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAAC;CACvE;AASD;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,MAExB,CAAC"}
package/dist/types.d.ts CHANGED
@@ -97,6 +97,10 @@ export type UnknownVerdictCause<T extends Label> = {
97
97
  } | {
98
98
  readonly type: "budget_exhausted";
99
99
  readonly scope: "per_call_timeout" | "chain_timeout" | "max_calls";
100
+ } | {
101
+ readonly type: "budget_exceeded";
102
+ readonly spent: number;
103
+ readonly limit: number;
100
104
  } | {
101
105
  readonly type: "cancelled";
102
106
  readonly reason?: string;