// ---------------------------------------------------------------------------
// LLM client for eval commands — supports multiple CLI tools and API providers
//
// Provider selection via VSKILL_EVAL_PROVIDER env var:
//   "claude-cli" — Claude Code CLI (delegates to your existing Claude Code
//                  session; the CLI handles quota. No API key, no OAuth, and
//                  no reads of ~/.claude/credentials|auth|token. See the
//                  compliance block above createClaudeCliClient() below.)
//   "codex-cli"  — OpenAI Codex CLI (uses ChatGPT session or CODEX_API_KEY)
//   "gemini-cli" — Google Gemini CLI (free tier or GOOGLE_API_KEY)
//   "anthropic"  — Anthropic API (requires ANTHROPIC_API_KEY)
//   "ollama"     — Local Ollama server (free, requires ollama running)
//
// Auto-detection when VSKILL_EVAL_PROVIDER is not set:
//   1. claude-cli (default — works everywhere, even inside Claude Code sessions)
//   Other providers only used when explicitly set via VSKILL_EVAL_PROVIDER
//
// Timeout via VSKILL_EVAL_TIMEOUT env var (seconds, default: 300):
//   export VSKILL_EVAL_TIMEOUT=600   # 10 minutes for complex cases
//
// Model selection via VSKILL_EVAL_MODEL env var:
//   claude-cli:  "sonnet" | "opus" | "haiku" (default: sonnet)
//   codex-cli:   "o4-mini" | "codex-1" | "gpt-5.3-codex" (default: o4-mini)
//   gemini-cli:  "gemini-2.5-pro" | "gemini-2.5-flash" (default: gemini-2.5-pro)
//   anthropic:   full model ID (default: claude-sonnet-4-6)
//   ollama:      model name (default: llama3.1:8b)
// ---------------------------------------------------------------------------
import { spawn } from "node:child_process";
import { resolveCliBinary, enhancedPath } from "../utils/resolve-binary.js";
import { calculateCost, getBillingMode } from "./pricing.js";
import { resolveOllamaBaseUrl } from "./env.js";
import { resolveAnthropicModel } from "./model-resolver.js";
function detectProvider() {
    return "claude-cli";
}
/** Timeout in ms — configurable via VSKILL_EVAL_TIMEOUT (seconds). Default: 300s */
function getTimeoutMs() {
    const envVal = process.env.VSKILL_EVAL_TIMEOUT;
    if (envVal) {
        const seconds = parseInt(envVal, 10);
        if (!isNaN(seconds) && seconds > 0)
            return seconds * 1000;
    }
    return 300_000;
}
/**
 * Estimate total eval duration in seconds based on provider and workload.
 * Each case requires 1 generate call + N judge calls (one per assertion).
 */
