@hourslabs/domovoi 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +4 -0
  3. package/README.md +312 -0
  4. package/dist/cache.d.ts +102 -0
  5. package/dist/cache.d.ts.map +1 -0
  6. package/dist/calibration/index.d.ts +45 -0
  7. package/dist/calibration/index.d.ts.map +1 -0
  8. package/dist/calibration/index.js +95 -0
  9. package/dist/calibration/index.js.map +1 -0
  10. package/dist/engine/abort.d.ts +43 -0
  11. package/dist/engine/abort.d.ts.map +1 -0
  12. package/dist/engine/config.d.ts +79 -0
  13. package/dist/engine/config.d.ts.map +1 -0
  14. package/dist/engine/decide.d.ts +18 -0
  15. package/dist/engine/decide.d.ts.map +1 -0
  16. package/dist/engine/distribution.d.ts +18 -0
  17. package/dist/engine/distribution.d.ts.map +1 -0
  18. package/dist/engine/error-recording.d.ts +35 -0
  19. package/dist/engine/error-recording.d.ts.map +1 -0
  20. package/dist/engine/finalize.d.ts +12 -0
  21. package/dist/engine/finalize.d.ts.map +1 -0
  22. package/dist/engine/hooks.d.ts +12 -0
  23. package/dist/engine/hooks.d.ts.map +1 -0
  24. package/dist/engine/index.d.ts +7 -0
  25. package/dist/engine/index.d.ts.map +1 -0
  26. package/dist/engine/meta.d.ts +37 -0
  27. package/dist/engine/meta.d.ts.map +1 -0
  28. package/dist/engine/threshold.d.ts +31 -0
  29. package/dist/engine/threshold.d.ts.map +1 -0
  30. package/dist/env.d.ts +46 -0
  31. package/dist/env.d.ts.map +1 -0
  32. package/dist/errors.d.ts +66 -0
  33. package/dist/errors.d.ts.map +1 -0
  34. package/dist/hash.d.ts +27 -0
  35. package/dist/hash.d.ts.map +1 -0
  36. package/dist/index.d.ts +34 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +1263 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/prompt.d.ts +14 -0
  41. package/dist/prompt.d.ts.map +1 -0
  42. package/dist/providers/index.d.ts +6 -0
  43. package/dist/providers/index.d.ts.map +1 -0
  44. package/dist/providers/index.js +301 -0
  45. package/dist/providers/index.js.map +1 -0
  46. package/dist/providers/openai/adapter.d.ts +25 -0
  47. package/dist/providers/openai/adapter.d.ts.map +1 -0
  48. package/dist/providers/openai/distribution.d.ts +26 -0
  49. package/dist/providers/openai/distribution.d.ts.map +1 -0
  50. package/dist/providers/openai/factory.d.ts +76 -0
  51. package/dist/providers/openai/factory.d.ts.map +1 -0
  52. package/dist/providers/openai/index.d.ts +6 -0
  53. package/dist/providers/openai/index.d.ts.map +1 -0
  54. package/dist/providers/provider.d.ts +50 -0
  55. package/dist/providers/provider.d.ts.map +1 -0
  56. package/dist/testing/index.d.ts +40 -0
  57. package/dist/testing/index.d.ts.map +1 -0
  58. package/dist/testing/index.js +34 -0
  59. package/dist/testing/index.js.map +1 -0
  60. package/dist/tokenizer.d.ts +56 -0
  61. package/dist/tokenizer.d.ts.map +1 -0
  62. package/dist/types.d.ts +180 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/validate.d.ts +47 -0
  65. package/dist/validate.d.ts.map +1 -0
  66. package/dist/verbs/boolean.d.ts +26 -0
  67. package/dist/verbs/boolean.d.ts.map +1 -0
  68. package/dist/verbs/classifier.d.ts +63 -0
  69. package/dist/verbs/classifier.d.ts.map +1 -0
  70. package/dist/verbs/classify.d.ts +24 -0
  71. package/dist/verbs/classify.d.ts.map +1 -0
  72. package/dist/verdict.d.ts +38 -0
  73. package/dist/verdict.d.ts.map +1 -0
  74. package/package.json +108 -0
