@noir-ai/model 1.14.0 → 1.15.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/dist/index.d.ts CHANGED
@@ -5,14 +5,14 @@ import * as z from 'zod/v4';
5
5
  *
6
6
  * Either a zod schema (its `.parse()` is the validator) or a raw validate-and-
7
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
8
+ * instructs the model to emit JSON, parses it, then runs this; a
9
9
  * failure triggers a single repair retry, then degradation to `null`.
10
10
  */
11
11
  type CompleteSchema = z.ZodType | ((raw: unknown) => unknown);
12
12
  /**
13
13
  * The four bounded task tiers. A tier is an OPTIONAL budget hint on a
14
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
15
+ * `maxTokens` (the per-tier default). A tier NEVER selects a provider or model: those stay
16
16
  * explicit on the request (provider-explicit, never inferred from env).
17
17
  * Tier→provider/model resolution is the caller's job (it reads resolved config);
18
18
  * `complete()` only consumes the tier for the output-cap default.
@@ -21,7 +21,7 @@ type Tier = 'draft' | 'title' | 'summarize' | 'consolidate';
21
21
  /**
22
22
  * One bounded model call. Deliberately minimal — there is NO `tools`,
23
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
24
+ * express an agent or tool-exec loop. `signal` lets a caller
25
25
  * bound wall-clock further (abort); there is never a stream to cancel.
26
26
  */
27
27
  interface CompleteRequest {
@@ -35,28 +35,47 @@ interface CompleteRequest {
35
35
  provider: string;
36
36
  /** Model id for this call (e.g. `claude-haiku`, `gpt-4o-mini`). */
37
37
  model: string;
38
- /** Output cap in tokens; omit to use the adapter/tier default (FR-10). */
38
+ /** Output cap in tokens; omit to use the adapter/tier default. */
39
39
  maxTokens?: number;
40
40
  /**
41
41
  * Optional task tier. When `maxTokens` is absent AND a tier is set,
42
- * `complete()` applies the per-tier output cap (FR-10: draft 2048 / title 64 /
42
+ * `complete()` applies the per-tier output cap (draft 2048 / title 64 /
43
43
  * summarize 512 / consolidate 2048). Never selects provider/model.
44
44
  */
45
45
  tier?: Tier;
46
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.
47
+ * Base URL for the provider endpoint — a local OpenAI-compatible server
48
+ * (Ollama / LM Studio / vLLM, e.g. `http://localhost:11434/v1`) or a gateway
49
+ * host. NOT caller-set: `complete()` forwards it from the resolved provider
50
+ * block (`cfg.providers[name].baseURL`). The `openai-compatible` adapter
51
+ * requires it; the `anthropic` and `openai` adapters honor it when present
52
+ * (the `anthropic` adapter pins an explicit default host when absent so an
53
+ * ambient `ANTHROPIC_BASE_URL` can never leak in).
52
54
  */
53
55
  baseURL?: string;
56
+ /**
57
+ * Bearer token forwarded to an Anthropic-shaped endpoint as
58
+ * `Authorization: Bearer`, for gateways that authenticate with a token rather
59
+ * than an API key. NOT caller-set: `complete()` reads the value from the env
60
+ * var named by the provider block's `authTokenEnv` and forwards it here — the
61
+ * same env-at-call-time rule as the API key, and the same reason: the config
62
+ * file and this request carry names and resolved values, never secrets on
63
+ * disk. Optional; an adapter that authenticates with a key ignores it.
64
+ */
65
+ authToken?: string;
66
+ /**
67
+ * Per-request timeout in milliseconds. NOT caller-set: `complete()` forwards
68
+ * it from the resolved provider block (`cfg.providers[name].timeoutMs`), and
69
+ * it is honored by the anthropic adapter (and any adapter that consumes it).
70
+ * Optional — omit to keep the adapter's own default.
71
+ */
72
+ timeoutMs?: number;
54
73
  /** Optional abort signal to bound the call (single shot, no streaming). */
55
74
  signal?: AbortSignal;
56
75
  }
57
76
  /**
58
77
  * Token accounting for a successful call. Carries COUNTS only — never prompts
59
- * or keys (NFR-4: values are never logged alongside usage).
78
+ * or keys (values are never logged alongside usage).
60
79
  */
61
80
  interface CompleteUsage {
62
81
  inputTokens?: number;
@@ -68,16 +87,17 @@ interface CompleteUsage {
68
87
  * - `{ ok: true, text, value?, usage? }` — completion succeeded. `text` is the
69
88
  * raw model output; `value` is present ONLY on the structured path (when a
70
89
  * `schema` was supplied and the parsed+validated JSON object is returned —
71
- * FR-1: structured mode returns an `object`). Free-text calls leave `value`
90
+ * structured mode returns an `object`). Free-text calls leave `value`
72
91
  * unset, so callers branch `if ('value' in result)` to consume structured.
73
92
  * - `{ ok: false, reason }` — a call was ATTEMPTED (provider configured + key
74
93
  * present + adapter ran) but failed (network, non-2xx, parse error, or the
75
94
  * structured path failed JSON parse/validate twice). This is a recoverable
76
95
  * error a caller may surface; it is NOT degradation.
77
96
  * - `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.
97
+ * env var was missing. The caller substitutes a template/stub (the model
98
+ * layer degrades to templates when no provider is configured). This is the
99
+ * always-available offline path; the full Noir test suite exercises it with
100
+ * zero network.
81
101
  */
82
102
  type CompleteResult = {
83
103
  ok: true;
@@ -104,8 +124,7 @@ interface ProviderAdapter {
104
124
  * One configured provider block — mirrors a `model.providers[name]` entry in
105
125
  * `.noir/config.yml`. `apiKeyEnv` is the NAME of the environment variable that
106
126
  * holds the secret, NEVER the value itself, so the config file stays safe to
107
- * commit and share (blueprint D5); `complete()` reads the value at call
108
- * time only.
127
+ * commit and share; `complete()` reads the value at call time only.
109
128
  */
110
129
  interface ProviderConfig {
111
130
  /** Default model id for this provider (a call's `req.model` overrides). */
@@ -114,9 +133,17 @@ interface ProviderConfig {
114
133
  baseURL?: string;
115
134
  /** Env-var NAME holding the API key; omit for anonymous local providers. */
116
135
  apiKeyEnv?: string;
136
+ /**
137
+ * Env-var NAME holding a bearer token for gateways that authenticate with
138
+ * `Authorization: Bearer` instead of an API key. NAME only, never the value —
139
+ * same rule as `apiKeyEnv`.
140
+ */
141
+ authTokenEnv?: string;
142
+ /** Per-request timeout in milliseconds; omit to keep the adapter default. */
143
+ timeoutMs?: number;
117
144
  }
118
145
  /**
119
- * The model-layer config slice. Mirrors the user-facing `model:` block added to
146
+ * The model-layer config block. Mirrors the user-facing `model:` block added to
120
147
  * `NoirConfigSchema`; every field is optional so an absent
121
148
  * `model:` block resolves to `{}` — full degradation, offline, the default.
122
149
  * `complete(req, cfg)` consumes this directly with no dependency on `@noir-ai/core`
@@ -144,13 +171,17 @@ declare const TIER_MAX_TOKENS: Readonly<Record<Tier, number>>;
144
171
  * 1. provider name — `req.provider || cfg.defaultProvider`. Empty ⇒ `null`.
145
172
  * 2. provider block — `cfg.providers[name]`. Absent ⇒ `null` (NOT configured,
146
173
  * so NO consent to spend; env presence is never consulted).
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 is planned).
174
+ * 3. credentials — `process.env[apiKeyEnv]` and/or `process.env[authTokenEnv]`;
175
+ * `undefined` for an anonymous local provider that names neither. A provider
176
+ * that names EITHER env var is keyed, and if NEITHER resolves to a value the
177
+ * call degrades to `null` (the miss is observable via the `null` return; a
178
+ * structured usage/miss sink is planned) — decided before any client exists,
179
+ * so nothing can fill the credential in from the ambient environment.
150
180
  * 4. adapter — the provider NAME is mapped to an adapter (direct match, else a
151
181
  * `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.
182
+ * `{ ok: false, reason }`. The per-tier `maxTokens` default, the
183
+ * provider-block `baseURL`, the resolved bearer token, and `timeoutMs` are
184
+ * folded onto the dispatched request here.
154
185
  * 5. dispatch — one call (or two via the structured repair retry when `schema`
155
186
  * is set); a throw becomes `{ ok: false, reason }` (never escapes).
156
187
  *
@@ -161,7 +192,7 @@ declare function complete(req: CompleteRequest, cfg?: ModelConfig): Promise<Comp
161
192
 
162
193
  /**
163
194
  * 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
195
+ * @noir-ai/core ships). Declared LOCALLY with every field optional so
165
196
  * this module type-checks WITHOUT a forward dependency on a core type (core
166
197
  * never imports model — no cycle; @noir-ai/model is not even in this package's
167
198
  * node_modules), AND so a config with no `model:` block (or a partial one) maps
@@ -195,6 +226,10 @@ interface ModelProviderEntry {
195
226
  baseURL?: string;
196
227
  /** Env-var NAME holding the API key; omit for anonymous local providers. */
197
228
  apiKeyEnv?: string;
229
+ /** Env-var NAME holding a bearer token (Authorization: Bearer); NAME only. */
230
+ authTokenEnv?: string;
231
+ /** Per-request timeout in milliseconds; omit for the adapter default. */
232
+ timeoutMs?: number;
198
233
  }
199
234
  /**
200
235
  * A provider block with its key RESOLVED from the environment. Superset of the
@@ -210,13 +245,17 @@ interface ResolvedProviderConfig {
210
245
  baseURL?: string;
211
246
  /** Env-var NAME passthrough — doctor prints this, NEVER the value. */
212
247
  apiKeyEnv?: string;
248
+ /** Env-var NAME passthrough for the bearer token (same rule as apiKeyEnv). */
249
+ authTokenEnv?: string;
250
+ /** Per-request timeout passthrough (milliseconds). */
251
+ timeoutMs?: number;
213
252
  /** VALUE resolved from `process.env[apiKeyEnv]`; `undefined` if anonymous or unset. */
214
253
  apiKey?: string;
215
254
  /**
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).
255
+ * Readiness signal for `noir doctor`: whether a keyed provider has at least
256
+ * one of its named credential env vars set (`apiKeyEnv` and/or `authTokenEnv`);
257
+ * an ANONYMOUS provider (neither named) is always `true` — no credential is
258
+ * required, so nothing is missing. Carries a boolean ONLY, never the value.
220
259
  */
221
260
  hasKey: boolean;
222
261
  }