export function estimateDurationSec(provider, totalCases, totalAssertions) {
    // Approximate seconds per LLM call by provider
    const perCall = {
        "claude-cli": [12, 30],
        "anthropic": [4, 12],
        "codex-cli": [8, 20],
        "gemini-cli": [8, 20],
        "ollama": [5, 30],
        "lm-studio": [5, 30],
        "openai": [4, 15],
        "openrouter": [4, 15],
        "stub": [0, 0],
    };
    const [lo, hi] = perCall[provider] ?? [5, 20];
    const totalCalls = totalCases + totalAssertions; // 1 generate + N judges
    const minSec = Math.round(totalCalls * lo);
    const maxSec = Math.round(totalCalls * hi);
    const fmt = (s) => s >= 60 ? `${Math.round(s / 60)}m` : `${s}s`;
    const label = minSec === maxSec ? fmt(minSec) : `${fmt(minSec)}–${fmt(maxSec)}`;
    return { minSec, maxSec, label };
}
export function createLlmClient(overrides) {
    const provider = (overrides?.provider || process.env.VSKILL_EVAL_PROVIDER || detectProvider());
    const modelOverride = overrides?.model;
    switch (provider) {
        case "anthropic":
            return createAnthropicClient(modelOverride);
        case "claude-cli":
            return createClaudeCliClient(modelOverride);
        case "codex-cli":
            return createCodexCliClient(modelOverride);
        case "gemini-cli":
            return createGeminiCliClient(modelOverride);
        case "ollama":
            return createOllamaClient(modelOverride);
        case "lm-studio":
            return createLmStudioClient(modelOverride);
        case "openai":
            return createOpenAIClient(modelOverride);
        case "openrouter":
            return createOpenRouterClient(modelOverride);
        case "stub":
            return createStubClient(modelOverride);
        default:
            throw new Error(`Unknown VSKILL_EVAL_PROVIDER: "${provider}". Use "claude-cli", "codex-cli", "gemini-cli", "anthropic", "openai", "ollama", "lm-studio", or "openrouter".`);
    }
}
// ---------------------------------------------------------------------------
// Model normalization — prevents cross-provider model ID leaks.
// e.g. "claude-sonnet" (a displayModel artifact) → "sonnet" for CLI,
//      "sonnet" (CLI shorthand) → "claude-sonnet-4-6" for Anthropic API.
// ---------------------------------------------------------------------------
const CLAUDE_CLI_NORMALIZE = {
    "claude-sonnet": "sonnet",
    "claude-opus": "opus",
    "claude-haiku": "haiku",
    "claude-sonnet-4-6": "sonnet",
    "claude-sonnet-4-20250514": "sonnet",
    "claude-opus-4-8": "opus",
    "claude-opus-4-7": "opus",
    "claude-opus-4-6": "opus",
    "claude-opus-4-20250514": "opus",
    "claude-haiku-4-5-20251001": "haiku",
};
// 0711: ANTHROPIC_NORMALIZE used to be a hand-maintained map keyed to specific
// Sonnet/Opus/Haiku versions, which silently rotted whenever Anthropic shipped
// a new model. The dated catalog at `anthropic-catalog.ts` is now the source
// of truth — `resolveAnthropicModel(...)` does the alias / id / env-override
// dance and surfaces provenance (snapshotDate, status) for callers that want
// to record what actually ran. The single-line wrapper below preserves the
// old `(input) → string` signature so we can swap it in without touching
// every call site at once.
function normalizeClaudeCliModel(model) {
    return CLAUDE_CLI_NORMALIZE[model] || model;
}
function normalizeAnthropicModel(model) {
    return resolveAnthropicModel(model).resolvedId || model;
}
// ---------------------------------------------------------------------------
// Provider: Anthropic API
// ---------------------------------------------------------------------------
function createAnthropicClient(modelOverride) {
    const DEFAULT_MODEL = "claude-sonnet-4-6";
    const apiKey = process.env.ANTHROPIC_API_KEY;
    if (!apiKey) {
        throw new Error("ANTHROPIC_API_KEY is not set. Export it before running eval commands:\n  export ANTHROPIC_API_KEY=sk-ant-...\n\nOr use a different provider:\n  export VSKILL_EVAL_PROVIDER=claude-cli   # uses your Claude Max plan\n  export VSKILL_EVAL_PROVIDER=ollama       # uses local Ollama");
    }
    const raw = modelOverride || process.env.VSKILL_EVAL_MODEL || DEFAULT_MODEL;
    const model = normalizeAnthropicModel(raw);
    let clientInstance = null;
    return {
        model,
        async generate(systemPrompt, userPrompt) {
            if (!clientInstance) {
                const { default: Anthropic } = await import("@anthropic-ai/sdk");
                clientInstance = new Anthropic({ apiKey });
            }
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), getTimeoutMs());
            const start = Date.now();
            try {
                const response = await clientInstance.messages.create({
                    model,
                    system: systemPrompt,
                    messages: [{ role: "user", content: userPrompt }],
                    max_tokens: 4096,
                }, { signal: controller.signal });
                const durationMs = Date.now() - start;
                const textBlock = response.content.find((b) => b.type === "text");
                const text = textBlock && "text" in textBlock ? textBlock.text : "";
                const inputTokens = response.usage?.input_tokens ?? null;
                const outputTokens = response.usage?.output_tokens ?? null;
                return {
                    text,
                    durationMs,
                    inputTokens,
                    outputTokens,
                    cost: calculateCost("anthropic", model, inputTokens, outputTokens),
                    billingMode: getBillingMode("anthropic"),
                };
            }
            finally {
                clearTimeout(timeout);
            }
        },
    };
}
// ---------------------------------------------------------------------------
// Provider: OpenAI API (0702 T-022)
//
// Mirrors createAnthropicClient. Env var: OPENAI_API_KEY. Default model
// gpt-4o-mini (cheap). Override via VSKILL_EVAL_MODEL or modelOverride arg.
// OPENAI_BASE_URL is honored by the openai SDK natively (used by tests).
// ---------------------------------------------------------------------------
function createOpenAIClient(modelOverride) {
    const DEFAULT_MODEL = "gpt-4o-mini";
    const apiKey = process.env.OPENAI_API_KEY;
    if (!apiKey) {
        throw new Error("OPENAI_API_KEY is not set. Visit https://platform.openai.com/api-keys to get your key, then run:\n  export OPENAI_API_KEY=sk-proj-...\n\nOr use a different provider:\n  export VSKILL_EVAL_PROVIDER=claude-cli");
    }
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || DEFAULT_MODEL;
    let clientInstance = null;
    return {
        model,
        async generate(systemPrompt, userPrompt) {
            if (!clientInstance) {
                const { default: OpenAI } = await import("openai");
                const baseURL = process.env.OPENAI_BASE_URL;
                clientInstance = new OpenAI(baseURL ? { apiKey, baseURL } : { apiKey });
            }
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), getTimeoutMs());
            const start = Date.now();
            try {
                const response = await clientInstance.chat.completions.create({
                    model,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: userPrompt },
                    ],
                    max_tokens: 4096,
                }, { signal: controller.signal });
                const durationMs = Date.now() - start;
                const text = response.choices?.[0]?.message?.content ?? "";
                const inputTokens = response.usage?.prompt_tokens ?? null;
                const outputTokens = response.usage?.completion_tokens ?? null;
                return {
                    text,
                    durationMs,
                    inputTokens,
                    outputTokens,
                    cost: calculateCost("openai", model, inputTokens, outputTokens),
                    billingMode: getBillingMode("openai"),
                };
            }
            finally {
                clearTimeout(timeout);
            }
        },
    };
}
function createCliClient(config) {
    // Resolve binary path once at client creation time (cached internally)
    const resolvedBinary = resolveCliBinary(config.binary);
    // Pre-compute enhanced PATH once — avoids repeated execSync per generate() call
    const cachedEnhancedPath = enhancedPath();
    return {
        model: config.displayModel,
        async generate(systemPrompt, userPrompt) {
            const combinedPrompt = `${systemPrompt}\n\n${userPrompt}`;
            const start = Date.now();
            const text = await new Promise((resolve, reject) => {
                // Build env: strip prefix vars if needed, always use enhanced PATH
                let env;
                if (config.stripEnvPrefix) {
                    env = {};
                    const prefix = config.stripEnvPrefix;
                    for (const [k, v] of Object.entries(process.env)) {
                        if (v !== undefined && !k.startsWith(prefix))
                            env[k] = v;
                    }
                }
                else {
                    env = { ...process.env };
                }
                env.PATH = cachedEnhancedPath;
                // On Windows, .cmd/.bat files require shell:true to execute via spawn
                const needsShell = process.platform === "win32" && /\.(cmd|bat)$/i.test(resolvedBinary);
                const proc = spawn(resolvedBinary, config.args, {
                    stdio: ["pipe", "pipe", "pipe"],
                    env,
                    ...(needsShell ? { shell: true } : {}),
                });
                let stdout = "";
                let stderr = "";
                proc.stdout.on("data", (d) => { stdout += d.toString(); });
                proc.stderr.on("data", (d) => { stderr += d.toString(); });
                const timeoutMs = getTimeoutMs();
                const timer = setTimeout(() => {
                    proc.kill("SIGTERM");
                    reject(new Error(`${config.name} CLI timed out after ${timeoutMs / 1000}s`));
                }, timeoutMs);
                proc.on("error", (err) => {
                    clearTimeout(timer);
                    if (err.code === "ENOENT") {
                        reject(new Error(config.notFoundMsg));
                    }
                    else {
                        reject(new Error(`${config.name} CLI failed: ${err.message}`));
                    }
                });
                proc.on("close", (code) => {
                    clearTimeout(timer);
                    if (code === 0) {
                        resolve(stdout.trim());
                    }
                    else {
                        const errMsg = (stderr || stdout).slice(0, 300);
                        reject(new Error(`${config.name} CLI exited with code ${code}${errMsg ? ": " + errMsg : ""}`));
                    }
                });
                proc.stdin.end(combinedPrompt);
            });
            return { text, durationMs: Date.now() - start, inputTokens: null, outputTokens: null, cost: null, billingMode: getBillingMode(config.provider) };
        },
    };
}
// ---------------------------------------------------------------------------
// Provider: Claude CLI — delegates to the official `claude` binary.
//
// COMPLIANCE CONTRACT (ToS, April 2026):
//   This adapter MUST NOT read any file under `~/.claude/credentials*`,
//   `~/.claude/auth*`, or `~/.claude/token*`. All session auth is owned by
//   the `claude` CLI child process; Skill Studio delegates and consumes
//   only stdout/stderr. See `src/eval/__tests__/claude-cli-compliance.test.ts`
//   which asserts zero fs reads against those paths at the Node API level,
//   and `scripts/check-bundle-compliance.sh` which greps the built bundle
//   for the literal path strings.
//
//   Env handling: `CLAUDE*` env vars are stripped from the child so the CLI
//   does not detect a nested session. No env var is added to compensate —
//   the CLI resolves its own auth state.
// ---------------------------------------------------------------------------
function createClaudeCliClient(modelOverride) {
    const raw = modelOverride || process.env.VSKILL_EVAL_MODEL || "sonnet";
    const model = normalizeClaudeCliModel(raw);
    return createCliClient({
        binary: "claude",
        name: "Claude",
        args: ["-p", "--model", model],
        displayModel: model,
        stripEnvPrefix: "CLAUDE",
        provider: "claude-cli",
        notFoundMsg: "Claude Code not found. Install it: `npm install -g @anthropic-ai/claude-code`. Or choose a provider with an API key.",
    });
}
// ---------------------------------------------------------------------------
// Provider: Codex CLI (uses your ChatGPT subscription — or CODEX_API_KEY for CI)
// ---------------------------------------------------------------------------
function createCodexCliClient(modelOverride) {
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || "o4-mini";
    return createCliClient({
        binary: "codex",
        name: "Codex",
        args: ["exec", "--model", model],
        displayModel: `codex-${model}`,
        provider: "codex-cli",
        notFoundMsg: "Codex CLI not found. Install it:\n  npm install -g @openai/codex\n\nOr use a different provider:\n  export VSKILL_EVAL_PROVIDER=claude-cli",
    });
}
// ---------------------------------------------------------------------------
// Provider: Gemini CLI (free tier — 60 req/min, 1000 req/day, or GOOGLE_API_KEY)
// NOTE: Gemini CLI headless flags are provisional — verify against actual binary.
// ---------------------------------------------------------------------------
function createGeminiCliClient(modelOverride) {
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || "gemini-2.5-pro";
    return createCliClient({
        binary: "gemini",
        name: "Gemini",
        args: ["-p", "--model", model],
        displayModel: model,
        provider: "gemini-cli",
        notFoundMsg: "Gemini CLI not found. Install it:\n  npm install -g @google/gemini-cli\n\nOr use a different provider:\n  export VSKILL_EVAL_PROVIDER=claude-cli",
    });
}
// ---------------------------------------------------------------------------
// Provider: Ollama (local models — free, no API key)
// ---------------------------------------------------------------------------
function createOllamaClient(modelOverride) {
    const baseUrl = resolveOllamaBaseUrl(process.env);
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || "llama3.1:8b";
    return {
        model,
        async generate(systemPrompt, userPrompt) {
            const fullPrompt = `${systemPrompt}\n\n${userPrompt}`;
            const start = Date.now();
            const response = await fetch(`${baseUrl}/api/generate`, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    model,
                    prompt: fullPrompt,
                    stream: false,
                    options: {
                        num_predict: 4096,
                        temperature: 0.3,
                    },
                }),
                signal: AbortSignal.timeout(getTimeoutMs()),
            });
            if (!response.ok) {
                const error = await response.text();
                if (error.includes("model") && error.includes("not found")) {
                    throw new Error(`Ollama model "${model}" not found. Pull it first:\n  ollama pull ${model}`);
                }
                throw new Error(`Ollama request failed: ${error}`);
            }
            const data = (await response.json());
            return {
                text: data.response || "",
                durationMs: Date.now() - start,
                inputTokens: data.prompt_eval_count ?? null,
                outputTokens: data.eval_count ?? null,
                cost: 0,
                billingMode: "free",
            };
        },
    };
}
// ---------------------------------------------------------------------------
// Provider: OpenRouter (100+ models via single API key)
// ---------------------------------------------------------------------------
function createOpenRouterClient(modelOverride) {
    const apiKey = process.env.OPENROUTER_API_KEY;
    if (!apiKey) {
        throw new Error("OPENROUTER_API_KEY is not set. Visit https://openrouter.ai/keys to get your key, then run:\n  export OPENROUTER_API_KEY=<your-key>");
    }
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || "anthropic/claude-sonnet-4";
    return {
        model,
        async generate(systemPrompt, userPrompt) {
            const start = Date.now();
            const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "Authorization": `Bearer ${apiKey}`,
                    "HTTP-Referer": "https://verified-skill.com",
                    "X-Title": "Skill Studio",
                },
                body: JSON.stringify({
                    model,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: userPrompt },
                    ],
                    max_tokens: 4096,
                }),
                signal: AbortSignal.timeout(getTimeoutMs()),
            });
            if (!response.ok) {
                const error = await response.text();
                throw new Error(`OpenRouter request failed (${response.status}): ${error}`);
            }
            const data = (await response.json());
            const text = data.choices?.[0]?.message?.content || "";
            const inputTokens = data.usage?.prompt_tokens ?? null;
            const outputTokens = data.usage?.completion_tokens ?? null;
            const apiCost = data.usage?.total_cost ?? null;
            return {
                text,
                durationMs: Date.now() - start,
                inputTokens,
                outputTokens,
                cost: apiCost ?? calculateCost("openrouter", model, inputTokens, outputTokens),
                billingMode: "per-token",
            };
        },
    };
}
// ---------------------------------------------------------------------------
// Provider: LM Studio (local OpenAI-compatible server — free, no API key)
//
// Mirrors the OpenRouter adapter at the HTTP level because LM Studio exposes
// the OpenAI chat completions surface verbatim at `<base>/chat/completions`.
// Three constants differ from OpenRouter:
//   - base URL: process.env.LM_STUDIO_BASE_URL ?? "http://localhost:1234/v1"
//   - API key:  the literal string "lm-studio" (LM Studio ignores the token)
//   - billing:  "free" (local inference)
//
// Env overrides:
//   - LM_STUDIO_BASE_URL    — custom endpoint (e.g. a LAN machine)
//   - VSKILL_EVAL_MODEL     — the model id loaded in LM Studio
//
// Note: LM Studio does not enforce the Bearer token, so the dummy value
// "lm-studio" is safe to hardcode. Forks that do enforce it can override via
// a future LM_STUDIO_API_KEY env var (not wired in this increment per
// ADR-0677-02).
//
// IMPORTANT — env binding timing: `process.env.LM_STUDIO_BASE_URL` is read
// once when `createLmStudioClient()` is called (at provider-selection time),
// not on each `generate()` invocation. Tests that mutate the env after
// construction will not see the change. To re-bind, re-create the client.
// The detection probe `probeLmStudio()` in api-routes.ts reads the env at
// probe time, which is sufficient for the integration-layer test pattern.
// ---------------------------------------------------------------------------
function createLmStudioClient(modelOverride) {
    const baseUrl = process.env.LM_STUDIO_BASE_URL || "http://localhost:1234/v1";
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || "local-model";
    return {
        model,
        async generate(systemPrompt, userPrompt) {
            const start = Date.now();
            const response = await fetch(`${baseUrl}/chat/completions`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                    "Authorization": "Bearer lm-studio",
                },
                body: JSON.stringify({
                    model,
                    messages: [
                        { role: "system", content: systemPrompt },
                        { role: "user", content: userPrompt },
                    ],
                    max_tokens: 4096,
                }),
                signal: AbortSignal.timeout(getTimeoutMs()),
            });
            if (!response.ok) {
                const errorBody = await response.text();
                throw new Error(`LM Studio request failed (${response.status}): ${errorBody.slice(0, 200)}`);
            }
            const data = (await response.json());
            const text = data.choices?.[0]?.message?.content || "";
            const inputTokens = data.usage?.prompt_tokens ?? null;
            const outputTokens = data.usage?.completion_tokens ?? null;
            return {
                text,
                durationMs: Date.now() - start,
                inputTokens,
                outputTokens,
                cost: 0,
                billingMode: "free",
            };
        },
    };
}
// ---------------------------------------------------------------------------
// Provider: stub (deterministic, in-process — TEST SEAM, NOT user-facing)
//
// 0857: a content-addressed fake provider that makes the
// create-skill -> install -> run-with-a-non-default-model pipeline runnable in
// CI with zero network, zero API key, and zero `claude` binary. It is the
// regression-guarantee engine behind `test/verify/units/golden-path.verify.mjs`.
//
// HIDDEN BY DESIGN: `stub` is NOT in `KNOWN_PROVIDER_NAMES` or
// `detectAvailableProviders()` (src/eval-server/api-routes.ts), so it never
// appears in /api/config or the Studio model picker. It is selectable ONLY when
// `overrides.provider === "stub"` OR `process.env.VSKILL_EVAL_PROVIDER === "stub"`.
//
// Determinism: `generate()` is a pure function of (systemPrompt, userPrompt).
// The same prompt yields byte-identical output across calls and processes
// (FNV-1a 32-bit hash → stable slug). No clock, no randomness in the payload.
//
// Model honoring: the resolved model (override → VSKILL_EVAL_MODEL → "sonnet")
// is exposed as `client.model` AND echoed inside the JSON envelope's `model`
// field, so "non-default model selection is honored" is directly assertable at
// both the client surface and in the emitted SKILL.md / benchmark.json.
//
// Single-envelope contract: the returned text is one JSON object carrying every
// field each downstream parser needs, so the SAME stub serves the skill-gen
// body parser (`name/description/model/body`), the skill-gen evals parser
// (`evals`), and the judge parser (`pass/reasoning`) without branching on which
// caller invoked it.
// ---------------------------------------------------------------------------
/** FNV-1a 32-bit hash → stable hex string. Pure, no deps. */
function fnv1a(input) {
    let h = 0x811c9dc5;
    for (let i = 0; i < input.length; i++) {
        h ^= input.charCodeAt(i);
        // 32-bit FNV prime multiply (use Math.imul to stay in 32-bit space).
        h = Math.imul(h, 0x01000193);
    }
    return (h >>> 0).toString(16).padStart(8, "0");
}
function createStubClient(modelOverride) {
    const model = modelOverride || process.env.VSKILL_EVAL_MODEL || "sonnet";
    return {
        model,
        async generate(systemPrompt, userPrompt) {
            const start = Date.now();
            // Content address: stable across calls/processes for the same prompt.
            const digest = fnv1a(`${systemPrompt}\n\n${userPrompt}`);
            // Skill slug must be lowercase-alphanumeric-dash to satisfy
            // parseBodyResponse's name sanitizer. Derive it deterministically.
            const slug = `stub-skill-${digest}`;
            // ONE envelope satisfies every consumer. Extra keys are ignored by each
            // parser, so every caller reads what it needs from this single object:
            //   - skill-gen body  (parseBodyResponse / cleanAndParseJson): name, model, body
            //   - skill-gen evals (parseEvalsResponse): evals
            //   - eval-init gen   (parseGeneratedEvals): skill_name + non-empty evals
            //   - judge           (parseJudgeResponse): pass, reasoning
            const envelope = {
                // eval-init generator requires skill_name + a non-empty evals array.
                skill_name: slug,
                // skill-gen body parser
                name: slug,
                description: `Deterministic stub skill (model=${model}, digest=${digest}). Generated by the 0857 stub provider for the verify harness.`,
                // echo the requested model so SKILL.md frontmatter + benchmark.model
                // both reflect the chosen (possibly non-default) model.
                model,
                allowedTools: "",
                body: `# ${slug}\n\nDeterministic stub output for content address ${digest}.\nOutput the line: \`hello from ${slug}\`.\n`,
                // skill-gen + eval-init evals parsers
                evals: [
                    {
                        id: 1,
                        name: "stub deterministic case",
                        prompt: "Say hello.",
                        expected_output: `hello from ${slug}`,
                        files: [],
                        assertions: [
                            { id: "a1", text: "Output greets deterministically", type: "boolean" },
                        ],
                    },
                ],
                // judge parser — every assertion passes deterministically.
                pass: true,
                reasoning: `stub judge: deterministic pass for digest ${digest}`,
            };
            // Wrap in a ```json fence: the eval-init generator REQUIRES a fence, while
            // every other consumer (body cleanAndParseJson, judge parseJudgeResponse)
            // strips or fence-matches first, so one fenced envelope serves them all.
            const text = "```json\n" + JSON.stringify(envelope) + "\n```";
            return {
                text,
                durationMs: Date.now() - start,
                inputTokens: null,
                outputTokens: null,
                cost: 0,
                billingMode: "free",
            };
        },
    };
}
//# sourceMappingURL=llm.js.map