@@ -0,0 +1,301 @@
1
+ import OpenAI from 'openai';
2
+ import { get_encoding } from 'tiktoken';
3
+
4
+ // src/providers/openai/factory.ts
5
+ var cl100kSingleton;
6
+ function cl100kTokenizer() {
7
+ if (cl100kSingleton !== void 0) return cl100kSingleton;
8
+ const enc = get_encoding("cl100k_base");
9
+ cl100kSingleton = {
10
+ id: "openai/cl100k_base",
11
+ encode(label) {
12
+ const withLeadingSpace = label.startsWith(" ") ? label : ` ${label}`;
13
+ const ids = enc.encode(withLeadingSpace);
14
+ return Array.from(ids);
15
+ },
16
+ firstTokenId(label) {
17
+ const ids = this.encode(label);
18
+ const first = ids[0];
19
+ if (first === void 0) {
20
+ throw new Error(`Tokenizer produced empty encoding for label ${JSON.stringify(label)}`);
21
+ }
22
+ return first;
23
+ }
24
+ };
25
+ return cl100kSingleton;
26
+ }
27
+ function findFirstTokenCollision(tokenizer, space) {
28
+ const seenByTokenId = /* @__PURE__ */ new Map();
29
+ for (const label of space) {
30
+ const tokenId = tokenizer.firstTokenId(label);
31
+ const priorLabel = seenByTokenId.get(tokenId);
32
+ if (priorLabel !== void 0 && priorLabel !== label) {
33
+ return { a: priorLabel, b: label, tokenId };
34
+ }
35
+ seenByTokenId.set(tokenId, label);
36
+ }
37
+ return void 0;
38
+ }
39
+ function buildLogitBias(tokenizer, space, bias = 100) {
40
+ return Object.fromEntries(
41
+ space.map((label) => [String(tokenizer.firstTokenId(label)), bias])
42
+ );
43
+ }
44
+
45
+ // src/errors.ts
46
+ var DomovoiError = class extends Error {
47
+ code;
48
+ constructor(message, options) {
49
+ super(message, options?.cause !== void 0 ? { cause: options.cause } : void 0);
50
+ this.name = "DomovoiError";
51
+ this.code = options?.code ?? "unspecified";
52
+ }
53
+ };
54
+ var ProviderError = class extends DomovoiError {
55
+ constructor(message, options) {
56
+ super(message, options);
57
+ this.name = "ProviderError";
58
+ }
59
+ };
60
+ var ConfigError = class extends DomovoiError {
61
+ constructor(message, options) {
62
+ super(message, options);
63
+ this.name = "ConfigError";
64
+ }
65
+ };
66
+ function canonicalizeProviderThrow(thrown) {
67
+ if (thrown instanceof DomovoiError) return thrown;
68
+ if (thrown instanceof Error) {
69
+ return new ProviderError(thrown.message || "Provider call failed", {
70
+ code: "provider_network",
71
+ cause: thrown
72
+ });
73
+ }
74
+ return new ProviderError(String(thrown), {
75
+ code: "provider_network",
76
+ cause: thrown
77
+ });
78
+ }
79
+
80
+ // src/prompt.ts
81
+ function renderSystemPrompt(template, space) {
82
+ if (template.systemPrompt === void 0) return void 0;
83
+ return template.systemPrompt.replace("{labels_csv}", space.join(", "));
84
+ }
85
+ function renderUserPrompt(template, input, space, question) {
86
+ return template.userTemplate(input, space, question);
87
+ }
88
+
89
+ // src/providers/openai/distribution.ts
90
+ function buildDistributionByTokenId(space, tokenLogprobs, inSpaceIds, tokenizer) {
91
+ const inSpace = /* @__PURE__ */ new Map();
92
+ let inSpaceMass = 0;
93
+ for (const entry of tokenLogprobs) {
94
+ const ids = tokenizer.encode(entry.token);
95
+ const firstId = ids[0];
96
+ if (firstId === void 0) continue;
97
+ const label = inSpaceIds.get(firstId);
98
+ if (label === void 0) continue;
99
+ const prob = Math.exp(entry.logprob);
100
+ const previous = inSpace.get(label) ?? 0;
101
+ if (prob > previous) {
102
+ inSpace.set(label, prob);
103
+ inSpaceMass += prob - previous;
104
+ }
105
+ }
106
+ return renormalize(space, inSpace, inSpaceMass);
107
+ }
108
+ function buildDistributionByStringMatch(space, tokenLogprobs) {
109
+ const inSpace = /* @__PURE__ */ new Map();
110
+ let inSpaceMass = 0;
111
+ for (const label of space) {
112
+ const trimmed = label.trim();
113
+ let bestProb = 0;
114
+ for (const entry of tokenLogprobs) {
115
+ const tok = entry.token.trim();
116
+ if (tok === trimmed || tok && trimmed.startsWith(tok)) {
117
+ const prob = Math.exp(entry.logprob);
118
+ if (prob > bestProb) bestProb = prob;
119
+ }
120
+ }
121
+ if (bestProb > 0) {
122
+ inSpace.set(label, bestProb);
123
+ inSpaceMass += bestProb;
124
+ }
125
+ }
126
+ return renormalize(space, inSpace, inSpaceMass);
127
+ }
128
+ function renormalize(space, inSpace, inSpaceMass) {
129
+ const coverage = Math.min(1, inSpaceMass);
130
+ const probs = {};
131
+ for (const label of space) {
132
+ const raw = inSpace.get(label) ?? 0;
133
+ probs[label] = inSpaceMass > 0 ? raw / inSpaceMass : 0;
134
+ }
135
+ return {
136
+ probs,
137
+ coverage
138
+ };
139
+ }
140
+
141
+ // src/providers/openai/adapter.ts
142
+ var LOGIT_BIAS_VALUE = 100;
143
+ function buildAdapter(args) {
144
+ const collisionMemo = /* @__PURE__ */ new Set();
145
+ const tokenizer = args.tokenizer;
146
+ const eagerValidate = tokenizer === void 0 ? {} : {
147
+ validate: (space) => {
148
+ ensureNoCollisions(tokenizer, space, collisionMemo);
149
+ }
150
+ };
151
+ return {
152
+ id: args.id,
153
+ modelId: args.modelId,
154
+ tokenizerId: args.tokenizerId,
155
+ capabilities: args.capabilities,
156
+ ...eagerValidate,
157
+ async sample(input, space, opts) {
158
+ let logitBias;
159
+ let inSpaceFirstTokenIds;
160
+ if (tokenizer !== void 0) {
161
+ ensureNoCollisions(tokenizer, space, collisionMemo);
162
+ logitBias = buildLogitBias(tokenizer, space, LOGIT_BIAS_VALUE);
163
+ inSpaceFirstTokenIds = mapFirstTokenIds(tokenizer, space);
164
+ }
165
+ const messages = [];
166
+ const system = renderSystemPrompt(opts.template, space);
167
+ if (system !== void 0) {
168
+ messages.push({ role: "system", content: system });
169
+ }
170
+ messages.push({ role: "user", content: renderUserPrompt(opts.template, input, space) });
171
+ let response;
172
+ try {
173
+ const params = {
174
+ model: args.modelId,
175
+ messages,
176
+ temperature: opts.temperature,
177
+ logprobs: true,
178
+ top_logprobs: Math.min(args.capabilities.maxTopLogprobs, Math.max(space.length * 2, 5)),
179
+ // One label is one short word; 16 tokens is enough headroom.
180
+ max_completion_tokens: 16
181
+ };
182
+ if (opts.seed !== void 0) params.seed = opts.seed;
183
+ if (logitBias !== void 0) params.logit_bias = logitBias;
184
+ const requestOpts = {
185
+ timeout: opts.timeoutMs
186
+ };
187
+ if (opts.signal !== void 0) requestOpts.signal = opts.signal;
188
+ response = await args.client.chat.completions.create(params, requestOpts);
189
+ } catch (err) {
190
+ throw canonicalizeProviderThrow(err);
191
+ }
192
+ const choice = response.choices[0];
193
+ if (choice === void 0) {
194
+ throw new ProviderError("OpenAI response had no choices.", {
195
+ code: "provider_malformed_response"
196
+ });
197
+ }
198
+ const tokenLogprobs = choice.logprobs?.content?.[0]?.top_logprobs;
199
+ if (tokenLogprobs === void 0 || tokenLogprobs.length === 0) {
200
+ throw new ProviderError("OpenAI response missing first-token logprobs.", {
201
+ code: "provider_malformed_response"
202
+ });
203
+ }
204
+ return tokenizer !== void 0 && inSpaceFirstTokenIds !== void 0 ? buildDistributionByTokenId(space, tokenLogprobs, inSpaceFirstTokenIds, tokenizer) : buildDistributionByStringMatch(space, tokenLogprobs);
205
+ }
206
+ };
207
+ }
208
+ function ensureNoCollisions(tokenizer, space, memo) {
209
+ const memoKey = JSON.stringify(space);
210
+ if (memo.has(memoKey)) return;
211
+ const collision = findFirstTokenCollision(tokenizer, space);
212
+ if (collision !== void 0) {
213
+ throw new ConfigError(
214
+ `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.`,
215
+ { code: "decision_space_collision" }
216
+ );
217
+ }
218
+ memo.add(memoKey);
219
+ }
220
+ function mapFirstTokenIds(tokenizer, space) {
221
+ const map = /* @__PURE__ */ new Map();
222
+ for (const label of space) {
223
+ map.set(tokenizer.firstTokenId(label), label);
224
+ }
225
+ return map;
226
+ }
227
+
228
+ // src/providers/openai/factory.ts
229
+ var LOGPROBS_CAPABILITIES = {
230
+ distributionSource: "logprobs",
231
+ coverageMeasurement: "exact",
232
+ // OpenAI hosted caps top_logprobs at 20; most OpenAI-compat backends match.
233
+ maxTopLogprobs: 20
234
+ };
235
+ function openai(model, opts) {
236
+ const client = new OpenAI({
237
+ apiKey: opts?.apiKey ?? process.env.OPENAI_API_KEY,
238
+ baseURL: opts?.baseURL,
239
+ timeout: opts?.timeout
240
+ });
241
+ return buildAdapter({
242
+ id: `openai/${model}`,
243
+ modelId: model,
244
+ tokenizerId: "openai/cl100k_base",
245
+ capabilities: LOGPROBS_CAPABILITIES,
246
+ client,
247
+ tokenizer: cl100kTokenizer()
248
+ });
249
+ }
250
+ var OLLAMA_DEFAULT_BASE_URL = "http://localhost:11434/v1";
251
+ var OLLAMA_DEFAULT_API_KEY = "ollama";
252
+ function ollama(model, opts) {
253
+ const client = new OpenAI({
254
+ apiKey: opts?.apiKey ?? OLLAMA_DEFAULT_API_KEY,
255
+ baseURL: opts?.baseURL ?? OLLAMA_DEFAULT_BASE_URL,
256
+ timeout: opts?.timeout
257
+ });
258
+ return buildAdapter({
259
+ id: `ollama/${model}`,
260
+ modelId: model,
261
+ // Tokenizer id for cache-key composition; treated opaquely.
262
+ tokenizerId: `ollama/${model}`,
263
+ capabilities: LOGPROBS_CAPABILITIES,
264
+ client
265
+ // No tokenizer — string-based fallback matches Ollama's varied tokenizers.
266
+ });
267
+ }
268
+ function openaiCompat(model, opts) {
269
+ const client = new OpenAI({
270
+ apiKey: opts.apiKey,
271
+ baseURL: opts.baseURL,
272
+ timeout: opts.timeout
273
+ });
274
+ const capabilities = {
275
+ ...LOGPROBS_CAPABILITIES,
276
+ ...opts.capabilities
277
+ };
278
+ const inferredId = opts.providerId ?? (() => {
279
+ try {
280
+ const host = new URL(opts.baseURL).host;
281
+ return `${host}/${model}`;
282
+ } catch {
283
+ return `compat/${model}`;
284
+ }
285
+ })();
286
+ const args = {
287
+ id: inferredId,
288
+ modelId: model,
289
+ tokenizerId: opts.tokenizerId ?? `compat/${model}`,
290
+ capabilities,
291
+ client
292
+ };
293
+ if (opts.useCl100kTokenizer === true) {
294
+ return buildAdapter({ ...args, tokenizer: cl100kTokenizer() });
295
+ }
296
+ return buildAdapter(args);
297
+ }
298
+
299
+ export { ollama, openai, openaiCompat };
300
+ //# sourceMappingURL=index.js.map
301
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The internal `buildAdapter` that all three openai-flavored factories
3
+ * (`openai`, `ollama`, `openaiCompat`) share. Wires up the OpenAI Chat
4
+ * Completions request, translates its top-K logprobs into a `Distribution<T>`,
5
+ * and exposes the eager `validate(space)` hook when a tokenizer is available
6
+ * for first-token collision detection.
7
+ */
8
+ import type OpenAI from "openai";
9
+ import { type Tokenizer } from "../../tokenizer.js";
10
+ import type { ProviderCapabilities } from "../../types.js";
11
+ import type { Provider } from "../provider.js";
12
+ export type AdapterArgs = {
13
+ readonly id: string;
14
+ readonly modelId: string;
15
+ readonly tokenizerId: string;
16
+ readonly capabilities: ProviderCapabilities;
17
+ readonly client: OpenAI;
18
+ /**
19
+ * Tokenizer for first-token-id resolution and logit_bias construction.
20
+ * When omitted, the adapter falls back to string-based logprob matching.
21
+ */
22
+ readonly tokenizer?: Tokenizer;
23
+ };
24
+ export declare function buildAdapter(args: AdapterArgs): Provider;
25
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAGjC,OAAO,EAA2C,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC7F,OAAO,KAAK,EAAgB,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AACzE,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,gBAAgB,CAAC;AAO9D,MAAM,MAAM,WAAW,GAAG;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC,CAAC;AAEF,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,QAAQ,CAsFxD"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Two paths from OpenAI's `top_logprobs` array to a `Distribution<T>` over
3
+ * the in-space labels:
4
+ *
5
+ * - `buildDistributionByTokenId` — preferred path when a tokenizer is
6
+ * wired up (hosted OpenAI's cl100k_base, or `openaiCompat` with
7
+ * `useCl100kTokenizer: true`). Each top-K entry's surface-form string
8
+ * is re-encoded to its first token id, then matched against the
9
+ * in-space first-token id map. Reliable across SDK versions and
10
+ * handles whitespace-padded variants.
11
+ * - `buildDistributionByStringMatch` — fallback when no tokenizer is
12
+ * available (Ollama with arbitrary models). Matches by trimmed string
13
+ * equality or label-prefix.
14
+ *
15
+ * Both end with `renormalize` over the in-space mass to produce a proper
16
+ * probability distribution; the pre-renormalization in-space mass is
17
+ * preserved as `coverage`.
18
+ */
19
+ import type OpenAI from "openai";
20
+ import type { Tokenizer } from "../../tokenizer.js";
21
+ import type { Distribution } from "../../types.js";
22
+ type TopLogprobEntry = OpenAI.Chat.Completions.ChatCompletionTokenLogprob.TopLogprob;
23
+ export declare function buildDistributionByTokenId<T extends string>(space: readonly T[], tokenLogprobs: readonly TopLogprobEntry[], inSpaceIds: Map<number, T>, tokenizer: Tokenizer): Distribution<T>;
24
+ export declare function buildDistributionByStringMatch<T extends string>(space: readonly T[], tokenLogprobs: readonly TopLogprobEntry[]): Distribution<T>;
25
+ export {};
26
+ //# sourceMappingURL=distribution.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"distribution.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/distribution.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,KAAK,eAAe,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,0BAA0B,CAAC,UAAU,CAAC;AAErF,wBAAgB,0BAA0B,CAAC,CAAC,SAAS,MAAM,EACzD,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,aAAa,EAAE,SAAS,eAAe,EAAE,EACzC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAC1B,SAAS,EAAE,SAAS,GACnB,YAAY,CAAC,CAAC,CAAC,CAmBjB;AAED,wBAAgB,8BAA8B,CAAC,CAAC,SAAS,MAAM,EAC7D,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,aAAa,EAAE,SAAS,eAAe,EAAE,GACxC,YAAY,CAAC,CAAC,CAAC,CAsBjB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Three factories for OpenAI Chat Completions backends — hosted, local
3
+ * Ollama, and generic OpenAI-compatible (vLLM, LM Studio, Together,
4
+ * Fireworks, OpenRouter, …). All share the internal adapter; they differ
5
+ * only in default base URL, default API key, and whether a tokenizer is
6
+ * wired up for first-token collision detection and logit_bias construction.
7
+ */
8
+ import type { ProviderCapabilities } from "../../types.js";
9
+ import type { Provider } from "../provider.js";
10
+ /**
11
+ * The known-models list provides autocomplete; the `(string & {})` member is
12
+ * an escape hatch so new models work without a library release.
13
+ */
14
+ export type OpenAIModel = "gpt-4o" | "gpt-4o-mini" | "gpt-4-turbo" | "o1" | "o1-mini" | "o1-preview" | "gpt-3.5-turbo" | (string & {});
15
+ export type OpenAIProviderOptions = {
16
+ /** Default: `"https://api.openai.com/v1"`. */
17
+ readonly baseURL?: string;
18
+ /**
19
+ * Default: `process.env.OPENAI_API_KEY`. For Ollama / LM Studio /
20
+ * compat backends, pass any non-empty string the SDK will accept.
21
+ */
22
+ readonly apiKey?: string;
23
+ /** SDK-level request timeout. Independent of the engine's own budget. */
24
+ readonly timeout?: number;
25
+ };
26
+ /**
27
+ * Hosted OpenAI provider. Defaults to `process.env.OPENAI_API_KEY` and
28
+ * `https://api.openai.com/v1`. Uses the cl100k_base tokenizer for exact
29
+ * first-token-id resolution + logit_bias on the request.
30
+ *
31
+ * @example
32
+ * const cloud = openai("gpt-4o-mini");
33
+ */
34
+ export declare function openai(model: OpenAIModel, opts?: OpenAIProviderOptions): Provider;
35
+ /**
36
+ * Local Ollama provider via OpenAI-compatible endpoint.
37
+ * Defaults to `http://localhost:11434/v1` with apiKey `"ollama"`.
38
+ *
39
+ * Ollama backends run various tokenizers depending on the model; the
40
+ * adapter falls back to string-based logprob matching by default. Users
41
+ * who need exact collision detection can supply a custom Provider
42
+ * implementing the public Provider interface.
43
+ *
44
+ * @example
45
+ * const local = ollama("llama-3.1-70b");
46
+ */
47
+ export declare function ollama(model: string, opts?: OpenAIProviderOptions): Provider;
48
+ export type OpenAICompatOptions = OpenAIProviderOptions & {
49
+ /** Required for openaiCompat — caller must specify the endpoint. */
50
+ readonly baseURL: string;
51
+ /** Optional override of the tokenizer identifier (used in cache keys). */
52
+ readonly tokenizerId?: string;
53
+ /** Optional override of the provider id (used in cache keys + meta.providerUsed). */
54
+ readonly providerId?: string;
55
+ /** Override capabilities (e.g., higher maxTopLogprobs than 20 if backend supports). */
56
+ readonly capabilities?: Partial<ProviderCapabilities>;
57
+ /**
58
+ * Opt into cl100k_base tokenizer (use only when backend's tokenizer matches
59
+ * OpenAI's cl100k_base — e.g., vLLM running an OpenAI-compatible model).
60
+ * Default: false (string-based fallback).
61
+ */
62
+ readonly useCl100kTokenizer?: boolean;
63
+ };
64
+ /**
65
+ * Generic OpenAI-compatible provider. Use for vLLM, LM Studio, Together,
66
+ * Fireworks, OpenRouter, or any backend that speaks the OpenAI Chat
67
+ * Completions wire format.
68
+ *
69
+ * @example
70
+ * const fireworks = openaiCompat("accounts/fireworks/models/llama-3", {
71
+ * baseURL: "https://api.fireworks.ai/inference/v1",
72
+ * apiKey: process.env.FIREWORKS_API_KEY,
73
+ * });
74
+ */
75
+ export declare function openaiCompat(model: string, opts: OpenAICompatOptions): Provider;
76
+ //# sourceMappingURL=factory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/factory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG/C;;;GAGG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,aAAa,GACb,aAAa,GACb,IAAI,GACJ,SAAS,GACT,YAAY,GACZ,eAAe,GACf,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AASF;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,QAAQ,CAcjF;AAKD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,qBAAqB,GAAG,QAAQ,CAe5E;AAED,MAAM,MAAM,mBAAmB,GAAG,qBAAqB,GAAG;IACxD,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,qFAAqF;IACrF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACtD;;;;OAIG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,mBAAmB,GAAG,QAAQ,CAgC/E"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Internal barrel for the OpenAI Chat Completions backend.
3
+ * Public consumers import from `domovoi/providers` (src/providers/index.ts).
4
+ */
5
+ export { type OpenAICompatOptions, type OpenAIModel, type OpenAIProviderOptions, ollama, openai, openaiCompat, } from "./factory.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,WAAW,EAChB,KAAK,qBAAqB,EAC1B,MAAM,EACN,MAAM,EACN,YAAY,GACb,MAAM,cAAc,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Public Provider extension point. The built-in factories (`openai`,
3
+ * `ollama`, `openaiCompat`) are the ergonomic path; implement this interface
4
+ * directly to back domovoi with a custom LLM gateway or internal adapter.
5
+ *
6
+ * The engine merges the user signal with a per-call timeout into a single
7
+ * `signal` and passes it to `sample()`; adapters must forward it to their
8
+ * HTTP client.
9
+ */
10
+ import type { Distribution, PromptTemplate, ProviderCapabilities } from "../types.js";
11
+ export type SampleOptions = {
12
+ readonly template: PromptTemplate;
13
+ readonly temperature: number;
14
+ readonly seed?: number;
15
+ /**
16
+ * Advisory timeout; the engine has already enforced it via the merged
17
+ * `signal`. Adapters may pass this to their HTTP client as belt-and-suspenders.
18
+ */
19
+ readonly timeoutMs: number;
20
+ /**
21
+ * Merged user signal + per-call timeout. Adapters must forward this to
22
+ * their HTTP client so cancellation aborts in-flight requests.
23
+ */
24
+ readonly signal?: AbortSignal;
25
+ };
26
+ /**
27
+ * Implementations must honor `opts.temperature`, must forward `opts.signal`
28
+ * to their HTTP client, and may use `opts.seed` when the backend supports it.
29
+ *
30
+ * - `id` — unique identifier; conventionally `"factory/model"` (e.g.
31
+ * `"openai/gpt-4o-mini"`).
32
+ * - `modelId` — model identifier within the factory.
33
+ * - `tokenizerId` — identifier used for cache keying.
34
+ * - `capabilities` — discloses how the engine should route and validate.
35
+ */
36
+ export interface Provider {
37
+ readonly id: string;
38
+ readonly modelId: string;
39
+ readonly tokenizerId: string;
40
+ readonly capabilities: ProviderCapabilities;
41
+ /**
42
+ * Optional eager check fired once per provider during
43
+ * `validateClassifierConfig`, before any `sample()` call. Throw
44
+ * `ConfigError` to surface decision-space problems (e.g. first-token
45
+ * collisions) at construction rather than on first sample.
46
+ */
47
+ validate?(space: readonly string[]): void;
48
+ sample<T extends string>(input: string, space: readonly T[], opts: SampleOptions): Promise<Distribution<T>>;
49
+ }
50
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/providers/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEtF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAE5C;;;;;OAKG;IACH,QAAQ,CAAC,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAE1C,MAAM,CAAC,CAAC,SAAS,MAAM,EACrB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;CAC7B"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Public test helpers exposed via `@hourslabs/domovoi/testing` subpath.
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.
7
+ */
8
+ import type { Provider, SampleOptions } from "../providers/provider.js";
9
+ import type { Distribution, ProviderCapabilities } from "../types.js";
10
+ export type MockProviderOptions<T extends string = string> = {
11
+ /**
12
+ * Function that produces the Distribution for a given input + space + opts.
13
+ * May be sync or async; engine awaits the result.
14
+ */
15
+ readonly behavior: (input: string, space: readonly T[], opts: SampleOptions) => Distribution<T> | Promise<Distribution<T>>;
16
+ /** Override default capabilities (for testing capability-mismatch logic). */
17
+ readonly capabilities?: ProviderCapabilities;
18
+ /** Override the provider id; defaults to "mock/test". */
19
+ readonly id?: string;
20
+ /** Override the model id; defaults to "test". */
21
+ readonly modelId?: string;
22
+ /** Override the tokenizer id; defaults to "mock". */
23
+ readonly tokenizerId?: string;
24
+ };
25
+ /**
26
+ * Construct a mock Provider for testing.
27
+ *
28
+ * @example
29
+ * const c = domovoi.classifier({
30
+ * space: ["a","b","c"] as const,
31
+ * thresholds: { high: 0.7, coverageMin: 0.5 },
32
+ * providers: [
33
+ * mockProvider({
34
+ * behavior: () => ({ probs: { a: 0.8, b: 0.1, c: 0.1 }, coverage: 0.95 }),
35
+ * }),
36
+ * ],
37
+ * });
38
+ */
39
+ export declare function mockProvider<T extends string = string>(options: MockProviderOptions<T>): Provider;
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}