@@ -250,12 +289,14 @@ interface ResolvedModelConfig {
250
289
  *
251
290
  * - `undefined` / missing block ⇒ `{ tiers: {}, providers: {} }` (full
252
291
  * 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).
292
+ * always-available offline path).
293
+ * - Each provider's API key is materialized from `process.env[apiKeyEnv]` into
294
+ * `apiKey`; `hasKey` is `true` when a keyed provider has at least one of its
295
+ * named credential env vars set (doctor surfaces a miss; `complete()` returns
296
+ * `null`).
297
+ * - Anonymous providers (neither `apiKeyEnv` nor `authTokenEnv`) keep
298
+ * `apiKey: undefined` but report `hasKey: true` (ready — no key needed, e.g.
299
+ * local Ollama).
259
300
  *
260
301
  * This mapper NEVER infers a provider from env-var presence and NEVER
261
302
  * mutates `raw` or `process.env` — it only READS env to resolve keys. It never
@@ -277,8 +318,8 @@ interface DraftPrdInput {
277
318
  memory?: string;
278
319
  }
279
320
  /**
280
- * Options for a PRD draft call. `provider` + `model` are EXPLICIT (blueprint D5
281
- * — the provider is never inferred from env-var presence). `signal`
321
+ * Options for a PRD draft call. `provider` + `model` are EXPLICIT — the
322
+ * provider is never inferred from env-var presence. `signal`
282
323
  * bounds wall-clock further; there is never a stream to cancel.
283
324
  */
284
325
  interface DraftPrdOptions {
@@ -304,7 +345,7 @@ declare const PRD_FALLBACK_TEMPLATE = "# PRD\n\n## Problem\n<fill in: what's bro
304
345
  * - the drafted PRD text on success (`{ ok: true }` from `complete()`);
305
346
  * - `null` when no provider/key is configured OR an attempted call failed —
306
347
  * callers substitute {@link PRD_FALLBACK_TEMPLATE} in both cases (the offline
307
- * path is first-class; blueprint D5).
348
+ * path is first-class).
308
349
  *
