@noir-ai/model 1.0.0-beta.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agaaaptr
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @noir-ai/model
2
+
3
+ An optional, bounded model layer — single-shot completion only, provider-explicit, that null-degrades cleanly without a key. Backed by three dynamically imported adapters (Anthropic, OpenAI, and OpenAI-compatible via `fetch` for Ollama / LM Studio / vLLM). Agent loops are impossible by design: no `tools` or `stream` parameters exist on the request.
4
+
5
+ Part of the **[Noir](https://github.com/agaaaptr/noir#readme)** toolkit — the discipline, context, and memory layer for any agentic CLI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @noir-ai/model
11
+ ```
12
+
13
+ > Most users install the CLI instead, which configures providers via `noir init` / `noir doctor`:
14
+ >
15
+ > ```bash
16
+ > npm install -g @noir-ai/cli
17
+ > ```
18
+
19
+ ## License
20
+
21
+ MIT © agaaaptr
@@ -0,0 +1,266 @@
1
+ import * as z from 'zod/v4';
2
+
3
+ /**
4
+ * A caller-supplied output contract for structured completion.
5
+ *
6
+ * Either a zod schema (its `.parse()` is the validator) or a raw validate-and-
7
+ * coerce function `unknown → unknown` (throw on invalid). The structured path
8
+ * (slice t4) instructs the model to emit JSON, parses it, then runs this; a
9
+ * failure triggers a single repair retry, then degradation to `null`.
10
+ */
11
+ type CompleteSchema = z.ZodType | ((raw: unknown) => unknown);
12
+ /**
13
+ * The four bounded task tiers (DS-9). A tier is an OPTIONAL budget hint on a
14
+ * request — it selects a per-tier `maxTokens` default when the caller omits
15
+ * `maxTokens` (FR-10). A tier NEVER selects a provider or model: those stay
16
+ * explicit on the request (DS-6 — provider-explicit, never inferred from env).
17
+ * Tier→provider/model resolution is the caller's job (it reads resolved config);
18
+ * `complete()` only consumes the tier for the output-cap default.
19
+ */
20
+ type Tier = 'draft' | 'title' | 'summarize' | 'consolidate';
21
+ /**
22
+ * One bounded model call. Deliberately minimal — there is NO `tools`,
23
+ * `functions`, or `stream` field, so by construction this surface cannot
24
+ * express an agent or tool-exec loop (blueprint D5). `signal` lets a caller
25
+ * bound wall-clock further (abort); there is never a stream to cancel.
26
+ */
27
+ interface CompleteRequest {
28
+ /** Optional system prompt (role: system). */
29
+ system?: string;
30
+ /** The user prompt (role: user). Required — the task to complete. */
31
+ prompt: string;
32
+ /** Optional structured-output contract; presence routes via the JSON path. */
33
+ schema?: CompleteSchema;
34
+ /** Provider block name (key into {@link ModelConfig.providers}). Explicit. */
35
+ provider: string;
36
+ /** Model id for this call (e.g. `claude-haiku`, `gpt-4o-mini`). */
37
+ model: string;
38
+ /** Output cap in tokens; omit to use the adapter/tier default (FR-10). */
39
+ maxTokens?: number;
40
+ /**
41
+ * Optional task tier (DS-9). When `maxTokens` is absent AND a tier is set,
42
+ * `complete()` applies the per-tier output cap (FR-10: draft 2048 / title 64 /
43
+ * summarize 512 / consolidate 2048). Never selects provider/model (DS-6).
44
+ */
45
+ tier?: Tier;
46
+ /**
47
+ * Base URL for an OpenAI-compatible endpoint (Ollama / LM Studio / vLLM),
48
+ * e.g. `http://localhost:11434/v1`. NOT caller-set: `complete()` forwards it
49
+ * from the resolved provider block (`cfg.providers[name].baseURL`) so the
50
+ * `openai-compatible` adapter — the only consumer — receives its endpoint. It
51
+ * is optional and ignored by the `anthropic` / `openai` (hosted) adapters.
52
+ */
53
+ baseURL?: string;
54
+ /** Optional abort signal to bound the call (single shot, no streaming). */
55
+ signal?: AbortSignal;
56
+ }
57
+ /**
58
+ * Token accounting for a successful call. Carries COUNTS only — never prompts
59
+ * or keys (NFR-4: values are never logged alongside usage).
60
+ */
61
+ interface CompleteUsage {
62
+ inputTokens?: number;
63
+ outputTokens?: number;
64
+ }
65
+ /**
66
+ * The result of a bounded model call. Three first-class variants:
67
+ *
68
+ * - `{ ok: true, text, value?, usage? }` — completion succeeded. `text` is the
69
+ * raw model output; `value` is present ONLY on the structured path (when a
70
+ * `schema` was supplied and the parsed+validated JSON object is returned —
71
+ * FR-1: structured mode returns an `object`). Free-text calls leave `value`
72
+ * unset, so callers branch `if ('value' in result)` to consume structured.
73
+ * - `{ ok: false, reason }` — a call was ATTEMPTED (provider configured + key
74
+ * present + adapter ran) but failed (network, non-2xx, parse error, or the
75
+ * structured path failed JSON parse/validate twice). This is a recoverable
76
+ * error a caller may surface; it is NOT degradation.
77
+ * - `null` — DEGRADATION: no provider was configured, or a keyed provider's
78
+ * env var was missing. The caller substitutes a template/stub (blueprint D5:
79
+ * "degrades to templates when no provider"). This is the always-available
80
+ * offline path; the full Noir test suite exercises it with zero network.
81
+ */
82
+ type CompleteResult = {
83
+ ok: true;
84
+ text: string;
85
+ value?: unknown;
86
+ usage?: CompleteUsage;
87
+ } | {
88
+ ok: false;
89
+ reason: string;
90
+ } | null;
91
+ /**
92
+ * A provider adapter implements one backend (`anthropic` / `openai` /
93
+ * `openai-compatible`). Adapters register into the module registry in
94
+ * `complete.ts` and dispatch by `CompleteRequest.provider`. `key` is the
95
+ * resolved secret (read from env by `complete()`, never by the adapter), or
96
+ * `undefined` for anonymous local providers (Ollama). Adapters MUST NOT retry
97
+ * beyond the bounded single shot + ≤1 JSON-repair retry (DS-12).
98
+ */
99
+ interface ProviderAdapter {
100
+ name: string;
101
+ complete(req: CompleteRequest, key?: string): Promise<CompleteResult>;
102
+ }
103
+ /**
104
+ * One configured provider block — mirrors a `model.providers[name]` entry in
105
+ * `.noir/config.yml`. `apiKeyEnv` is the NAME of the environment variable that
106
+ * holds the secret, NEVER the value itself, so the config file stays safe to
107
+ * commit and share (blueprint D5 / DS-8); `complete()` reads the value at call
108
+ * time only.
109
+ */
110
+ interface ProviderConfig {
111
+ /** Default model id for this provider (a call's `req.model` overrides). */
112
+ model?: string;
113
+ /** Base URL for openai-compatible endpoints (Ollama / LM Studio / …). */
114
+ baseURL?: string;
115
+ /** Env-var NAME holding the API key; omit for anonymous local providers. */
116
+ apiKeyEnv?: string;
117
+ }
118
+ /**
119
+ * The model-layer config slice. Mirrors the user-facing `model:` block added to
120
+ * `NoirConfigSchema` in slice t5; every field is optional so an absent
121
+ * `model:` block resolves to `{}` — full degradation, offline, the default.
122
+ * `complete(req, cfg)` consumes this directly with no dependency on `@noir-ai/core`
123
+ * (the core→model bridge lives in t5, avoiding a cycle), and the fully-resolved
124
+ * zod output is structurally assignable to this permissive shape.
125
+ */
126
+ interface ModelConfig {
127
+ /** Fallback provider name when a call's `req.provider` is empty/unset. */
128
+ defaultProvider?: string;
129
+ /** Configured provider blocks, keyed by provider name. */
130
+ providers?: Record<string, ProviderConfig>;
131
+ }
132
+
133
+ /** Register a provider adapter under `name` (e.g. `anthropic`, `openai`). */
134
+ declare function registerProviderAdapter(name: string, adapter: ProviderAdapter): void;
135
+ /** Look up a registered adapter by provider name (or `undefined`). */
136
+ declare function getProviderAdapter(name: string): ProviderAdapter | undefined;
137
+ /** Clear the registry. Exported for test isolation between cases. */
138
+ declare function clearProviderAdapters(): void;
139
+ declare const TIER_MAX_TOKENS: Readonly<Record<Tier, number>>;
140
+ /**
141
+ * Execute one bounded model call.
142
+ *
143
+ * Resolution + degradation order (each `null` is the first-class offline path):
144
+ * 1. provider name — `req.provider || cfg.defaultProvider`. Empty ⇒ `null`.
145
+ * 2. provider block — `cfg.providers[name]`. Absent ⇒ `null` (NOT configured,
146
+ * so NO consent to spend; env presence is never consulted — DS-6).
147
+ * 3. key — `process.env[apiKeyEnv]`; `undefined` (anonymous) if no `apiKeyEnv`.
148
+ * A keyed provider whose env var is unset ⇒ `null` (the miss is observable
149
+ * via the `null` return; a structured usage/miss sink lands with t6).
150
+ * 4. adapter — the provider NAME is mapped to an adapter (direct match, else a
151
+ * `baseURL` block routes to `openai-compatible`); unresolvable ⇒
152
+ * `{ ok: false, reason }`. The per-tier `maxTokens` default (FR-10) and the
153
+ * provider-block `baseURL` are folded onto the dispatched request here.
154
+ * 5. dispatch — one call (or two via the structured repair retry when `schema`
155
+ * is set — DS-4); a throw becomes `{ ok: false, reason }` (never escapes).
156
+ *
157
+ * `null` (steps 1–3) is degradation → caller substitutes a template.
158
+ * `{ ok: false }` (steps 4–5) is an attempted-call failure → caller may surface it.
159
+ */
160
+ declare function complete(req: CompleteRequest, cfg?: ModelConfig): Promise<CompleteResult>;
161
+
162
+ /**
163
+ * User-facing model config shape — mirrors `NoirConfig['model']` (the zod block
164
+ * @noir-ai/core ships, slice S8). Declared LOCALLY with every field optional so
165
+ * this module type-checks WITHOUT a forward dependency on a core type (core
166
+ * never imports model — no cycle; @noir-ai/model is not even in this package's
167
+ * node_modules), AND so a config with no `model:` block (or a partial one) maps
168
+ * cleanly to a fully-degraded runtime config. The fully-resolved zod output is
169
+ * structurally assignable to this permissive shape, so the mapper accepts
170
+ * `NoirConfig['model']` directly.
171
+ */
172
+ interface ModelUserConfig {
173
+ /** Fallback provider key (into `providers`) when a tier resolves none. */
174
+ defaultProvider?: string;
175
+ /** Per-tier provider-key overrides (value = key into `providers{}`). */
176
+ tiers?: {
177
+ draft?: string;
178
+ title?: string;
179
+ summarize?: string;
180
+ consolidate?: string;
181
+ };
182
+ /** Configured provider blocks, keyed by provider name. */
183
+ providers?: Record<string, ModelProviderEntry>;
184
+ }
185
+ /**
186
+ * One user-facing provider block — mirrors a `model.providers[name]` entry in
187
+ * `.noir/config.yml`. `model` is optional HERE (the mirror is permissive) even
188
+ * though the zod schema requires it, so hand-built configs in tests type-check;
189
+ * `resolveModelConfig` passes `model` straight through when present.
190
+ */
191
+ interface ModelProviderEntry {
192
+ /** Default model id for this provider (a call's `req.model` overrides). */
193
+ model?: string;
194
+ /** Base URL for openai-compatible endpoints (Ollama / LM Studio / …). */
195
+ baseURL?: string;
196
+ /** Env-var NAME holding the API key; omit for anonymous local providers. */
197
+ apiKeyEnv?: string;
198
+ }
199
+ /**
200
+ * A provider block with its key RESOLVED from the environment. Superset of the
201
+ * runtime `ProviderConfig` (`@noir-ai/model`'s `complete()` consumes), so a
202
+ * {@link ResolvedModelConfig} drops cleanly into `complete(req, cfg)` — the extra
203
+ * `apiKey` / `hasKey` fields are ignored by `complete()` (which re-reads env at
204
+ * call time, idempotently) and exist for `noir doctor` + direct consumers.
205
+ */
206
+ interface ResolvedProviderConfig {
207
+ /** Provider model id (passthrough from config). */
208
+ model?: string;
209
+ /** Base URL passthrough (openai-compatible endpoints). */
210
+ baseURL?: string;
211
+ /** Env-var NAME passthrough — doctor prints this, NEVER the value (DS-8). */
212
+ apiKeyEnv?: string;
213
+ /** VALUE resolved from `process.env[apiKeyEnv]`; `undefined` if anonymous or unset. */
214
+ apiKey?: string;
215
+ /**
216
+ * Readiness signal for `noir doctor` (OQ-5): for a keyed provider, whether the
217
+ * env var named by `apiKeyEnv` is set; for an ANONYMOUS provider (no
218
+ * `apiKeyEnv`), `true` — no key is required, so nothing is missing. Carries a
219
+ * boolean ONLY, never the key value (NFR-4).
220
+ */
221
+ hasKey: boolean;
222
+ }
223
+ /**
224
+ * Per-tier provider-key overrides, normalized to a full object (absent `tiers`
225
+ * ⇒ `{}`) so consumers index without a separate undefined check.
226
+ */
227
+ interface ResolvedTiers {
228
+ draft?: string;
229
+ title?: string;
230
+ summarize?: string;
231
+ consolidate?: string;
232
+ }
233
+ /**
234
+ * The resolved model-layer config — the runtime shape `complete()` consumes and
235
+ * `noir doctor` inspects. `providers` is always present (possibly `{}`); each
236
+ * entry carries its resolved key. Assignable to `ModelConfig` (`complete()`'s
237
+ * param type), so `complete(req, resolveModelConfig(cfg.model))` type-checks.
238
+ */
239
+ interface ResolvedModelConfig {
240
+ /** Fallback provider key when a call resolves no explicit provider. */
241
+ defaultProvider?: string;
242
+ /** Per-tier provider-key overrides (absent ⇒ empty object). */
243
+ tiers: ResolvedTiers;
244
+ /** Provider blocks with resolved keys (absent ⇒ empty map). */
245
+ providers: Record<string, ResolvedProviderConfig>;
246
+ }
247
+ /**
248
+ * Resolve a user-facing {@link ModelUserConfig} into the runtime
249
+ * {@link ResolvedModelConfig}.
250
+ *
251
+ * - `undefined` / missing block ⇒ `{ tiers: {}, providers: {} }` (full
252
+ * degradation — `complete()` will then return `null` for every call, the
253
+ * always-available offline path; blueprint D5).
254
+ * - Each provider's key is materialized from `process.env[apiKeyEnv]` into
255
+ * `apiKey`; a keyed provider whose env var is unset gets `apiKey: undefined`
256
+ * + `hasKey: false` (doctor surfaces this; `complete()` returns `null`).
257
+ * - Anonymous providers (no `apiKeyEnv`) keep `apiKey: undefined` but report
258
+ * `hasKey: true` (ready — no key needed, e.g. local Ollama).
259
+ *
260
+ * This mapper NEVER infers a provider from env-var presence (DS-6) and NEVER
261
+ * mutates `raw` or `process.env` — it only READS env to resolve keys. It never
262
+ * throws; an unusable config degrades to empty, not an exception.
263
+ */
264
+ declare function resolveModelConfig(raw?: ModelUserConfig): ResolvedModelConfig;
265
+
266
+ export { type CompleteRequest, type CompleteResult, type CompleteSchema, type CompleteUsage, type ModelConfig, type ModelProviderEntry, type ModelUserConfig, type ProviderAdapter, type ProviderConfig, type ResolvedModelConfig, type ResolvedProviderConfig, type ResolvedTiers, TIER_MAX_TOKENS, type Tier, clearProviderAdapters, complete, getProviderAdapter, registerProviderAdapter, resolveModelConfig };
package/dist/index.js ADDED
@@ -0,0 +1,391 @@
1
+ // src/structured.ts
2
+ var JSON_INSTRUCTION = "Respond with ONLY a single valid JSON value that matches the requested schema. Do not include markdown, code fences, commentary, or any surrounding prose \u2014 output the JSON and nothing else.";
3
+ function schemaDescription(schema) {
4
+ if (!schema) return "";
5
+ const desc = schema.description;
6
+ return typeof desc === "string" && desc.trim().length > 0 ? desc.trim() : "";
7
+ }
8
+ function withJsonInstruction(req) {
9
+ const hint = schemaDescription(req.schema);
10
+ const addendum = hint ? `${JSON_INSTRUCTION}
11
+
12
+ JSON must satisfy: ${hint}` : JSON_INSTRUCTION;
13
+ const system = req.system ? `${req.system}
14
+
15
+ ${addendum}` : addendum;
16
+ return { ...req, system };
17
+ }
18
+ function truncate(s, max) {
19
+ return s.length > max ? `${s.slice(0, max)}\u2026` : s;
20
+ }
21
+ function withCorrectiveInstruction(req, error, badOutput) {
22
+ const base = withJsonInstruction(req);
23
+ const correction = `Your previous response failed validation and was NOT valid JSON matching the schema.
24
+
25
+ Error: ${error}
26
+
27
+ Previous output (do NOT repeat it):
28
+ ${truncate(badOutput, 500)}
29
+
30
+ Return ONLY valid JSON matching the schema \u2014 no markdown, no prose.`;
31
+ const system = base.system ? `${base.system}
32
+
33
+ ${correction}` : correction;
34
+ return { ...base, system };
35
+ }
36
+ function tryJSON(s) {
37
+ try {
38
+ return { ok: true, value: JSON.parse(s) };
39
+ } catch (err) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ return { ok: false, error: msg };
42
+ }
43
+ }
44
+ function extractJSON(text) {
45
+ const trimmed = text.trim();
46
+ const direct = tryJSON(trimmed);
47
+ if (direct.ok) return direct;
48
+ const fenced = trimmed.match(/```(?:json|JSON)?\s*([\s\S]*?)```/)?.[1];
49
+ if (fenced) {
50
+ const inner = tryJSON(fenced.trim());
51
+ if (inner.ok) return inner;
52
+ }
53
+ const candidates = [];
54
+ const objStart = trimmed.indexOf("{");
55
+ const objEnd = trimmed.lastIndexOf("}");
56
+ const arrStart = trimmed.indexOf("[");
57
+ const arrEnd = trimmed.lastIndexOf("]");
58
+ if (objStart !== -1 && objEnd > objStart) candidates.push(trimmed.slice(objStart, objEnd + 1));
59
+ if (arrStart !== -1 && arrEnd > arrStart) candidates.push(trimmed.slice(arrStart, arrEnd + 1));
60
+ candidates.sort((a, b) => a.length - b.length);
61
+ for (const cand of candidates) {
62
+ const parsed = tryJSON(cand);
63
+ if (parsed.ok) return parsed;
64
+ }
65
+ const excerpt = truncate(trimmed.replace(/\s+/g, " "), 120);
66
+ return { ok: false, error: `response was not valid JSON: ${JSON.stringify(excerpt)}` };
67
+ }
68
+ function validateSchema(schema, raw) {
69
+ if (typeof schema === "function") {
70
+ return schema(raw);
71
+ }
72
+ return schema.parse(raw);
73
+ }
74
+ function parseAndValidate(text, schema) {
75
+ const extracted = extractJSON(text);
76
+ if (!extracted.ok) return extracted;
77
+ try {
78
+ return { ok: true, value: validateSchema(schema, extracted.value) };
79
+ } catch (err) {
80
+ const msg = err instanceof Error ? err.message : String(err);
81
+ return { ok: false, error: `schema validation failed: ${msg}` };
82
+ }
83
+ }
84
+ async function runStructured(adapter, req, key) {
85
+ const schema = req.schema;
86
+ if (!schema) {
87
+ return adapter.complete(req, key);
88
+ }
89
+ const first = await adapter.complete(withJsonInstruction(req), key);
90
+ if (!first?.ok) return first;
91
+ const parsed1 = parseAndValidate(first.text, schema);
92
+ if (parsed1.ok) {
93
+ return {
94
+ ok: true,
95
+ text: first.text,
96
+ value: parsed1.value,
97
+ ...first.usage ? { usage: first.usage } : {}
98
+ };
99
+ }
100
+ const second = await adapter.complete(
101
+ withCorrectiveInstruction(req, parsed1.error, first.text),
102
+ key
103
+ );
104
+ if (!second?.ok) {
105
+ return { ok: false, reason: `schema-validation-failed: ${parsed1.error}` };
106
+ }
107
+ const parsed2 = parseAndValidate(second.text, schema);
108
+ if (parsed2.ok) {
109
+ return {
110
+ ok: true,
111
+ text: second.text,
112
+ value: parsed2.value,
113
+ ...second.usage ? { usage: second.usage } : {}
114
+ };
115
+ }
116
+ return { ok: false, reason: `schema-validation-failed: ${parsed2.error}` };
117
+ }
118
+
119
+ // src/complete.ts
120
+ var adapters = /* @__PURE__ */ new Map();
121
+ function registerProviderAdapter(name, adapter) {
122
+ adapters.set(name, adapter);
123
+ }
124
+ function getProviderAdapter(name) {
125
+ return adapters.get(name);
126
+ }
127
+ function clearProviderAdapters() {
128
+ adapters.clear();
129
+ }
130
+ function resolveKey(providerCfg) {
131
+ if (!providerCfg.apiKeyEnv) return void 0;
132
+ return process.env[providerCfg.apiKeyEnv];
133
+ }
134
+ var TIER_MAX_TOKENS = {
135
+ draft: 2048,
136
+ title: 64,
137
+ summarize: 512,
138
+ consolidate: 2048
139
+ };
140
+ function resolveAdapterName(providerName, providerCfg) {
141
+ if (getProviderAdapter(providerName)) return providerName;
142
+ if (providerCfg.baseURL) return "openai-compatible";
143
+ return void 0;
144
+ }
145
+ async function complete(req, cfg = {}) {
146
+ const providerName = req.provider || cfg.defaultProvider;
147
+ if (!providerName) return null;
148
+ const providerCfg = cfg.providers?.[providerName];
149
+ if (!providerCfg) return null;
150
+ const key = resolveKey(providerCfg);
151
+ if (providerCfg.apiKeyEnv && !key) return null;
152
+ const adapterName = resolveAdapterName(providerName, providerCfg);
153
+ const adapter = adapterName ? getProviderAdapter(adapterName) : void 0;
154
+ if (!adapter) {
155
+ return {
156
+ ok: false,
157
+ reason: providerCfg.baseURL ? `no adapter for provider "${providerName}" (baseURL set but the "openai-compatible" adapter is not registered)` : `no adapter registered for provider "${providerName}"`
158
+ };
159
+ }
160
+ const tierMax = req.tier ? TIER_MAX_TOKENS[req.tier] : void 0;
161
+ const maxTokens = req.maxTokens ?? tierMax;
162
+ const dispatchReq = {
163
+ ...req,
164
+ ...providerCfg.baseURL ? { baseURL: providerCfg.baseURL } : {},
165
+ ...maxTokens !== void 0 ? { maxTokens } : {}
166
+ };
167
+ try {
168
+ return dispatchReq.schema ? await runStructured(adapter, dispatchReq, key) : await adapter.complete(dispatchReq, key);
169
+ } catch (err) {
170
+ const reason = err instanceof Error ? err.message : String(err);
171
+ return { ok: false, reason: `provider "${providerName}" failed: ${reason}` };
172
+ }
173
+ }
174
+
175
+ // src/config.ts
176
+ function resolveModelConfig(raw) {
177
+ const providers = {};
178
+ const rawProviders = raw?.providers;
179
+ if (rawProviders) {
180
+ for (const [name, entry] of Object.entries(rawProviders)) {
181
+ const apiKeyEnv = entry.apiKeyEnv;
182
+ const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : void 0;
183
+ const hasKey = apiKeyEnv ? apiKey !== void 0 : true;
184
+ const resolved = { hasKey };
185
+ if (entry.model !== void 0) resolved.model = entry.model;
186
+ if (entry.baseURL !== void 0) resolved.baseURL = entry.baseURL;
187
+ if (apiKeyEnv !== void 0) resolved.apiKeyEnv = apiKeyEnv;
188
+ if (apiKey !== void 0) resolved.apiKey = apiKey;
189
+ providers[name] = resolved;
190
+ }
191
+ }
192
+ const tiers = {};
193
+ const rawTiers = raw?.tiers;
194
+ if (rawTiers) {
195
+ if (rawTiers.draft !== void 0) tiers.draft = rawTiers.draft;
196
+ if (rawTiers.title !== void 0) tiers.title = rawTiers.title;
197
+ if (rawTiers.summarize !== void 0) tiers.summarize = rawTiers.summarize;
198
+ if (rawTiers.consolidate !== void 0) tiers.consolidate = rawTiers.consolidate;
199
+ }
200
+ const result = { tiers, providers };
201
+ const defaultProvider = raw?.defaultProvider;
202
+ if (defaultProvider !== void 0) result.defaultProvider = defaultProvider;
203
+ return result;
204
+ }
205
+
206
+ // src/providers/anthropic.ts
207
+ var DEFAULT_MAX_TOKENS = 2048;
208
+ var anthropicAdapter = {
209
+ name: "anthropic",
210
+ complete: async (req, key) => {
211
+ if (!key) {
212
+ return { ok: false, reason: "anthropic: missing API key (provider block has no apiKeyEnv)" };
213
+ }
214
+ try {
215
+ const sdk = await import("@anthropic-ai/sdk");
216
+ const client = new sdk.default({
217
+ apiKey: key,
218
+ // the env VALUE resolved by complete() (DS-8).
219
+ maxRetries: 0
220
+ // DS-12: never silently retry (bounded cost).
221
+ });
222
+ const res = await client.messages.create(
223
+ {
224
+ model: req.model,
225
+ max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,
226
+ ...req.system !== void 0 ? { system: req.system } : {},
227
+ messages: [{ role: "user", content: req.prompt }]
228
+ },
229
+ {
230
+ maxRetries: 0,
231
+ // belt-and-suspenders: per-request as well as constructor.
232
+ // Honor an abort signal so a caller can bound wall-clock further.
233
+ ...req.signal ? { signal: req.signal } : {}
234
+ }
235
+ );
236
+ const block = res.content[0];
237
+ const text = block?.text;
238
+ if (typeof text !== "string") {
239
+ return {
240
+ ok: false,
241
+ reason: `anthropic: response had no text block (stop_reason: ${res.stop_reason ?? "unknown"})`
242
+ };
243
+ }
244
+ const usage = {
245
+ inputTokens: res.usage.input_tokens,
246
+ outputTokens: res.usage.output_tokens
247
+ };
248
+ return { ok: true, text, usage };
249
+ } catch (err) {
250
+ const reason = err instanceof Error ? err.message : String(err);
251
+ return { ok: false, reason: `anthropic: ${reason}` };
252
+ }
253
+ }
254
+ };
255
+ registerProviderAdapter("anthropic", anthropicAdapter);
256
+
257
+ // src/providers/openai-compatible.ts
258
+ function buildMessages(req) {
259
+ const messages = [];
260
+ if (req.system) messages.push({ role: "system", content: req.system });
261
+ messages.push({ role: "user", content: req.prompt });
262
+ return messages;
263
+ }
264
+ function joinEndpoint(baseURL) {
265
+ return `${baseURL.replace(/\/+$/, "")}/chat/completions`;
266
+ }
267
+ var openaiCompatibleAdapter = {
268
+ name: "openai-compatible",
269
+ complete: async (req, key) => {
270
+ const baseURL = req.baseURL;
271
+ if (!baseURL) {
272
+ return { ok: false, reason: "openai-compatible: provider block has no baseURL" };
273
+ }
274
+ const endpoint = joinEndpoint(baseURL);
275
+ const body = {
276
+ model: req.model,
277
+ messages: buildMessages(req),
278
+ ...req.maxTokens !== void 0 ? { max_tokens: req.maxTokens } : {}
279
+ };
280
+ const headers = { "content-type": "application/json" };
281
+ if (key) headers.authorization = `Bearer ${key}`;
282
+ try {
283
+ const res = await fetch(endpoint, {
284
+ method: "POST",
285
+ headers,
286
+ body: JSON.stringify(body),
287
+ // Pass-through abort so a caller can bound wall-clock (single shot; no
288
+ // stream to cancel). `undefined` is a no-op for fetch.
289
+ signal: req.signal
290
+ });
291
+ if (!res.ok) {
292
+ return { ok: false, reason: `openai-compatible: HTTP ${res.status}` };
293
+ }
294
+ const json = await res.json();
295
+ const text = json.choices?.[0]?.message?.content;
296
+ if (typeof text !== "string") {
297
+ return { ok: false, reason: "openai-compatible: completion had no message content" };
298
+ }
299
+ const usage = json.usage;
300
+ return {
301
+ ok: true,
302
+ text,
303
+ ...usage ? {
304
+ usage: {
305
+ ...usage.prompt_tokens !== void 0 ? { inputTokens: usage.prompt_tokens } : {},
306
+ ...usage.completion_tokens !== void 0 ? { outputTokens: usage.completion_tokens } : {}
307
+ }
308
+ } : {}
309
+ };
310
+ } catch (err) {
311
+ if (err instanceof Error && err.name === "AbortError") {
312
+ return { ok: false, reason: "openai-compatible: request aborted" };
313
+ }
314
+ const reason = err instanceof Error ? err.message : String(err);
315
+ return { ok: false, reason: `openai-compatible: ${reason}` };
316
+ }
317
+ }
318
+ };
319
+ registerProviderAdapter("openai-compatible", openaiCompatibleAdapter);
320
+
321
+ // src/providers/openai.ts
322
+ function buildMessages2(req) {
323
+ const messages = [];
324
+ if (req.system) messages.push({ role: "system", content: req.system });
325
+ messages.push({ role: "user", content: req.prompt });
326
+ return messages;
327
+ }
328
+ var openaiAdapter = {
329
+ name: "openai",
330
+ complete: async (req, key) => {
331
+ if (!key) {
332
+ return { ok: false, reason: "openai: missing API key (set apiKeyEnv on the provider block)" };
333
+ }
334
+ try {
335
+ const sdk = await import("openai");
336
+ const client = new sdk.default({
337
+ apiKey: key,
338
+ // the VALUE complete() resolved from process.env[apiKeyEnv]
339
+ // Honor a forwarded baseURL if present (lets this adapter target a
340
+ // custom OpenAI-shaped endpoint; the common local case uses the
341
+ // dedicated `openai-compatible` adapter instead).
342
+ ...req.baseURL ? { baseURL: req.baseURL } : {},
343
+ maxRetries: 0
344
+ // DS-12: never silently retry (bounded cost).
345
+ });
346
+ const res = await client.chat.completions.create(
347
+ {
348
+ model: req.model,
349
+ messages: buildMessages2(req),
350
+ // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.
351
+ ...req.maxTokens !== void 0 ? { max_tokens: req.maxTokens } : {}
352
+ },
353
+ {
354
+ maxRetries: 0,
355
+ // belt-and-suspenders: per-request as well as constructor.
356
+ // Forward the caller's wall-clock bound (NFR-3) — same conditional
357
+ // spread as the other optional fields (only when a signal is present).
358
+ ...req.signal ? { signal: req.signal } : {}
359
+ }
360
+ );
361
+ const text = res.choices?.[0]?.message?.content;
362
+ if (typeof text !== "string") {
363
+ return { ok: false, reason: "openai: completion had no message content" };
364
+ }
365
+ const usage = res.usage;
366
+ return {
367
+ ok: true,
368
+ text,
369
+ ...usage ? {
370
+ usage: {
371
+ ...usage.prompt_tokens !== void 0 ? { inputTokens: usage.prompt_tokens } : {},
372
+ ...usage.completion_tokens !== void 0 ? { outputTokens: usage.completion_tokens } : {}
373
+ }
374
+ } : {}
375
+ };
376
+ } catch (err) {
377
+ const reason = err instanceof Error ? err.message : String(err);
378
+ return { ok: false, reason: `openai: ${reason}` };
379
+ }
380
+ }
381
+ };
382
+ registerProviderAdapter("openai", openaiAdapter);
383
+ export {
384
+ TIER_MAX_TOKENS,
385
+ clearProviderAdapters,
386
+ complete,
387
+ getProviderAdapter,
388
+ registerProviderAdapter,
389
+ resolveModelConfig
390
+ };
391
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/structured.ts","../src/complete.ts","../src/config.ts","../src/providers/anthropic.ts","../src/providers/openai-compatible.ts","../src/providers/openai.ts"],"sourcesContent":["// Structured output — prompt-based JSON + validate + at most ONE repair retry\n// (slice S8 / t4, blueprint D5 / DS-4).\n//\n// This module is the ONLY retry site in the model layer (DS-12: SDK retries are\n// 0; bounded wall-clock + cost). It does NOT call a provider directly — it is\n// given a resolved {@link ProviderAdapter} by `complete()` and orchestrates the\n// JSON round-trip on top of the adapter's single-shot `complete()`. Because the\n// adapter surface has no `tools` / `stream` (FR-8), this path cannot mutate into\n// an agent loop either: it makes at most TWO adapter calls (initial + one\n// repair), parses + validates each, and returns.\n//\n// Strategy (DS-4 / FR-3, v1 — provider-native strict modes deferred):\n// 1. Inject a \"respond with ONLY JSON matching the schema\" system addendum.\n// 2. Call the adapter once (single shot).\n// 3. Extract JSON from the text (tolerant: direct, markdown-fence, span).\n// 4. Validate against `req.schema` — a ZodType (`.parse`) or a function.\n// 5. On parse/validate failure, retry ONCE with the error fed back.\n// 6. Still bad ⇒ `{ ok: false, reason: \"schema-validation-failed: …\" }`.\n//\n// NO zod is imported at runtime here (NFR-2 / the types.ts contract): validation\n// calls `.parse` ON the caller-supplied schema object, so the built library has\n// no value-level zod dependency. A best-effort `.description` (if the ZodType\n// carries one) is the only schema introspection — we never serialize the shape,\n// so the caller's prompt is expected to describe the desired JSON; `schema` is\n// the validator, not the spec.\n\nimport type { CompleteRequest, CompleteResult, CompleteSchema, ProviderAdapter } from './types.js';\n\n// --- Prompt construction ----------------------------------------------------\n\n// The JSON contract appended to the system prompt. Strong + specific so the\n// model emits parseable JSON without relying on a provider-native JSON mode\n// (deferred per DS-4). \"single valid JSON value\" (not \"object\") so an array or\n// scalar schema is also honored.\nconst JSON_INSTRUCTION =\n 'Respond with ONLY a single valid JSON value that matches the requested schema. ' +\n 'Do not include markdown, code fences, commentary, or any surrounding prose — ' +\n 'output the JSON and nothing else.';\n\n/**\n * Best-effort schema hint for the prompt. A ZodType MAY carry a `.description`\n * (set via `z.desc(...)` / `.meta({ description })`); a validator function\n * carries none. We never import zod at runtime, so we do NOT serialize the full\n * shape — the caller's prompt describes the desired JSON; `schema` validates.\n * Returns the trimmed description, or `''` when none is available.\n */\nfunction schemaDescription(schema: CompleteSchema | undefined): string {\n if (!schema) return '';\n const desc = (schema as { description?: unknown }).description;\n return typeof desc === 'string' && desc.trim().length > 0 ? desc.trim() : '';\n}\n\n/** Append the JSON instruction (and any schema description) to the system prompt. */\nfunction withJsonInstruction(req: CompleteRequest): CompleteRequest {\n const hint = schemaDescription(req.schema);\n const addendum = hint ? `${JSON_INSTRUCTION}\\n\\nJSON must satisfy: ${hint}` : JSON_INSTRUCTION;\n const system = req.system ? `${req.system}\\n\\n${addendum}` : addendum;\n return { ...req, system };\n}\n\n/** Bound a snippet so a runaway model output cannot blow up the corrective prompt. */\nfunction truncate(s: string, max: number): string {\n return s.length > max ? `${s.slice(0, max)}…` : s;\n}\n\n/**\n * Build the ONE repair retry: re-state the JSON contract, then surface the\n * precise parse/validate error and the offending output so the model can fix it.\n * The original task (`prompt`) is preserved verbatim — only the system gains the\n * correction, so the retry is still a bounded single shot at the SAME task.\n */\nfunction withCorrectiveInstruction(\n req: CompleteRequest,\n error: string,\n badOutput: string,\n): CompleteRequest {\n const base = withJsonInstruction(req);\n const correction =\n `Your previous response failed validation and was NOT valid JSON matching the schema.\\n\\n` +\n `Error: ${error}\\n\\n` +\n `Previous output (do NOT repeat it):\\n${truncate(badOutput, 500)}\\n\\n` +\n `Return ONLY valid JSON matching the schema — no markdown, no prose.`;\n const system = base.system ? `${base.system}\\n\\n${correction}` : correction;\n return { ...base, system };\n}\n\n// --- JSON extraction (tolerant) ---------------------------------------------\n\ntype ParseOutcome = { ok: true; value: unknown } | { ok: false; error: string };\n\n/** `JSON.parse` with a typed outcome (never throws). */\nfunction tryJSON(s: string): ParseOutcome {\n try {\n return { ok: true, value: JSON.parse(s) };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { ok: false, error: msg };\n }\n}\n\n/**\n * Extract a JSON value from a model's free-form text. Tries, in order:\n * 1. the trimmed text directly (the model obeyed),\n * 2. the inside of a ```json …``` markdown fence (a common deviation),\n * 3. the outermost `{ … }` or `[ … ]` span (prose around the JSON).\n *\n * Returns the first parse success, else the parse error from the last attempt.\n * This tolerance keeps the ≤1 retry budget for genuine validation failures\n * rather than spending it on a stray code fence.\n */\nfunction extractJSON(text: string): ParseOutcome {\n const trimmed = text.trim();\n\n // 1. Direct.\n const direct = tryJSON(trimmed);\n if (direct.ok) return direct;\n\n // 2. Markdown code fence (optional `json`/`JSON` lang tag). Capture the group\n // into a const so the truthy guard narrows it (a re-indexed `fence[1]` would\n // read as `string | undefined` under noUncheckedIndexedAccess).\n const fenced = trimmed.match(/```(?:json|JSON)?\\s*([\\s\\S]*?)```/)?.[1];\n if (fenced) {\n const inner = tryJSON(fenced.trim());\n if (inner.ok) return inner;\n }\n\n // 3. Outermost object/array span — collect valid candidates, shortest first\n // (a tighter valid span beats a looser one that happens to balance).\n const candidates: string[] = [];\n const objStart = trimmed.indexOf('{');\n const objEnd = trimmed.lastIndexOf('}');\n const arrStart = trimmed.indexOf('[');\n const arrEnd = trimmed.lastIndexOf(']');\n if (objStart !== -1 && objEnd > objStart) candidates.push(trimmed.slice(objStart, objEnd + 1));\n if (arrStart !== -1 && arrEnd > arrStart) candidates.push(trimmed.slice(arrStart, arrEnd + 1));\n candidates.sort((a, b) => a.length - b.length);\n for (const cand of candidates) {\n const parsed = tryJSON(cand);\n if (parsed.ok) return parsed;\n }\n\n // No extraction worked — surface a short, safe excerpt (never the whole prompt\n // body, which could be large; NFR-4 keeps usage/logs free of raw content).\n const excerpt = truncate(trimmed.replace(/\\s+/g, ' '), 120);\n return { ok: false, error: `response was not valid JSON: ${JSON.stringify(excerpt)}` };\n}\n\n// --- Schema validation ------------------------------------------------------\n\n/**\n * Validate `raw` against a caller-supplied schema WITHOUT importing zod. A\n * ZodType exposes `.parse(input)` (throws on invalid); a function schema IS the\n * validator (throw on invalid). A throw ⇒ invalid; a returned value ⇒ the\n * coerced/validated value to hand back to the caller.\n *\n * `typeof === 'function'` cleanly splits the union: a `ZodType` instance is an\n * object (it has `.parse`, no call signature), so the function branch is the\n * validator-function member and the fall-through is the ZodType member. The\n * ZodType `.parse` is invoked via method syntax (`schema.parse(raw)`) so its\n * internal `this` binding is preserved — extracting `const p = schema.parse;\n * p(raw)` would lose `this` and break zod's internal state reads.\n */\nfunction validateSchema(schema: CompleteSchema, raw: unknown): unknown {\n if (typeof schema === 'function') {\n return schema(raw);\n }\n return schema.parse(raw);\n}\n\n/** Extract + validate in one step; never throws. */\nfunction parseAndValidate(text: string, schema: CompleteSchema): ParseOutcome {\n const extracted = extractJSON(text);\n if (!extracted.ok) return extracted;\n try {\n return { ok: true, value: validateSchema(schema, extracted.value) };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { ok: false, error: `schema validation failed: ${msg}` };\n }\n}\n\n// --- The structured round-trip ----------------------------------------------\n\n/**\n * Run the structured (prompt-JSON) flow against a resolved adapter.\n *\n * Makes at most TWO adapter calls: an initial attempt, plus ONE repair retry on\n * parse/validate failure (DS-4). An adapter/transport failure (`{ ok: false }`\n * or `null`) is propagated immediately — the retry budget is for JSON repair,\n * NOT for transient network errors (those stay bounded at one call, DS-12).\n *\n * On success the validated object is returned as `value` (FR-1), with `text`\n * kept as the raw model output of the successful call and `usage` from that\n * call. `req.schema` is required; `complete()` only routes here when it is set.\n */\nexport async function runStructured(\n adapter: ProviderAdapter,\n req: CompleteRequest,\n key: string | undefined,\n): Promise<CompleteResult> {\n // `complete()` only calls here when `schema` is present; guard once so a\n // direct caller cannot trip on a missing schema mid-flow.\n const schema = req.schema;\n if (!schema) {\n return adapter.complete(req, key);\n }\n\n // --- Attempt 1: the initial JSON request. ---\n const first = await adapter.complete(withJsonInstruction(req), key);\n // Adapter/transport failure (incl. null degradation) — propagate, do NOT spend\n // the retry budget on a non-JSON failure (DS-12: transport stays single-shot).\n if (!first?.ok) return first;\n\n const parsed1 = parseAndValidate(first.text, schema);\n if (parsed1.ok) {\n return {\n ok: true,\n text: first.text,\n value: parsed1.value,\n ...(first.usage ? { usage: first.usage } : {}),\n };\n }\n\n // --- Attempt 2: the single repair retry, error fed back. ---\n const second = await adapter.complete(\n withCorrectiveInstruction(req, parsed1.error, first.text),\n key,\n );\n // If the retry's transport itself failed, the overall result is still a\n // schema-validation failure (the first output was invalid JSON and we could\n // not repair it) — surface that, not the transport error, so the caller sees\n // the real cause. The first attempt's parse error is the actionable detail.\n if (!second?.ok) {\n return { ok: false, reason: `schema-validation-failed: ${parsed1.error}` };\n }\n\n const parsed2 = parseAndValidate(second.text, schema);\n if (parsed2.ok) {\n return {\n ok: true,\n text: second.text,\n value: parsed2.value,\n ...(second.usage ? { usage: second.usage } : {}),\n };\n }\n\n // Two strikes: the model could not produce schema-valid JSON. Degrade to a\n // structured failure (NOT null — a call was attempted and billed; the caller\n // may surface this). Reason is fixed-suffix `schema-validation-failed` so\n // callers can branch on it, followed by the last parse/validate error.\n return { ok: false, reason: `schema-validation-failed: ${parsed2.error}` };\n}\n","// complete() — the single bounded model entry point (slice S8, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - Provider-EXPLICIT, never silent paid calls (DS-6): the provider is resolved\n// ONLY from the explicit `req.provider` (or `cfg.defaultProvider`). Env-var\n// presence is NEVER read to infer a provider — `ANTHROPIC_API_KEY` being set\n// for another tool does NOT make Anthropic active in Noir. No explicit,\n// configured provider ⇒ `null`.\n// - null-degradation FIRST-CLASS (DS-5): the unconfigured / missing-key paths\n// return `null` (NEVER throw), so callers branch on presence and the full\n// Noir test suite runs offline + free. `null` is the always-available default.\n// - SINGLE-SHOT (D5): there is no loop here — `complete()` dispatches one\n// adapter call and returns. The `CompleteRequest` type forbids `tools` /\n// `stream`, so even an adapter cannot turn this into an agent loop.\n//\n// The adapter registry is the seam slices t2/t3 fill: each adapter module\n// calls {@link registerProviderAdapter} at import time. Until a configured\n// provider has a registered adapter, `complete()` returns `{ ok: false, reason }`\n// (a real misconfiguration, intentionally distinct from the first-class `null`\n// degradation so callers can tell \"offline\" from \"wired wrong\").\n\nimport { runStructured } from './structured.js';\nimport type {\n CompleteRequest,\n CompleteResult,\n ModelConfig,\n ProviderAdapter,\n ProviderConfig,\n Tier,\n} from './types.js';\n\n// --- Adapter registry (the t2/t3 seam) --------------------------------------\n//\n// A plain module-level map. Adapters self-register on import; `complete()`\n// looks one up by provider name. Kept here (not a separate file) because t1's\n// file list is {index,types,complete} and the registry is small + tightly\n// coupled to dispatch. `clearProviderAdapters` exists for test isolation.\nconst adapters = new Map<string, ProviderAdapter>();\n\n/** Register a provider adapter under `name` (e.g. `anthropic`, `openai`). */\nexport function registerProviderAdapter(name: string, adapter: ProviderAdapter): void {\n adapters.set(name, adapter);\n}\n\n/** Look up a registered adapter by provider name (or `undefined`). */\nexport function getProviderAdapter(name: string): ProviderAdapter | undefined {\n return adapters.get(name);\n}\n\n/** Clear the registry. Exported for test isolation between cases. */\nexport function clearProviderAdapters(): void {\n adapters.clear();\n}\n\n// --- Key resolution ---------------------------------------------------------\n//\n// Secrets live in env vars; config holds only the env-var NAME (DS-8). A\n// provider block with no `apiKeyEnv` is an ANONYMOUS local provider (Ollama,\n// LM Studio) and is allowed to proceed with `undefined` — no auth header.\nfunction resolveKey(providerCfg: ProviderConfig): string | undefined {\n if (!providerCfg.apiKeyEnv) return undefined; // anonymous local provider\n return process.env[providerCfg.apiKeyEnv];\n}\n\n// --- Per-tier output caps (FR-10) -------------------------------------------\n//\n// Applied when a request omits `maxTokens` AND signals a tier. A tier ONLY\n// picks the output cap — it never selects a provider or model (DS-6), so this\n// table is a flat budget map, not a routing table. When neither `maxTokens` nor\n// a tier is given, the request keeps `maxTokens: undefined` and each adapter\n// applies its own last-resort bound (e.g. the Anthropic Messages API's required\n// `max_tokens` defaults to 2048 inside that adapter).\nexport const TIER_MAX_TOKENS: Readonly<Record<Tier, number>> = {\n draft: 2048,\n title: 64,\n summarize: 512,\n consolidate: 2048,\n};\n\n// --- Adapter resolution (t4) ------------------------------------------------\n//\n// A configured provider block is keyed by an arbitrary NAME the user picks\n// (`anthropic`, `openai`, `ollama`, `lm-studio`, …). The ADAPTER set is fixed\n// (`anthropic`, `openai`, `openai-compatible`). The two only coincide for the\n// hosted built-ins; a local endpoint like Ollama is configured under a free-form\n// name (e.g. `ollama`) but must reach the `openai-compatible` adapter.\n//\n// Resolution rule (the openai-compatible adapter's closing comment flags this as\n// the t4 dispatch job):\n// 1. DIRECT match — a registered adapter exists under `providerName` (covers\n// `anthropic`, `openai`, `openai-compatible`, and any custom-registered\n// adapter that self-registers under its provider name). Always preferred.\n// 2. BASE URL fallback — a provider block with a `baseURL` but no direct\n// adapter is an OpenAI-shaped LOCAL endpoint (Ollama / LM Studio / vLLM),\n// so route it to `openai-compatible` (raw fetch, zero SDK dep — NFR-2).\n// 3. otherwise undefined — a named provider with no adapter and no `baseURL`\n// is a wiring fault ⇒ `{ ok: false, reason }` (distinct from `null`).\nfunction resolveAdapterName(providerName: string, providerCfg: ProviderConfig): string | undefined {\n if (getProviderAdapter(providerName)) return providerName; // direct\n if (providerCfg.baseURL) return 'openai-compatible'; // local OpenAI-shaped endpoint\n return undefined;\n}\n\n/**\n * Execute one bounded model call.\n *\n * Resolution + degradation order (each `null` is the first-class offline path):\n * 1. provider name — `req.provider || cfg.defaultProvider`. Empty ⇒ `null`.\n * 2. provider block — `cfg.providers[name]`. Absent ⇒ `null` (NOT configured,\n * so NO consent to spend; env presence is never consulted — DS-6).\n * 3. key — `process.env[apiKeyEnv]`; `undefined` (anonymous) if no `apiKeyEnv`.\n * A keyed provider whose env var is unset ⇒ `null` (the miss is observable\n * via the `null` return; a structured usage/miss sink lands with t6).\n * 4. adapter — the provider NAME is mapped to an adapter (direct match, else a\n * `baseURL` block routes to `openai-compatible`); unresolvable ⇒\n * `{ ok: false, reason }`. The per-tier `maxTokens` default (FR-10) and the\n * provider-block `baseURL` are folded onto the dispatched request here.\n * 5. dispatch — one call (or two via the structured repair retry when `schema`\n * is set — DS-4); a throw becomes `{ ok: false, reason }` (never escapes).\n *\n * `null` (steps 1–3) is degradation → caller substitutes a template.\n * `{ ok: false }` (steps 4–5) is an attempted-call failure → caller may surface it.\n */\nexport async function complete(\n req: CompleteRequest,\n cfg: ModelConfig = {},\n): Promise<CompleteResult> {\n // 1. Provider name — explicit only (NEVER inferred from env-var presence).\n const providerName = req.provider || cfg.defaultProvider;\n if (!providerName) return null;\n\n // 2. Provider block must be configured — explicit consent to spend. A name\n // that isn't in `providers{}` is NOT a provider, regardless of env vars.\n const providerCfg = cfg.providers?.[providerName];\n if (!providerCfg) return null;\n\n // 3. Key resolution — env-var NAME → value; anonymous local providers OK.\n const key = resolveKey(providerCfg);\n if (providerCfg.apiKeyEnv && !key) return null;\n\n // 4. Adapter resolution — map the configured provider NAME to an adapter. The\n // hosted built-ins (`anthropic`, `openai`) match directly; a free-form local\n // name (e.g. `ollama`) with a `baseURL` routes to `openai-compatible`. No\n // resolvable adapter ⇒ `{ ok: false }` (a wiring fault, NOT `null`).\n const adapterName = resolveAdapterName(providerName, providerCfg);\n const adapter = adapterName ? getProviderAdapter(adapterName) : undefined;\n if (!adapter) {\n return {\n ok: false,\n reason: providerCfg.baseURL\n ? `no adapter for provider \"${providerName}\" (baseURL set but the \"openai-compatible\" adapter is not registered)`\n : `no adapter registered for provider \"${providerName}\"`,\n };\n }\n\n // Forward provider-block config + the per-tier output cap onto the request so\n // the adapter stays uniform (ProviderAdapter.complete(req, key)):\n // • baseURL — only `openai-compatible` consumes it (Ollama / LM Studio /\n // vLLM endpoint); the hosted adapters simply ignore it.\n // • maxTokens — apply the FR-10 per-tier default ONLY when the caller omitted\n // it AND a tier is signalled. An explicit maxTokens always wins; absent both\n // ⇒ `undefined`, and the adapter applies its own last-resort bound.\n const tierMax = req.tier ? TIER_MAX_TOKENS[req.tier] : undefined;\n const maxTokens = req.maxTokens ?? tierMax;\n const dispatchReq: CompleteRequest = {\n ...req,\n ...(providerCfg.baseURL ? { baseURL: providerCfg.baseURL } : {}),\n ...(maxTokens !== undefined ? { maxTokens } : {}),\n };\n\n // 5. Single bounded call; complete() never throws. When a `schema` is present\n // the call routes through the structured path (prompt-JSON + validate + ≤1\n // repair retry — the ONLY retry in the model layer, DS-4/DS-12). Otherwise a\n // plain free-text adapter call. Either way at most two adapter invocations\n // total, and no `tools`/`stream` exist on the request to loop on (FR-8).\n try {\n return dispatchReq.schema\n ? await runStructured(adapter, dispatchReq, key)\n : await adapter.complete(dispatchReq, key);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `provider \"${providerName}\" failed: ${reason}` };\n }\n}\n","// Model config resolver for @noir-ai/model (slice S8).\n//\n// The single bridge from @noir-ai/core's user-facing `model` zod schema to the\n// runtime shape `complete()` (and `noir doctor`) consume. Lives HERE, in model,\n// so @noir-ai/core never imports @noir-ai/model (no core→model cycle): core owns\n// the user-facing schema, model owns this mapper + the runtime types (blueprint\n// D5 / hard rule). The fully-resolved zod output is structurally assignable to\n// the permissive {@link ModelUserConfig} mirror below, so the mapper accepts a\n// `NoirConfig['model']` directly — callers pass `resolveModelConfig(cfg.model)`.\n//\n// Provider-EXPLICIT, never silent paid (DS-6): this mapper is a PURE projection\n// of what the user wrote — it never selects a provider from env-var presence.\n// Whether a configured provider is actually USABLE (key present) is reported as\n// `hasKey` for inspection (`noir doctor`); it does NOT change the provider set.\n// The first-class `null`-degradation decision lives in `complete()`, which reads\n// the same env at call time — both readings are idempotent and stay in-process.\n//\n// Secrets stay in env (DS-8): `apiKeyEnv` is the env-var NAME (passthrough, safe\n// to print); `apiKey` is the VALUE resolved here from `process.env[apiKeyEnv]`,\n// materialized so doctor / direct consumers can branch without each re-reading\n// env. The value never touches disk via Noir and is never logged with usage.\n\n/**\n * User-facing model config shape — mirrors `NoirConfig['model']` (the zod block\n * @noir-ai/core ships, slice S8). Declared LOCALLY with every field optional so\n * this module type-checks WITHOUT a forward dependency on a core type (core\n * never imports model — no cycle; @noir-ai/model is not even in this package's\n * node_modules), AND so a config with no `model:` block (or a partial one) maps\n * cleanly to a fully-degraded runtime config. The fully-resolved zod output is\n * structurally assignable to this permissive shape, so the mapper accepts\n * `NoirConfig['model']` directly.\n */\nexport interface ModelUserConfig {\n /** Fallback provider key (into `providers`) when a tier resolves none. */\n defaultProvider?: string;\n /** Per-tier provider-key overrides (value = key into `providers{}`). */\n tiers?: {\n draft?: string;\n title?: string;\n summarize?: string;\n consolidate?: string;\n };\n /** Configured provider blocks, keyed by provider name. */\n providers?: Record<string, ModelProviderEntry>;\n}\n\n/**\n * One user-facing provider block — mirrors a `model.providers[name]` entry in\n * `.noir/config.yml`. `model` is optional HERE (the mirror is permissive) even\n * though the zod schema requires it, so hand-built configs in tests type-check;\n * `resolveModelConfig` passes `model` straight through when present.\n */\nexport interface ModelProviderEntry {\n /** Default model id for this provider (a call's `req.model` overrides). */\n model?: string;\n /** Base URL for openai-compatible endpoints (Ollama / LM Studio / …). */\n baseURL?: string;\n /** Env-var NAME holding the API key; omit for anonymous local providers. */\n apiKeyEnv?: string;\n}\n\n/**\n * A provider block with its key RESOLVED from the environment. Superset of the\n * runtime `ProviderConfig` (`@noir-ai/model`'s `complete()` consumes), so a\n * {@link ResolvedModelConfig} drops cleanly into `complete(req, cfg)` — the extra\n * `apiKey` / `hasKey` fields are ignored by `complete()` (which re-reads env at\n * call time, idempotently) and exist for `noir doctor` + direct consumers.\n */\nexport interface ResolvedProviderConfig {\n /** Provider model id (passthrough from config). */\n model?: string;\n /** Base URL passthrough (openai-compatible endpoints). */\n baseURL?: string;\n /** Env-var NAME passthrough — doctor prints this, NEVER the value (DS-8). */\n apiKeyEnv?: string;\n /** VALUE resolved from `process.env[apiKeyEnv]`; `undefined` if anonymous or unset. */\n apiKey?: string;\n /**\n * Readiness signal for `noir doctor` (OQ-5): for a keyed provider, whether the\n * env var named by `apiKeyEnv` is set; for an ANONYMOUS provider (no\n * `apiKeyEnv`), `true` — no key is required, so nothing is missing. Carries a\n * boolean ONLY, never the key value (NFR-4).\n */\n hasKey: boolean;\n}\n\n/**\n * Per-tier provider-key overrides, normalized to a full object (absent `tiers`\n * ⇒ `{}`) so consumers index without a separate undefined check.\n */\nexport interface ResolvedTiers {\n draft?: string;\n title?: string;\n summarize?: string;\n consolidate?: string;\n}\n\n/**\n * The resolved model-layer config — the runtime shape `complete()` consumes and\n * `noir doctor` inspects. `providers` is always present (possibly `{}`); each\n * entry carries its resolved key. Assignable to `ModelConfig` (`complete()`'s\n * param type), so `complete(req, resolveModelConfig(cfg.model))` type-checks.\n */\nexport interface ResolvedModelConfig {\n /** Fallback provider key when a call resolves no explicit provider. */\n defaultProvider?: string;\n /** Per-tier provider-key overrides (absent ⇒ empty object). */\n tiers: ResolvedTiers;\n /** Provider blocks with resolved keys (absent ⇒ empty map). */\n providers: Record<string, ResolvedProviderConfig>;\n}\n\n/**\n * Resolve a user-facing {@link ModelUserConfig} into the runtime\n * {@link ResolvedModelConfig}.\n *\n * - `undefined` / missing block ⇒ `{ tiers: {}, providers: {} }` (full\n * degradation — `complete()` will then return `null` for every call, the\n * always-available offline path; blueprint D5).\n * - Each provider's key is materialized from `process.env[apiKeyEnv]` into\n * `apiKey`; a keyed provider whose env var is unset gets `apiKey: undefined`\n * + `hasKey: false` (doctor surfaces this; `complete()` returns `null`).\n * - Anonymous providers (no `apiKeyEnv`) keep `apiKey: undefined` but report\n * `hasKey: true` (ready — no key needed, e.g. local Ollama).\n *\n * This mapper NEVER infers a provider from env-var presence (DS-6) and NEVER\n * mutates `raw` or `process.env` — it only READS env to resolve keys. It never\n * throws; an unusable config degrades to empty, not an exception.\n */\nexport function resolveModelConfig(raw?: ModelUserConfig): ResolvedModelConfig {\n const providers: Record<string, ResolvedProviderConfig> = {};\n\n // Destructure once into locals so every narrowing below is unambiguous (the\n // context bridge follows the same `const e = ctx?.embedder` shape). The input\n // is zod-validated or typed, so direct field access on each entry is safe.\n const rawProviders = raw?.providers;\n if (rawProviders) {\n for (const [name, entry] of Object.entries(rawProviders)) {\n const apiKeyEnv = entry.apiKeyEnv;\n // Anonymous provider (no apiKeyEnv) ⇒ no key to resolve; ready by default.\n const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;\n const hasKey = apiKeyEnv ? apiKey !== undefined : true;\n\n const resolved: ResolvedProviderConfig = { hasKey };\n if (entry.model !== undefined) resolved.model = entry.model;\n if (entry.baseURL !== undefined) resolved.baseURL = entry.baseURL;\n if (apiKeyEnv !== undefined) resolved.apiKeyEnv = apiKeyEnv;\n if (apiKey !== undefined) resolved.apiKey = apiKey;\n providers[name] = resolved;\n }\n }\n\n const tiers: ResolvedTiers = {};\n const rawTiers = raw?.tiers;\n if (rawTiers) {\n if (rawTiers.draft !== undefined) tiers.draft = rawTiers.draft;\n if (rawTiers.title !== undefined) tiers.title = rawTiers.title;\n if (rawTiers.summarize !== undefined) tiers.summarize = rawTiers.summarize;\n if (rawTiers.consolidate !== undefined) tiers.consolidate = rawTiers.consolidate;\n }\n\n const result: ResolvedModelConfig = { tiers, providers };\n const defaultProvider = raw?.defaultProvider;\n if (defaultProvider !== undefined) result.defaultProvider = defaultProvider;\n return result;\n}\n","// Anthropic provider adapter — single-shot Messages call via `@anthropic-ai/sdk`\n// (slice S8 / t2, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the request to `messages.create` carries ONLY\n// `model`, `max_tokens`, `messages`, and (optionally) `system`. There is no\n// `tools`, `tool_choice`, or `stream` key — so this adapter cannot express an\n// agent/tool loop even if a caller tried. One bounded call, then return.\n// - SDK retries DISABLED (`maxRetries: 0`, DS-12 / NFR-3): the hosted SDK\n// defaults to retrying transient failures; we opt out so one call can never\n// silently multi-charge. The only retry lives in the structured path (t4).\n// - IMPORT-ISOLATED (NFR-2): the `@anthropic-ai/sdk` is imported DYNAMICALLY\n// inside `complete()`. A bundle whose configured provider never resolves to\n// this adapter pays zero SDK bytes — the SDK is only pulled in at call time.\n// - SECRETS stay in env (DS-8): `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`; this module never touches `process.env`\n// and never logs the value. Only token COUNTS leave via `usage`.\n// - NO SILENT PAID CALLS (DS-6): if `key` is absent this adapter does NOT let\n// the SDK fall back to `ANTHROPIC_API_KEY` in env — that would be a silent\n// paid call. It returns `{ ok: false }` instead. (`complete()` normally\n// degrades to `null` for a keyed provider whose env var is missing, so an\n// absent key here means the provider block was wired without `apiKeyEnv` — a\n// recoverable misconfiguration, surfaced rather than silently charged.)\n//\n// Errors (network, non-2xx, empty body, SDK throw) become `{ ok: false, reason }`\n// — a structured failure a caller may surface. `null` degradation (no provider /\n// missing key) is decided one layer up in `complete()`, BEFORE this runs.\n//\n// Structured output is the CALLER's concern: if `req.schema` is present, the\n// t4 structured path instructs the model to emit JSON, parses the returned\n// `text`, and validates it. This adapter ignores `schema` and returns raw text,\n// so it stays a single, provider-agnostic completion primitive.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, CompleteUsage, ProviderAdapter } from '../types.js';\n\n// Structural aliases for the dynamically-imported SDK, so this file does NOT\n// depend on the SDK's exact exported types at compile time (resilient to minor\n// version churn) AND does not pull the SDK into the module's top-level import\n// graph (NFR-2 import isolation). The shape is exactly what we use: a\n// constructor taking `{ apiKey?, maxRetries }` and a `messages.create(...)`\n// returning the Anthropic Message shape. `content` is modeled loosely (an array\n// of `{ type, text? }`) because we only consume the text block; narrowing is\n// done by a `typeof text === 'string'` guard rather than a discriminated union,\n// since the structural aliases intentionally do not enumerate every block type.\ninterface AnthropicMessage {\n content: Array<{ type: string; text?: string }>;\n stop_reason: string | null;\n usage: { input_tokens: number; output_tokens: number };\n}\n\ninterface AnthropicMessages {\n create(\n params: {\n model: string;\n max_tokens: number;\n system?: string;\n messages: Array<{ role: 'user'; content: string }>;\n },\n options?: { signal?: AbortSignal; maxRetries?: number },\n ): Promise<AnthropicMessage>;\n}\n\ninterface AnthropicClient {\n messages: AnthropicMessages;\n}\n\ntype AnthropicSDK = new (opts: { apiKey?: string; maxRetries: number }) => AnthropicClient;\n\n// Anthropic's Messages API REQUIRES `max_tokens` (unlike OpenAI, where it is\n// optional). Per-tier caps (FR-10: draft 2048 / title 64 / summarize 512 /\n// consolidate 2048) are the caller's job, resolved before this adapter runs;\n// this is the adapter's last-resort bound so the required field is always set.\nconst DEFAULT_MAX_TOKENS = 2048;\n\n/**\n * Anthropic provider adapter. Registered under the name `anthropic` and\n * dispatched by `complete()` when a configured provider block's name is\n * `anthropic` (the hosted Messages endpoint). Returns raw text only — the\n * structured-output path (t4) wraps this adapter when a `schema` is present.\n */\nexport const anthropicAdapter: ProviderAdapter = {\n name: 'anthropic',\n complete: async (req, key): Promise<CompleteResult> => {\n // DS-6 defense: Anthropic is a hosted, keyed provider. If `key` is absent\n // the provider block was wired without `apiKeyEnv`; do NOT let the SDK fall\n // back to `ANTHROPIC_API_KEY` in env (silent paid call). complete() returns\n // null for a keyed provider whose env var is missing, so reaching here\n // without a key is a wiring fault — surface it, never charge silently.\n if (!key) {\n return { ok: false, reason: 'anthropic: missing API key (provider block has no apiKeyEnv)' };\n }\n\n try {\n // Dynamic import — a bundle that never selects the `anthropic` adapter\n // ships no `@anthropic-ai/sdk` dependency (NFR-2). `default` is the\n // `Anthropic` client class. Cast through `unknown` into a structural view\n // so this file never depends on the SDK's exact exported types (resilient\n // to minor version churn).\n const sdk = (await import('@anthropic-ai/sdk')) as unknown as { default: AnthropicSDK };\n const client = new sdk.default({\n apiKey: key, // the env VALUE resolved by complete() (DS-8).\n maxRetries: 0, // DS-12: never silently retry (bounded cost).\n });\n\n // Single bounded Messages call. The body carries ONLY bounded fields — no\n // `tools`, no `stream` (FR-8). `system` is a top-level Anthropic param\n // (not folded into messages, as OpenAI does); `max_tokens` is REQUIRED by\n // the Messages API, so a default is applied when the request omits it.\n const res = await client.messages.create(\n {\n model: req.model,\n max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(req.system !== undefined ? { system: req.system } : {}),\n messages: [{ role: 'user', content: req.prompt }],\n },\n {\n maxRetries: 0, // belt-and-suspenders: per-request as well as constructor.\n // Honor an abort signal so a caller can bound wall-clock further.\n ...(req.signal ? { signal: req.signal } : {}),\n },\n );\n\n // Content is an array of blocks; with no `tools` configured the first\n // (and usually only) block is a text block. noUncheckedIndexedAccess ⇒\n // guard both presence and the text field's type before returning it.\n const block = res.content[0];\n const text = block?.text;\n if (typeof text !== 'string') {\n return {\n ok: false,\n reason: `anthropic: response had no text block (stop_reason: ${res.stop_reason ?? 'unknown'})`,\n };\n }\n\n const usage: CompleteUsage = {\n inputTokens: res.usage.input_tokens,\n outputTokens: res.usage.output_tokens,\n };\n\n return { ok: true, text, usage };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `anthropic: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` then dispatches by provider name `anthropic`. The SDK itself is\n// NOT loaded by this registration — only inside `complete()` above (NFR-2).\nregisterProviderAdapter('anthropic', anthropicAdapter);\n","// OpenAI-COMPATIBLE provider adapter — single-shot chat completions over the\n// GLOBAL `fetch` (slice S8 / t3, blueprint D5). Zero non-fetch dependencies.\n//\n// This is the local / self-host escape hatch: any endpoint that speaks the\n// OpenAI Chat Completions JSON shape is reached here via its `baseURL` — Ollama\n// (`http://localhost:11434/v1`), LM Studio, vLLM, llama.cpp server, … — without\n// a per-vendor adapter and without a single SDK dependency.\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the POST body carries ONLY `model`, `messages`,\n// and (optionally) `max_tokens`. No `tools` / `functions` / `stream` — this\n// adapter cannot express an agent/tool loop. One request, parse, return.\n// - NO SDK, NO RETRY MACHINERY (DS-12 / NFR-2/3): raw `fetch` is a single shot;\n// the only retry lives in the structured path (t4). Uses the GLOBAL `fetch`\n// (Node ≥20, per `engines.node \">=20\"`) — zero added dependency.\n// - SECRETS stay in env (DS-8): `key` is the VALUE resolved by `complete()`; an\n// ANONYMOUS local provider (Ollama with no `apiKeyEnv`) reaches here with\n// `key === undefined` and we simply omit the `Authorization` header. This\n// module never reads `process.env` and never logs the value.\n//\n// Errors (network failure, non-2xx, empty body, fetch throw) become\n// `{ ok: false, reason }`. `null` degradation (no provider / missing key) is\n// decided one layer up in `complete()`, BEFORE this runs.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, ProviderAdapter } from '../types.js';\n\n// The OpenAI Chat Completions response shape — only the fields we read. The\n// `content` may be `null` (e.g. a tool-call frame, which we never request, or a\n// content-filtered refusal); we treat non-string content as a structured miss.\ninterface OpenAICompatibleResponse {\n choices?: Array<{ message?: { content?: string | null } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n}\n\n/** Build the OpenAI-shaped `messages` array: optional system, then the user turn. */\nfunction buildMessages(req: CompleteRequest): Array<{ role: 'system' | 'user'; content: string }> {\n const messages: Array<{ role: 'system' | 'user'; content: string }> = [];\n if (req.system) messages.push({ role: 'system', content: req.system });\n messages.push({ role: 'user', content: req.prompt });\n return messages;\n}\n\n/** Join a `baseURL` and `/chat/completions`, tolerating trailing slashes. */\nfunction joinEndpoint(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, '')}/chat/completions`;\n}\n\n/**\n * OpenAI-compatible provider adapter. Registered under the name\n * `openai-compatible` and dispatched by `complete()`. `baseURL` (forwarded onto\n * {@link CompleteRequest} from the provider block by `complete()`) selects the\n * endpoint; the request/response are the OpenAI Chat Completions JSON shape, so\n * any compatible server works with no vendor-specific code.\n */\nexport const openaiCompatibleAdapter: ProviderAdapter = {\n name: 'openai-compatible',\n complete: async (req, key): Promise<CompleteResult> => {\n // baseURL is the ONE piece of provider-block config this adapter needs; it\n // is forwarded from `cfg.providers[name].baseURL` by complete() (the only\n // adapter that consumes it). Missing ⇒ misconfiguration, not degradation.\n const baseURL = req.baseURL;\n if (!baseURL) {\n return { ok: false, reason: 'openai-compatible: provider block has no baseURL' };\n }\n const endpoint = joinEndpoint(baseURL);\n\n // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.\n const body: Record<string, unknown> = {\n model: req.model,\n messages: buildMessages(req),\n ...(req.maxTokens !== undefined ? { max_tokens: req.maxTokens } : {}),\n };\n\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n // Anonymous local provider (Ollama without `apiKeyEnv`): key is undefined ⇒\n // NO Authorization header is sent. A keyed compatible endpoint gets Bearer.\n if (key) headers.authorization = `Bearer ${key}`;\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n // Pass-through abort so a caller can bound wall-clock (single shot; no\n // stream to cancel). `undefined` is a no-op for fetch.\n signal: req.signal,\n });\n\n if (!res.ok) {\n // NFR-4: surface ONLY the HTTP status — NEVER embed the raw response\n // body in `reason`. A malicious or echoing endpoint (Ollama / LM\n // Studio / vLLM / any gateway) could echo the request body (the\n // prompt) or reflect headers (the `Bearer` / `sk-` key) into its\n // error frame, and callers surface `reason` directly. Reading the\n // body here would only invite a leak, so the body is not read at all.\n return { ok: false, reason: `openai-compatible: HTTP ${res.status}` };\n }\n\n const json = (await res.json()) as OpenAICompatibleResponse;\n const text = json.choices?.[0]?.message?.content;\n if (typeof text !== 'string') {\n return { ok: false, reason: 'openai-compatible: completion had no message content' };\n }\n\n const usage = json.usage;\n return {\n ok: true,\n text,\n ...(usage\n ? {\n usage: {\n ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),\n ...(usage.completion_tokens !== undefined\n ? { outputTokens: usage.completion_tokens }\n : {}),\n },\n }\n : {}),\n };\n } catch (err) {\n // Abort is an expected caller-initiated bound, not a paid failure.\n if (err instanceof Error && err.name === 'AbortError') {\n return { ok: false, reason: 'openai-compatible: request aborted' };\n }\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `openai-compatible: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` dispatches by provider name `openai-compatible`. (Routing a\n// free-form provider key like `ollama` to this adapter is the t4 dispatch job.)\nregisterProviderAdapter('openai-compatible', openaiCompatibleAdapter);\n","// OpenAI provider adapter — single-shot chat completions via the `openai` SDK\n// (slice S8 / t3, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the request to `chat.completions.create` carries\n// ONLY `model`, `messages`, and (optionally) `max_tokens`. There is no\n// `tools`, `functions`, or `stream` key — so this adapter cannot express an\n// agent/tool loop even if a caller tried. Single bounded call, then return.\n// - SDK retries DISABLED (`maxRetries: 0`, DS-12 / NFR-3): the hosted SDK\n// defaults to retrying transient failures; we opt out so one call can never\n// silently multi-charge. The only retry lives in the structured path (t4).\n// - IMPORT-ISOLATED (NFR-2): the `openai` SDK is imported DYNAMICICALLY inside\n// `complete()`. A bundle whose configured provider never resolves to this\n// adapter pays zero `openai` bytes — the SDK is only pulled in at call time.\n// - SECRETS stay in env (DS-8): `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`; this module never touches `process.env`\n// and never logs the value. Only token COUNTS leave via `usage`.\n//\n// Errors (network, non-2xx, empty body, SDK throw) become `{ ok: false, reason }`\n// — a structured failure a caller may surface. `null` degradation (no provider /\n// missing key) is decided one layer up in `complete()`, BEFORE this runs.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, ProviderAdapter } from '../types.js';\n\n// Structural aliases for the dynamically-imported SDK, so this file does NOT\n// depend on the SDK's exact exported types at compile time (resilient to minor\n// version churn) AND does not pull the SDK into the module's top-level import\n// graph (NFR-2 import isolation). The shape is exactly what we use: a\n// constructor taking `{ apiKey?, baseURL?, maxRetries }` and a\n// `chat.completions.create(...)` returning the OpenAI ChatCompletion shape.\ntype OpenAIChatCompletionsCreate = (\n params: {\n model: string;\n messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;\n max_tokens?: number;\n },\n // Second options argument mirrors the real SDK's `(params, options?)` shape,\n // so this adapter can forward the caller's wall-clock bound (NFR-3) and\n // re-assert no retries (DS-12) at the per-request level — same pattern the\n // anthropic adapter uses for `messages.create`.\n options?: { signal?: AbortSignal; maxRetries?: number },\n) => Promise<{\n choices?: Array<{ message?: { content?: string | null } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n}>;\n\ninterface OpenAIClient {\n chat: { completions: { create: OpenAIChatCompletionsCreate } };\n}\n\ntype OpenAISDK = new (opts: {\n apiKey?: string;\n baseURL?: string;\n maxRetries: number;\n}) => OpenAIClient;\n\n/** Build the OpenAI-shaped `messages` array: optional system, then the user turn. */\nfunction buildMessages(req: CompleteRequest): Array<{ role: 'system' | 'user'; content: string }> {\n const messages: Array<{ role: 'system' | 'user'; content: string }> = [];\n if (req.system) messages.push({ role: 'system', content: req.system });\n messages.push({ role: 'user', content: req.prompt });\n return messages;\n}\n\n/**\n * OpenAI provider adapter. Registered under the name `openai` and dispatched by\n * `complete()` when a configured provider block's name is `openai` (the hosted\n * endpoint). The `openai-compatible` adapter covers OpenAI-shaped LOCAL\n * endpoints (Ollama / LM Studio / vLLM) via raw `fetch` + `baseURL`.\n */\nexport const openaiAdapter: ProviderAdapter = {\n name: 'openai',\n complete: async (req, key): Promise<CompleteResult> => {\n // Hosted OpenAI ALWAYS requires a key. Guard explicitly so a misconfigured\n // anonymous `openai` block (no `apiKeyEnv`) cannot fall through to the SDK's\n // OWN env fallback (`OPENAI_API_KEY`) — that would be a silent paid call via\n // env presence, which DS-6 forbids. Anonymous LOCAL endpoints belong to the\n // dedicated `openai-compatible` adapter, not this one.\n if (!key) {\n return { ok: false, reason: 'openai: missing API key (set apiKeyEnv on the provider block)' };\n }\n try {\n // Dynamic import — a bundle that never selects the `openai` adapter ships\n // no `openai` dependency (NFR-2). `default` is the `OpenAI` client class.\n // Cast through `unknown` into a structural view so this file never depends\n // on the SDK's exact exported types (resilient to minor version churn).\n const sdk = (await import('openai')) as unknown as { default: OpenAISDK };\n const client = new sdk.default({\n apiKey: key, // the VALUE complete() resolved from process.env[apiKeyEnv]\n // Honor a forwarded baseURL if present (lets this adapter target a\n // custom OpenAI-shaped endpoint; the common local case uses the\n // dedicated `openai-compatible` adapter instead).\n ...(req.baseURL ? { baseURL: req.baseURL } : {}),\n maxRetries: 0, // DS-12: never silently retry (bounded cost).\n });\n\n const res = await client.chat.completions.create(\n {\n model: req.model,\n messages: buildMessages(req),\n // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.\n ...(req.maxTokens !== undefined ? { max_tokens: req.maxTokens } : {}),\n },\n {\n maxRetries: 0, // belt-and-suspenders: per-request as well as constructor.\n // Forward the caller's wall-clock bound (NFR-3) — same conditional\n // spread as the other optional fields (only when a signal is present).\n ...(req.signal ? { signal: req.signal } : {}),\n },\n );\n\n const text = res.choices?.[0]?.message?.content;\n if (typeof text !== 'string') {\n return { ok: false, reason: 'openai: completion had no message content' };\n }\n\n const usage = res.usage;\n return {\n ok: true,\n text,\n ...(usage\n ? {\n usage: {\n ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),\n ...(usage.completion_tokens !== undefined\n ? { outputTokens: usage.completion_tokens }\n : {}),\n },\n }\n : {}),\n };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `openai: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` then dispatches by provider name `openai`. The SDK itself is NOT\n// loaded by this registration — only inside `complete()` above (NFR-2).\nregisterProviderAdapter('openai', openaiAdapter);\n"],"mappings":"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","names":["buildMessages"]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@noir-ai/model",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Noir model — optional bounded single-shot LLM layer (anthropic / openai / openai-compatible); agent loops are impossible by design.",
5
+ "license": "MIT",
6
+ "author": "agaaaptr",
7
+ "homepage": "https://github.com/agaaaptr/noir#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/agaaaptr/noir.git",
11
+ "directory": "packages/model"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/agaaaptr/noir/issues"
15
+ },
16
+ "keywords": [
17
+ "noir",
18
+ "model",
19
+ "llm",
20
+ "anthropic",
21
+ "openai",
22
+ "ollama",
23
+ "single-shot"
24
+ ],
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true
31
+ },
32
+ "type": "module",
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "README.md"
44
+ ],
45
+ "dependencies": {
46
+ "@anthropic-ai/sdk": "^0.115.0",
47
+ "openai": "^6.0.0"
48
+ },
49
+ "peerDependencies": {
50
+ "zod": "^4.2.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^26.1.1",
54
+ "zod": "^4.2.0"
55
+ },
56
+ "scripts": {
57
+ "build": "tsup",
58
+ "typecheck": "tsc --noEmit"
59
+ }
60
+ }