@noir-ai/model 1.0.0-beta.1 → 1.2.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 +62 -1
- package/dist/index.js +85 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -263,4 +263,65 @@ interface ResolvedModelConfig {
|
|
|
263
263
|
*/
|
|
264
264
|
declare function resolveModelConfig(raw?: ModelUserConfig): ResolvedModelConfig;
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
/**
|
|
267
|
+
* The inputs to a PRD draft — the same three signals the `noir-prd` skill
|
|
268
|
+
* grounds in before drafting. `memory` is RETRIEVED context (never fabricated);
|
|
269
|
+
* `clarify` is resolved clarification Q&A; `intake` is the raw intake notes.
|
|
270
|
+
*/
|
|
271
|
+
interface DraftPrdInput {
|
|
272
|
+
/** Raw intake notes (typically `.noir/intake/<taskId>.md`). Required. */
|
|
273
|
+
intake: string;
|
|
274
|
+
/** Resolved clarification Q&A, one entry per line (optional). */
|
|
275
|
+
clarify?: string[];
|
|
276
|
+
/** Retrieved memory context (optional; never fabricated — Evidence needs a source). */
|
|
277
|
+
memory?: string;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Options for a PRD draft call. `provider` + `model` are EXPLICIT (blueprint D5
|
|
281
|
+
* / DS-6 — the provider is never inferred from env-var presence). `signal`
|
|
282
|
+
* bounds wall-clock further; there is never a stream to cancel.
|
|
283
|
+
*/
|
|
284
|
+
interface DraftPrdOptions {
|
|
285
|
+
/** Provider block name (key into `cfg.providers`). Explicit, never inferred. */
|
|
286
|
+
provider: string;
|
|
287
|
+
/** Model id for this call (e.g. `claude-haiku`, `gpt-4o-mini`). */
|
|
288
|
+
model: string;
|
|
289
|
+
/** Optional abort signal to bound the call (single shot, no streaming). */
|
|
290
|
+
signal?: AbortSignal;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The canonical offline PRD section template (mirrors the `noir-prd` skill).
|
|
294
|
+
* Callers substitute this when {@link draftPrd} returns `null` (no provider/key
|
|
295
|
+
* configured). Every section is present with a `<fill in>` placeholder so a
|
|
296
|
+
* human (or a later model-assisted pass once a provider is configured) can
|
|
297
|
+
* complete it in place at `.noir/prd/<taskId>-<slug>.md`.
|
|
298
|
+
*/
|
|
299
|
+
declare 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 \u2014 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 \u2014 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 \u2014 not the technical design>\n\n## No-gos\n<fill in: explicitly out of scope \u2014 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";
|
|
300
|
+
/**
|
|
301
|
+
* Draft a PRD via one bounded {@link complete} call.
|
|
302
|
+
*
|
|
303
|
+
* Returns:
|
|
304
|
+
* - the drafted PRD text on success (`{ ok: true }` from `complete()`);
|
|
305
|
+
* - `null` when no provider/key is configured OR an attempted call failed —
|
|
306
|
+
* callers substitute {@link PRD_FALLBACK_TEMPLATE} in both cases (the offline
|
|
307
|
+
* path is first-class; blueprint D5 / DS-5).
|
|
308
|
+
*
|
|
309
|
+
* Never throws — `complete()`'s adapter try/catch surfaces failures as
|
|
310
|
+
* `{ ok: false, reason }`, which this helper collapses to `null` (a missed
|
|
311
|
+
* draft is recoverable: the template lands and a later pass refines it). The
|
|
312
|
+
* PRD itself is the caller's responsibility to write via `writePrd` — this
|
|
313
|
+
* helper ONLY produces the body text.
|
|
314
|
+
*
|
|
315
|
+
* @example
|
|
316
|
+
* ```ts
|
|
317
|
+
* const body = await draftPrd(
|
|
318
|
+
* { provider: 'anthropic', model: 'claude-haiku' },
|
|
319
|
+
* { intake, clarify, memory },
|
|
320
|
+
* resolveModelConfig(cfg.model),
|
|
321
|
+
* );
|
|
322
|
+
* writePrd(root, taskId, slug, body ?? PRD_FALLBACK_TEMPLATE);
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
declare function draftPrd(opts: DraftPrdOptions, input: DraftPrdInput, cfg?: ModelConfig): Promise<string | null>;
|
|
326
|
+
|
|
327
|
+
export { type CompleteRequest, type CompleteResult, type CompleteSchema, type CompleteUsage, type DraftPrdInput, type DraftPrdOptions, type ModelConfig, type ModelProviderEntry, type ModelUserConfig, PRD_FALLBACK_TEMPLATE, type ProviderAdapter, type ProviderConfig, type ResolvedModelConfig, type ResolvedProviderConfig, type ResolvedTiers, TIER_MAX_TOKENS, type Tier, clearProviderAdapters, complete, draftPrd, getProviderAdapter, registerProviderAdapter, resolveModelConfig };
|
package/dist/index.js
CHANGED
|
@@ -203,6 +203,89 @@ function resolveModelConfig(raw) {
|
|
|
203
203
|
return result;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// src/draft.ts
|
|
207
|
+
var PRD_FALLBACK_TEMPLATE = `# PRD
|
|
208
|
+
|
|
209
|
+
## Problem
|
|
210
|
+
<fill in: what's broken or missing, in user terms>
|
|
211
|
+
|
|
212
|
+
## Evidence
|
|
213
|
+
<fill in: proof it's real \u2014 data, tickets, user reports. Never fabricate; cite a source.>
|
|
214
|
+
|
|
215
|
+
## Audience
|
|
216
|
+
<fill in: for whom>
|
|
217
|
+
|
|
218
|
+
## Success Criteria
|
|
219
|
+
<fill in: machine-verifiable, quantified thresholds \u2014 not "fast" or "intuitive">
|
|
220
|
+
|
|
221
|
+
## Appetite / Mode
|
|
222
|
+
<fill in: time-box; small batch or bet>
|
|
223
|
+
|
|
224
|
+
## Proposed Direction
|
|
225
|
+
<fill in: product-altitude solution sketch \u2014 not the technical design>
|
|
226
|
+
|
|
227
|
+
## No-gos
|
|
228
|
+
<fill in: explicitly out of scope \u2014 the highest-signal section>
|
|
229
|
+
|
|
230
|
+
## Rabbit holes
|
|
231
|
+
<fill in: known pitfalls to avoid>
|
|
232
|
+
|
|
233
|
+
## Open Questions
|
|
234
|
+
<fill in: unresolved items that need human input>
|
|
235
|
+
`;
|
|
236
|
+
var PRD_SECTIONS = [
|
|
237
|
+
"Problem",
|
|
238
|
+
"Evidence",
|
|
239
|
+
"Audience",
|
|
240
|
+
"Success Criteria",
|
|
241
|
+
"Appetite / Mode",
|
|
242
|
+
"Proposed Direction",
|
|
243
|
+
"No-gos",
|
|
244
|
+
"Rabbit holes",
|
|
245
|
+
"Open Questions"
|
|
246
|
+
];
|
|
247
|
+
function buildPrdPrompt(input) {
|
|
248
|
+
const lines = [];
|
|
249
|
+
lines.push("# Intake");
|
|
250
|
+
lines.push(input.intake.trim() || "<no intake notes provided>");
|
|
251
|
+
if (input.clarify && input.clarify.length > 0) {
|
|
252
|
+
lines.push("");
|
|
253
|
+
lines.push("# Clarification Q&A (resolved)");
|
|
254
|
+
for (const q of input.clarify) {
|
|
255
|
+
const trimmed = q.trim();
|
|
256
|
+
if (trimmed) lines.push(`- ${trimmed}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
if (input.memory && input.memory.trim().length > 0) {
|
|
260
|
+
lines.push("");
|
|
261
|
+
lines.push("# Retrieved memory (grounding context \u2014 do not fabricate beyond this)");
|
|
262
|
+
lines.push(input.memory.trim());
|
|
263
|
+
}
|
|
264
|
+
lines.push("");
|
|
265
|
+
lines.push("# Required output");
|
|
266
|
+
lines.push(
|
|
267
|
+
`Draft a Product Requirements Document with EXACTLY these sections in order, each a level-2 markdown heading, no filler: ${PRD_SECTIONS.join(", ")}.`
|
|
268
|
+
);
|
|
269
|
+
lines.push(
|
|
270
|
+
'Stay at product altitude (the spec later handles the technical design). For Evidence, cite the intake/memory verbatim or write "<fill in: needs source>" \u2014 NEVER fabricate a metric or ticket. Success Criteria must be machine-verifiable (a check an implementer can run), not adjectives.'
|
|
271
|
+
);
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
var PRD_SYSTEM_PROMPT = "You are drafting a Noir Product Requirements Document (PRD): the pre-SDD product artifact the technical spec later @imports. Output ONLY the markdown PRD \u2014 no preamble, no closing remarks. The user message carries the intake, clarification Q&A, and grounding memory; never fabricate Evidence beyond what they provide.";
|
|
275
|
+
async function draftPrd(opts, input, cfg = {}) {
|
|
276
|
+
const req = {
|
|
277
|
+
provider: opts.provider,
|
|
278
|
+
model: opts.model,
|
|
279
|
+
system: PRD_SYSTEM_PROMPT,
|
|
280
|
+
prompt: buildPrdPrompt(input),
|
|
281
|
+
tier: "draft",
|
|
282
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
283
|
+
};
|
|
284
|
+
const result = await complete(req, cfg);
|
|
285
|
+
if (!result?.ok) return null;
|
|
286
|
+
return result.text;
|
|
287
|
+
}
|
|
288
|
+
|
|
206
289
|
// src/providers/anthropic.ts
|
|
207
290
|
var DEFAULT_MAX_TOKENS = 2048;
|
|
208
291
|
var anthropicAdapter = {
|
|
@@ -381,9 +464,11 @@ var openaiAdapter = {
|
|
|
381
464
|
};
|
|
382
465
|
registerProviderAdapter("openai", openaiAdapter);
|
|
383
466
|
export {
|
|
467
|
+
PRD_FALLBACK_TEMPLATE,
|
|
384
468
|
TIER_MAX_TOKENS,
|
|
385
469
|
clearProviderAdapters,
|
|
386
470
|
complete,
|
|
471
|
+
draftPrd,
|
|
387
472
|
getProviderAdapter,
|
|
388
473
|
registerProviderAdapter,
|
|
389
474
|
resolveModelConfig
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/structured.ts","../src/complete.ts","../src/config.ts","../src/providers/anthropic.ts","../src/providers/openai-compatible.ts","../src/providers/openai.ts"],"sourcesContent":["// Structured output — prompt-based JSON + validate + at most ONE repair retry\n// (slice S8 / t4, blueprint D5 / DS-4).\n//\n// This module is the ONLY retry site in the model layer (DS-12: SDK retries are\n// 0; bounded wall-clock + cost). It does NOT call a provider directly — it is\n// given a resolved {@link ProviderAdapter} by `complete()` and orchestrates the\n// JSON round-trip on top of the adapter's single-shot `complete()`. Because the\n// adapter surface has no `tools` / `stream` (FR-8), this path cannot mutate into\n// an agent loop either: it makes at most TWO adapter calls (initial + one\n// repair), parses + validates each, and returns.\n//\n// Strategy (DS-4 / FR-3, v1 — provider-native strict modes deferred):\n// 1. Inject a \"respond with ONLY JSON matching the schema\" system addendum.\n// 2. Call the adapter once (single shot).\n// 3. Extract JSON from the text (tolerant: direct, markdown-fence, span).\n// 4. Validate against `req.schema` — a ZodType (`.parse`) or a function.\n// 5. On parse/validate failure, retry ONCE with the error fed back.\n// 6. Still bad ⇒ `{ ok: false, reason: \"schema-validation-failed: …\" }`.\n//\n// NO zod is imported at runtime here (NFR-2 / the types.ts contract): validation\n// calls `.parse` ON the caller-supplied schema object, so the built library has\n// no value-level zod dependency. A best-effort `.description` (if the ZodType\n// carries one) is the only schema introspection — we never serialize the shape,\n// so the caller's prompt is expected to describe the desired JSON; `schema` is\n// the validator, not the spec.\n\nimport type { CompleteRequest, CompleteResult, CompleteSchema, ProviderAdapter } from './types.js';\n\n// --- Prompt construction ----------------------------------------------------\n\n// The JSON contract appended to the system prompt. Strong + specific so the\n// model emits parseable JSON without relying on a provider-native JSON mode\n// (deferred per DS-4). \"single valid JSON value\" (not \"object\") so an array or\n// scalar schema is also honored.\nconst JSON_INSTRUCTION =\n 'Respond with ONLY a single valid JSON value that matches the requested schema. ' +\n 'Do not include markdown, code fences, commentary, or any surrounding prose — ' +\n 'output the JSON and nothing else.';\n\n/**\n * Best-effort schema hint for the prompt. A ZodType MAY carry a `.description`\n * (set via `z.desc(...)` / `.meta({ description })`); a validator function\n * carries none. We never import zod at runtime, so we do NOT serialize the full\n * shape — the caller's prompt describes the desired JSON; `schema` validates.\n * Returns the trimmed description, or `''` when none is available.\n */\nfunction schemaDescription(schema: CompleteSchema | undefined): string {\n if (!schema) return '';\n const desc = (schema as { description?: unknown }).description;\n return typeof desc === 'string' && desc.trim().length > 0 ? desc.trim() : '';\n}\n\n/** Append the JSON instruction (and any schema description) to the system prompt. */\nfunction withJsonInstruction(req: CompleteRequest): CompleteRequest {\n const hint = schemaDescription(req.schema);\n const addendum = hint ? `${JSON_INSTRUCTION}\\n\\nJSON must satisfy: ${hint}` : JSON_INSTRUCTION;\n const system = req.system ? `${req.system}\\n\\n${addendum}` : addendum;\n return { ...req, system };\n}\n\n/** Bound a snippet so a runaway model output cannot blow up the corrective prompt. */\nfunction truncate(s: string, max: number): string {\n return s.length > max ? `${s.slice(0, max)}…` : s;\n}\n\n/**\n * Build the ONE repair retry: re-state the JSON contract, then surface the\n * precise parse/validate error and the offending output so the model can fix it.\n * The original task (`prompt`) is preserved verbatim — only the system gains the\n * correction, so the retry is still a bounded single shot at the SAME task.\n */\nfunction withCorrectiveInstruction(\n req: CompleteRequest,\n error: string,\n badOutput: string,\n): CompleteRequest {\n const base = withJsonInstruction(req);\n const correction =\n `Your previous response failed validation and was NOT valid JSON matching the schema.\\n\\n` +\n `Error: ${error}\\n\\n` +\n `Previous output (do NOT repeat it):\\n${truncate(badOutput, 500)}\\n\\n` +\n `Return ONLY valid JSON matching the schema — no markdown, no prose.`;\n const system = base.system ? `${base.system}\\n\\n${correction}` : correction;\n return { ...base, system };\n}\n\n// --- JSON extraction (tolerant) ---------------------------------------------\n\ntype ParseOutcome = { ok: true; value: unknown } | { ok: false; error: string };\n\n/** `JSON.parse` with a typed outcome (never throws). */\nfunction tryJSON(s: string): ParseOutcome {\n try {\n return { ok: true, value: JSON.parse(s) };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { ok: false, error: msg };\n }\n}\n\n/**\n * Extract a JSON value from a model's free-form text. Tries, in order:\n * 1. the trimmed text directly (the model obeyed),\n * 2. the inside of a ```json …``` markdown fence (a common deviation),\n * 3. the outermost `{ … }` or `[ … ]` span (prose around the JSON).\n *\n * Returns the first parse success, else the parse error from the last attempt.\n * This tolerance keeps the ≤1 retry budget for genuine validation failures\n * rather than spending it on a stray code fence.\n */\nfunction extractJSON(text: string): ParseOutcome {\n const trimmed = text.trim();\n\n // 1. Direct.\n const direct = tryJSON(trimmed);\n if (direct.ok) return direct;\n\n // 2. Markdown code fence (optional `json`/`JSON` lang tag). Capture the group\n // into a const so the truthy guard narrows it (a re-indexed `fence[1]` would\n // read as `string | undefined` under noUncheckedIndexedAccess).\n const fenced = trimmed.match(/```(?:json|JSON)?\\s*([\\s\\S]*?)```/)?.[1];\n if (fenced) {\n const inner = tryJSON(fenced.trim());\n if (inner.ok) return inner;\n }\n\n // 3. Outermost object/array span — collect valid candidates, shortest first\n // (a tighter valid span beats a looser one that happens to balance).\n const candidates: string[] = [];\n const objStart = trimmed.indexOf('{');\n const objEnd = trimmed.lastIndexOf('}');\n const arrStart = trimmed.indexOf('[');\n const arrEnd = trimmed.lastIndexOf(']');\n if (objStart !== -1 && objEnd > objStart) candidates.push(trimmed.slice(objStart, objEnd + 1));\n if (arrStart !== -1 && arrEnd > arrStart) candidates.push(trimmed.slice(arrStart, arrEnd + 1));\n candidates.sort((a, b) => a.length - b.length);\n for (const cand of candidates) {\n const parsed = tryJSON(cand);\n if (parsed.ok) return parsed;\n }\n\n // No extraction worked — surface a short, safe excerpt (never the whole prompt\n // body, which could be large; NFR-4 keeps usage/logs free of raw content).\n const excerpt = truncate(trimmed.replace(/\\s+/g, ' '), 120);\n return { ok: false, error: `response was not valid JSON: ${JSON.stringify(excerpt)}` };\n}\n\n// --- Schema validation ------------------------------------------------------\n\n/**\n * Validate `raw` against a caller-supplied schema WITHOUT importing zod. A\n * ZodType exposes `.parse(input)` (throws on invalid); a function schema IS the\n * validator (throw on invalid). A throw ⇒ invalid; a returned value ⇒ the\n * coerced/validated value to hand back to the caller.\n *\n * `typeof === 'function'` cleanly splits the union: a `ZodType` instance is an\n * object (it has `.parse`, no call signature), so the function branch is the\n * validator-function member and the fall-through is the ZodType member. The\n * ZodType `.parse` is invoked via method syntax (`schema.parse(raw)`) so its\n * internal `this` binding is preserved — extracting `const p = schema.parse;\n * p(raw)` would lose `this` and break zod's internal state reads.\n */\nfunction validateSchema(schema: CompleteSchema, raw: unknown): unknown {\n if (typeof schema === 'function') {\n return schema(raw);\n }\n return schema.parse(raw);\n}\n\n/** Extract + validate in one step; never throws. */\nfunction parseAndValidate(text: string, schema: CompleteSchema): ParseOutcome {\n const extracted = extractJSON(text);\n if (!extracted.ok) return extracted;\n try {\n return { ok: true, value: validateSchema(schema, extracted.value) };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { ok: false, error: `schema validation failed: ${msg}` };\n }\n}\n\n// --- The structured round-trip ----------------------------------------------\n\n/**\n * Run the structured (prompt-JSON) flow against a resolved adapter.\n *\n * Makes at most TWO adapter calls: an initial attempt, plus ONE repair retry on\n * parse/validate failure (DS-4). An adapter/transport failure (`{ ok: false }`\n * or `null`) is propagated immediately — the retry budget is for JSON repair,\n * NOT for transient network errors (those stay bounded at one call, DS-12).\n *\n * On success the validated object is returned as `value` (FR-1), with `text`\n * kept as the raw model output of the successful call and `usage` from that\n * call. `req.schema` is required; `complete()` only routes here when it is set.\n */\nexport async function runStructured(\n adapter: ProviderAdapter,\n req: CompleteRequest,\n key: string | undefined,\n): Promise<CompleteResult> {\n // `complete()` only calls here when `schema` is present; guard once so a\n // direct caller cannot trip on a missing schema mid-flow.\n const schema = req.schema;\n if (!schema) {\n return adapter.complete(req, key);\n }\n\n // --- Attempt 1: the initial JSON request. ---\n const first = await adapter.complete(withJsonInstruction(req), key);\n // Adapter/transport failure (incl. null degradation) — propagate, do NOT spend\n // the retry budget on a non-JSON failure (DS-12: transport stays single-shot).\n if (!first?.ok) return first;\n\n const parsed1 = parseAndValidate(first.text, schema);\n if (parsed1.ok) {\n return {\n ok: true,\n text: first.text,\n value: parsed1.value,\n ...(first.usage ? { usage: first.usage } : {}),\n };\n }\n\n // --- Attempt 2: the single repair retry, error fed back. ---\n const second = await adapter.complete(\n withCorrectiveInstruction(req, parsed1.error, first.text),\n key,\n );\n // If the retry's transport itself failed, the overall result is still a\n // schema-validation failure (the first output was invalid JSON and we could\n // not repair it) — surface that, not the transport error, so the caller sees\n // the real cause. The first attempt's parse error is the actionable detail.\n if (!second?.ok) {\n return { ok: false, reason: `schema-validation-failed: ${parsed1.error}` };\n }\n\n const parsed2 = parseAndValidate(second.text, schema);\n if (parsed2.ok) {\n return {\n ok: true,\n text: second.text,\n value: parsed2.value,\n ...(second.usage ? { usage: second.usage } : {}),\n };\n }\n\n // Two strikes: the model could not produce schema-valid JSON. Degrade to a\n // structured failure (NOT null — a call was attempted and billed; the caller\n // may surface this). Reason is fixed-suffix `schema-validation-failed` so\n // callers can branch on it, followed by the last parse/validate error.\n return { ok: false, reason: `schema-validation-failed: ${parsed2.error}` };\n}\n","// complete() — the single bounded model entry point (slice S8, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - Provider-EXPLICIT, never silent paid calls (DS-6): the provider is resolved\n// ONLY from the explicit `req.provider` (or `cfg.defaultProvider`). Env-var\n// presence is NEVER read to infer a provider — `ANTHROPIC_API_KEY` being set\n// for another tool does NOT make Anthropic active in Noir. No explicit,\n// configured provider ⇒ `null`.\n// - null-degradation FIRST-CLASS (DS-5): the unconfigured / missing-key paths\n// return `null` (NEVER throw), so callers branch on presence and the full\n// Noir test suite runs offline + free. `null` is the always-available default.\n// - SINGLE-SHOT (D5): there is no loop here — `complete()` dispatches one\n// adapter call and returns. The `CompleteRequest` type forbids `tools` /\n// `stream`, so even an adapter cannot turn this into an agent loop.\n//\n// The adapter registry is the seam slices t2/t3 fill: each adapter module\n// calls {@link registerProviderAdapter} at import time. Until a configured\n// provider has a registered adapter, `complete()` returns `{ ok: false, reason }`\n// (a real misconfiguration, intentionally distinct from the first-class `null`\n// degradation so callers can tell \"offline\" from \"wired wrong\").\n\nimport { runStructured } from './structured.js';\nimport type {\n CompleteRequest,\n CompleteResult,\n ModelConfig,\n ProviderAdapter,\n ProviderConfig,\n Tier,\n} from './types.js';\n\n// --- Adapter registry (the t2/t3 seam) --------------------------------------\n//\n// A plain module-level map. Adapters self-register on import; `complete()`\n// looks one up by provider name. Kept here (not a separate file) because t1's\n// file list is {index,types,complete} and the registry is small + tightly\n// coupled to dispatch. `clearProviderAdapters` exists for test isolation.\nconst adapters = new Map<string, ProviderAdapter>();\n\n/** Register a provider adapter under `name` (e.g. `anthropic`, `openai`). */\nexport function registerProviderAdapter(name: string, adapter: ProviderAdapter): void {\n adapters.set(name, adapter);\n}\n\n/** Look up a registered adapter by provider name (or `undefined`). */\nexport function getProviderAdapter(name: string): ProviderAdapter | undefined {\n return adapters.get(name);\n}\n\n/** Clear the registry. Exported for test isolation between cases. */\nexport function clearProviderAdapters(): void {\n adapters.clear();\n}\n\n// --- Key resolution ---------------------------------------------------------\n//\n// Secrets live in env vars; config holds only the env-var NAME (DS-8). A\n// provider block with no `apiKeyEnv` is an ANONYMOUS local provider (Ollama,\n// LM Studio) and is allowed to proceed with `undefined` — no auth header.\nfunction resolveKey(providerCfg: ProviderConfig): string | undefined {\n if (!providerCfg.apiKeyEnv) return undefined; // anonymous local provider\n return process.env[providerCfg.apiKeyEnv];\n}\n\n// --- Per-tier output caps (FR-10) -------------------------------------------\n//\n// Applied when a request omits `maxTokens` AND signals a tier. A tier ONLY\n// picks the output cap — it never selects a provider or model (DS-6), so this\n// table is a flat budget map, not a routing table. When neither `maxTokens` nor\n// a tier is given, the request keeps `maxTokens: undefined` and each adapter\n// applies its own last-resort bound (e.g. the Anthropic Messages API's required\n// `max_tokens` defaults to 2048 inside that adapter).\nexport const TIER_MAX_TOKENS: Readonly<Record<Tier, number>> = {\n draft: 2048,\n title: 64,\n summarize: 512,\n consolidate: 2048,\n};\n\n// --- Adapter resolution (t4) ------------------------------------------------\n//\n// A configured provider block is keyed by an arbitrary NAME the user picks\n// (`anthropic`, `openai`, `ollama`, `lm-studio`, …). The ADAPTER set is fixed\n// (`anthropic`, `openai`, `openai-compatible`). The two only coincide for the\n// hosted built-ins; a local endpoint like Ollama is configured under a free-form\n// name (e.g. `ollama`) but must reach the `openai-compatible` adapter.\n//\n// Resolution rule (the openai-compatible adapter's closing comment flags this as\n// the t4 dispatch job):\n// 1. DIRECT match — a registered adapter exists under `providerName` (covers\n// `anthropic`, `openai`, `openai-compatible`, and any custom-registered\n// adapter that self-registers under its provider name). Always preferred.\n// 2. BASE URL fallback — a provider block with a `baseURL` but no direct\n// adapter is an OpenAI-shaped LOCAL endpoint (Ollama / LM Studio / vLLM),\n// so route it to `openai-compatible` (raw fetch, zero SDK dep — NFR-2).\n// 3. otherwise undefined — a named provider with no adapter and no `baseURL`\n// is a wiring fault ⇒ `{ ok: false, reason }` (distinct from `null`).\nfunction resolveAdapterName(providerName: string, providerCfg: ProviderConfig): string | undefined {\n if (getProviderAdapter(providerName)) return providerName; // direct\n if (providerCfg.baseURL) return 'openai-compatible'; // local OpenAI-shaped endpoint\n return undefined;\n}\n\n/**\n * Execute one bounded model call.\n *\n * Resolution + degradation order (each `null` is the first-class offline path):\n * 1. provider name — `req.provider || cfg.defaultProvider`. Empty ⇒ `null`.\n * 2. provider block — `cfg.providers[name]`. Absent ⇒ `null` (NOT configured,\n * so NO consent to spend; env presence is never consulted — DS-6).\n * 3. key — `process.env[apiKeyEnv]`; `undefined` (anonymous) if no `apiKeyEnv`.\n * A keyed provider whose env var is unset ⇒ `null` (the miss is observable\n * via the `null` return; a structured usage/miss sink lands with t6).\n * 4. adapter — the provider NAME is mapped to an adapter (direct match, else a\n * `baseURL` block routes to `openai-compatible`); unresolvable ⇒\n * `{ ok: false, reason }`. The per-tier `maxTokens` default (FR-10) and the\n * provider-block `baseURL` are folded onto the dispatched request here.\n * 5. dispatch — one call (or two via the structured repair retry when `schema`\n * is set — DS-4); a throw becomes `{ ok: false, reason }` (never escapes).\n *\n * `null` (steps 1–3) is degradation → caller substitutes a template.\n * `{ ok: false }` (steps 4–5) is an attempted-call failure → caller may surface it.\n */\nexport async function complete(\n req: CompleteRequest,\n cfg: ModelConfig = {},\n): Promise<CompleteResult> {\n // 1. Provider name — explicit only (NEVER inferred from env-var presence).\n const providerName = req.provider || cfg.defaultProvider;\n if (!providerName) return null;\n\n // 2. Provider block must be configured — explicit consent to spend. A name\n // that isn't in `providers{}` is NOT a provider, regardless of env vars.\n const providerCfg = cfg.providers?.[providerName];\n if (!providerCfg) return null;\n\n // 3. Key resolution — env-var NAME → value; anonymous local providers OK.\n const key = resolveKey(providerCfg);\n if (providerCfg.apiKeyEnv && !key) return null;\n\n // 4. Adapter resolution — map the configured provider NAME to an adapter. The\n // hosted built-ins (`anthropic`, `openai`) match directly; a free-form local\n // name (e.g. `ollama`) with a `baseURL` routes to `openai-compatible`. No\n // resolvable adapter ⇒ `{ ok: false }` (a wiring fault, NOT `null`).\n const adapterName = resolveAdapterName(providerName, providerCfg);\n const adapter = adapterName ? getProviderAdapter(adapterName) : undefined;\n if (!adapter) {\n return {\n ok: false,\n reason: providerCfg.baseURL\n ? `no adapter for provider \"${providerName}\" (baseURL set but the \"openai-compatible\" adapter is not registered)`\n : `no adapter registered for provider \"${providerName}\"`,\n };\n }\n\n // Forward provider-block config + the per-tier output cap onto the request so\n // the adapter stays uniform (ProviderAdapter.complete(req, key)):\n // • baseURL — only `openai-compatible` consumes it (Ollama / LM Studio /\n // vLLM endpoint); the hosted adapters simply ignore it.\n // • maxTokens — apply the FR-10 per-tier default ONLY when the caller omitted\n // it AND a tier is signalled. An explicit maxTokens always wins; absent both\n // ⇒ `undefined`, and the adapter applies its own last-resort bound.\n const tierMax = req.tier ? TIER_MAX_TOKENS[req.tier] : undefined;\n const maxTokens = req.maxTokens ?? tierMax;\n const dispatchReq: CompleteRequest = {\n ...req,\n ...(providerCfg.baseURL ? { baseURL: providerCfg.baseURL } : {}),\n ...(maxTokens !== undefined ? { maxTokens } : {}),\n };\n\n // 5. Single bounded call; complete() never throws. When a `schema` is present\n // the call routes through the structured path (prompt-JSON + validate + ≤1\n // repair retry — the ONLY retry in the model layer, DS-4/DS-12). Otherwise a\n // plain free-text adapter call. Either way at most two adapter invocations\n // total, and no `tools`/`stream` exist on the request to loop on (FR-8).\n try {\n return dispatchReq.schema\n ? await runStructured(adapter, dispatchReq, key)\n : await adapter.complete(dispatchReq, key);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `provider \"${providerName}\" failed: ${reason}` };\n }\n}\n","// Model config resolver for @noir-ai/model (slice S8).\n//\n// The single bridge from @noir-ai/core's user-facing `model` zod schema to the\n// runtime shape `complete()` (and `noir doctor`) consume. Lives HERE, in model,\n// so @noir-ai/core never imports @noir-ai/model (no core→model cycle): core owns\n// the user-facing schema, model owns this mapper + the runtime types (blueprint\n// D5 / hard rule). The fully-resolved zod output is structurally assignable to\n// the permissive {@link ModelUserConfig} mirror below, so the mapper accepts a\n// `NoirConfig['model']` directly — callers pass `resolveModelConfig(cfg.model)`.\n//\n// Provider-EXPLICIT, never silent paid (DS-6): this mapper is a PURE projection\n// of what the user wrote — it never selects a provider from env-var presence.\n// Whether a configured provider is actually USABLE (key present) is reported as\n// `hasKey` for inspection (`noir doctor`); it does NOT change the provider set.\n// The first-class `null`-degradation decision lives in `complete()`, which reads\n// the same env at call time — both readings are idempotent and stay in-process.\n//\n// Secrets stay in env (DS-8): `apiKeyEnv` is the env-var NAME (passthrough, safe\n// to print); `apiKey` is the VALUE resolved here from `process.env[apiKeyEnv]`,\n// materialized so doctor / direct consumers can branch without each re-reading\n// env. The value never touches disk via Noir and is never logged with usage.\n\n/**\n * User-facing model config shape — mirrors `NoirConfig['model']` (the zod block\n * @noir-ai/core ships, slice S8). Declared LOCALLY with every field optional so\n * this module type-checks WITHOUT a forward dependency on a core type (core\n * never imports model — no cycle; @noir-ai/model is not even in this package's\n * node_modules), AND so a config with no `model:` block (or a partial one) maps\n * cleanly to a fully-degraded runtime config. The fully-resolved zod output is\n * structurally assignable to this permissive shape, so the mapper accepts\n * `NoirConfig['model']` directly.\n */\nexport interface ModelUserConfig {\n /** Fallback provider key (into `providers`) when a tier resolves none. */\n defaultProvider?: string;\n /** Per-tier provider-key overrides (value = key into `providers{}`). */\n tiers?: {\n draft?: string;\n title?: string;\n summarize?: string;\n consolidate?: string;\n };\n /** Configured provider blocks, keyed by provider name. */\n providers?: Record<string, ModelProviderEntry>;\n}\n\n/**\n * One user-facing provider block — mirrors a `model.providers[name]` entry in\n * `.noir/config.yml`. `model` is optional HERE (the mirror is permissive) even\n * though the zod schema requires it, so hand-built configs in tests type-check;\n * `resolveModelConfig` passes `model` straight through when present.\n */\nexport interface ModelProviderEntry {\n /** Default model id for this provider (a call's `req.model` overrides). */\n model?: string;\n /** Base URL for openai-compatible endpoints (Ollama / LM Studio / …). */\n baseURL?: string;\n /** Env-var NAME holding the API key; omit for anonymous local providers. */\n apiKeyEnv?: string;\n}\n\n/**\n * A provider block with its key RESOLVED from the environment. Superset of the\n * runtime `ProviderConfig` (`@noir-ai/model`'s `complete()` consumes), so a\n * {@link ResolvedModelConfig} drops cleanly into `complete(req, cfg)` — the extra\n * `apiKey` / `hasKey` fields are ignored by `complete()` (which re-reads env at\n * call time, idempotently) and exist for `noir doctor` + direct consumers.\n */\nexport interface ResolvedProviderConfig {\n /** Provider model id (passthrough from config). */\n model?: string;\n /** Base URL passthrough (openai-compatible endpoints). */\n baseURL?: string;\n /** Env-var NAME passthrough — doctor prints this, NEVER the value (DS-8). */\n apiKeyEnv?: string;\n /** VALUE resolved from `process.env[apiKeyEnv]`; `undefined` if anonymous or unset. */\n apiKey?: string;\n /**\n * Readiness signal for `noir doctor` (OQ-5): for a keyed provider, whether the\n * env var named by `apiKeyEnv` is set; for an ANONYMOUS provider (no\n * `apiKeyEnv`), `true` — no key is required, so nothing is missing. Carries a\n * boolean ONLY, never the key value (NFR-4).\n */\n hasKey: boolean;\n}\n\n/**\n * Per-tier provider-key overrides, normalized to a full object (absent `tiers`\n * ⇒ `{}`) so consumers index without a separate undefined check.\n */\nexport interface ResolvedTiers {\n draft?: string;\n title?: string;\n summarize?: string;\n consolidate?: string;\n}\n\n/**\n * The resolved model-layer config — the runtime shape `complete()` consumes and\n * `noir doctor` inspects. `providers` is always present (possibly `{}`); each\n * entry carries its resolved key. Assignable to `ModelConfig` (`complete()`'s\n * param type), so `complete(req, resolveModelConfig(cfg.model))` type-checks.\n */\nexport interface ResolvedModelConfig {\n /** Fallback provider key when a call resolves no explicit provider. */\n defaultProvider?: string;\n /** Per-tier provider-key overrides (absent ⇒ empty object). */\n tiers: ResolvedTiers;\n /** Provider blocks with resolved keys (absent ⇒ empty map). */\n providers: Record<string, ResolvedProviderConfig>;\n}\n\n/**\n * Resolve a user-facing {@link ModelUserConfig} into the runtime\n * {@link ResolvedModelConfig}.\n *\n * - `undefined` / missing block ⇒ `{ tiers: {}, providers: {} }` (full\n * degradation — `complete()` will then return `null` for every call, the\n * always-available offline path; blueprint D5).\n * - Each provider's key is materialized from `process.env[apiKeyEnv]` into\n * `apiKey`; a keyed provider whose env var is unset gets `apiKey: undefined`\n * + `hasKey: false` (doctor surfaces this; `complete()` returns `null`).\n * - Anonymous providers (no `apiKeyEnv`) keep `apiKey: undefined` but report\n * `hasKey: true` (ready — no key needed, e.g. local Ollama).\n *\n * This mapper NEVER infers a provider from env-var presence (DS-6) and NEVER\n * mutates `raw` or `process.env` — it only READS env to resolve keys. It never\n * throws; an unusable config degrades to empty, not an exception.\n */\nexport function resolveModelConfig(raw?: ModelUserConfig): ResolvedModelConfig {\n const providers: Record<string, ResolvedProviderConfig> = {};\n\n // Destructure once into locals so every narrowing below is unambiguous (the\n // context bridge follows the same `const e = ctx?.embedder` shape). The input\n // is zod-validated or typed, so direct field access on each entry is safe.\n const rawProviders = raw?.providers;\n if (rawProviders) {\n for (const [name, entry] of Object.entries(rawProviders)) {\n const apiKeyEnv = entry.apiKeyEnv;\n // Anonymous provider (no apiKeyEnv) ⇒ no key to resolve; ready by default.\n const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;\n const hasKey = apiKeyEnv ? apiKey !== undefined : true;\n\n const resolved: ResolvedProviderConfig = { hasKey };\n if (entry.model !== undefined) resolved.model = entry.model;\n if (entry.baseURL !== undefined) resolved.baseURL = entry.baseURL;\n if (apiKeyEnv !== undefined) resolved.apiKeyEnv = apiKeyEnv;\n if (apiKey !== undefined) resolved.apiKey = apiKey;\n providers[name] = resolved;\n }\n }\n\n const tiers: ResolvedTiers = {};\n const rawTiers = raw?.tiers;\n if (rawTiers) {\n if (rawTiers.draft !== undefined) tiers.draft = rawTiers.draft;\n if (rawTiers.title !== undefined) tiers.title = rawTiers.title;\n if (rawTiers.summarize !== undefined) tiers.summarize = rawTiers.summarize;\n if (rawTiers.consolidate !== undefined) tiers.consolidate = rawTiers.consolidate;\n }\n\n const result: ResolvedModelConfig = { tiers, providers };\n const defaultProvider = raw?.defaultProvider;\n if (defaultProvider !== undefined) result.defaultProvider = defaultProvider;\n return result;\n}\n","// Anthropic provider adapter — single-shot Messages call via `@anthropic-ai/sdk`\n// (slice S8 / t2, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the request to `messages.create` carries ONLY\n// `model`, `max_tokens`, `messages`, and (optionally) `system`. There is no\n// `tools`, `tool_choice`, or `stream` key — so this adapter cannot express an\n// agent/tool loop even if a caller tried. One bounded call, then return.\n// - SDK retries DISABLED (`maxRetries: 0`, DS-12 / NFR-3): the hosted SDK\n// defaults to retrying transient failures; we opt out so one call can never\n// silently multi-charge. The only retry lives in the structured path (t4).\n// - IMPORT-ISOLATED (NFR-2): the `@anthropic-ai/sdk` is imported DYNAMICALLY\n// inside `complete()`. A bundle whose configured provider never resolves to\n// this adapter pays zero SDK bytes — the SDK is only pulled in at call time.\n// - SECRETS stay in env (DS-8): `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`; this module never touches `process.env`\n// and never logs the value. Only token COUNTS leave via `usage`.\n// - NO SILENT PAID CALLS (DS-6): if `key` is absent this adapter does NOT let\n// the SDK fall back to `ANTHROPIC_API_KEY` in env — that would be a silent\n// paid call. It returns `{ ok: false }` instead. (`complete()` normally\n// degrades to `null` for a keyed provider whose env var is missing, so an\n// absent key here means the provider block was wired without `apiKeyEnv` — a\n// recoverable misconfiguration, surfaced rather than silently charged.)\n//\n// Errors (network, non-2xx, empty body, SDK throw) become `{ ok: false, reason }`\n// — a structured failure a caller may surface. `null` degradation (no provider /\n// missing key) is decided one layer up in `complete()`, BEFORE this runs.\n//\n// Structured output is the CALLER's concern: if `req.schema` is present, the\n// t4 structured path instructs the model to emit JSON, parses the returned\n// `text`, and validates it. This adapter ignores `schema` and returns raw text,\n// so it stays a single, provider-agnostic completion primitive.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, CompleteUsage, ProviderAdapter } from '../types.js';\n\n// Structural aliases for the dynamically-imported SDK, so this file does NOT\n// depend on the SDK's exact exported types at compile time (resilient to minor\n// version churn) AND does not pull the SDK into the module's top-level import\n// graph (NFR-2 import isolation). The shape is exactly what we use: a\n// constructor taking `{ apiKey?, maxRetries }` and a `messages.create(...)`\n// returning the Anthropic Message shape. `content` is modeled loosely (an array\n// of `{ type, text? }`) because we only consume the text block; narrowing is\n// done by a `typeof text === 'string'` guard rather than a discriminated union,\n// since the structural aliases intentionally do not enumerate every block type.\ninterface AnthropicMessage {\n content: Array<{ type: string; text?: string }>;\n stop_reason: string | null;\n usage: { input_tokens: number; output_tokens: number };\n}\n\ninterface AnthropicMessages {\n create(\n params: {\n model: string;\n max_tokens: number;\n system?: string;\n messages: Array<{ role: 'user'; content: string }>;\n },\n options?: { signal?: AbortSignal; maxRetries?: number },\n ): Promise<AnthropicMessage>;\n}\n\ninterface AnthropicClient {\n messages: AnthropicMessages;\n}\n\ntype AnthropicSDK = new (opts: { apiKey?: string; maxRetries: number }) => AnthropicClient;\n\n// Anthropic's Messages API REQUIRES `max_tokens` (unlike OpenAI, where it is\n// optional). Per-tier caps (FR-10: draft 2048 / title 64 / summarize 512 /\n// consolidate 2048) are the caller's job, resolved before this adapter runs;\n// this is the adapter's last-resort bound so the required field is always set.\nconst DEFAULT_MAX_TOKENS = 2048;\n\n/**\n * Anthropic provider adapter. Registered under the name `anthropic` and\n * dispatched by `complete()` when a configured provider block's name is\n * `anthropic` (the hosted Messages endpoint). Returns raw text only — the\n * structured-output path (t4) wraps this adapter when a `schema` is present.\n */\nexport const anthropicAdapter: ProviderAdapter = {\n name: 'anthropic',\n complete: async (req, key): Promise<CompleteResult> => {\n // DS-6 defense: Anthropic is a hosted, keyed provider. If `key` is absent\n // the provider block was wired without `apiKeyEnv`; do NOT let the SDK fall\n // back to `ANTHROPIC_API_KEY` in env (silent paid call). complete() returns\n // null for a keyed provider whose env var is missing, so reaching here\n // without a key is a wiring fault — surface it, never charge silently.\n if (!key) {\n return { ok: false, reason: 'anthropic: missing API key (provider block has no apiKeyEnv)' };\n }\n\n try {\n // Dynamic import — a bundle that never selects the `anthropic` adapter\n // ships no `@anthropic-ai/sdk` dependency (NFR-2). `default` is the\n // `Anthropic` client class. Cast through `unknown` into a structural view\n // so this file never depends on the SDK's exact exported types (resilient\n // to minor version churn).\n const sdk = (await import('@anthropic-ai/sdk')) as unknown as { default: AnthropicSDK };\n const client = new sdk.default({\n apiKey: key, // the env VALUE resolved by complete() (DS-8).\n maxRetries: 0, // DS-12: never silently retry (bounded cost).\n });\n\n // Single bounded Messages call. The body carries ONLY bounded fields — no\n // `tools`, no `stream` (FR-8). `system` is a top-level Anthropic param\n // (not folded into messages, as OpenAI does); `max_tokens` is REQUIRED by\n // the Messages API, so a default is applied when the request omits it.\n const res = await client.messages.create(\n {\n model: req.model,\n max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(req.system !== undefined ? { system: req.system } : {}),\n messages: [{ role: 'user', content: req.prompt }],\n },\n {\n maxRetries: 0, // belt-and-suspenders: per-request as well as constructor.\n // Honor an abort signal so a caller can bound wall-clock further.\n ...(req.signal ? { signal: req.signal } : {}),\n },\n );\n\n // Content is an array of blocks; with no `tools` configured the first\n // (and usually only) block is a text block. noUncheckedIndexedAccess ⇒\n // guard both presence and the text field's type before returning it.\n const block = res.content[0];\n const text = block?.text;\n if (typeof text !== 'string') {\n return {\n ok: false,\n reason: `anthropic: response had no text block (stop_reason: ${res.stop_reason ?? 'unknown'})`,\n };\n }\n\n const usage: CompleteUsage = {\n inputTokens: res.usage.input_tokens,\n outputTokens: res.usage.output_tokens,\n };\n\n return { ok: true, text, usage };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `anthropic: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` then dispatches by provider name `anthropic`. The SDK itself is\n// NOT loaded by this registration — only inside `complete()` above (NFR-2).\nregisterProviderAdapter('anthropic', anthropicAdapter);\n","// OpenAI-COMPATIBLE provider adapter — single-shot chat completions over the\n// GLOBAL `fetch` (slice S8 / t3, blueprint D5). Zero non-fetch dependencies.\n//\n// This is the local / self-host escape hatch: any endpoint that speaks the\n// OpenAI Chat Completions JSON shape is reached here via its `baseURL` — Ollama\n// (`http://localhost:11434/v1`), LM Studio, vLLM, llama.cpp server, … — without\n// a per-vendor adapter and without a single SDK dependency.\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the POST body carries ONLY `model`, `messages`,\n// and (optionally) `max_tokens`. No `tools` / `functions` / `stream` — this\n// adapter cannot express an agent/tool loop. One request, parse, return.\n// - NO SDK, NO RETRY MACHINERY (DS-12 / NFR-2/3): raw `fetch` is a single shot;\n// the only retry lives in the structured path (t4). Uses the GLOBAL `fetch`\n// (Node ≥20, per `engines.node \">=20\"`) — zero added dependency.\n// - SECRETS stay in env (DS-8): `key` is the VALUE resolved by `complete()`; an\n// ANONYMOUS local provider (Ollama with no `apiKeyEnv`) reaches here with\n// `key === undefined` and we simply omit the `Authorization` header. This\n// module never reads `process.env` and never logs the value.\n//\n// Errors (network failure, non-2xx, empty body, fetch throw) become\n// `{ ok: false, reason }`. `null` degradation (no provider / missing key) is\n// decided one layer up in `complete()`, BEFORE this runs.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, ProviderAdapter } from '../types.js';\n\n// The OpenAI Chat Completions response shape — only the fields we read. The\n// `content` may be `null` (e.g. a tool-call frame, which we never request, or a\n// content-filtered refusal); we treat non-string content as a structured miss.\ninterface OpenAICompatibleResponse {\n choices?: Array<{ message?: { content?: string | null } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n}\n\n/** Build the OpenAI-shaped `messages` array: optional system, then the user turn. */\nfunction buildMessages(req: CompleteRequest): Array<{ role: 'system' | 'user'; content: string }> {\n const messages: Array<{ role: 'system' | 'user'; content: string }> = [];\n if (req.system) messages.push({ role: 'system', content: req.system });\n messages.push({ role: 'user', content: req.prompt });\n return messages;\n}\n\n/** Join a `baseURL` and `/chat/completions`, tolerating trailing slashes. */\nfunction joinEndpoint(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, '')}/chat/completions`;\n}\n\n/**\n * OpenAI-compatible provider adapter. Registered under the name\n * `openai-compatible` and dispatched by `complete()`. `baseURL` (forwarded onto\n * {@link CompleteRequest} from the provider block by `complete()`) selects the\n * endpoint; the request/response are the OpenAI Chat Completions JSON shape, so\n * any compatible server works with no vendor-specific code.\n */\nexport const openaiCompatibleAdapter: ProviderAdapter = {\n name: 'openai-compatible',\n complete: async (req, key): Promise<CompleteResult> => {\n // baseURL is the ONE piece of provider-block config this adapter needs; it\n // is forwarded from `cfg.providers[name].baseURL` by complete() (the only\n // adapter that consumes it). Missing ⇒ misconfiguration, not degradation.\n const baseURL = req.baseURL;\n if (!baseURL) {\n return { ok: false, reason: 'openai-compatible: provider block has no baseURL' };\n }\n const endpoint = joinEndpoint(baseURL);\n\n // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.\n const body: Record<string, unknown> = {\n model: req.model,\n messages: buildMessages(req),\n ...(req.maxTokens !== undefined ? { max_tokens: req.maxTokens } : {}),\n };\n\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n // Anonymous local provider (Ollama without `apiKeyEnv`): key is undefined ⇒\n // NO Authorization header is sent. A keyed compatible endpoint gets Bearer.\n if (key) headers.authorization = `Bearer ${key}`;\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n // Pass-through abort so a caller can bound wall-clock (single shot; no\n // stream to cancel). `undefined` is a no-op for fetch.\n signal: req.signal,\n });\n\n if (!res.ok) {\n // NFR-4: surface ONLY the HTTP status — NEVER embed the raw response\n // body in `reason`. A malicious or echoing endpoint (Ollama / LM\n // Studio / vLLM / any gateway) could echo the request body (the\n // prompt) or reflect headers (the `Bearer` / `sk-` key) into its\n // error frame, and callers surface `reason` directly. Reading the\n // body here would only invite a leak, so the body is not read at all.\n return { ok: false, reason: `openai-compatible: HTTP ${res.status}` };\n }\n\n const json = (await res.json()) as OpenAICompatibleResponse;\n const text = json.choices?.[0]?.message?.content;\n if (typeof text !== 'string') {\n return { ok: false, reason: 'openai-compatible: completion had no message content' };\n }\n\n const usage = json.usage;\n return {\n ok: true,\n text,\n ...(usage\n ? {\n usage: {\n ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),\n ...(usage.completion_tokens !== undefined\n ? { outputTokens: usage.completion_tokens }\n : {}),\n },\n }\n : {}),\n };\n } catch (err) {\n // Abort is an expected caller-initiated bound, not a paid failure.\n if (err instanceof Error && err.name === 'AbortError') {\n return { ok: false, reason: 'openai-compatible: request aborted' };\n }\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `openai-compatible: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` dispatches by provider name `openai-compatible`. (Routing a\n// free-form provider key like `ollama` to this adapter is the t4 dispatch job.)\nregisterProviderAdapter('openai-compatible', openaiCompatibleAdapter);\n","// OpenAI provider adapter — single-shot chat completions via the `openai` SDK\n// (slice S8 / t3, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the request to `chat.completions.create` carries\n// ONLY `model`, `messages`, and (optionally) `max_tokens`. There is no\n// `tools`, `functions`, or `stream` key — so this adapter cannot express an\n// agent/tool loop even if a caller tried. Single bounded call, then return.\n// - SDK retries DISABLED (`maxRetries: 0`, DS-12 / NFR-3): the hosted SDK\n// defaults to retrying transient failures; we opt out so one call can never\n// silently multi-charge. The only retry lives in the structured path (t4).\n// - IMPORT-ISOLATED (NFR-2): the `openai` SDK is imported DYNAMICICALLY inside\n// `complete()`. A bundle whose configured provider never resolves to this\n// adapter pays zero `openai` bytes — the SDK is only pulled in at call time.\n// - SECRETS stay in env (DS-8): `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`; this module never touches `process.env`\n// and never logs the value. Only token COUNTS leave via `usage`.\n//\n// Errors (network, non-2xx, empty body, SDK throw) become `{ ok: false, reason }`\n// — a structured failure a caller may surface. `null` degradation (no provider /\n// missing key) is decided one layer up in `complete()`, BEFORE this runs.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, ProviderAdapter } from '../types.js';\n\n// Structural aliases for the dynamically-imported SDK, so this file does NOT\n// depend on the SDK's exact exported types at compile time (resilient to minor\n// version churn) AND does not pull the SDK into the module's top-level import\n// graph (NFR-2 import isolation). The shape is exactly what we use: a\n// constructor taking `{ apiKey?, baseURL?, maxRetries }` and a\n// `chat.completions.create(...)` returning the OpenAI ChatCompletion shape.\ntype OpenAIChatCompletionsCreate = (\n params: {\n model: string;\n messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;\n max_tokens?: number;\n },\n // Second options argument mirrors the real SDK's `(params, options?)` shape,\n // so this adapter can forward the caller's wall-clock bound (NFR-3) and\n // re-assert no retries (DS-12) at the per-request level — same pattern the\n // anthropic adapter uses for `messages.create`.\n options?: { signal?: AbortSignal; maxRetries?: number },\n) => Promise<{\n choices?: Array<{ message?: { content?: string | null } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n}>;\n\ninterface OpenAIClient {\n chat: { completions: { create: OpenAIChatCompletionsCreate } };\n}\n\ntype OpenAISDK = new (opts: {\n apiKey?: string;\n baseURL?: string;\n maxRetries: number;\n}) => OpenAIClient;\n\n/** Build the OpenAI-shaped `messages` array: optional system, then the user turn. */\nfunction buildMessages(req: CompleteRequest): Array<{ role: 'system' | 'user'; content: string }> {\n const messages: Array<{ role: 'system' | 'user'; content: string }> = [];\n if (req.system) messages.push({ role: 'system', content: req.system });\n messages.push({ role: 'user', content: req.prompt });\n return messages;\n}\n\n/**\n * OpenAI provider adapter. Registered under the name `openai` and dispatched by\n * `complete()` when a configured provider block's name is `openai` (the hosted\n * endpoint). The `openai-compatible` adapter covers OpenAI-shaped LOCAL\n * endpoints (Ollama / LM Studio / vLLM) via raw `fetch` + `baseURL`.\n */\nexport const openaiAdapter: ProviderAdapter = {\n name: 'openai',\n complete: async (req, key): Promise<CompleteResult> => {\n // Hosted OpenAI ALWAYS requires a key. Guard explicitly so a misconfigured\n // anonymous `openai` block (no `apiKeyEnv`) cannot fall through to the SDK's\n // OWN env fallback (`OPENAI_API_KEY`) — that would be a silent paid call via\n // env presence, which DS-6 forbids. Anonymous LOCAL endpoints belong to the\n // dedicated `openai-compatible` adapter, not this one.\n if (!key) {\n return { ok: false, reason: 'openai: missing API key (set apiKeyEnv on the provider block)' };\n }\n try {\n // Dynamic import — a bundle that never selects the `openai` adapter ships\n // no `openai` dependency (NFR-2). `default` is the `OpenAI` client class.\n // Cast through `unknown` into a structural view so this file never depends\n // on the SDK's exact exported types (resilient to minor version churn).\n const sdk = (await import('openai')) as unknown as { default: OpenAISDK };\n const client = new sdk.default({\n apiKey: key, // the VALUE complete() resolved from process.env[apiKeyEnv]\n // Honor a forwarded baseURL if present (lets this adapter target a\n // custom OpenAI-shaped endpoint; the common local case uses the\n // dedicated `openai-compatible` adapter instead).\n ...(req.baseURL ? { baseURL: req.baseURL } : {}),\n maxRetries: 0, // DS-12: never silently retry (bounded cost).\n });\n\n const res = await client.chat.completions.create(\n {\n model: req.model,\n messages: buildMessages(req),\n // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.\n ...(req.maxTokens !== undefined ? { max_tokens: req.maxTokens } : {}),\n },\n {\n maxRetries: 0, // belt-and-suspenders: per-request as well as constructor.\n // Forward the caller's wall-clock bound (NFR-3) — same conditional\n // spread as the other optional fields (only when a signal is present).\n ...(req.signal ? { signal: req.signal } : {}),\n },\n );\n\n const text = res.choices?.[0]?.message?.content;\n if (typeof text !== 'string') {\n return { ok: false, reason: 'openai: completion had no message content' };\n }\n\n const usage = res.usage;\n return {\n ok: true,\n text,\n ...(usage\n ? {\n usage: {\n ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),\n ...(usage.completion_tokens !== undefined\n ? { outputTokens: usage.completion_tokens }\n : {}),\n },\n }\n : {}),\n };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `openai: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` then dispatches by provider name `openai`. The SDK itself is NOT\n// loaded by this registration — only inside `complete()` above (NFR-2).\nregisterProviderAdapter('openai', openaiAdapter);\n"],"mappings":"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","names":["buildMessages"]}
|
|
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 / DS-4).\n//\n// This module is the ONLY retry site in the model layer (DS-12: SDK retries are\n// 0; bounded wall-clock + cost). It does NOT call a provider directly — it is\n// given a resolved {@link ProviderAdapter} by `complete()` and orchestrates the\n// JSON round-trip on top of the adapter's single-shot `complete()`. Because the\n// adapter surface has no `tools` / `stream` (FR-8), this path cannot mutate into\n// an agent loop either: it makes at most TWO adapter calls (initial + one\n// repair), parses + validates each, and returns.\n//\n// Strategy (DS-4 / FR-3, v1 — provider-native strict modes deferred):\n// 1. Inject a \"respond with ONLY JSON matching the schema\" system addendum.\n// 2. Call the adapter once (single shot).\n// 3. Extract JSON from the text (tolerant: direct, markdown-fence, span).\n// 4. Validate against `req.schema` — a ZodType (`.parse`) or a function.\n// 5. On parse/validate failure, retry ONCE with the error fed back.\n// 6. Still bad ⇒ `{ ok: false, reason: \"schema-validation-failed: …\" }`.\n//\n// NO zod is imported at runtime here (NFR-2 / the types.ts contract): validation\n// calls `.parse` ON the caller-supplied schema object, so the built library has\n// no value-level zod dependency. A best-effort `.description` (if the ZodType\n// carries one) is the only schema introspection — we never serialize the shape,\n// so the caller's prompt is expected to describe the desired JSON; `schema` is\n// the validator, not the spec.\n\nimport type { CompleteRequest, CompleteResult, CompleteSchema, ProviderAdapter } from './types.js';\n\n// --- Prompt construction ----------------------------------------------------\n\n// The JSON contract appended to the system prompt. Strong + specific so the\n// model emits parseable JSON without relying on a provider-native JSON mode\n// (deferred per DS-4). \"single valid JSON value\" (not \"object\") so an array or\n// scalar schema is also honored.\nconst JSON_INSTRUCTION =\n 'Respond with ONLY a single valid JSON value that matches the requested schema. ' +\n 'Do not include markdown, code fences, commentary, or any surrounding prose — ' +\n 'output the JSON and nothing else.';\n\n/**\n * Best-effort schema hint for the prompt. A ZodType MAY carry a `.description`\n * (set via `z.desc(...)` / `.meta({ description })`); a validator function\n * carries none. We never import zod at runtime, so we do NOT serialize the full\n * shape — the caller's prompt describes the desired JSON; `schema` validates.\n * Returns the trimmed description, or `''` when none is available.\n */\nfunction schemaDescription(schema: CompleteSchema | undefined): string {\n if (!schema) return '';\n const desc = (schema as { description?: unknown }).description;\n return typeof desc === 'string' && desc.trim().length > 0 ? desc.trim() : '';\n}\n\n/** Append the JSON instruction (and any schema description) to the system prompt. */\nfunction withJsonInstruction(req: CompleteRequest): CompleteRequest {\n const hint = schemaDescription(req.schema);\n const addendum = hint ? `${JSON_INSTRUCTION}\\n\\nJSON must satisfy: ${hint}` : JSON_INSTRUCTION;\n const system = req.system ? `${req.system}\\n\\n${addendum}` : addendum;\n return { ...req, system };\n}\n\n/** Bound a snippet so a runaway model output cannot blow up the corrective prompt. */\nfunction truncate(s: string, max: number): string {\n return s.length > max ? `${s.slice(0, max)}…` : s;\n}\n\n/**\n * Build the ONE repair retry: re-state the JSON contract, then surface the\n * precise parse/validate error and the offending output so the model can fix it.\n * The original task (`prompt`) is preserved verbatim — only the system gains the\n * correction, so the retry is still a bounded single shot at the SAME task.\n */\nfunction withCorrectiveInstruction(\n req: CompleteRequest,\n error: string,\n badOutput: string,\n): CompleteRequest {\n const base = withJsonInstruction(req);\n const correction =\n `Your previous response failed validation and was NOT valid JSON matching the schema.\\n\\n` +\n `Error: ${error}\\n\\n` +\n `Previous output (do NOT repeat it):\\n${truncate(badOutput, 500)}\\n\\n` +\n `Return ONLY valid JSON matching the schema — no markdown, no prose.`;\n const system = base.system ? `${base.system}\\n\\n${correction}` : correction;\n return { ...base, system };\n}\n\n// --- JSON extraction (tolerant) ---------------------------------------------\n\ntype ParseOutcome = { ok: true; value: unknown } | { ok: false; error: string };\n\n/** `JSON.parse` with a typed outcome (never throws). */\nfunction tryJSON(s: string): ParseOutcome {\n try {\n return { ok: true, value: JSON.parse(s) };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { ok: false, error: msg };\n }\n}\n\n/**\n * Extract a JSON value from a model's free-form text. Tries, in order:\n * 1. the trimmed text directly (the model obeyed),\n * 2. the inside of a ```json …``` markdown fence (a common deviation),\n * 3. the outermost `{ … }` or `[ … ]` span (prose around the JSON).\n *\n * Returns the first parse success, else the parse error from the last attempt.\n * This tolerance keeps the ≤1 retry budget for genuine validation failures\n * rather than spending it on a stray code fence.\n */\nfunction extractJSON(text: string): ParseOutcome {\n const trimmed = text.trim();\n\n // 1. Direct.\n const direct = tryJSON(trimmed);\n if (direct.ok) return direct;\n\n // 2. Markdown code fence (optional `json`/`JSON` lang tag). Capture the group\n // into a const so the truthy guard narrows it (a re-indexed `fence[1]` would\n // read as `string | undefined` under noUncheckedIndexedAccess).\n const fenced = trimmed.match(/```(?:json|JSON)?\\s*([\\s\\S]*?)```/)?.[1];\n if (fenced) {\n const inner = tryJSON(fenced.trim());\n if (inner.ok) return inner;\n }\n\n // 3. Outermost object/array span — collect valid candidates, shortest first\n // (a tighter valid span beats a looser one that happens to balance).\n const candidates: string[] = [];\n const objStart = trimmed.indexOf('{');\n const objEnd = trimmed.lastIndexOf('}');\n const arrStart = trimmed.indexOf('[');\n const arrEnd = trimmed.lastIndexOf(']');\n if (objStart !== -1 && objEnd > objStart) candidates.push(trimmed.slice(objStart, objEnd + 1));\n if (arrStart !== -1 && arrEnd > arrStart) candidates.push(trimmed.slice(arrStart, arrEnd + 1));\n candidates.sort((a, b) => a.length - b.length);\n for (const cand of candidates) {\n const parsed = tryJSON(cand);\n if (parsed.ok) return parsed;\n }\n\n // No extraction worked — surface a short, safe excerpt (never the whole prompt\n // body, which could be large; NFR-4 keeps usage/logs free of raw content).\n const excerpt = truncate(trimmed.replace(/\\s+/g, ' '), 120);\n return { ok: false, error: `response was not valid JSON: ${JSON.stringify(excerpt)}` };\n}\n\n// --- Schema validation ------------------------------------------------------\n\n/**\n * Validate `raw` against a caller-supplied schema WITHOUT importing zod. A\n * ZodType exposes `.parse(input)` (throws on invalid); a function schema IS the\n * validator (throw on invalid). A throw ⇒ invalid; a returned value ⇒ the\n * coerced/validated value to hand back to the caller.\n *\n * `typeof === 'function'` cleanly splits the union: a `ZodType` instance is an\n * object (it has `.parse`, no call signature), so the function branch is the\n * validator-function member and the fall-through is the ZodType member. The\n * ZodType `.parse` is invoked via method syntax (`schema.parse(raw)`) so its\n * internal `this` binding is preserved — extracting `const p = schema.parse;\n * p(raw)` would lose `this` and break zod's internal state reads.\n */\nfunction validateSchema(schema: CompleteSchema, raw: unknown): unknown {\n if (typeof schema === 'function') {\n return schema(raw);\n }\n return schema.parse(raw);\n}\n\n/** Extract + validate in one step; never throws. */\nfunction parseAndValidate(text: string, schema: CompleteSchema): ParseOutcome {\n const extracted = extractJSON(text);\n if (!extracted.ok) return extracted;\n try {\n return { ok: true, value: validateSchema(schema, extracted.value) };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { ok: false, error: `schema validation failed: ${msg}` };\n }\n}\n\n// --- The structured round-trip ----------------------------------------------\n\n/**\n * Run the structured (prompt-JSON) flow against a resolved adapter.\n *\n * Makes at most TWO adapter calls: an initial attempt, plus ONE repair retry on\n * parse/validate failure (DS-4). An adapter/transport failure (`{ ok: false }`\n * or `null`) is propagated immediately — the retry budget is for JSON repair,\n * NOT for transient network errors (those stay bounded at one call, DS-12).\n *\n * On success the validated object is returned as `value` (FR-1), with `text`\n * kept as the raw model output of the successful call and `usage` from that\n * call. `req.schema` is required; `complete()` only routes here when it is set.\n */\nexport async function runStructured(\n adapter: ProviderAdapter,\n req: CompleteRequest,\n key: string | undefined,\n): Promise<CompleteResult> {\n // `complete()` only calls here when `schema` is present; guard once so a\n // direct caller cannot trip on a missing schema mid-flow.\n const schema = req.schema;\n if (!schema) {\n return adapter.complete(req, key);\n }\n\n // --- Attempt 1: the initial JSON request. ---\n const first = await adapter.complete(withJsonInstruction(req), key);\n // Adapter/transport failure (incl. null degradation) — propagate, do NOT spend\n // the retry budget on a non-JSON failure (DS-12: transport stays single-shot).\n if (!first?.ok) return first;\n\n const parsed1 = parseAndValidate(first.text, schema);\n if (parsed1.ok) {\n return {\n ok: true,\n text: first.text,\n value: parsed1.value,\n ...(first.usage ? { usage: first.usage } : {}),\n };\n }\n\n // --- Attempt 2: the single repair retry, error fed back. ---\n const second = await adapter.complete(\n withCorrectiveInstruction(req, parsed1.error, first.text),\n key,\n );\n // If the retry's transport itself failed, the overall result is still a\n // schema-validation failure (the first output was invalid JSON and we could\n // not repair it) — surface that, not the transport error, so the caller sees\n // the real cause. The first attempt's parse error is the actionable detail.\n if (!second?.ok) {\n return { ok: false, reason: `schema-validation-failed: ${parsed1.error}` };\n }\n\n const parsed2 = parseAndValidate(second.text, schema);\n if (parsed2.ok) {\n return {\n ok: true,\n text: second.text,\n value: parsed2.value,\n ...(second.usage ? { usage: second.usage } : {}),\n };\n }\n\n // Two strikes: the model could not produce schema-valid JSON. Degrade to a\n // structured failure (NOT null — a call was attempted and billed; the caller\n // may surface this). Reason is fixed-suffix `schema-validation-failed` so\n // callers can branch on it, followed by the last parse/validate error.\n return { ok: false, reason: `schema-validation-failed: ${parsed2.error}` };\n}\n","// complete() — the single bounded model entry point (slice S8, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - Provider-EXPLICIT, never silent paid calls (DS-6): the provider is resolved\n// ONLY from the explicit `req.provider` (or `cfg.defaultProvider`). Env-var\n// presence is NEVER read to infer a provider — `ANTHROPIC_API_KEY` being set\n// for another tool does NOT make Anthropic active in Noir. No explicit,\n// configured provider ⇒ `null`.\n// - null-degradation FIRST-CLASS (DS-5): the unconfigured / missing-key paths\n// return `null` (NEVER throw), so callers branch on presence and the full\n// Noir test suite runs offline + free. `null` is the always-available default.\n// - SINGLE-SHOT (D5): there is no loop here — `complete()` dispatches one\n// adapter call and returns. The `CompleteRequest` type forbids `tools` /\n// `stream`, so even an adapter cannot turn this into an agent loop.\n//\n// The adapter registry is the seam slices t2/t3 fill: each adapter module\n// calls {@link registerProviderAdapter} at import time. Until a configured\n// provider has a registered adapter, `complete()` returns `{ ok: false, reason }`\n// (a real misconfiguration, intentionally distinct from the first-class `null`\n// degradation so callers can tell \"offline\" from \"wired wrong\").\n\nimport { runStructured } from './structured.js';\nimport type {\n CompleteRequest,\n CompleteResult,\n ModelConfig,\n ProviderAdapter,\n ProviderConfig,\n Tier,\n} from './types.js';\n\n// --- Adapter registry (the t2/t3 seam) --------------------------------------\n//\n// A plain module-level map. Adapters self-register on import; `complete()`\n// looks one up by provider name. Kept here (not a separate file) because t1's\n// file list is {index,types,complete} and the registry is small + tightly\n// coupled to dispatch. `clearProviderAdapters` exists for test isolation.\nconst adapters = new Map<string, ProviderAdapter>();\n\n/** Register a provider adapter under `name` (e.g. `anthropic`, `openai`). */\nexport function registerProviderAdapter(name: string, adapter: ProviderAdapter): void {\n adapters.set(name, adapter);\n}\n\n/** Look up a registered adapter by provider name (or `undefined`). */\nexport function getProviderAdapter(name: string): ProviderAdapter | undefined {\n return adapters.get(name);\n}\n\n/** Clear the registry. Exported for test isolation between cases. */\nexport function clearProviderAdapters(): void {\n adapters.clear();\n}\n\n// --- Key resolution ---------------------------------------------------------\n//\n// Secrets live in env vars; config holds only the env-var NAME (DS-8). A\n// provider block with no `apiKeyEnv` is an ANONYMOUS local provider (Ollama,\n// LM Studio) and is allowed to proceed with `undefined` — no auth header.\nfunction resolveKey(providerCfg: ProviderConfig): string | undefined {\n if (!providerCfg.apiKeyEnv) return undefined; // anonymous local provider\n return process.env[providerCfg.apiKeyEnv];\n}\n\n// --- Per-tier output caps (FR-10) -------------------------------------------\n//\n// Applied when a request omits `maxTokens` AND signals a tier. A tier ONLY\n// picks the output cap — it never selects a provider or model (DS-6), so this\n// table is a flat budget map, not a routing table. When neither `maxTokens` nor\n// a tier is given, the request keeps `maxTokens: undefined` and each adapter\n// applies its own last-resort bound (e.g. the Anthropic Messages API's required\n// `max_tokens` defaults to 2048 inside that adapter).\nexport const TIER_MAX_TOKENS: Readonly<Record<Tier, number>> = {\n draft: 2048,\n title: 64,\n summarize: 512,\n consolidate: 2048,\n};\n\n// --- Adapter resolution (t4) ------------------------------------------------\n//\n// A configured provider block is keyed by an arbitrary NAME the user picks\n// (`anthropic`, `openai`, `ollama`, `lm-studio`, …). The ADAPTER set is fixed\n// (`anthropic`, `openai`, `openai-compatible`). The two only coincide for the\n// hosted built-ins; a local endpoint like Ollama is configured under a free-form\n// name (e.g. `ollama`) but must reach the `openai-compatible` adapter.\n//\n// Resolution rule (the openai-compatible adapter's closing comment flags this as\n// the t4 dispatch job):\n// 1. DIRECT match — a registered adapter exists under `providerName` (covers\n// `anthropic`, `openai`, `openai-compatible`, and any custom-registered\n// adapter that self-registers under its provider name). Always preferred.\n// 2. BASE URL fallback — a provider block with a `baseURL` but no direct\n// adapter is an OpenAI-shaped LOCAL endpoint (Ollama / LM Studio / vLLM),\n// so route it to `openai-compatible` (raw fetch, zero SDK dep — NFR-2).\n// 3. otherwise undefined — a named provider with no adapter and no `baseURL`\n// is a wiring fault ⇒ `{ ok: false, reason }` (distinct from `null`).\nfunction resolveAdapterName(providerName: string, providerCfg: ProviderConfig): string | undefined {\n if (getProviderAdapter(providerName)) return providerName; // direct\n if (providerCfg.baseURL) return 'openai-compatible'; // local OpenAI-shaped endpoint\n return undefined;\n}\n\n/**\n * Execute one bounded model call.\n *\n * Resolution + degradation order (each `null` is the first-class offline path):\n * 1. provider name — `req.provider || cfg.defaultProvider`. Empty ⇒ `null`.\n * 2. provider block — `cfg.providers[name]`. Absent ⇒ `null` (NOT configured,\n * so NO consent to spend; env presence is never consulted — DS-6).\n * 3. key — `process.env[apiKeyEnv]`; `undefined` (anonymous) if no `apiKeyEnv`.\n * A keyed provider whose env var is unset ⇒ `null` (the miss is observable\n * via the `null` return; a structured usage/miss sink lands with t6).\n * 4. adapter — the provider NAME is mapped to an adapter (direct match, else a\n * `baseURL` block routes to `openai-compatible`); unresolvable ⇒\n * `{ ok: false, reason }`. The per-tier `maxTokens` default (FR-10) and the\n * provider-block `baseURL` are folded onto the dispatched request here.\n * 5. dispatch — one call (or two via the structured repair retry when `schema`\n * is set — DS-4); a throw becomes `{ ok: false, reason }` (never escapes).\n *\n * `null` (steps 1–3) is degradation → caller substitutes a template.\n * `{ ok: false }` (steps 4–5) is an attempted-call failure → caller may surface it.\n */\nexport async function complete(\n req: CompleteRequest,\n cfg: ModelConfig = {},\n): Promise<CompleteResult> {\n // 1. Provider name — explicit only (NEVER inferred from env-var presence).\n const providerName = req.provider || cfg.defaultProvider;\n if (!providerName) return null;\n\n // 2. Provider block must be configured — explicit consent to spend. A name\n // that isn't in `providers{}` is NOT a provider, regardless of env vars.\n const providerCfg = cfg.providers?.[providerName];\n if (!providerCfg) return null;\n\n // 3. Key resolution — env-var NAME → value; anonymous local providers OK.\n const key = resolveKey(providerCfg);\n if (providerCfg.apiKeyEnv && !key) return null;\n\n // 4. Adapter resolution — map the configured provider NAME to an adapter. The\n // hosted built-ins (`anthropic`, `openai`) match directly; a free-form local\n // name (e.g. `ollama`) with a `baseURL` routes to `openai-compatible`. No\n // resolvable adapter ⇒ `{ ok: false }` (a wiring fault, NOT `null`).\n const adapterName = resolveAdapterName(providerName, providerCfg);\n const adapter = adapterName ? getProviderAdapter(adapterName) : undefined;\n if (!adapter) {\n return {\n ok: false,\n reason: providerCfg.baseURL\n ? `no adapter for provider \"${providerName}\" (baseURL set but the \"openai-compatible\" adapter is not registered)`\n : `no adapter registered for provider \"${providerName}\"`,\n };\n }\n\n // Forward provider-block config + the per-tier output cap onto the request so\n // the adapter stays uniform (ProviderAdapter.complete(req, key)):\n // • baseURL — only `openai-compatible` consumes it (Ollama / LM Studio /\n // vLLM endpoint); the hosted adapters simply ignore it.\n // • maxTokens — apply the FR-10 per-tier default ONLY when the caller omitted\n // it AND a tier is signalled. An explicit maxTokens always wins; absent both\n // ⇒ `undefined`, and the adapter applies its own last-resort bound.\n const tierMax = req.tier ? TIER_MAX_TOKENS[req.tier] : undefined;\n const maxTokens = req.maxTokens ?? tierMax;\n const dispatchReq: CompleteRequest = {\n ...req,\n ...(providerCfg.baseURL ? { baseURL: providerCfg.baseURL } : {}),\n ...(maxTokens !== undefined ? { maxTokens } : {}),\n };\n\n // 5. Single bounded call; complete() never throws. When a `schema` is present\n // the call routes through the structured path (prompt-JSON + validate + ≤1\n // repair retry — the ONLY retry in the model layer, DS-4/DS-12). Otherwise a\n // plain free-text adapter call. Either way at most two adapter invocations\n // total, and no `tools`/`stream` exist on the request to loop on (FR-8).\n try {\n return dispatchReq.schema\n ? await runStructured(adapter, dispatchReq, key)\n : await adapter.complete(dispatchReq, key);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `provider \"${providerName}\" failed: ${reason}` };\n }\n}\n","// Model config resolver for @noir-ai/model (slice S8).\n//\n// The single bridge from @noir-ai/core's user-facing `model` zod schema to the\n// runtime shape `complete()` (and `noir doctor`) consume. Lives HERE, in model,\n// so @noir-ai/core never imports @noir-ai/model (no core→model cycle): core owns\n// the user-facing schema, model owns this mapper + the runtime types (blueprint\n// D5 / hard rule). The fully-resolved zod output is structurally assignable to\n// the permissive {@link ModelUserConfig} mirror below, so the mapper accepts a\n// `NoirConfig['model']` directly — callers pass `resolveModelConfig(cfg.model)`.\n//\n// Provider-EXPLICIT, never silent paid (DS-6): this mapper is a PURE projection\n// of what the user wrote — it never selects a provider from env-var presence.\n// Whether a configured provider is actually USABLE (key present) is reported as\n// `hasKey` for inspection (`noir doctor`); it does NOT change the provider set.\n// The first-class `null`-degradation decision lives in `complete()`, which reads\n// the same env at call time — both readings are idempotent and stay in-process.\n//\n// Secrets stay in env (DS-8): `apiKeyEnv` is the env-var NAME (passthrough, safe\n// to print); `apiKey` is the VALUE resolved here from `process.env[apiKeyEnv]`,\n// materialized so doctor / direct consumers can branch without each re-reading\n// env. The value never touches disk via Noir and is never logged with usage.\n\n/**\n * User-facing model config shape — mirrors `NoirConfig['model']` (the zod block\n * @noir-ai/core ships, slice S8). Declared LOCALLY with every field optional so\n * this module type-checks WITHOUT a forward dependency on a core type (core\n * never imports model — no cycle; @noir-ai/model is not even in this package's\n * node_modules), AND so a config with no `model:` block (or a partial one) maps\n * cleanly to a fully-degraded runtime config. The fully-resolved zod output is\n * structurally assignable to this permissive shape, so the mapper accepts\n * `NoirConfig['model']` directly.\n */\nexport interface ModelUserConfig {\n /** Fallback provider key (into `providers`) when a tier resolves none. */\n defaultProvider?: string;\n /** Per-tier provider-key overrides (value = key into `providers{}`). */\n tiers?: {\n draft?: string;\n title?: string;\n summarize?: string;\n consolidate?: string;\n };\n /** Configured provider blocks, keyed by provider name. */\n providers?: Record<string, ModelProviderEntry>;\n}\n\n/**\n * One user-facing provider block — mirrors a `model.providers[name]` entry in\n * `.noir/config.yml`. `model` is optional HERE (the mirror is permissive) even\n * though the zod schema requires it, so hand-built configs in tests type-check;\n * `resolveModelConfig` passes `model` straight through when present.\n */\nexport interface ModelProviderEntry {\n /** Default model id for this provider (a call's `req.model` overrides). */\n model?: string;\n /** Base URL for openai-compatible endpoints (Ollama / LM Studio / …). */\n baseURL?: string;\n /** Env-var NAME holding the API key; omit for anonymous local providers. */\n apiKeyEnv?: string;\n}\n\n/**\n * A provider block with its key RESOLVED from the environment. Superset of the\n * runtime `ProviderConfig` (`@noir-ai/model`'s `complete()` consumes), so a\n * {@link ResolvedModelConfig} drops cleanly into `complete(req, cfg)` — the extra\n * `apiKey` / `hasKey` fields are ignored by `complete()` (which re-reads env at\n * call time, idempotently) and exist for `noir doctor` + direct consumers.\n */\nexport interface ResolvedProviderConfig {\n /** Provider model id (passthrough from config). */\n model?: string;\n /** Base URL passthrough (openai-compatible endpoints). */\n baseURL?: string;\n /** Env-var NAME passthrough — doctor prints this, NEVER the value (DS-8). */\n apiKeyEnv?: string;\n /** VALUE resolved from `process.env[apiKeyEnv]`; `undefined` if anonymous or unset. */\n apiKey?: string;\n /**\n * Readiness signal for `noir doctor` (OQ-5): for a keyed provider, whether the\n * env var named by `apiKeyEnv` is set; for an ANONYMOUS provider (no\n * `apiKeyEnv`), `true` — no key is required, so nothing is missing. Carries a\n * boolean ONLY, never the key value (NFR-4).\n */\n hasKey: boolean;\n}\n\n/**\n * Per-tier provider-key overrides, normalized to a full object (absent `tiers`\n * ⇒ `{}`) so consumers index without a separate undefined check.\n */\nexport interface ResolvedTiers {\n draft?: string;\n title?: string;\n summarize?: string;\n consolidate?: string;\n}\n\n/**\n * The resolved model-layer config — the runtime shape `complete()` consumes and\n * `noir doctor` inspects. `providers` is always present (possibly `{}`); each\n * entry carries its resolved key. Assignable to `ModelConfig` (`complete()`'s\n * param type), so `complete(req, resolveModelConfig(cfg.model))` type-checks.\n */\nexport interface ResolvedModelConfig {\n /** Fallback provider key when a call resolves no explicit provider. */\n defaultProvider?: string;\n /** Per-tier provider-key overrides (absent ⇒ empty object). */\n tiers: ResolvedTiers;\n /** Provider blocks with resolved keys (absent ⇒ empty map). */\n providers: Record<string, ResolvedProviderConfig>;\n}\n\n/**\n * Resolve a user-facing {@link ModelUserConfig} into the runtime\n * {@link ResolvedModelConfig}.\n *\n * - `undefined` / missing block ⇒ `{ tiers: {}, providers: {} }` (full\n * degradation — `complete()` will then return `null` for every call, the\n * always-available offline path; blueprint D5).\n * - Each provider's key is materialized from `process.env[apiKeyEnv]` into\n * `apiKey`; a keyed provider whose env var is unset gets `apiKey: undefined`\n * + `hasKey: false` (doctor surfaces this; `complete()` returns `null`).\n * - Anonymous providers (no `apiKeyEnv`) keep `apiKey: undefined` but report\n * `hasKey: true` (ready — no key needed, e.g. local Ollama).\n *\n * This mapper NEVER infers a provider from env-var presence (DS-6) and NEVER\n * mutates `raw` or `process.env` — it only READS env to resolve keys. It never\n * throws; an unusable config degrades to empty, not an exception.\n */\nexport function resolveModelConfig(raw?: ModelUserConfig): ResolvedModelConfig {\n const providers: Record<string, ResolvedProviderConfig> = {};\n\n // Destructure once into locals so every narrowing below is unambiguous (the\n // context bridge follows the same `const e = ctx?.embedder` shape). The input\n // is zod-validated or typed, so direct field access on each entry is safe.\n const rawProviders = raw?.providers;\n if (rawProviders) {\n for (const [name, entry] of Object.entries(rawProviders)) {\n const apiKeyEnv = entry.apiKeyEnv;\n // Anonymous provider (no apiKeyEnv) ⇒ no key to resolve; ready by default.\n const apiKey = apiKeyEnv ? process.env[apiKeyEnv] : undefined;\n const hasKey = apiKeyEnv ? apiKey !== undefined : true;\n\n const resolved: ResolvedProviderConfig = { hasKey };\n if (entry.model !== undefined) resolved.model = entry.model;\n if (entry.baseURL !== undefined) resolved.baseURL = entry.baseURL;\n if (apiKeyEnv !== undefined) resolved.apiKeyEnv = apiKeyEnv;\n if (apiKey !== undefined) resolved.apiKey = apiKey;\n providers[name] = resolved;\n }\n }\n\n const tiers: ResolvedTiers = {};\n const rawTiers = raw?.tiers;\n if (rawTiers) {\n if (rawTiers.draft !== undefined) tiers.draft = rawTiers.draft;\n if (rawTiers.title !== undefined) tiers.title = rawTiers.title;\n if (rawTiers.summarize !== undefined) tiers.summarize = rawTiers.summarize;\n if (rawTiers.consolidate !== undefined) tiers.consolidate = rawTiers.consolidate;\n }\n\n const result: ResolvedModelConfig = { tiers, providers };\n const defaultProvider = raw?.defaultProvider;\n if (defaultProvider !== undefined) result.defaultProvider = defaultProvider;\n return result;\n}\n","// draft.ts — bounded PRD drafting helper (debt-batch A / P3, slice P).\n//\n// The PRD is the pre-SDD product artifact (`.noir/prd/<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 (DS-5): 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/<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 * / DS-6 — 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/<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 / DS-5).\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`, DS-12 / NFR-3): the hosted SDK\n// defaults to retrying transient failures; we opt out so one call can never\n// silently multi-charge. The only retry lives in the structured path (t4).\n// - IMPORT-ISOLATED (NFR-2): the `@anthropic-ai/sdk` is imported DYNAMICALLY\n// inside `complete()`. A bundle whose configured provider never resolves to\n// this adapter pays zero SDK bytes — the SDK is only pulled in at call time.\n// - SECRETS stay in env (DS-8): `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`; this module never touches `process.env`\n// and never logs the value. Only token COUNTS leave via `usage`.\n// - NO SILENT PAID CALLS (DS-6): if `key` is absent this adapter does NOT let\n// the SDK fall back to `ANTHROPIC_API_KEY` in env — that would be a silent\n// paid call. It returns `{ ok: false }` instead. (`complete()` normally\n// degrades to `null` for a keyed provider whose env var is missing, so an\n// absent key here means the provider block was wired without `apiKeyEnv` — a\n// recoverable misconfiguration, surfaced rather than silently charged.)\n//\n// Errors (network, non-2xx, empty body, SDK throw) become `{ ok: false, reason }`\n// — a structured failure a caller may surface. `null` degradation (no provider /\n// missing key) is decided one layer up in `complete()`, BEFORE this runs.\n//\n// Structured output is the CALLER's concern: if `req.schema` is present, the\n// t4 structured path instructs the model to emit JSON, parses the returned\n// `text`, and validates it. This adapter ignores `schema` and returns raw text,\n// so it stays a single, provider-agnostic completion primitive.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteResult, CompleteUsage, ProviderAdapter } from '../types.js';\n\n// Structural aliases for the dynamically-imported SDK, so this file does NOT\n// depend on the SDK's exact exported types at compile time (resilient to minor\n// version churn) AND does not pull the SDK into the module's top-level import\n// graph (NFR-2 import isolation). The shape is exactly what we use: a\n// constructor taking `{ apiKey?, maxRetries }` and a `messages.create(...)`\n// returning the Anthropic Message shape. `content` is modeled loosely (an array\n// of `{ type, text? }`) because we only consume the text block; narrowing is\n// done by a `typeof text === 'string'` guard rather than a discriminated union,\n// since the structural aliases intentionally do not enumerate every block type.\ninterface AnthropicMessage {\n content: Array<{ type: string; text?: string }>;\n stop_reason: string | null;\n usage: { input_tokens: number; output_tokens: number };\n}\n\ninterface AnthropicMessages {\n create(\n params: {\n model: string;\n max_tokens: number;\n system?: string;\n messages: Array<{ role: 'user'; content: string }>;\n },\n options?: { signal?: AbortSignal; maxRetries?: number },\n ): Promise<AnthropicMessage>;\n}\n\ninterface AnthropicClient {\n messages: AnthropicMessages;\n}\n\ntype AnthropicSDK = new (opts: { apiKey?: string; maxRetries: number }) => AnthropicClient;\n\n// Anthropic's Messages API REQUIRES `max_tokens` (unlike OpenAI, where it is\n// optional). Per-tier caps (FR-10: draft 2048 / title 64 / summarize 512 /\n// consolidate 2048) are the caller's job, resolved before this adapter runs;\n// this is the adapter's last-resort bound so the required field is always set.\nconst DEFAULT_MAX_TOKENS = 2048;\n\n/**\n * Anthropic provider adapter. Registered under the name `anthropic` and\n * dispatched by `complete()` when a configured provider block's name is\n * `anthropic` (the hosted Messages endpoint). Returns raw text only — the\n * structured-output path (t4) wraps this adapter when a `schema` is present.\n */\nexport const anthropicAdapter: ProviderAdapter = {\n name: 'anthropic',\n complete: async (req, key): Promise<CompleteResult> => {\n // DS-6 defense: Anthropic is a hosted, keyed provider. If `key` is absent\n // the provider block was wired without `apiKeyEnv`; do NOT let the SDK fall\n // back to `ANTHROPIC_API_KEY` in env (silent paid call). complete() returns\n // null for a keyed provider whose env var is missing, so reaching here\n // without a key is a wiring fault — surface it, never charge silently.\n if (!key) {\n return { ok: false, reason: 'anthropic: missing API key (provider block has no apiKeyEnv)' };\n }\n\n try {\n // Dynamic import — a bundle that never selects the `anthropic` adapter\n // ships no `@anthropic-ai/sdk` dependency (NFR-2). `default` is the\n // `Anthropic` client class. Cast through `unknown` into a structural view\n // so this file never depends on the SDK's exact exported types (resilient\n // to minor version churn).\n const sdk = (await import('@anthropic-ai/sdk')) as unknown as { default: AnthropicSDK };\n const client = new sdk.default({\n apiKey: key, // the env VALUE resolved by complete() (DS-8).\n maxRetries: 0, // DS-12: never silently retry (bounded cost).\n });\n\n // Single bounded Messages call. The body carries ONLY bounded fields — no\n // `tools`, no `stream` (FR-8). `system` is a top-level Anthropic param\n // (not folded into messages, as OpenAI does); `max_tokens` is REQUIRED by\n // the Messages API, so a default is applied when the request omits it.\n const res = await client.messages.create(\n {\n model: req.model,\n max_tokens: req.maxTokens ?? DEFAULT_MAX_TOKENS,\n ...(req.system !== undefined ? { system: req.system } : {}),\n messages: [{ role: 'user', content: req.prompt }],\n },\n {\n maxRetries: 0, // belt-and-suspenders: per-request as well as constructor.\n // Honor an abort signal so a caller can bound wall-clock further.\n ...(req.signal ? { signal: req.signal } : {}),\n },\n );\n\n // Content is an array of blocks; with no `tools` configured the first\n // (and usually only) block is a text block. noUncheckedIndexedAccess ⇒\n // guard both presence and the text field's type before returning it.\n const block = res.content[0];\n const text = block?.text;\n if (typeof text !== 'string') {\n return {\n ok: false,\n reason: `anthropic: response had no text block (stop_reason: ${res.stop_reason ?? 'unknown'})`,\n };\n }\n\n const usage: CompleteUsage = {\n inputTokens: res.usage.input_tokens,\n outputTokens: res.usage.output_tokens,\n };\n\n return { ok: true, text, usage };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `anthropic: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` then dispatches by provider name `anthropic`. The SDK itself is\n// NOT loaded by this registration — only inside `complete()` above (NFR-2).\nregisterProviderAdapter('anthropic', anthropicAdapter);\n","// OpenAI-COMPATIBLE provider adapter — single-shot chat completions over the\n// GLOBAL `fetch` (slice S8 / t3, blueprint D5). Zero non-fetch dependencies.\n//\n// This is the local / self-host escape hatch: any endpoint that speaks the\n// OpenAI Chat Completions JSON shape is reached here via its `baseURL` — Ollama\n// (`http://localhost:11434/v1`), LM Studio, vLLM, llama.cpp server, … — without\n// a per-vendor adapter and without a single SDK dependency.\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the POST body carries ONLY `model`, `messages`,\n// and (optionally) `max_tokens`. No `tools` / `functions` / `stream` — this\n// adapter cannot express an agent/tool loop. One request, parse, return.\n// - NO SDK, NO RETRY MACHINERY (DS-12 / NFR-2/3): raw `fetch` is a single shot;\n// the only retry lives in the structured path (t4). Uses the GLOBAL `fetch`\n// (Node ≥20, per `engines.node \">=20\"`) — zero added dependency.\n// - SECRETS stay in env (DS-8): `key` is the VALUE resolved by `complete()`; an\n// ANONYMOUS local provider (Ollama with no `apiKeyEnv`) reaches here with\n// `key === undefined` and we simply omit the `Authorization` header. This\n// module never reads `process.env` and never logs the value.\n//\n// Errors (network failure, non-2xx, empty body, fetch throw) become\n// `{ ok: false, reason }`. `null` degradation (no provider / missing key) is\n// decided one layer up in `complete()`, BEFORE this runs.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, ProviderAdapter } from '../types.js';\n\n// The OpenAI Chat Completions response shape — only the fields we read. The\n// `content` may be `null` (e.g. a tool-call frame, which we never request, or a\n// content-filtered refusal); we treat non-string content as a structured miss.\ninterface OpenAICompatibleResponse {\n choices?: Array<{ message?: { content?: string | null } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n}\n\n/** Build the OpenAI-shaped `messages` array: optional system, then the user turn. */\nfunction buildMessages(req: CompleteRequest): Array<{ role: 'system' | 'user'; content: string }> {\n const messages: Array<{ role: 'system' | 'user'; content: string }> = [];\n if (req.system) messages.push({ role: 'system', content: req.system });\n messages.push({ role: 'user', content: req.prompt });\n return messages;\n}\n\n/** Join a `baseURL` and `/chat/completions`, tolerating trailing slashes. */\nfunction joinEndpoint(baseURL: string): string {\n return `${baseURL.replace(/\\/+$/, '')}/chat/completions`;\n}\n\n/**\n * OpenAI-compatible provider adapter. Registered under the name\n * `openai-compatible` and dispatched by `complete()`. `baseURL` (forwarded onto\n * {@link CompleteRequest} from the provider block by `complete()`) selects the\n * endpoint; the request/response are the OpenAI Chat Completions JSON shape, so\n * any compatible server works with no vendor-specific code.\n */\nexport const openaiCompatibleAdapter: ProviderAdapter = {\n name: 'openai-compatible',\n complete: async (req, key): Promise<CompleteResult> => {\n // baseURL is the ONE piece of provider-block config this adapter needs; it\n // is forwarded from `cfg.providers[name].baseURL` by complete() (the only\n // adapter that consumes it). Missing ⇒ misconfiguration, not degradation.\n const baseURL = req.baseURL;\n if (!baseURL) {\n return { ok: false, reason: 'openai-compatible: provider block has no baseURL' };\n }\n const endpoint = joinEndpoint(baseURL);\n\n // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.\n const body: Record<string, unknown> = {\n model: req.model,\n messages: buildMessages(req),\n ...(req.maxTokens !== undefined ? { max_tokens: req.maxTokens } : {}),\n };\n\n const headers: Record<string, string> = { 'content-type': 'application/json' };\n // Anonymous local provider (Ollama without `apiKeyEnv`): key is undefined ⇒\n // NO Authorization header is sent. A keyed compatible endpoint gets Bearer.\n if (key) headers.authorization = `Bearer ${key}`;\n\n try {\n const res = await fetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n // Pass-through abort so a caller can bound wall-clock (single shot; no\n // stream to cancel). `undefined` is a no-op for fetch.\n signal: req.signal,\n });\n\n if (!res.ok) {\n // NFR-4: surface ONLY the HTTP status — NEVER embed the raw response\n // body in `reason`. A malicious or echoing endpoint (Ollama / LM\n // Studio / vLLM / any gateway) could echo the request body (the\n // prompt) or reflect headers (the `Bearer` / `sk-` key) into its\n // error frame, and callers surface `reason` directly. Reading the\n // body here would only invite a leak, so the body is not read at all.\n return { ok: false, reason: `openai-compatible: HTTP ${res.status}` };\n }\n\n const json = (await res.json()) as OpenAICompatibleResponse;\n const text = json.choices?.[0]?.message?.content;\n if (typeof text !== 'string') {\n return { ok: false, reason: 'openai-compatible: completion had no message content' };\n }\n\n const usage = json.usage;\n return {\n ok: true,\n text,\n ...(usage\n ? {\n usage: {\n ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),\n ...(usage.completion_tokens !== undefined\n ? { outputTokens: usage.completion_tokens }\n : {}),\n },\n }\n : {}),\n };\n } catch (err) {\n // Abort is an expected caller-initiated bound, not a paid failure.\n if (err instanceof Error && err.name === 'AbortError') {\n return { ok: false, reason: 'openai-compatible: request aborted' };\n }\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `openai-compatible: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` dispatches by provider name `openai-compatible`. (Routing a\n// free-form provider key like `ollama` to this adapter is the t4 dispatch job.)\nregisterProviderAdapter('openai-compatible', openaiCompatibleAdapter);\n","// OpenAI provider adapter — single-shot chat completions via the `openai` SDK\n// (slice S8 / t3, blueprint D5).\n//\n// HARD RULES enforced here, by construction:\n//\n// - SINGLE-SHOT (D5 / FR-8): the request to `chat.completions.create` carries\n// ONLY `model`, `messages`, and (optionally) `max_tokens`. There is no\n// `tools`, `functions`, or `stream` key — so this adapter cannot express an\n// agent/tool loop even if a caller tried. Single bounded call, then return.\n// - SDK retries DISABLED (`maxRetries: 0`, DS-12 / NFR-3): the hosted SDK\n// defaults to retrying transient failures; we opt out so one call can never\n// silently multi-charge. The only retry lives in the structured path (t4).\n// - IMPORT-ISOLATED (NFR-2): the `openai` SDK is imported DYNAMICICALLY inside\n// `complete()`. A bundle whose configured provider never resolves to this\n// adapter pays zero `openai` bytes — the SDK is only pulled in at call time.\n// - SECRETS stay in env (DS-8): `key` is the resolved VALUE that `complete()`\n// read from `process.env[apiKeyEnv]`; this module never touches `process.env`\n// and never logs the value. Only token COUNTS leave via `usage`.\n//\n// Errors (network, non-2xx, empty body, SDK throw) become `{ ok: false, reason }`\n// — a structured failure a caller may surface. `null` degradation (no provider /\n// missing key) is decided one layer up in `complete()`, BEFORE this runs.\n\nimport { registerProviderAdapter } from '../complete.js';\nimport type { CompleteRequest, CompleteResult, ProviderAdapter } from '../types.js';\n\n// Structural aliases for the dynamically-imported SDK, so this file does NOT\n// depend on the SDK's exact exported types at compile time (resilient to minor\n// version churn) AND does not pull the SDK into the module's top-level import\n// graph (NFR-2 import isolation). The shape is exactly what we use: a\n// constructor taking `{ apiKey?, baseURL?, maxRetries }` and a\n// `chat.completions.create(...)` returning the OpenAI ChatCompletion shape.\ntype OpenAIChatCompletionsCreate = (\n params: {\n model: string;\n messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;\n max_tokens?: number;\n },\n // Second options argument mirrors the real SDK's `(params, options?)` shape,\n // so this adapter can forward the caller's wall-clock bound (NFR-3) and\n // re-assert no retries (DS-12) at the per-request level — same pattern the\n // anthropic adapter uses for `messages.create`.\n options?: { signal?: AbortSignal; maxRetries?: number },\n) => Promise<{\n choices?: Array<{ message?: { content?: string | null } }>;\n usage?: { prompt_tokens?: number; completion_tokens?: number };\n}>;\n\ninterface OpenAIClient {\n chat: { completions: { create: OpenAIChatCompletionsCreate } };\n}\n\ntype OpenAISDK = new (opts: {\n apiKey?: string;\n baseURL?: string;\n maxRetries: number;\n}) => OpenAIClient;\n\n/** Build the OpenAI-shaped `messages` array: optional system, then the user turn. */\nfunction buildMessages(req: CompleteRequest): Array<{ role: 'system' | 'user'; content: string }> {\n const messages: Array<{ role: 'system' | 'user'; content: string }> = [];\n if (req.system) messages.push({ role: 'system', content: req.system });\n messages.push({ role: 'user', content: req.prompt });\n return messages;\n}\n\n/**\n * OpenAI provider adapter. Registered under the name `openai` and dispatched by\n * `complete()` when a configured provider block's name is `openai` (the hosted\n * endpoint). The `openai-compatible` adapter covers OpenAI-shaped LOCAL\n * endpoints (Ollama / LM Studio / vLLM) via raw `fetch` + `baseURL`.\n */\nexport const openaiAdapter: ProviderAdapter = {\n name: 'openai',\n complete: async (req, key): Promise<CompleteResult> => {\n // Hosted OpenAI ALWAYS requires a key. Guard explicitly so a misconfigured\n // anonymous `openai` block (no `apiKeyEnv`) cannot fall through to the SDK's\n // OWN env fallback (`OPENAI_API_KEY`) — that would be a silent paid call via\n // env presence, which DS-6 forbids. Anonymous LOCAL endpoints belong to the\n // dedicated `openai-compatible` adapter, not this one.\n if (!key) {\n return { ok: false, reason: 'openai: missing API key (set apiKeyEnv on the provider block)' };\n }\n try {\n // Dynamic import — a bundle that never selects the `openai` adapter ships\n // no `openai` dependency (NFR-2). `default` is the `OpenAI` client class.\n // Cast through `unknown` into a structural view so this file never depends\n // on the SDK's exact exported types (resilient to minor version churn).\n const sdk = (await import('openai')) as unknown as { default: OpenAISDK };\n const client = new sdk.default({\n apiKey: key, // the VALUE complete() resolved from process.env[apiKeyEnv]\n // Honor a forwarded baseURL if present (lets this adapter target a\n // custom OpenAI-shaped endpoint; the common local case uses the\n // dedicated `openai-compatible` adapter instead).\n ...(req.baseURL ? { baseURL: req.baseURL } : {}),\n maxRetries: 0, // DS-12: never silently retry (bounded cost).\n });\n\n const res = await client.chat.completions.create(\n {\n model: req.model,\n messages: buildMessages(req),\n // FR-8: ONLY bounded fields. No `tools` / `stream` — by construction.\n ...(req.maxTokens !== undefined ? { max_tokens: req.maxTokens } : {}),\n },\n {\n maxRetries: 0, // belt-and-suspenders: per-request as well as constructor.\n // Forward the caller's wall-clock bound (NFR-3) — same conditional\n // spread as the other optional fields (only when a signal is present).\n ...(req.signal ? { signal: req.signal } : {}),\n },\n );\n\n const text = res.choices?.[0]?.message?.content;\n if (typeof text !== 'string') {\n return { ok: false, reason: 'openai: completion had no message content' };\n }\n\n const usage = res.usage;\n return {\n ok: true,\n text,\n ...(usage\n ? {\n usage: {\n ...(usage.prompt_tokens !== undefined ? { inputTokens: usage.prompt_tokens } : {}),\n ...(usage.completion_tokens !== undefined\n ? { outputTokens: usage.completion_tokens }\n : {}),\n },\n }\n : {}),\n };\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n return { ok: false, reason: `openai: ${reason}` };\n }\n },\n};\n\n// Self-register on import: a consumer that imports `@noir-ai/model` (whose\n// index side-effect-imports this module) gets the adapter wired automatically;\n// `complete()` then dispatches by provider name `openai`. The SDK itself is NOT\n// loaded by this registration — only inside `complete()` above (NFR-2).\nregisterProviderAdapter('openai', openaiAdapter);\n"],"mappings":"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","names":["buildMessages"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noir-ai/model",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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",
|