309
350
  * Never throws — `complete()`'s adapter try/catch surfaces failures as
310
351
  * `{ ok: false, reason }`, which this helper collapses to `null` (a missed
package/dist/index.js CHANGED
@@ -131,6 +131,10 @@ function resolveKey(providerCfg) {
131
131
  if (!providerCfg.apiKeyEnv) return void 0;
132
132
  return process.env[providerCfg.apiKeyEnv];
133
133
  }
134
+ function resolveAuthToken(providerCfg) {
135
+ if (!providerCfg.authTokenEnv) return void 0;
136
+ return process.env[providerCfg.authTokenEnv];
137
+ }
134
138
  var TIER_MAX_TOKENS = {
135
139
  draft: 2048,
136
140
  title: 64,
@@ -148,7 +152,9 @@ async function complete(req, cfg = {}) {
148
152
  const providerCfg = cfg.providers?.[providerName];
149
153
  if (!providerCfg) return null;
150
154
  const key = resolveKey(providerCfg);
151
- if (providerCfg.apiKeyEnv && !key) return null;
155
+ const authToken = resolveAuthToken(providerCfg);
156
+ const isKeyed = Boolean(providerCfg.apiKeyEnv || providerCfg.authTokenEnv);
157
+ if (isKeyed && !key && !authToken) return null;
152
158
  const adapterName = resolveAdapterName(providerName, providerCfg);
153
159
  const adapter = adapterName ? getProviderAdapter(adapterName) : void 0;
154
160
  if (!adapter) {
@@ -162,6 +168,8 @@ async function complete(req, cfg = {}) {
162
168
  const dispatchReq = {
163
169
  ...req,
164
170
  ...providerCfg.baseURL ? { baseURL: providerCfg.baseURL } : {},
171
+ ...authToken !== void 0 ? { authToken } : {},
172
+ ...providerCfg.timeoutMs !== void 0 ? { timeoutMs: providerCfg.timeoutMs } : {},
165
173
  ...maxTokens !== void 0 ? { maxTokens } : {}
166
174
  };
167
175
  try {
@@ -179,12 +187,17 @@ function resolveModelConfig(raw) {
179
187
  if (rawProviders) {
180
188
  for (const [name, entry] of Object.entries(rawProviders)) {
181
189
  const apiKeyEnv = entry.apiKeyEnv;
190
+ const authTokenEnv = entry.authTokenEnv;
182
191
  const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : void 0;
183
- const hasKey = apiKeyEnv ? apiKey !== void 0 : true;
192
+ const authToken = authTokenEnv ? process.env[authTokenEnv] : void 0;
193
+ const keyed = Boolean(apiKeyEnv) || Boolean(authTokenEnv);
194
+ const hasKey = !keyed || apiKey !== void 0 && apiKey !== "" || authToken !== void 0 && authToken !== "";
184
195
  const resolved = { hasKey };
185
196
  if (entry.model !== void 0) resolved.model = entry.model;
186
197
  if (entry.baseURL !== void 0) resolved.baseURL = entry.baseURL;
187
198
  if (apiKeyEnv !== void 0) resolved.apiKeyEnv = apiKeyEnv;
199
+ if (entry.authTokenEnv !== void 0) resolved.authTokenEnv = entry.authTokenEnv;
200
+ if (entry.timeoutMs !== void 0) resolved.timeoutMs = entry.timeoutMs;
188
201
  if (apiKey !== void 0) resolved.apiKey = apiKey;
189
202
  providers[name] = resolved;
190
203
  }
@@ -291,14 +304,26 @@ var DEFAULT_MAX_TOKENS = 2048;
291
304
  var anthropicAdapter = {
292
305
  name: "anthropic",
293
306
  complete: async (req, key) => {
294
- if (!key) {
295
- return { ok: false, reason: "anthropic: missing API key (provider block has no apiKeyEnv)" };
307
+ if (!key && !req.authToken) {
308
+ return {
309
+ ok: false,
310
+ reason: "anthropic: missing credentials (provider block has no apiKeyEnv or authTokenEnv)"
311
+ };
296
312
  }
297
313
  try {
298
314
  const sdk = await import("@anthropic-ai/sdk");
299
315
  const client = new sdk.default({
300
- apiKey: key,
316
+ // Pin every transport-affecting option explicitly: the SDK reads
317
+ // ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL only
318
+ // when the matching option is `undefined`, so an absent credential is
319
+ // set to `null` (never left undefined) and an absent base URL to the
320
+ // real hosted default. Ambient env therefore cannot route this call.
321
+ apiKey: key || null,
301
322
  // the env VALUE resolved by complete().
323
+ authToken: req.authToken || null,
324
+ // resolved bearer token (or null).
325
+ baseURL: req.baseURL ?? "https://api.anthropic.com",
326
+ ...req.timeoutMs !== void 0 ? { timeout: req.timeoutMs } : {},
302
327
  maxRetries: 0
303
328
  // Never silently retry (bounded cost).
304
329
  });
@@ -421,10 +446,13 @@ var openaiAdapter = {
421
446
  const client = new sdk.default({
422
447
  apiKey: key,
423
448
  // the VALUE complete() resolved from process.env[apiKeyEnv]
424
- // Honor a forwarded baseURL if present (lets this adapter target a
425
- // custom OpenAI-shaped endpoint; the common local case uses the
426
- // dedicated `openai-compatible` adapter instead).
427
- ...req.baseURL ? { baseURL: req.baseURL } : {},
449
+ // Pin the endpoint explicitly, exactly as the anthropic adapter does:
450
+ // the SDK reads OPENAI_BASE_URL ONLY when `baseURL` is `undefined`, so
451
+ // omitting it would hand the endpoint to the ambient environment — a
452
+ // committable `.noir/.env` could then route every call to a host the
453
+ // config never named. A configured `baseURL` still wins; the hosted
454
+ // default (the SDK's own) is the fallback.
455
+ baseURL: req.baseURL ?? "https://api.openai.com/v1",
428
456
  maxRetries: 0
429
457
  // Never silently retry (bounded cost).
430
458
  });
@@ -432,13 +460,13 @@ var openaiAdapter = {
432
460
  {
433
461
  model: req.model,
434
462
  messages: buildMessages2(req),
435
- // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.
463
+ // ONLY bounded fields. No `tools` / `stream` — by construction.
436
464
  ...req.maxTokens !== void 0 ? { max_tokens: req.maxTokens } : {}
437
465
  },
438
466
  {
439
467
  maxRetries: 0,
440
468
  // belt-and-suspenders: per-request as well as constructor.
441
- // Forward the caller's wall-clock bound (NFR-3) — same conditional
469
+ // Forward the caller's wall-clock bound — same conditional
442
470
  // spread as the other optional fields (only when a signal is present).
443
471
  ...req.signal ? { signal: req.signal } : {}
444
472
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/structured.ts","../src/complete.ts","../src/config.ts","../src/draft.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).\n//\n// This module is the ONLY retry site in the model layer (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 (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). \"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. 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).\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 (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: 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: 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. 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, 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).\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 is planned).\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); 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). 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: 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: `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. */\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 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","// draft.ts — bounded PRD drafting helper (debt-batch A, slice P).\n//\n// The PRD is the pre-SDD product artifact (`.noir/prd/PRD-<NNNN>-<taskId>-<slug>.md`) the\n// spec later `@import`s. This helper drafts it from the intake + clarification\n// Q&A + retrieved memory via ONE bounded `complete()` call (blueprint D5 —\n// single-shot, no tools/stream, provider-EXPLICIT). It mirrors the structure\n// the `noir-prd` skill documents (Problem · Evidence · Audience · Success\n// Criteria · Appetite/Mode · Proposed Direction · No-gos · Rabbit holes · Open\n// Questions) so a model-drafted PRD drops cleanly into the artifact the skill\n// writes via `@noir-ai/workflow`'s `writePrd`.\n//\n// Graceful degradation is FIRST-CLASS: when `complete()` returns `null`\n// (no provider configured, or a keyed provider's env var is missing) `draftPrd`\n// returns `null` too and the caller substitutes {@link PRD_FALLBACK_TEMPLATE}.\n// This is the always-available offline path — the full Noir test suite runs\n// with zero network. `draftPrd` itself NEVER throws; an attempted-call failure\n// (`{ ok: false }`) is surfaced to the caller as `null` after a structured\n// miss, distinct from a clean offline `null` only at the call site (the\n// caller's substitution is identical — the template — but a future miss-audit\n// sink can distinguish them).\n//\n// `draftSpec` (the sibling this mirrors) does NOT exist yet in @noir-ai/model —\n// slice P ships `draftPrd` first because the PRD is the new artifact kind; the\n// spec draft helper lands later and will follow the SAME shape (single bounded\n// `complete()` call, `string | null`, section template constant).\n\nimport { complete } from './complete.js';\nimport type { CompleteRequest, ModelConfig } from './types.js';\n\n/**\n * The inputs to a PRD draft — the same three signals the `noir-prd` skill\n * grounds in before drafting. `memory` is RETRIEVED context (never fabricated);\n * `clarify` is resolved clarification Q&A; `intake` is the raw intake notes.\n */\nexport interface DraftPrdInput {\n /** Raw intake notes (typically `.noir/intake/IN-<NNNN>-<taskId>.md`). Required. */\n intake: string;\n /** Resolved clarification Q&A, one entry per line (optional). */\n clarify?: string[];\n /** Retrieved memory context (optional; never fabricated — Evidence needs a source). */\n memory?: string;\n}\n\n/**\n * Options for a PRD draft call. `provider` + `model` are EXPLICIT (blueprint D5\n * — the provider is never inferred from env-var presence). `signal`\n * bounds wall-clock further; there is never a stream to cancel.\n */\nexport interface DraftPrdOptions {\n /** Provider block name (key into `cfg.providers`). Explicit, never inferred. */\n provider: string;\n /** Model id for this call (e.g. `claude-haiku`, `gpt-4o-mini`). */\n model: string;\n /** Optional abort signal to bound the call (single shot, no streaming). */\n signal?: AbortSignal;\n}\n\n/**\n * The canonical offline PRD section template (mirrors the `noir-prd` skill).\n * Callers substitute this when {@link draftPrd} returns `null` (no provider/key\n * configured). Every section is present with a `<fill in>` placeholder so a\n * human (or a later model-assisted pass once a provider is configured) can\n * complete it in place at `.noir/prd/PRD-<NNNN>-<taskId>-<slug>.md`.\n */\nexport const PRD_FALLBACK_TEMPLATE = `# PRD\n\n## Problem\n<fill in: what's broken or missing, in user terms>\n\n## Evidence\n<fill in: proof it's real — data, tickets, user reports. Never fabricate; cite a source.>\n\n## Audience\n<fill in: for whom>\n\n## Success Criteria\n<fill in: machine-verifiable, quantified thresholds — not \"fast\" or \"intuitive\">\n\n## Appetite / Mode\n<fill in: time-box; small batch or bet>\n\n## Proposed Direction\n<fill in: product-altitude solution sketch — not the technical design>\n\n## No-gos\n<fill in: explicitly out of scope — the highest-signal section>\n\n## Rabbit holes\n<fill in: known pitfalls to avoid>\n\n## Open Questions\n<fill in: unresolved items that need human input>\n`;\n\n// The 9 PRD sections in canonical order (mirrors PRD_FALLBACK_TEMPLATE). Used\n// both to build the prompt and to keep the template + prompt in sync (a single\n// edit point if the section list ever changes).\nconst PRD_SECTIONS = [\n 'Problem',\n 'Evidence',\n 'Audience',\n 'Success Criteria',\n 'Appetite / Mode',\n 'Proposed Direction',\n 'No-gos',\n 'Rabbit holes',\n 'Open Questions',\n] as const;\n\n/**\n * Build the user prompt for the bounded draft call. Kept pure (no I/O) so the\n * prompt is testable independently of `complete()`; the caller threads\n * `input.intake/clarify/memory` straight through. The system prompt instructs\n * the model to stay product-altitude, never fabricate Evidence, and emit the 9\n * sections in order so the result parses cleanly into the artifact shape.\n */\nfunction buildPrdPrompt(input: DraftPrdInput): string {\n const lines: string[] = [];\n lines.push('# Intake');\n lines.push(input.intake.trim() || '<no intake notes provided>');\n\n if (input.clarify && input.clarify.length > 0) {\n lines.push('');\n lines.push('# Clarification Q&A (resolved)');\n for (const q of input.clarify) {\n const trimmed = q.trim();\n if (trimmed) lines.push(`- ${trimmed}`);\n }\n }\n\n if (input.memory && input.memory.trim().length > 0) {\n lines.push('');\n lines.push('# Retrieved memory (grounding context — do not fabricate beyond this)');\n lines.push(input.memory.trim());\n }\n\n lines.push('');\n lines.push('# Required output');\n lines.push(\n `Draft a Product Requirements Document with EXACTLY these sections in order, each a level-2 markdown heading, no filler: ${PRD_SECTIONS.join(', ')}.`,\n );\n lines.push(\n 'Stay at product altitude (the spec later handles the technical design). For Evidence, cite the intake/memory verbatim or write \"<fill in: needs source>\" — NEVER fabricate a metric or ticket. Success Criteria must be machine-verifiable (a check an implementer can run), not adjectives.',\n );\n return lines.join('\\n');\n}\n\nconst PRD_SYSTEM_PROMPT =\n 'You are drafting a Noir Product Requirements Document (PRD): the pre-SDD ' +\n 'product artifact the technical spec later @imports. Output ONLY the markdown ' +\n 'PRD — no preamble, no closing remarks. The user message carries the intake, ' +\n 'clarification Q&A, and grounding memory; never fabricate Evidence beyond what ' +\n 'they provide.';\n\n/**\n * Draft a PRD via one bounded {@link complete} call.\n *\n * Returns:\n * - the drafted PRD text on success (`{ ok: true }` from `complete()`);\n * - `null` when no provider/key is configured OR an attempted call failed —\n * callers substitute {@link PRD_FALLBACK_TEMPLATE} in both cases (the offline\n * path is first-class; blueprint D5).\n *\n * Never throws — `complete()`'s adapter try/catch surfaces failures as\n * `{ ok: false, reason }`, which this helper collapses to `null` (a missed\n * draft is recoverable: the template lands and a later pass refines it). The\n * PRD itself is the caller's responsibility to write via `writePrd` — this\n * helper ONLY produces the body text.\n *\n * @example\n * ```ts\n * const body = await draftPrd(\n * { provider: 'anthropic', model: 'claude-haiku' },\n * { intake, clarify, memory },\n * resolveModelConfig(cfg.model),\n * );\n * writePrd(root, taskId, slug, body ?? PRD_FALLBACK_TEMPLATE);\n * ```\n */\nexport async function draftPrd(\n opts: DraftPrdOptions,\n input: DraftPrdInput,\n cfg: ModelConfig = {},\n): Promise<string | null> {\n const req: CompleteRequest = {\n provider: opts.provider,\n model: opts.model,\n system: PRD_SYSTEM_PROMPT,\n prompt: buildPrdPrompt(input),\n tier: 'draft',\n ...(opts.signal ? { signal: opts.signal } : {}),\n };\n const result = await complete(req, cfg);\n // null (offline) and `{ ok: false }` (attempted-call failure) both degrade to\n // null — the caller substitutes PRD_FALLBACK_TEMPLATE. Optional chain: when\n // `result` is null, `result?.ok` short-circuits to undefined → `!undefined`\n // is true, so the offline case is covered without a separate null check.\n if (!result?.ok) return null;\n return result.text;\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` / 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: `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: 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 { 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 // 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().\n maxRetries: 0, // 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 (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: `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/** Default wall-clock bound on the completion POST when the caller passes no\n * signal (the common path — `complete()`/consolidation/draft inject none). A\n * local compatible endpoint that accepts TCP but never answers (model loading,\n * hung gateway) must not hang the daemon MCP handler indefinitely; 120s is\n * generous for a single-shot completion. */\nconst OPENAI_COMPATIBLE_FETCH_TIMEOUT_MS = 120_000;\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). Default a bound when the caller passes none — this\n // is the only raw-fetch provider path and must not hang indefinitely.\n signal: req.signal ?? AbortSignal.timeout(OPENAI_COMPATIBLE_FETCH_TIMEOUT_MS),\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` / 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: `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 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 is forbidden. 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, // 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"]}
1
+ {"version":3,"sources":["../src/structured.ts","../src/complete.ts","../src/config.ts","../src/draft.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//\n// This module is the ONLY retry site in the model layer (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`, 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:\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 (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). \"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; raw content never reaches logs or usage).\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. 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).\n *\n * On success the validated object is returned as `value`, 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 (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.\n//\n// HARD RULES enforced here, by construction:\n//\n// - Provider-EXPLICIT, never silent paid calls: 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: 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: 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 the provider adapters fill: each adapter\n// module 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 provider-adapter 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 the\n// registry is small and tightly coupled to dispatch. `clearProviderAdapters`\n// 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// --- Credential resolution --------------------------------------------------\n//\n// Secrets live in env vars; config holds only the env-var NAME. 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// Bearer tokens follow the exact same name-indirection rule as the API key: the\n// config names the env var, the value is read from the overlaid `process.env`\n// at call time only. A provider that names a token but no key authenticates\n// with `Authorization: Bearer` instead of an API key.\nfunction resolveAuthToken(providerCfg: ProviderConfig): string | undefined {\n if (!providerCfg.authTokenEnv) return undefined;\n return process.env[providerCfg.authTokenEnv];\n}\n\n// --- Per-tier output caps --------------------------------------------------\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, 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 -----------------------------------------------------\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:\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, no SDK dependency).\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).\n * 3. credentials — `process.env[apiKeyEnv]` and/or `process.env[authTokenEnv]`;\n * `undefined` for an anonymous local provider that names neither. A provider\n * that names EITHER env var is keyed, and if NEITHER resolves to a value the\n * call degrades to `null` (the miss is observable via the `null` return; a\n * structured usage/miss sink is planned) — decided before any client exists,\n * so nothing can fill the credential in from the ambient environment.\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, the\n * provider-block `baseURL`, the resolved bearer token, and `timeoutMs` are\n * folded onto the dispatched request here.\n * 5. dispatch — one call (or two via the structured repair retry when `schema`\n * is set); 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. Credential resolution — env-var NAME → value; anonymous local providers\n // OK. A provider that names EITHER `apiKeyEnv` OR `authTokenEnv` is keyed:\n // if NEITHER resolves to a value, degrade to `null` before any client is\n // built (the ambient env can never fill in for a credential the config did\n // not ask for — that would be a silent paid call).\n const key = resolveKey(providerCfg);\n const authToken = resolveAuthToken(providerCfg);\n const isKeyed = Boolean(providerCfg.apiKeyEnv || providerCfg.authTokenEnv);\n if (isKeyed && !key && !authToken) 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 — the endpoint host (required by `openai-compatible`, honored by\n // the hosted adapters).\n // • authToken — the bearer-token VALUE resolved from `process.env[authTokenEnv]`\n // above; the config carries the name, the request carries the resolved value.\n // • timeoutMs — per-request timeout in milliseconds, forwarded verbatim.\n // • maxTokens — apply the per-tier default ONLY when the caller omitted it AND\n // 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 ...(authToken !== undefined ? { authToken } : {}),\n ...(providerCfg.timeoutMs !== undefined ? { timeoutMs: providerCfg.timeoutMs } : {}),\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). 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.\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.\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. The\n// 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: 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: `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// `authTokenEnv` travels through as a NAME only — whether it is set feeds\n// `hasKey` (a boolean, never the value); the value itself is read at call\n// time in `complete()`, alongside the key it already resolves there.\n\n/**\n * User-facing model config shape — mirrors `NoirConfig['model']` (the zod block\n * @noir-ai/core ships). 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 /** Env-var NAME holding a bearer token (Authorization: Bearer); NAME only. */\n authTokenEnv?: string;\n /** Per-request timeout in milliseconds; omit for the adapter default. */\n timeoutMs?: number;\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. */\n apiKeyEnv?: string;\n /** Env-var NAME passthrough for the bearer token (same rule as apiKeyEnv). */\n authTokenEnv?: string;\n /** Per-request timeout passthrough (milliseconds). */\n timeoutMs?: number;\n /** VALUE resolved from `process.env[apiKeyEnv]`; `undefined` if anonymous or unset. */\n apiKey?: string;\n /**\n * Readiness signal for `noir doctor`: whether a keyed provider has at least\n * one of its named credential env vars set (`apiKeyEnv` and/or `authTokenEnv`);\n * an ANONYMOUS provider (neither named) is always `true` — no credential is\n * required, so nothing is missing. Carries a boolean ONLY, never the value.\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).\n * - Each provider's API key is materialized from `process.env[apiKeyEnv]` into\n * `apiKey`; `hasKey` is `true` when a keyed provider has at least one of its\n * named credential env vars set (doctor surfaces a miss; `complete()` returns\n * `null`).\n * - Anonymous providers (neither `apiKeyEnv` nor `authTokenEnv`) keep\n * `apiKey: undefined` but report `hasKey: true` (ready — no key needed, e.g.\n * local Ollama).\n *\n * This mapper NEVER infers a provider from env-var presence 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 const authTokenEnv = entry.authTokenEnv;\n // A keyed provider names at least one credential env var. It is ready\n // when AT LEAST ONE of those vars holds a value — the model layer sends\n // whichever resolved. Anonymous providers (neither named) need no\n // credential, so they are ready by default.\n const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;\n const authToken = authTokenEnv ? process.env[authTokenEnv] : undefined;\n const keyed = Boolean(apiKeyEnv) || Boolean(authTokenEnv);\n // A credential set to the empty string is \"unset\", matching the runtime\n // path (complete.ts treats a falsy key as absent) so doctor and the model\n // layer never disagree about readiness.\n const hasKey =\n !keyed ||\n (apiKey !== undefined && apiKey !== '') ||\n (authToken !== undefined && authToken !== '');\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 (entry.authTokenEnv !== undefined) resolved.authTokenEnv = entry.authTokenEnv;\n if (entry.timeoutMs !== undefined) resolved.timeoutMs = entry.timeoutMs;\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","// draft.ts — bounded PRD drafting helper.\n//\n// The PRD is the pre-SDD product artifact (`.noir/prd/PRD-<NNNN>-<taskId>-<slug>.md`) the\n// spec later `@import`s. This helper drafts it from the intake + clarification\n// Q&A + retrieved memory via ONE bounded `complete()` call (single-shot, no\n// tools/stream, provider-EXPLICIT). It mirrors the structure\n// the `noir-prd` skill documents (Problem · Evidence · Audience · Success\n// Criteria · Appetite/Mode · Proposed Direction · No-gos · Rabbit holes · Open\n// Questions) so a model-drafted PRD drops cleanly into the artifact the skill\n// writes via `@noir-ai/workflow`'s `writePrd`.\n//\n// Graceful degradation is FIRST-CLASS: when `complete()` returns `null`\n// (no provider configured, or a keyed provider's env var is missing) `draftPrd`\n// returns `null` too and the caller substitutes {@link PRD_FALLBACK_TEMPLATE}.\n// This is the always-available offline path — the full Noir test suite runs\n// with zero network. `draftPrd` itself NEVER throws; an attempted-call failure\n// (`{ ok: false }`) is surfaced to the caller as `null` after a structured\n// miss, distinct from a clean offline `null` only at the call site (the\n// caller's substitution is identical — the template — but a future miss-audit\n// sink can distinguish them).\n//\n// `draftSpec` (the sibling this mirrors) does NOT exist yet in @noir-ai/model —\n// the PRD draft helper ships first because the PRD is the new artifact kind; the\n// spec draft helper lands later and will follow the SAME shape (single bounded\n// `complete()` call, `string | null`, section template constant).\n\nimport { complete } from './complete.js';\nimport type { CompleteRequest, ModelConfig } from './types.js';\n\n/**\n * The inputs to a PRD draft — the same three signals the `noir-prd` skill\n * grounds in before drafting. `memory` is RETRIEVED context (never fabricated);\n * `clarify` is resolved clarification Q&A; `intake` is the raw intake notes.\n */\nexport interface DraftPrdInput {\n /** Raw intake notes (typically `.noir/intake/IN-<NNNN>-<taskId>.md`). Required. */\n intake: string;\n /** Resolved clarification Q&A, one entry per line (optional). */\n clarify?: string[];\n /** Retrieved memory context (optional; never fabricated — Evidence needs a source). */\n memory?: string;\n}\n\n/**\n * Options for a PRD draft call. `provider` + `model` are EXPLICIT — the\n * provider is never inferred from env-var presence. `signal`\n * bounds wall-clock further; there is never a stream to cancel.\n */\nexport interface DraftPrdOptions {\n /** Provider block name (key into `cfg.providers`). Explicit, never inferred. */\n provider: string;\n /** Model id for this call (e.g. `claude-haiku`, `gpt-4o-mini`). */\n model: string;\n /** Optional abort signal to bound the call (single shot, no streaming). */\n signal?: AbortSignal;\n}\n\n/**\n * The canonical offline PRD section template (mirrors the `noir-prd` skill).\n * Callers substitute this when {@link draftPrd} returns `null` (no provider/key\n * configured). Every section is present with a `<fill in>` placeholder so a\n * human (or a later model-assisted pass once a provider is configured) can\n * complete it in place at `.noir/prd/PRD-<NNNN>-<taskId>-<slug>.md`.\n */\nexport const PRD_FALLBACK_TEMPLATE = `# PRD\n\n## Problem\n<fill in: what's broken or missing, in user terms>\n\n## Evidence\n<fill in: proof it's real — data, tickets, user reports. Never fabricate; cite a source.>\n\n## Audience\n<fill in: for whom>\n\n## Success Criteria\n<fill in: machine-verifiable, quantified thresholds — not \"fast\" or \"intuitive\">\n\n## Appetite / Mode\n<fill in: time-box; small batch or bet>\n\n## Proposed Direction\n<fill in: product-altitude solution sketch — not the technical design>\n\n## No-gos\n<fill in: explicitly out of scope — the highest-signal section>\n\n## Rabbit holes\n<fill in: known pitfalls to avoid>\n\n## Open Questions\n<fill in: unresolved items that need human input>\n`;\n\n// The 9 PRD sections in canonical order (mirrors PRD_FALLBACK_TEMPLATE). Used\n// both to build the prompt and to keep the template + prompt in sync (a single\n// edit point if the section list ever changes).\nconst PRD_SECTIONS = [\n 'Problem',\n 'Evidence',\n 'Audience',\n 'Success Criteria',\n 'Appetite / Mode',\n 'Proposed Direction',\n 'No-gos',\n 'Rabbit holes',\n 'Open Questions',\n] as const;\n\n/**\n * Build the user prompt for the bounded draft call. Kept pure (no I/O) so the\n * prompt is testable independently of `complete()`; the caller threads\n * `input.intake/clarify/memory` straight through. The system prompt instructs\n * the model to stay product-altitude, never fabricate Evidence, and emit the 9\n * sections in order so the result parses cleanly into the artifact shape.\n */\nfunction buildPrdPrompt(input: DraftPrdInput): string {\n const lines: string[] = [];\n lines.push('# Intake');\n lines.push(input.intake.trim() || '<no intake notes provided>');\n\n if (input.clarify && input.clarify.length > 0) {\n lines.push('');\n lines.push('# Clarification Q&A (resolved)');\n for (const q of input.clarify) {\n const trimmed = q.trim();\n if (trimmed) lines.push(`- ${trimmed}`);\n }\n }\n\n if (input.memory && input.memory.trim().length > 0) {\n lines.push('');\n lines.push('# Retrieved memory (grounding context — do not fabricate beyond this)');\n lines.push(input.memory.trim());\n }\n\n lines.push('');\n lines.push('# Required output');\n lines.push(\n `Draft a Product Requirements Document with EXACTLY these sections in order, each a level-2 markdown heading, no filler: ${PRD_SECTIONS.join(', ')}.`,\n );\n lines.push(\n 'Stay at product altitude (the spec later handles the technical design). For Evidence, cite the intake/memory verbatim or write \"<fill in: needs source>\" — NEVER fabricate a metric or ticket. Success Criteria must be machine-verifiable (a check an implementer can run), not adjectives.',\n );\n return lines.join('\\n');\n}\n\nconst PRD_SYSTEM_PROMPT =\n 'You are drafting a Noir Product Requirements Document (PRD): the pre-SDD ' +\n 'product artifact the technical spec later @imports. Output ONLY the markdown ' +\n 'PRD — no preamble, no closing remarks. The user message carries the intake, ' +\n 'clarification Q&A, and grounding memory; never fabricate Evidence beyond what ' +\n 'they provide.';\n\n/**\n * Draft a PRD via one bounded {@link complete} call.\n *\n * Returns:\n * - the drafted PRD text on success (`{ ok: true }` from `complete()`);\n * - `null` when no provider/key is configured OR an attempted call failed —\n * callers substitute {@link PRD_FALLBACK_TEMPLATE} in both cases (the offline\n * path is first-class).\n *\n * Never throws — `complete()`'s adapter try/catch surfaces failures as\n * `{ ok: false, reason }`, which this helper collapses to `null` (a missed\n * draft is recoverable: the template lands and a later pass refines it). The\n * PRD itself is the caller's responsibility to write via `writePrd` — this\n * helper ONLY produces the body text.\n *\n * @example\n * ```ts\n * const body = await draftPrd(\n * { provider: 'anthropic', model: 'claude-haiku' },\n * { intake, clarify, memory },\n * resolveModelConfig(cfg.model),\n * );\n * writePrd(root, taskId, slug, body ?? PRD_FALLBACK_TEMPLATE);\n * ```\n */\nexport async function draftPrd(\n opts: DraftPrdOptions,\n input: DraftPrdInput,\n cfg: ModelConfig = {},\n): Promise<string | null> {\n const req: CompleteRequest = {\n provider: opts.provider,\n model: opts.model,\n system: PRD_SYSTEM_PROMPT,\n prompt: buildPrdPrompt(input),\n tier: 'draft',\n ...(opts.signal ? { signal: opts.signal } : {}),\n };\n const result = await complete(req, cfg);\n // null (offline) and `{ ok: false }` (attempted-call failure) both degrade to\n // null — the caller substitutes PRD_FALLBACK_TEMPLATE. Optional chain: when\n // `result` is null, `result?.ok` short-circuits to undefined → `!undefined`\n // is true, so the offline case is covered without a separate null check.\n if (!result?.ok) return null;\n return result.text;\n}\n","// Anthropic provider adapter — single-shot Messages call via `@anthropic-ai/sdk`.\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT: 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`): 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.\n// - IMPORT-ISOLATED: 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: `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`, and `req.authToken` likewise carries the\n// value read from `process.env[authTokenEnv]`; this module never touches\n// `process.env` and never logs either value. Only token COUNTS leave via\n// `usage`.\n// - NO SILENT PAID CALLS: every transport-affecting client option (`apiKey`,\n// `authToken`, `baseURL`) is pinned explicitly, so the SDK never falls back to\n// `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` in the\n// ambient env — such a fallback would be a silent paid call to a host the\n// config did not choose. When neither credential is present the adapter\n// returns `{ ok: false }` instead of constructing a client. (`complete()`\n// normally degrades to `null` for a keyed provider whose env vars are missing,\n// so reaching here without either credential means the provider block was\n// wired without `apiKeyEnv`/`authTokenEnv` — a recoverable misconfiguration,\n// 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// 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 { 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 (import isolation). The shape is exactly what we use: a\n// constructor taking the client options below 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\n// `apiKey` / `authToken` accept `null` — the SDK only reads its env fallbacks\n// when a credential is `undefined`, so an absent credential is pinned to `null`\n// to keep `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` out of the request.\ntype AnthropicSDK = new (opts: {\n apiKey?: string | null;\n authToken?: string | null;\n baseURL?: string;\n timeout?: number;\n maxRetries: number;\n}) => AnthropicClient;\n\n// Anthropic's Messages API REQUIRES `max_tokens` (unlike OpenAI, where it is\n// optional). The per-tier caps (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 wraps this adapter when a `schema` is present.\n */\nexport const anthropicAdapter: ProviderAdapter = {\n name: 'anthropic',\n complete: async (req, key): Promise<CompleteResult> => {\n // Defense: Anthropic is a hosted, keyed provider. If NEITHER `key` nor\n // `req.authToken` is present the provider block was wired without a\n // credential env var; refuse rather than let the SDK fall back to the\n // ambient env (a silent paid call, or a request to a host the config did\n // not choose). complete() degrades to null for a keyed provider whose env\n // vars are missing, so reaching here without either is a wiring fault —\n // surface it, never charge silently.\n if (!key && !req.authToken) {\n return {\n ok: false,\n reason: 'anthropic: missing credentials (provider block has no apiKeyEnv or authTokenEnv)',\n };\n }\n\n try {\n // Dynamic import — a bundle that never selects the `anthropic` adapter\n // ships no `@anthropic-ai/sdk` dependency. `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 // Pin every transport-affecting option explicitly: the SDK reads\n // ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN / ANTHROPIC_BASE_URL only\n // when the matching option is `undefined`, so an absent credential is\n // set to `null` (never left undefined) and an absent base URL to the\n // real hosted default. Ambient env therefore cannot route this call.\n apiKey: key || null, // the env VALUE resolved by complete().\n authToken: req.authToken || null, // resolved bearer token (or null).\n baseURL: req.baseURL ?? 'https://api.anthropic.com',\n ...(req.timeoutMs !== undefined ? { timeout: req.timeoutMs } : {}),\n maxRetries: 0, // Never silently retry (bounded cost).\n });\n\n // Single bounded Messages call. The body carries ONLY bounded fields — no\n // `tools`, no `stream`. `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.\nregisterProviderAdapter('anthropic', anthropicAdapter);\n","// OpenAI-COMPATIBLE provider adapter — single-shot chat completions over the\n// GLOBAL `fetch`. 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: 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: raw `fetch` is a single shot;\n// the only retry lives in the structured path. Uses the GLOBAL `fetch`\n// (Node ≥20, per `engines.node \">=20\"`) — zero added dependency.\n// - SECRETS stay in env: `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/** Default wall-clock bound on the completion POST when the caller passes no\n * signal (the common path — `complete()`/consolidation/draft inject none). A\n * local compatible endpoint that accepts TCP but never answers (model loading,\n * hung gateway) must not hang the daemon MCP handler indefinitely; 120s is\n * generous for a single-shot completion. */\nconst OPENAI_COMPATIBLE_FETCH_TIMEOUT_MS = 120_000;\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 // 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). Default a bound when the caller passes none — this\n // is the only raw-fetch provider path and must not hang indefinitely.\n signal: req.signal ?? AbortSignal.timeout(OPENAI_COMPATIBLE_FETCH_TIMEOUT_MS),\n });\n\n if (!res.ok) {\n // 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 `complete()`'s job.)\nregisterProviderAdapter('openai-compatible', openaiCompatibleAdapter);\n","// OpenAI provider adapter — single-shot chat completions via the `openai` SDK.\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT: 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`): 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.\n// - IMPORT-ISOLATED: the `openai` SDK is imported DYNAMICALLY 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: `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. 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 and\n // re-assert no retries 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 is forbidden. Anonymous LOCAL endpoints belong to the\n // dedicated `openai-compatible` adapter, not this one. This guards the\n // CREDENTIAL only; the endpoint is pinned at the client below, so neither\n // transport-affecting option is ever left for the environment to answer.\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. `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 // Pin the endpoint explicitly, exactly as the anthropic adapter does:\n // the SDK reads OPENAI_BASE_URL ONLY when `baseURL` is `undefined`, so\n // omitting it would hand the endpoint to the ambient environment — a\n // committable `.noir/.env` could then route every call to a host the\n // config never named. A configured `baseURL` still wins; the hosted\n // default (the SDK's own) is the fallback.\n baseURL: req.baseURL ?? 'https://api.openai.com/v1',\n maxRetries: 0, // 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 // 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 — 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.\nregisterProviderAdapter('openai', openaiAdapter);\n"],"mappings":"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","names":["buildMessages"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noir-ai/model",
3
- "version": "1.14.0",
3
+ "version": "1.15.0-beta.1",
4
4
  "description": "Noir model — optional bounded single-shot LLM layer (anthropic / openai / openai-compatible); agent loops are impossible by design.",
5
5
  "license": "MIT",
6
6
  "author": "agaaaptr",