Quellcode durchsuchen

merge(qmd): land the stranded 429-abort fix onto the commercial-policy head (i-yghj098h)

89f3222 ("stop one 429 from aborting a whole embedding run") was committed on
branch `oivo` and then walked away from: HEAD was checked out detached at the
divergent 34c9570 ("enforce commercial-api-only model operations"), so every
shipping `qmd embed` still ran the original defect — dist/store.js:1314/1322
`errors += remaining` booking never-attempted chunks as failures, with the
cumulative >80% ratio re-tripping on each following document.

This merges 34c9570 into `oivo` (textually clean, zero conflicts in
src/embedding/openai.ts and src/store.ts) and rebuilds dist/ from the merged
source, so the branch now carries BOTH the 429 handling and the
commercial-api-only policy, and the bundle that actually runs carries the fix.

Verified on the merged tree:
  - grep -c BulkLaneGate dist/embedding/openai.js -> 3   (was 0)
  - grep -c 'errors += remaining' dist/store.js    -> 0   (was 2, lines 1314/1322)
  - positive control: grep -c embedding dist/embedding/openai.js -> 31
  - vitest test/embedding-openai.test.ts test/embedding-store-integration.test.ts
    test/commercial-model-policy.test.ts -> 96/96 passed
  - negative control: the recurrence test at
    test/embedding-store-integration.test.ts:420 run against pre-merge 34c9570
    fails with errors=6 (expected 1), so the test can see the defect.

Part 1 of the issue (the qmd-embed.mm.mk vhost / gateway per-caller budget)
remains open and undecided — this lands the client-side half only.

Refs: i-yghj098h
Session-Id: ab2d1bd2
Claude vor 3 Wochen
Ursprung
Commit
4f1e3deb9b

+ 5 - 5
dist/cli/qmd.js

@@ -1458,12 +1458,12 @@ function buildProviderOpts(values, providerCliKind) {
             ...(timeoutMs !== undefined ? { timeoutMs } : {}),
         }
         : undefined;
-    // Historical flag is passed through so the factory can reject it as typed HOLD.
-    const autoFallback = values["embed-auto-fallback"] === true ? true : undefined;
+    if (values["embed-auto-fallback"] === true) {
+        throw commercialApiHold("embedding auto-fallback is forbidden");
+    }
     return {
         ...(providerCliKind ? { kind: providerCliKind } : {}),
         ...(openai ? { openai } : {}),
-        ...(autoFallback !== undefined ? { autoFallback } : {}),
     };
 }
 function optionalString(v) {
@@ -2204,14 +2204,14 @@ function parseCLI() {
             force: { type: "boolean", short: "f" },
             "max-docs-per-batch": { type: "string" },
             "max-batch-mb": { type: "string" },
-            provider: { type: "string" }, // "local" | "openai"
+            provider: { type: "string" }, // commercial OpenAI-compatible API
             "embed-endpoint": { type: "string" }, // OpenAI-compatible endpoint URL
             "embed-api-key": { type: "string" }, // Bearer token
             "embed-model-id": { type: "string" }, // Stable model id (default: embeddinggemma)
             "embed-upstream-model": { type: "string" }, // Upstream model name in HTTP body
             "embed-batch-size": { type: "string" }, // Batch size for HTTP provider
             "embed-timeout-ms": { type: "string" }, // Per-request timeout
-            "embed-auto-fallback": { type: "boolean" }, // Wrap openai in AutoFallback (local fallback)
+            "embed-auto-fallback": { type: "boolean" }, // forbidden compatibility flag
             "no-vacuum": { type: "boolean" }, // cleanup: skip VACUUM for cron-safe pruning
             // Update options
             pull: { type: "boolean" }, // git pull before update

+ 4 - 10
dist/embedding/autofallback.d.ts

@@ -1,13 +1,8 @@
 /**
  * autofallback.ts - AutoFallbackEmbeddingProvider.
  *
- * Composes a primary `EmbeddingProvider` (typically `OpenAIEmbeddingsProvider`)
- * and a fallback (typically `LocalLlamaCppProvider`). When the primary trips
- * its circuit breaker — or when persistent failures cross a threshold — calls
- * are routed to the fallback. After a recovery cooldown, the primary is
- * probed again; success closes the breaker and routing returns.
- *
- * Acceptance criterion 4 from i-qkarfffa: "Endpoint down → fallback local + WARN".
+ * Historical internal helper for two commercial API providers. It is not
+ * exported by the package or selected by the factory.
  *
  * Behavior summary:
  *   - Primary call succeeds → return; record success.
@@ -59,9 +54,8 @@ export declare class AutoFallbackEmbeddingProvider implements EmbeddingProvider
     /**
      * Stable model id reported by the primary. The model-id guard runs against
      * the primary's id because that's what callers actually want when the
-     * remote endpoint is online; on fallback-only operation, the local
-     * provider should report a compatible id (in the default config, both
-     * report "embeddinggemma" so this is moot).
+     * remote endpoint is online; a secondary commercial provider must report a
+     * compatible id.
      */
     getModelId(): string;
     getDimensions(): number | undefined;

+ 6 - 10
dist/embedding/autofallback.js

@@ -1,13 +1,8 @@
 /**
  * autofallback.ts - AutoFallbackEmbeddingProvider.
  *
- * Composes a primary `EmbeddingProvider` (typically `OpenAIEmbeddingsProvider`)
- * and a fallback (typically `LocalLlamaCppProvider`). When the primary trips
- * its circuit breaker — or when persistent failures cross a threshold — calls
- * are routed to the fallback. After a recovery cooldown, the primary is
- * probed again; success closes the breaker and routing returns.
- *
- * Acceptance criterion 4 from i-qkarfffa: "Endpoint down → fallback local + WARN".
+ * Historical internal helper for two commercial API providers. It is not
+ * exported by the package or selected by the factory.
  *
  * Behavior summary:
  *   - Primary call succeeds → return; record success.
@@ -22,6 +17,7 @@
  *     provider is currently active (or to the primary if both are usable).
  */
 import { CircuitOpenError } from "./openai.js";
+import { commercialApiHold } from "../model-policy.js";
 const DEFAULT_FAILURE_STREAK = 3;
 const DEFAULT_COOLDOWN_MS = 5 * 60_000;
 function defaultWarn(msg) {
@@ -39,6 +35,7 @@ export class AutoFallbackEmbeddingProvider {
     fallbackUntil = null;
     lastTransitionState = "primary";
     constructor(config) {
+        throw commercialApiHold("automatic provider fallback is disabled; commercial API failures must remain HOLD");
         if (!config.primary)
             throw new Error("AutoFallbackEmbeddingProvider: primary is required");
         if (!config.fallback)
@@ -58,9 +55,8 @@ export class AutoFallbackEmbeddingProvider {
     /**
      * Stable model id reported by the primary. The model-id guard runs against
      * the primary's id because that's what callers actually want when the
-     * remote endpoint is online; on fallback-only operation, the local
-     * provider should report a compatible id (in the default config, both
-     * report "embeddinggemma" so this is moot).
+     * remote endpoint is online; a secondary commercial provider must report a
+     * compatible id.
      */
     getModelId() {
         return this.primary.getModelId();

+ 1 - 8
dist/embedding/factory.d.ts

@@ -24,8 +24,6 @@ export type EmbedProviderConfigFile = {
          */
         concurrency?: number;
         timeoutMs?: number;
-        /** Historical only. `true` is rejected because local fallback is forbidden. */
-        autoFallback?: boolean;
     };
 };
 export declare function defaultConfigPath(): string;
@@ -35,17 +33,12 @@ export declare function defaultConfigPath(): string;
  */
 export declare function loadConfigFile(path?: string): EmbedProviderConfigFile;
 export type CreateEmbeddingProviderOptions = {
-    /** Force a specific provider kind. Overrides env + config. */
+    /** Force the commercial provider kind. Overrides env + config. */
     kind?: ProviderKind;
     /** Override config file path (mostly for tests) */
     configPath?: string;
     /** OpenAI-provider overrides — merged on top of env/config */
     openai?: Partial<OpenAIProviderConfig>;
-    /**
-     * Historical compatibility input. Any truthy value produces typed HOLD;
-     * commercial provider failures must never fall back to a local model.
-     */
-    autoFallback?: boolean;
     /**
      * Custom env source (mostly for tests). Defaults to `process.env`.
      * Read keys: QMD_EMBED_PROVIDER, QMD_EMBED_ENDPOINT, QMD_EMBED_API_KEY,

+ 22 - 15
dist/embedding/factory.js

@@ -43,11 +43,15 @@ export function resolveProviderKind(opts = {}) {
     const env = opts.env ?? process.env;
     const cfg = loadConfigFile(opts.configPath);
     // 1. Explicit kind argument
-    if (opts.kind === "local") {
+    const explicitKind = opts.kind;
+    if (explicitKind === "local") {
         throw commercialApiHold('provider kind "local" is disabled; configure an approved commercial API');
     }
-    if (opts.kind === "openai")
-        return opts.kind;
+    if (explicitKind === "openai")
+        return "openai";
+    if (explicitKind) {
+        throw commercialApiHold(`unsupported commercial provider kind "${explicitKind}"`);
+    }
     // 2a. Explicit env override
     const envKind = env.QMD_EMBED_PROVIDER?.trim().toLowerCase();
     if (envKind === "local") {
@@ -55,17 +59,24 @@ export function resolveProviderKind(opts = {}) {
     }
     if (envKind === "openai")
         return envKind;
+    if (envKind) {
+        throw commercialApiHold(`unsupported QMD_EMBED_PROVIDER=${envKind}`);
+    }
     // 2b. Endpoint env present → openai
     if (env.QMD_EMBED_ENDPOINT && env.QMD_EMBED_ENDPOINT.trim() !== "") {
         return "openai";
     }
     // 3. Config file
-    if (cfg.embedProvider?.kind === "local") {
+    const configKind = cfg.embedProvider?.kind;
+    if (configKind === "local") {
         throw commercialApiHold("embedProvider.kind=local is forbidden");
     }
-    if (cfg.embedProvider?.kind === "openai") {
+    if (configKind === "openai") {
         return "openai";
     }
+    if (configKind) {
+        throw commercialApiHold(`unsupported embedProvider.kind=${configKind}`);
+    }
     if (cfg.embedProvider?.endpoint && cfg.embedProvider.endpoint.trim() !== "") {
         return "openai";
     }
@@ -79,10 +90,7 @@ export function resolveProviderKind(opts = {}) {
 export function createEmbeddingProvider(opts = {}) {
     const env = opts.env ?? process.env;
     const cfg = loadConfigFile(opts.configPath);
-    const kind = resolveProviderKind(opts);
-    if (kind === "local") {
-        throw commercialApiHold('provider kind "local" is disabled');
-    }
+    resolveProviderKind(opts);
     // OpenAI
     const endpoint = opts.openai?.endpoint ??
         env.QMD_EMBED_ENDPOINT ??
@@ -126,7 +134,7 @@ export function createEmbeddingProvider(opts = {}) {
         now: opts.openai?.now,
     });
     // Historical fallback inputs are rejected instead of silently weakening policy.
-    const autoFallback = resolveAutoFallback(opts, env, cfg);
+    const autoFallback = resolveAutoFallback(env, cfg);
     if (autoFallback) {
         throw commercialApiHold("local auto-fallback is forbidden; commercial API failures must remain HOLD");
     }
@@ -148,16 +156,15 @@ export function assertCommercialEndpoint(endpoint) {
         throw commercialApiHold(`endpoint ${parsed.protocol}//${host} is local, private, or non-TLS; use an approved commercial HTTPS API`);
     }
 }
-function resolveAutoFallback(opts, env, cfg) {
-    if (typeof opts.autoFallback === "boolean")
-        return opts.autoFallback;
+function resolveAutoFallback(env, cfg) {
     const envVal = env.QMD_EMBED_AUTO_FALLBACK?.trim().toLowerCase();
     if (envVal === "1" || envVal === "true" || envVal === "yes")
         return true;
     if (envVal === "0" || envVal === "false" || envVal === "no")
         return false;
-    if (typeof cfg.embedProvider?.autoFallback === "boolean") {
-        return cfg.embedProvider.autoFallback;
+    const configAutoFallback = cfg.embedProvider?.autoFallback;
+    if (typeof configAutoFallback === "boolean") {
+        return configAutoFallback;
     }
     return false;
 }

+ 0 - 2
dist/embedding/index.d.ts

@@ -2,7 +2,5 @@
  * embedding/index.ts - re-exports for the embedding provider abstraction.
  */
 export { type EmbeddingProvider, type ProviderKind, type ProviderEmbedding, type ProviderEmbedOptions, type ProviderHealth, ModelMismatchError, assertModelCompatible, } from "./provider.js";
-export { LocalLlamaCppProvider, type LocalLlamaCppProviderConfig, } from "./local.js";
 export { OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, isRetryableStatus, chunkArray, type OpenAIProviderConfig, type CircuitState, DEFAULT_BATCH_SIZE, DEFAULT_TIMEOUT_MS, RETRY_BACKOFFS_MS, } from "./openai.js";
 export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, loadConfigFile, defaultConfigPath, type CreateEmbeddingProviderOptions, type EmbedProviderConfigFile, } from "./factory.js";
-export { AutoFallbackEmbeddingProvider, type AutoFallbackProviderConfig, type FallbackState, } from "./autofallback.js";

+ 0 - 2
dist/embedding/index.js

@@ -2,7 +2,5 @@
  * embedding/index.ts - re-exports for the embedding provider abstraction.
  */
 export { ModelMismatchError, assertModelCompatible, } from "./provider.js";
-export { LocalLlamaCppProvider, } from "./local.js";
 export { OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, isRetryableStatus, chunkArray, DEFAULT_BATCH_SIZE, DEFAULT_TIMEOUT_MS, RETRY_BACKOFFS_MS, } from "./openai.js";
 export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, loadConfigFile, defaultConfigPath, } from "./factory.js";
-export { AutoFallbackEmbeddingProvider, } from "./autofallback.js";

+ 2 - 11
dist/embedding/local.d.ts

@@ -1,18 +1,9 @@
 import type { LlamaCpp } from "../llm.js";
-import type { EmbeddingProvider, ProviderEmbedOptions, ProviderEmbedding, ProviderHealth, ProviderKind } from "./provider.js";
 export type LocalLlamaCppProviderConfig = {
     llm?: LlamaCpp;
     modelId?: string;
 };
-/** Historical SDK symbol. Construction is blocked by the commercial-only policy. */
-export declare class LocalLlamaCppProvider implements EmbeddingProvider {
-    readonly kind: ProviderKind;
+/** Historical direct-import symbol. It is not part of the package API. */
+export declare class LocalLlamaCppProvider {
     constructor(_config?: LocalLlamaCppProviderConfig);
-    getModelId(): string;
-    getDimensions(): number | undefined;
-    getLastError(): string | undefined;
-    healthcheck(_signal?: AbortSignal): Promise<ProviderHealth>;
-    embed(_text: string, _options?: ProviderEmbedOptions): Promise<ProviderEmbedding | null>;
-    embedBatch(_texts: string[], _options?: ProviderEmbedOptions): Promise<(ProviderEmbedding | null)[]>;
-    dispose(): Promise<void>;
 }

+ 1 - 15
dist/embedding/local.js

@@ -1,21 +1,7 @@
 import { commercialApiHold } from "../model-policy.js";
-/** Historical SDK symbol. Construction is blocked by the commercial-only policy. */
+/** Historical direct-import symbol. It is not part of the package API. */
 export class LocalLlamaCppProvider {
-    kind = "local";
     constructor(_config = {}) {
         throw commercialApiHold("LocalLlamaCppProvider is disabled; configure an approved commercial API");
     }
-    getModelId() { return "disabled-local-provider"; }
-    getDimensions() { return undefined; }
-    getLastError() { return "QMD_COMMERCIAL_API_HOLD"; }
-    async healthcheck(_signal) {
-        throw commercialApiHold("local provider healthcheck is disabled");
-    }
-    async embed(_text, _options = {}) {
-        throw commercialApiHold("local embedding is disabled");
-    }
-    async embedBatch(_texts, _options = {}) {
-        throw commercialApiHold("local batch embedding is disabled");
-    }
-    async dispose() { }
 }

+ 2 - 3
dist/embedding/openai.d.ts

@@ -18,8 +18,8 @@
  *     (AIMD), so reindex traffic yields the bucket to interactive callers
  *     instead of contending head-on with them
  *   - 4xx (non-429) → no retry, count as failure
- *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
- *     callers receive failures; local fallback is forbidden
+ *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min;
+ *     callers receive failures and no model fallback is selected
  *   - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  *   - Healthcheck via `GET /health` if available, else a probe embed call
  */
@@ -203,7 +203,6 @@ export declare class CircuitBreaker {
 }
 /**
  * Raised when the circuit breaker is OPEN and a call is short-circuited.
- * Callers (e.g. fallback wrapper) can catch this to switch to local provider.
  */
 export declare class CircuitOpenError extends Error {
     constructor(message?: string);

+ 2 - 3
dist/embedding/openai.js

@@ -18,8 +18,8 @@
  *     (AIMD), so reindex traffic yields the bucket to interactive callers
  *     instead of contending head-on with them
  *   - 4xx (non-429) → no retry, count as failure
- *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
- *     callers receive failures; local fallback is forbidden
+ *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min;
+ *     callers receive failures and no model fallback is selected
  *   - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  *   - Healthcheck via `GET /health` if available, else a probe embed call
  */
@@ -314,7 +314,6 @@ export class CircuitBreaker {
 // ─────────────────────────── Errors ──────────────────────────────────────────
 /**
  * Raised when the circuit breaker is OPEN and a call is short-circuited.
- * Callers (e.g. fallback wrapper) can catch this to switch to local provider.
  */
 export class CircuitOpenError extends Error {
     constructor(message = "OpenAIEmbeddingsProvider circuit is OPEN") {

+ 4 - 4
dist/embedding/provider.d.ts

@@ -1,8 +1,8 @@
 /**
  * provider.ts - Embedding provider abstraction
  *
- * Production embeddings use a commercial OpenAI-compatible API. The `local`
- * kind remains readable only for historical config and is rejected by the factory.
+ * Production embeddings use a commercial OpenAI-compatible API. Historical
+ * local configuration is parsed only by the factory so it can fail closed.
  *
  * The factory in `./factory.ts` selects an implementation based on env vars,
  * a CLI flag, or `~/.config/qmd/config.json`.
@@ -18,7 +18,7 @@ export type ProviderEmbedding = {
 /**
  * Supported provider kinds
  */
-export type ProviderKind = "local" | "openai";
+export type ProviderKind = "openai";
 /**
  * Healthcheck result for provider startup verification
  */
@@ -100,7 +100,7 @@ export interface EmbeddingProvider {
      * compatible; callers must guard with `provider.getLastError?.()`.
      */
     getLastError?(): string | undefined;
-    /** Release any held resources (HTTP keep-alive sockets, model handles, …) */
+    /** Release any held resources, such as HTTP keep-alive sockets. */
     dispose(): Promise<void>;
 }
 /**

+ 2 - 2
dist/embedding/provider.js

@@ -1,8 +1,8 @@
 /**
  * provider.ts - Embedding provider abstraction
  *
- * Production embeddings use a commercial OpenAI-compatible API. The `local`
- * kind remains readable only for historical config and is rejected by the factory.
+ * Production embeddings use a commercial OpenAI-compatible API. Historical
+ * local configuration is parsed only by the factory so it can fail closed.
  *
  * The factory in `./factory.ts` selects an implementation based on env vars,
  * a CLI flag, or `~/.config/qmd/config.json`.

+ 11 - 13
dist/index.d.ts

@@ -25,7 +25,7 @@ export type { ChunkStrategy } from "./store.js";
 export { getDefaultDbPath } from "./store.js";
 export { Maintenance } from "./maintenance.js";
 import type { EmbeddingProvider } from "./embedding/index.js";
-export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, LocalLlamaCppProvider, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, type EmbeddingProvider, type ProviderKind, type ProviderEmbedding, type ProviderEmbedOptions, type ProviderHealth, type CreateEmbeddingProviderOptions, type OpenAIProviderConfig, type LocalLlamaCppProviderConfig, type EmbedProviderConfigFile, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
+export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, type EmbeddingProvider, type ProviderKind, type ProviderEmbedding, type ProviderEmbedOptions, type ProviderHealth, type CreateEmbeddingProviderOptions, type OpenAIProviderConfig, type EmbedProviderConfigFile, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
 export { CommercialApiHoldError, COMMERCIAL_API_HOLD_CODE, commercialApiHold, } from "./model-policy.js";
 export { getDistinctEmbeddingModels } from "./store.js";
 /**
@@ -75,9 +75,8 @@ export interface SearchOptions {
     /**
      * Optional embedding provider for query-side encoding (i-loazq6ze).
      * When supplied, vec/hyde sub-queries are encoded through the provider
-     * (HTTP / GPU worker / AutoFallback chain) instead of the local llama-cpp
-     * model. Omit to keep pre-patch behavior — the SDK store still works
-     * unchanged for callers that have not opted into a remote provider.
+     * through the configured commercial API. Without one, learned work returns
+     * typed HOLD.
      */
     embedProvider?: EmbeddingProvider;
 }
@@ -96,7 +95,7 @@ export interface VectorSearchOptions {
     collection?: string;
     /**
      * Optional embedding provider for query encoding (i-loazq6ze). Forwarded
-     * through to `searchVec`. Defaults to local llama-cpp.
+     * through to `searchVec`. Without one, learned work returns typed HOLD.
      */
     embedProvider?: EmbeddingProvider;
 }
@@ -121,11 +120,10 @@ export interface StoreOptions {
     /** Inline collection config (mutually exclusive with `configPath`) */
     config?: CollectionConfig;
     /**
-     * Optional default embedding provider for query encoding (i-loazq6ze).
-     * When set, every `store.search(...)` call uses this provider unless the
-     * caller passes its own `embedProvider` in `SearchOptions`. MCP / HTTP
-     * server constructs the provider once at startup and injects it here so
-     * every query routes through the GPU worker.
+     * Optional default commercial embedding provider. When set, every learned
+     * embedding operation uses this provider unless a search call supplies its
+     * own `embedProvider`. MCP / HTTP constructs the provider once at startup
+     * and injects it here so every query routes through the approved API.
      */
     embedProvider?: EmbeddingProvider;
 }
@@ -133,8 +131,8 @@ export interface StoreOptions {
  * The QMD SDK store — provides search, retrieval, collection management,
  * context management, and indexing operations.
  *
- * All methods are async. The store manages its own LlamaCpp instance
- * (lazy-loaded, auto-unloaded after inactivity) — no global singletons.
+ * All methods are async. Learned work uses an injected commercial provider or
+ * the fail-closed compatibility adapter.
  */
 export interface QMDStore {
     /** The underlying internal store (for advanced use) */
@@ -220,7 +218,7 @@ export interface QMDStore {
     getStatus(): Promise<IndexStatus>;
     /** Get index health info (stale embeddings, etc.) */
     getIndexHealth(): Promise<IndexHealthInfo>;
-    /** Close the store and release all resources (LLM models, DB connection) */
+    /** Close the store and release provider and database resources. */
     close(): Promise<void>;
 }
 /**

+ 6 - 5
dist/index.js

@@ -26,9 +26,9 @@ export { getDefaultDbPath } from "./store.js";
 // Re-export Maintenance class for CLI housekeeping operations
 export { Maintenance } from "./maintenance.js";
 // Re-export embedding provider abstraction for SDK consumers (i-qkarfffa).
-// `createEmbeddingProvider` is commercial-only. The historical local symbol
-// remains exported for source compatibility but its constructor returns HOLD.
-export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, LocalLlamaCppProvider, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
+// `createEmbeddingProvider` is commercial-only. Local and fallback providers
+// are intentionally absent from the package API.
+export { createEmbeddingProvider, resolveProviderKind, assertCommercialEndpoint, OpenAIEmbeddingsProvider, CircuitBreaker, CircuitOpenError, HttpError, ModelMismatchError, assertModelCompatible, DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE, DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS, RETRY_BACKOFFS_MS as PROVIDER_RETRY_BACKOFFS_MS, } from "./embedding/index.js";
 export { CommercialApiHoldError, COMMERCIAL_API_HOLD_CODE, commercialApiHold, } from "./model-policy.js";
 export { getDistinctEmbeddingModels } from "./store.js";
 /**
@@ -83,8 +83,8 @@ export async function createStore(options) {
         syncConfigToDb(db, config);
     }
     // else: DB-only mode — no external config, use existing store_collections
-    // Create a per-store LlamaCpp instance — lazy-loads models on first use,
-    // auto-unloads after 5 min inactivity to free VRAM.
+    // Compatibility adapter for learned operations that lack an injected
+    // commercial provider. Every such call returns typed HOLD.
     const llm = new LlamaCpp({
         embedModel: config?.models?.embed,
         generateModel: config?.models?.generate,
@@ -231,6 +231,7 @@ export async function createStore(options) {
                 maxBatchBytes: embedOpts?.maxBatchBytes,
                 chunkStrategy: embedOpts?.chunkStrategy,
                 onProgress: embedOpts?.onProgress,
+                embedProvider: options.embedProvider,
             });
         },
         // Index Health

+ 4 - 5
dist/store.d.ts

@@ -884,9 +884,8 @@ export interface HybridQueryOptions {
     /**
      * Optional embedding provider for query-side encoding (i-loazq6ze).
      * When supplied, the original-query vector AND any vec/hyde expansion
-     * variants are encoded through this provider (HTTP, GPU worker,
-     * AutoFallback chain) instead of `getLlm(store).embedBatch(...)`. Skip
-     * to keep pre-patch behavior (uses local LlamaCpp).
+     * variants are encoded through this commercial provider. Without one,
+     * learned work returns typed HOLD.
      */
     embedProvider?: EmbeddingProvider;
 }
@@ -929,8 +928,8 @@ export interface VectorSearchOptions {
     hooks?: Pick<SearchHooks, 'onExpand'>;
     /**
      * Optional embedding provider for query-side encoding (i-loazq6ze).
-     * When supplied, query vectors are encoded via the provider (HTTP /
-     * GPU worker / fallback chain) instead of the local llama-cpp model.
+     * When supplied, query vectors are encoded through the commercial API.
+     * Without one, learned work returns typed HOLD.
      */
     embedProvider?: EmbeddingProvider;
 }

+ 7 - 16
dist/store.js

@@ -1251,16 +1251,14 @@ export async function generateEmbeddings(store, options) {
         // rest of the loop is unchanged.
         const embedOne = async (text, modelArg) => {
             if (provider) {
-                const sig = provider.kind === 'local' ? session.signal : undefined;
-                const r = await provider.embed(text, { model: modelArg, signal: sig });
+                const r = await provider.embed(text, { model: modelArg });
                 return r ? { embedding: r.embedding, model: r.model } : null;
             }
             return session.embed(text, { model: modelArg });
         };
         const embedMany = async (texts, modelArg) => {
             if (provider) {
-                const sig = provider.kind === 'local' ? session.signal : undefined;
-                const r = await provider.embedBatch(texts, { model: modelArg, signal: sig });
+                const r = await provider.embedBatch(texts, { model: modelArg });
                 return r.map((x) => (x ? { embedding: x.embedding, model: x.model } : null));
             }
             return session.embedBatch(texts, { model: modelArg });
@@ -2793,11 +2791,7 @@ async function getEmbedding(text, model, isQuery, session, llmOverride, embedPro
         const formattedText = isQuery
             ? formatQueryForEmbedding(text, providerModel)
             : formatDocForEmbedding(text, undefined, providerModel);
-        // Only forward an AbortSignal when the provider is local-backed;
-        // remote providers manage their own timeouts and an LLM-session signal
-        // would abort their HTTP request prematurely (i-08ovbvtb).
-        const sig = embedProvider.kind === "local" ? session?.signal : undefined;
-        const result = await embedProvider.embed(formattedText, sig ? { model: providerModel, signal: sig } : { model: providerModel });
+        const result = await embedProvider.embed(formattedText, { model: providerModel });
         return result?.embedding ?? null;
     }
     // Format text using the appropriate prompt template
@@ -3530,9 +3524,8 @@ export async function hybridQuery(store, query, options) {
             }
         }
         // Batch embed all vector queries in a single call.
-        // When `embedProvider` is supplied (i-loazq6ze), route the encode through
-        // it (HTTP / GPU worker / AutoFallback chain) instead of warming the
-        // local llama-cpp model — this is the whole point of the GPU worker.
+        // Route query embeddings through the injected commercial provider. The
+        // compatibility path below is fail-closed and returns typed HOLD.
         const embedModelName = embedProvider
             ? embedProvider.getModelId()
             : getLlm(store).embedModelName;
@@ -3853,10 +3846,8 @@ export async function structuredSearch(store, searches, options) {
     if (hasVectors) {
         const vecSearches = searches.filter((s) => s.type === 'vec' || s.type === 'hyde');
         if (vecSearches.length > 0) {
-            // Route batch encoding through the supplied EmbeddingProvider when
-            // present (i-loazq6ze). Otherwise fall back to the local llama-cpp
-            // singleton — preserves pre-patch behavior for callers that don't
-            // configure a provider.
+            // Route batch encoding through the supplied commercial provider. The
+            // compatibility branch returns typed HOLD when no provider is configured.
             const embedModelName = embedProvider
                 ? embedProvider.getModelId()
                 : getLlm(store).embedModelName;

+ 5 - 5
src/cli/qmd.ts

@@ -1712,13 +1712,13 @@ function buildProviderOpts(
         }
       : undefined;
 
-  // Historical flag is passed through so the factory can reject it as typed HOLD.
-  const autoFallback = values["embed-auto-fallback"] === true ? true : undefined;
+  if (values["embed-auto-fallback"] === true) {
+    throw commercialApiHold("embedding auto-fallback is forbidden");
+  }
 
   return {
     ...(providerCliKind ? { kind: providerCliKind } : {}),
     ...(openai ? { openai } : {}),
-    ...(autoFallback !== undefined ? { autoFallback } : {}),
   };
 }
 
@@ -2600,14 +2600,14 @@ function parseCLI() {
       force: { type: "boolean", short: "f" },
       "max-docs-per-batch": { type: "string" },
       "max-batch-mb": { type: "string" },
-      provider: { type: "string" },                  // "local" | "openai"
+      provider: { type: "string" },                  // commercial OpenAI-compatible API
       "embed-endpoint": { type: "string" },          // OpenAI-compatible endpoint URL
       "embed-api-key": { type: "string" },           // Bearer token
       "embed-model-id": { type: "string" },          // Stable model id (default: embeddinggemma)
       "embed-upstream-model": { type: "string" },    // Upstream model name in HTTP body
       "embed-batch-size": { type: "string" },        // Batch size for HTTP provider
       "embed-timeout-ms": { type: "string" },        // Per-request timeout
-      "embed-auto-fallback": { type: "boolean" },    // Wrap openai in AutoFallback (local fallback)
+      "embed-auto-fallback": { type: "boolean" },    // forbidden compatibility flag
       "no-vacuum": { type: "boolean" },              // cleanup: skip VACUUM for cron-safe pruning
       // Update options
       pull: { type: "boolean" },  // git pull before update

+ 8 - 10
src/embedding/autofallback.ts

@@ -1,13 +1,8 @@
 /**
  * autofallback.ts - AutoFallbackEmbeddingProvider.
  *
- * Composes a primary `EmbeddingProvider` (typically `OpenAIEmbeddingsProvider`)
- * and a fallback (typically `LocalLlamaCppProvider`). When the primary trips
- * its circuit breaker — or when persistent failures cross a threshold — calls
- * are routed to the fallback. After a recovery cooldown, the primary is
- * probed again; success closes the breaker and routing returns.
- *
- * Acceptance criterion 4 from i-qkarfffa: "Endpoint down → fallback local + WARN".
+ * Historical internal helper for two commercial API providers. It is not
+ * exported by the package or selected by the factory.
  *
  * Behavior summary:
  *   - Primary call succeeds → return; record success.
@@ -30,6 +25,7 @@ import type {
   ProviderKind,
 } from "./provider.js";
 import { CircuitOpenError } from "./openai.js";
+import { commercialApiHold } from "../model-policy.js";
 
 export type AutoFallbackProviderConfig = {
   primary: EmbeddingProvider;
@@ -76,6 +72,9 @@ export class AutoFallbackEmbeddingProvider implements EmbeddingProvider {
   private lastTransitionState: FallbackState = "primary";
 
   constructor(config: AutoFallbackProviderConfig) {
+    throw commercialApiHold(
+      "automatic provider fallback is disabled; commercial API failures must remain HOLD",
+    );
     if (!config.primary) throw new Error("AutoFallbackEmbeddingProvider: primary is required");
     if (!config.fallback) throw new Error("AutoFallbackEmbeddingProvider: fallback is required");
     if (config.primary === config.fallback) {
@@ -95,9 +94,8 @@ export class AutoFallbackEmbeddingProvider implements EmbeddingProvider {
   /**
    * Stable model id reported by the primary. The model-id guard runs against
    * the primary's id because that's what callers actually want when the
-   * remote endpoint is online; on fallback-only operation, the local
-   * provider should report a compatible id (in the default config, both
-   * report "embeddinggemma" so this is moot).
+   * remote endpoint is online; a secondary commercial provider must report a
+   * compatible id.
    */
   getModelId(): string {
     return this.primary.getModelId();

+ 22 - 25
src/embedding/factory.ts

@@ -36,8 +36,6 @@ export type EmbedProviderConfigFile = {
      */
     concurrency?: number;
     timeoutMs?: number;
-    /** Historical only. `true` is rejected because local fallback is forbidden. */
-    autoFallback?: boolean;
   };
 };
 
@@ -66,17 +64,12 @@ export function loadConfigFile(path: string = defaultConfigPath()): EmbedProvide
 // ─────────────────────────── Factory options ────────────────────────────────
 
 export type CreateEmbeddingProviderOptions = {
-  /** Force a specific provider kind. Overrides env + config. */
+  /** Force the commercial provider kind. Overrides env + config. */
   kind?: ProviderKind;
   /** Override config file path (mostly for tests) */
   configPath?: string;
   /** OpenAI-provider overrides — merged on top of env/config */
   openai?: Partial<OpenAIProviderConfig>;
-  /**
-   * Historical compatibility input. Any truthy value produces typed HOLD;
-   * commercial provider failures must never fall back to a local model.
-   */
-  autoFallback?: boolean;
   /**
    * Custom env source (mostly for tests). Defaults to `process.env`.
    * Read keys: QMD_EMBED_PROVIDER, QMD_EMBED_ENDPOINT, QMD_EMBED_API_KEY,
@@ -95,10 +88,14 @@ export function resolveProviderKind(opts: CreateEmbeddingProviderOptions = {}):
   const cfg = loadConfigFile(opts.configPath);
 
   // 1. Explicit kind argument
-  if (opts.kind === "local") {
+  const explicitKind = opts.kind as string | undefined;
+  if (explicitKind === "local") {
     throw commercialApiHold('provider kind "local" is disabled; configure an approved commercial API');
   }
-  if (opts.kind === "openai") return opts.kind;
+  if (explicitKind === "openai") return "openai";
+  if (explicitKind) {
+    throw commercialApiHold(`unsupported commercial provider kind "${explicitKind}"`);
+  }
 
   // 2a. Explicit env override
   const envKind = env.QMD_EMBED_PROVIDER?.trim().toLowerCase();
@@ -106,6 +103,9 @@ export function resolveProviderKind(opts: CreateEmbeddingProviderOptions = {}):
     throw commercialApiHold("QMD_EMBED_PROVIDER=local is forbidden");
   }
   if (envKind === "openai") return envKind;
+  if (envKind) {
+    throw commercialApiHold(`unsupported QMD_EMBED_PROVIDER=${envKind}`);
+  }
 
   // 2b. Endpoint env present → openai
   if (env.QMD_EMBED_ENDPOINT && env.QMD_EMBED_ENDPOINT.trim() !== "") {
@@ -113,12 +113,16 @@ export function resolveProviderKind(opts: CreateEmbeddingProviderOptions = {}):
   }
 
   // 3. Config file
-  if (cfg.embedProvider?.kind === "local") {
+  const configKind = cfg.embedProvider?.kind as string | undefined;
+  if (configKind === "local") {
     throw commercialApiHold("embedProvider.kind=local is forbidden");
   }
-  if (cfg.embedProvider?.kind === "openai") {
+  if (configKind === "openai") {
     return "openai";
   }
+  if (configKind) {
+    throw commercialApiHold(`unsupported embedProvider.kind=${configKind}`);
+  }
   if (cfg.embedProvider?.endpoint && cfg.embedProvider.endpoint.trim() !== "") {
     return "openai";
   }
@@ -136,11 +140,7 @@ export function createEmbeddingProvider(
 ): EmbeddingProvider {
   const env = opts.env ?? process.env;
   const cfg = loadConfigFile(opts.configPath);
-  const kind = resolveProviderKind(opts);
-
-  if (kind === "local") {
-    throw commercialApiHold('provider kind "local" is disabled');
-  }
+  resolveProviderKind(opts);
 
   // OpenAI
   const endpoint =
@@ -202,7 +202,7 @@ export function createEmbeddingProvider(
   });
 
   // Historical fallback inputs are rejected instead of silently weakening policy.
-  const autoFallback = resolveAutoFallback(opts, env, cfg);
+  const autoFallback = resolveAutoFallback(env, cfg);
   if (autoFallback) {
     throw commercialApiHold("local auto-fallback is forbidden; commercial API failures must remain HOLD");
   }
@@ -229,18 +229,15 @@ export function assertCommercialEndpoint(endpoint: string): void {
 }
 
 function resolveAutoFallback(
-  opts: CreateEmbeddingProviderOptions,
   env: Record<string, string | undefined>,
-  cfg: EmbedProviderConfigFile & {
-    embedProvider?: { autoFallback?: boolean };
-  },
+  cfg: EmbedProviderConfigFile,
 ): boolean {
-  if (typeof opts.autoFallback === "boolean") return opts.autoFallback;
   const envVal = env.QMD_EMBED_AUTO_FALLBACK?.trim().toLowerCase();
   if (envVal === "1" || envVal === "true" || envVal === "yes") return true;
   if (envVal === "0" || envVal === "false" || envVal === "no") return false;
-  if (typeof cfg.embedProvider?.autoFallback === "boolean") {
-    return cfg.embedProvider.autoFallback;
+  const configAutoFallback = (cfg.embedProvider as { autoFallback?: unknown } | undefined)?.autoFallback;
+  if (typeof configAutoFallback === "boolean") {
+    return configAutoFallback;
   }
   return false;
 }

+ 0 - 11
src/embedding/index.ts

@@ -12,11 +12,6 @@ export {
   assertModelCompatible,
 } from "./provider.js";
 
-export {
-  LocalLlamaCppProvider,
-  type LocalLlamaCppProviderConfig,
-} from "./local.js";
-
 export {
   OpenAIEmbeddingsProvider,
   CircuitBreaker,
@@ -40,9 +35,3 @@ export {
   type CreateEmbeddingProviderOptions,
   type EmbedProviderConfigFile,
 } from "./factory.js";
-
-export {
-  AutoFallbackEmbeddingProvider,
-  type AutoFallbackProviderConfig,
-  type FallbackState,
-} from "./autofallback.js";

+ 2 - 25
src/embedding/local.ts

@@ -1,34 +1,11 @@
 import { commercialApiHold } from "../model-policy.js";
 import type { LlamaCpp } from "../llm.js";
-import type {
-  EmbeddingProvider,
-  ProviderEmbedOptions,
-  ProviderEmbedding,
-  ProviderHealth,
-  ProviderKind,
-} from "./provider.js";
 
 export type LocalLlamaCppProviderConfig = { llm?: LlamaCpp; modelId?: string };
 
-/** Historical SDK symbol. Construction is blocked by the commercial-only policy. */
-export class LocalLlamaCppProvider implements EmbeddingProvider {
-  readonly kind: ProviderKind = "local";
-
+/** Historical direct-import symbol. It is not part of the package API. */
+export class LocalLlamaCppProvider {
   constructor(_config: LocalLlamaCppProviderConfig = {}) {
     throw commercialApiHold("LocalLlamaCppProvider is disabled; configure an approved commercial API");
   }
-
-  getModelId(): string { return "disabled-local-provider"; }
-  getDimensions(): number | undefined { return undefined; }
-  getLastError(): string | undefined { return "QMD_COMMERCIAL_API_HOLD"; }
-  async healthcheck(_signal?: AbortSignal): Promise<ProviderHealth> {
-    throw commercialApiHold("local provider healthcheck is disabled");
-  }
-  async embed(_text: string, _options: ProviderEmbedOptions = {}): Promise<ProviderEmbedding | null> {
-    throw commercialApiHold("local embedding is disabled");
-  }
-  async embedBatch(_texts: string[], _options: ProviderEmbedOptions = {}): Promise<(ProviderEmbedding | null)[]> {
-    throw commercialApiHold("local batch embedding is disabled");
-  }
-  async dispose(): Promise<void> {}
 }

+ 2 - 3
src/embedding/openai.ts

@@ -18,8 +18,8 @@
  *     (AIMD), so reindex traffic yields the bucket to interactive callers
  *     instead of contending head-on with them
  *   - 4xx (non-429) → no retry, count as failure
- *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min,
- *     callers receive failures; local fallback is forbidden
+ *   - Circuit breaker: >50% failures in a 60s window → OPEN for 5 min;
+ *     callers receive failures and no model fallback is selected
  *   - Per-call timeout via AbortSignal (default QMD_EMBED_TIMEOUT_MS=30000)
  *   - Healthcheck via `GET /health` if available, else a probe embed call
  */
@@ -433,7 +433,6 @@ export class CircuitBreaker {
 
 /**
  * Raised when the circuit breaker is OPEN and a call is short-circuited.
- * Callers (e.g. fallback wrapper) can catch this to switch to local provider.
  */
 export class CircuitOpenError extends Error {
   constructor(message = "OpenAIEmbeddingsProvider circuit is OPEN") {

+ 4 - 4
src/embedding/provider.ts

@@ -1,8 +1,8 @@
 /**
  * provider.ts - Embedding provider abstraction
  *
- * Production embeddings use a commercial OpenAI-compatible API. The `local`
- * kind remains readable only for historical config and is rejected by the factory.
+ * Production embeddings use a commercial OpenAI-compatible API. Historical
+ * local configuration is parsed only by the factory so it can fail closed.
  *
  * The factory in `./factory.ts` selects an implementation based on env vars,
  * a CLI flag, or `~/.config/qmd/config.json`.
@@ -20,7 +20,7 @@ export type ProviderEmbedding = {
 /**
  * Supported provider kinds
  */
-export type ProviderKind = "local" | "openai";
+export type ProviderKind = "openai";
 
 /**
  * Healthcheck result for provider startup verification
@@ -112,7 +112,7 @@ export interface EmbeddingProvider {
    */
   getLastError?(): string | undefined;
 
-  /** Release any held resources (HTTP keep-alive sockets, model handles, …) */
+  /** Release any held resources, such as HTTP keep-alive sockets. */
   dispose(): Promise<void>;
 }
 

+ 15 - 18
src/index.ts

@@ -128,13 +128,12 @@ export { Maintenance } from "./maintenance.js";
 import type { EmbeddingProvider } from "./embedding/index.js";
 
 // Re-export embedding provider abstraction for SDK consumers (i-qkarfffa).
-// `createEmbeddingProvider` is commercial-only. The historical local symbol
-// remains exported for source compatibility but its constructor returns HOLD.
+// `createEmbeddingProvider` is commercial-only. Local and fallback providers
+// are intentionally absent from the package API.
 export {
   createEmbeddingProvider,
   resolveProviderKind,
   assertCommercialEndpoint,
-  LocalLlamaCppProvider,
   OpenAIEmbeddingsProvider,
   CircuitBreaker,
   CircuitOpenError,
@@ -148,7 +147,6 @@ export {
   type ProviderHealth,
   type CreateEmbeddingProviderOptions,
   type OpenAIProviderConfig,
-  type LocalLlamaCppProviderConfig,
   type EmbedProviderConfigFile,
   DEFAULT_BATCH_SIZE as DEFAULT_PROVIDER_BATCH_SIZE,
   DEFAULT_TIMEOUT_MS as DEFAULT_PROVIDER_TIMEOUT_MS,
@@ -212,9 +210,8 @@ export interface SearchOptions {
   /**
    * Optional embedding provider for query-side encoding (i-loazq6ze).
    * When supplied, vec/hyde sub-queries are encoded through the provider
-   * (HTTP / GPU worker / AutoFallback chain) instead of the local llama-cpp
-   * model. Omit to keep pre-patch behavior — the SDK store still works
-   * unchanged for callers that have not opted into a remote provider.
+   * through the configured commercial API. Without one, learned work returns
+   * typed HOLD.
    */
   embedProvider?: EmbeddingProvider;
 }
@@ -235,7 +232,7 @@ export interface VectorSearchOptions {
   collection?: string;
   /**
    * Optional embedding provider for query encoding (i-loazq6ze). Forwarded
-   * through to `searchVec`. Defaults to local llama-cpp.
+   * through to `searchVec`. Without one, learned work returns typed HOLD.
    */
   embedProvider?: EmbeddingProvider;
 }
@@ -262,11 +259,10 @@ export interface StoreOptions {
   /** Inline collection config (mutually exclusive with `configPath`) */
   config?: CollectionConfig;
   /**
-   * Optional default embedding provider for query encoding (i-loazq6ze).
-   * When set, every `store.search(...)` call uses this provider unless the
-   * caller passes its own `embedProvider` in `SearchOptions`. MCP / HTTP
-   * server constructs the provider once at startup and injects it here so
-   * every query routes through the GPU worker.
+   * Optional default commercial embedding provider. When set, every learned
+   * embedding operation uses this provider unless a search call supplies its
+   * own `embedProvider`. MCP / HTTP constructs the provider once at startup
+   * and injects it here so every query routes through the approved API.
    */
   embedProvider?: EmbeddingProvider;
 }
@@ -275,8 +271,8 @@ export interface StoreOptions {
  * The QMD SDK store — provides search, retrieval, collection management,
  * context management, and indexing operations.
  *
- * All methods are async. The store manages its own LlamaCpp instance
- * (lazy-loaded, auto-unloaded after inactivity) — no global singletons.
+ * All methods are async. Learned work uses an injected commercial provider or
+ * the fail-closed compatibility adapter.
  */
 export interface QMDStore {
   /** The underlying internal store (for advanced use) */
@@ -371,7 +367,7 @@ export interface QMDStore {
 
   // ── Lifecycle ───────────────────────────────────────────────────────
 
-  /** Close the store and release all resources (LLM models, DB connection) */
+  /** Close the store and release provider and database resources. */
   close(): Promise<void>;
 }
 
@@ -430,8 +426,8 @@ export async function createStore(options: StoreOptions): Promise<QMDStore> {
   }
   // else: DB-only mode — no external config, use existing store_collections
 
-  // Create a per-store LlamaCpp instance — lazy-loads models on first use,
-  // auto-unloads after 5 min inactivity to free VRAM.
+  // Compatibility adapter for learned operations that lack an injected
+  // commercial provider. Every such call returns typed HOLD.
   const llm = new LlamaCpp({
     embedModel: config?.models?.embed,
     generateModel: config?.models?.generate,
@@ -592,6 +588,7 @@ export async function createStore(options: StoreOptions): Promise<QMDStore> {
         maxBatchBytes: embedOpts?.maxBatchBytes,
         chunkStrategy: embedOpts?.chunkStrategy,
         onProgress: embedOpts?.onProgress,
+        embedProvider: options.embedProvider,
       });
     },
 

+ 11 - 21
src/store.ts

@@ -1689,8 +1689,7 @@ export async function generateEmbeddings(
       modelArg: string,
     ): Promise<{ embedding: number[]; model: string } | null> => {
       if (provider) {
-        const sig = provider.kind === 'local' ? session.signal : undefined;
-        const r = await provider.embed(text, { model: modelArg, signal: sig });
+        const r = await provider.embed(text, { model: modelArg });
         return r ? { embedding: r.embedding, model: r.model } : null;
       }
       return session.embed(text, { model: modelArg });
@@ -1700,8 +1699,7 @@ export async function generateEmbeddings(
       modelArg: string,
     ): Promise<({ embedding: number[]; model: string } | null)[]> => {
       if (provider) {
-        const sig = provider.kind === 'local' ? session.signal : undefined;
-        const r = await provider.embedBatch(texts, { model: modelArg, signal: sig });
+        const r = await provider.embedBatch(texts, { model: modelArg });
         return r.map((x) => (x ? { embedding: x.embedding, model: x.model } : null));
       }
       return session.embedBatch(texts, { model: modelArg });
@@ -3615,11 +3613,7 @@ async function getEmbedding(text: string, model: string, isQuery: boolean, sessi
     const formattedText = isQuery
       ? formatQueryForEmbedding(text, providerModel)
       : formatDocForEmbedding(text, undefined, providerModel);
-    // Only forward an AbortSignal when the provider is local-backed;
-    // remote providers manage their own timeouts and an LLM-session signal
-    // would abort their HTTP request prematurely (i-08ovbvtb).
-    const sig = embedProvider.kind === "local" ? session?.signal : undefined;
-    const result = await embedProvider.embed(formattedText, sig ? { model: providerModel, signal: sig } : { model: providerModel });
+    const result = await embedProvider.embed(formattedText, { model: providerModel });
     return result?.embedding ?? null;
   }
   // Format text using the appropriate prompt template
@@ -4421,9 +4415,8 @@ export interface HybridQueryOptions {
   /**
    * Optional embedding provider for query-side encoding (i-loazq6ze).
    * When supplied, the original-query vector AND any vec/hyde expansion
-   * variants are encoded through this provider (HTTP, GPU worker,
-   * AutoFallback chain) instead of `getLlm(store).embedBatch(...)`. Skip
-   * to keep pre-patch behavior (uses local LlamaCpp).
+   * variants are encoded through this commercial provider. Without one,
+   * learned work returns typed HOLD.
    */
   embedProvider?: EmbeddingProvider;
 }
@@ -4548,9 +4541,8 @@ export async function hybridQuery(
     }
 
     // Batch embed all vector queries in a single call.
-    // When `embedProvider` is supplied (i-loazq6ze), route the encode through
-    // it (HTTP / GPU worker / AutoFallback chain) instead of warming the
-    // local llama-cpp model — this is the whole point of the GPU worker.
+    // Route query embeddings through the injected commercial provider. The
+    // compatibility path below is fail-closed and returns typed HOLD.
     const embedModelName = embedProvider
       ? embedProvider.getModelId()
       : getLlm(store).embedModelName;
@@ -4757,8 +4749,8 @@ export interface VectorSearchOptions {
   hooks?: Pick<SearchHooks, 'onExpand'>;
   /**
    * Optional embedding provider for query-side encoding (i-loazq6ze).
-   * When supplied, query vectors are encoded via the provider (HTTP /
-   * GPU worker / fallback chain) instead of the local llama-cpp model.
+   * When supplied, query vectors are encoded through the commercial API.
+   * Without one, learned work returns typed HOLD.
    */
   embedProvider?: EmbeddingProvider;
 }
@@ -4957,10 +4949,8 @@ export async function structuredSearch(
         s.type === 'vec' || s.type === 'hyde'
     );
     if (vecSearches.length > 0) {
-      // Route batch encoding through the supplied EmbeddingProvider when
-      // present (i-loazq6ze). Otherwise fall back to the local llama-cpp
-      // singleton — preserves pre-patch behavior for callers that don't
-      // configure a provider.
+      // Route batch encoding through the supplied commercial provider. The
+      // compatibility branch returns typed HOLD when no provider is configured.
       const embedModelName = embedProvider
         ? embedProvider.getModelId()
         : getLlm(store).embedModelName;

+ 28 - 0
test/cli.test.ts

@@ -295,6 +295,34 @@ describe("CLI Embed", () => {
   });
 });
 
+describe("CLI commercial-only model policy", () => {
+  test("qmd pull returns typed HOLD and never downloads model artifacts", async () => {
+    const { stderr, exitCode } = await runQmd(["pull"]);
+
+    expect(exitCode).toBe(1);
+    expect(stderr).toContain("QMD_COMMERCIAL_API_HOLD");
+  });
+
+  test("local provider selection returns typed HOLD", async () => {
+    const { stderr, exitCode } = await runQmd(["embed", "--provider", "local"]);
+
+    expect(exitCode).toBe(1);
+    expect(stderr).toContain("QMD_COMMERCIAL_API_HOLD");
+  });
+
+  test("automatic model fallback returns typed HOLD", async () => {
+    const { stderr, exitCode } = await runQmd([
+      "embed",
+      "--embed-auto-fallback",
+      "--embed-endpoint",
+      "https://commercial.example.com/v1",
+    ]);
+
+    expect(exitCode).toBe(1);
+    expect(stderr).toContain("QMD_COMMERCIAL_API_HOLD");
+  });
+});
+
 describe("CLI Skill Commands", () => {
   test("shows embedded skill with --skill alias", async () => {
     const { stdout, exitCode } = await runQmd(["--skill"]);

+ 82 - 0
test/commercial-model-policy.test.ts

@@ -0,0 +1,82 @@
+import { readFileSync } from "node:fs";
+
+import { describe, expect, test } from "vitest";
+
+import * as embeddingApi from "../src/embedding/index.js";
+import * as sdk from "../src/index.js";
+import { LocalLlamaCppProvider } from "../src/embedding/local.js";
+import { LlamaCpp, pullModels } from "../src/llm.js";
+import {
+  COMMERCIAL_API_HOLD_CODE,
+  CommercialApiHoldError,
+} from "../src/model-policy.js";
+
+function expectTypedHold(error: unknown): void {
+  expect(error).toBeInstanceOf(CommercialApiHoldError);
+  expect(error).toMatchObject({
+    code: COMMERCIAL_API_HOLD_CODE,
+    disposition: "HOLD",
+  });
+}
+
+describe("commercial-only package policy", () => {
+  test("the public SDK does not export local or fallback providers", () => {
+    for (const api of [sdk, embeddingApi]) {
+      expect(api).not.toHaveProperty("LocalLlamaCppProvider");
+      expect(api).not.toHaveProperty("AutoFallbackEmbeddingProvider");
+    }
+  });
+
+  test("package manifests contain no local model runtime or model artifact dependency", () => {
+    const manifest = JSON.parse(
+      readFileSync(new URL("../package.json", import.meta.url), "utf8"),
+    ) as Record<string, Record<string, string> | undefined>;
+    const dependencyNames = [
+      ...Object.keys(manifest.dependencies ?? {}),
+      ...Object.keys(manifest.optionalDependencies ?? {}),
+      ...Object.keys(manifest.devDependencies ?? {}),
+    ];
+
+    expect(dependencyNames).not.toContain("node-llama-cpp");
+    for (const lockFile of ["../pnpm-lock.yaml", "../bun.lock"]) {
+      const lock = readFileSync(new URL(lockFile, import.meta.url), "utf8");
+      expect(lock).not.toMatch(/node-llama-cpp|\.gguf\b|huggingface\.co/i);
+    }
+  });
+
+  test("historical local construction and model pulls return typed HOLD", async () => {
+    try {
+      new LocalLlamaCppProvider();
+      throw new Error("local provider construction unexpectedly succeeded");
+    } catch (error) {
+      expectTypedHold(error);
+    }
+
+    try {
+      await pullModels(["forbidden-model-artifact"]);
+      throw new Error("model pull unexpectedly succeeded");
+    } catch (error) {
+      expectTypedHold(error);
+    }
+  });
+
+  test("learned compatibility operations fail closed without a commercial adapter", async () => {
+    const llm = new LlamaCpp();
+    const operations = [
+      () => llm.embed("text"),
+      () => llm.embedBatch(["text"]),
+      () => llm.generate("prompt"),
+      () => llm.expandQuery("query"),
+      () => llm.rerank("query", [{ file: "doc", text: "body" }]),
+    ];
+
+    for (const operation of operations) {
+      try {
+        await operation();
+        throw new Error("learned operation unexpectedly succeeded");
+      } catch (error) {
+        expectTypedHold(error);
+      }
+    }
+  });
+});

+ 20 - 12
test/embedding-autofallback.test.ts

@@ -1,5 +1,6 @@
 /**
- * embedding-autofallback.test.ts - Tests for AutoFallbackEmbeddingProvider.
+ * Historical fallback behavior is retained below as skipped documentation.
+ * The active contract requires construction to fail closed.
  */
 
 import { describe, test, expect } from "vitest";
@@ -8,6 +9,7 @@ import {
   type AutoFallbackProviderConfig,
 } from "../src/embedding/autofallback.js";
 import { CircuitOpenError } from "../src/embedding/openai.js";
+import { CommercialApiHoldError } from "../src/model-policy.js";
 import type {
   EmbeddingProvider,
   ProviderEmbedOptions,
@@ -92,7 +94,7 @@ function buildAutoFallback(opts: Partial<AutoFallbackProviderConfig> = {}): {
   setNow: (n: number) => void;
 } {
   const primary = new FakeProvider("openai", "embeddinggemma");
-  const fallback = new FakeProvider("local", "embeddinggemma");
+  const fallback = new FakeProvider("openai", "embeddinggemma");
   const warns: string[] = [];
   let now = 1_000_000;
   const af = new AutoFallbackEmbeddingProvider({
@@ -109,14 +111,20 @@ function buildAutoFallback(opts: Partial<AutoFallbackProviderConfig> = {}): {
 
 // ─────────────────────────── Construction ────────────────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — construction", () => {
+describe("AutoFallbackEmbeddingProvider — commercial-only policy", () => {
+  test("construction returns typed HOLD", () => {
+    expect(() => buildAutoFallback()).toThrow(CommercialApiHoldError);
+  });
+});
+
+describe.skip("AutoFallbackEmbeddingProvider — historical construction", () => {
   test("requires primary", () => {
     expect(
       () =>
         new AutoFallbackEmbeddingProvider({
           // @ts-expect-error testing runtime guard
           primary: undefined,
-          fallback: new FakeProvider("local", "x"),
+          fallback: new FakeProvider("openai", "x"),
         }),
     ).toThrow(/primary is required/);
   });
@@ -151,7 +159,7 @@ describe("AutoFallbackEmbeddingProvider — construction", () => {
 
 // ─────────────────────────── Happy path ──────────────────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — happy path", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical happy path", () => {
   test("primary succeeds → fallback never called", async () => {
     const { af, primary, fallback } = buildAutoFallback();
     const r = await af.embed("hello");
@@ -178,7 +186,7 @@ describe("AutoFallbackEmbeddingProvider — happy path", () => {
 
 // ─────────────────────────── Circuit-open fallback ───────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — CircuitOpenError handling", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical CircuitOpenError handling", () => {
   test("primary throws CircuitOpenError → fallback served + cooldown opens", async () => {
     const { af, primary, fallback, warns } = buildAutoFallback();
     primary.nextThrows.push(new CircuitOpenError());
@@ -231,7 +239,7 @@ describe("AutoFallbackEmbeddingProvider — CircuitOpenError handling", () => {
 
 // ─────────────────────────── Failure-streak threshold ────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — failure streak", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical failure streak", () => {
   test("non-CircuitOpen errors below threshold → no cooldown", async () => {
     const { af, primary, fallback } = buildAutoFallback({ failureStreakThreshold: 3 });
     primary.nextThrows.push(new Error("transient"));
@@ -274,7 +282,7 @@ describe("AutoFallbackEmbeddingProvider — failure streak", () => {
 
 // ─────────────────────────── Recovery transition ─────────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — recovery transitions", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical recovery transitions", () => {
   test("recovery WARN fires when primary call succeeds after fallback", async () => {
     const { af, primary, warns, setNow } = buildAutoFallback({ cooldownMs: 5000 });
     primary.nextThrows.push(new CircuitOpenError());
@@ -299,7 +307,7 @@ describe("AutoFallbackEmbeddingProvider — recovery transitions", () => {
 
 // ─────────────────────────── Both fail ───────────────────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — both providers fail", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical both providers fail", () => {
   test("primary throws + fallback throws → embedBatch returns nulls", async () => {
     const { af, primary, fallback } = buildAutoFallback();
     primary.alwaysThrows = new Error("primary down");
@@ -318,7 +326,7 @@ describe("AutoFallbackEmbeddingProvider — both providers fail", () => {
 
 // ─────────────────────────── Healthcheck ─────────────────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — healthcheck", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical healthcheck", () => {
   test("primary healthy → returns primary health", async () => {
     const { af, primary, fallback } = buildAutoFallback();
     const h = await af.healthcheck();
@@ -350,7 +358,7 @@ describe("AutoFallbackEmbeddingProvider — healthcheck", () => {
 
 // ─────────────────────────── getLastError (i-vm1lxwry) ──────────────────────
 
-describe("AutoFallbackEmbeddingProvider — getLastError (i-vm1lxwry)", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical getLastError (i-vm1lxwry)", () => {
   test("returns undefined when both legs are clean", () => {
     const { af, primary, fallback } = buildAutoFallback();
     primary.lastErr = undefined;
@@ -386,7 +394,7 @@ describe("AutoFallbackEmbeddingProvider — getLastError (i-vm1lxwry)", () => {
 
 // ─────────────────────────── dispose ─────────────────────────────────────────
 
-describe("AutoFallbackEmbeddingProvider — dispose", () => {
+describe.skip("AutoFallbackEmbeddingProvider — historical dispose", () => {
   test("dispose cascades to both providers", async () => {
     const { af, primary, fallback } = buildAutoFallback();
     await af.dispose();

+ 9 - 7
test/embedding-factory.test.ts

@@ -48,7 +48,7 @@ const EMPTY_ENV: Record<string, string | undefined> = {};
 describe("resolveProviderKind", () => {
   test("explicit local kind is rejected", () => {
     expect(() => resolveProviderKind({
-        kind: "local",
+        kind: "local" as never,
         env: { QMD_EMBED_ENDPOINT: "https://x" },
         configPath,
       })).toThrow(CommercialApiHoldError);
@@ -100,13 +100,13 @@ describe("resolveProviderKind", () => {
     expect(resolveProviderKind({ env: EMPTY_ENV, configPath })).toBe("openai");
   });
 
-  test("invalid env QMD_EMBED_PROVIDER is ignored", () => {
-    expect(
+  test("unsupported env provider returns typed HOLD", () => {
+    expect(() =>
       resolveProviderKind({
         env: { QMD_EMBED_PROVIDER: "garbage" },
         configPath,
       }),
-    ).toBe("openai");
+    ).toThrow(CommercialApiHoldError);
   });
 
   test("uppercase env QMD_EMBED_PROVIDER normalized", () => {
@@ -225,7 +225,7 @@ describe("createEmbeddingProvider", () => {
 
   test("local kind explicitly requested → typed HOLD", () => {
     expect(() => createEmbeddingProvider({
-      kind: "local",
+      kind: "local" as never,
       env: EMPTY_ENV,
       configPath,
     })).toThrow(CommercialApiHoldError);
@@ -238,8 +238,10 @@ describe("createEmbeddingProvider", () => {
 
   test("legacy auto-fallback request → typed HOLD", () => {
     expect(() => createEmbeddingProvider({
-      env: { QMD_EMBED_ENDPOINT: "https://commercial.example.com" },
-      autoFallback: true,
+      env: {
+        QMD_EMBED_ENDPOINT: "https://commercial.example.com",
+        QMD_EMBED_AUTO_FALLBACK: "1",
+      },
       configPath,
     })).toThrow(CommercialApiHoldError);
   });

+ 40 - 89
test/embedding-vsearch.test.ts

@@ -3,10 +3,8 @@
  * (issue i-loazq6ze).
  *
  * Verifies that `searchVec`, `structuredSearch`, and `vectorSearchQuery`
- * route query encoding through the supplied `EmbeddingProvider` instead
- * of the local `node-llama-cpp` model when one is configured. Also covers
- * the AutoFallback path so a transient remote outage degrades to local
- * instead of throwing.
+ * route query encoding through the supplied commercial `EmbeddingProvider`.
+ * Provider failures propagate without selecting a model fallback.
  *
  * The store is in-memory (sqlite + sqlite-vec); the provider is a stub
  * that records calls and returns deterministic vectors so we can verify
@@ -26,7 +24,6 @@ import {
   type ExpandedQuery,
 } from "../src/store.js";
 import {
-  AutoFallbackEmbeddingProvider,
   CircuitOpenError,
   type EmbeddingProvider,
   type ProviderEmbedding,
@@ -125,7 +122,7 @@ beforeEach(() => {
     .run("hashA", "Alpha document body about query encoding via remote provider.", now);
   store.db
     .prepare(`INSERT INTO content (hash, doc, created_at) VALUES (?, ?, ?)`)
-    .run("hashB", "Beta document body about fallback chain semantics.", now);
+    .run("hashB", "Beta document body about commercial API failure semantics.", now);
   store.db
     .prepare(`INSERT INTO documents (hash, collection, path, title, created_at, modified_at, active) VALUES (?, ?, ?, ?, ?, ?, ?)`)
     .run("hashA", "test", "alpha.md", "Alpha", now, now, 1);
@@ -162,8 +159,7 @@ describe("searchVec with EmbeddingProvider", () => {
   test("encodes the query through the provider when supplied", async () => {
     const provider = new FixedProvider("embeddinggemma", FIXED_VEC);
 
-    // Sanity: store.llm is not set; if searchVec touched local llama-cpp
-    // it would fail (no model loaded). Provider routing must be exclusive.
+    // Provider routing must be exclusive.
     const results = await searchVec(
       store.db, "hello", "embeddinggemma", 10,
       undefined, undefined, undefined, provider,
@@ -177,7 +173,7 @@ describe("searchVec with EmbeddingProvider", () => {
     expect(filepaths).toEqual(["qmd://test/alpha.md", "qmd://test/beta.md"]);
   });
 
-  test("provider mode does not access the local llama-cpp instance", async () => {
+  test("provider mode does not access the compatibility adapter", async () => {
     const provider = new FixedProvider("embeddinggemma", FIXED_VEC);
 
     // If anything touches `store.llm` while the provider is set, the proxy
@@ -198,38 +194,24 @@ describe("searchVec with EmbeddingProvider", () => {
     expect(results.length).toBeGreaterThan(0);
   });
 
-  test("survives transient primary failure via AutoFallback", async () => {
-    const primary = new CircuitOpenProvider("embeddinggemma");
-    const fallback = new FixedProvider("embeddinggemma", FIXED_VEC);
-    const wrapped = new AutoFallbackEmbeddingProvider({
-      primary,
-      fallback,
-      warn: () => { /* swallow noisy WARN in tests */ },
-    });
+  test("does not select a model fallback when the provider circuit is open", async () => {
+    const provider = new CircuitOpenProvider("embeddinggemma");
 
-    const results = await searchVec(
-      store.db, "fallback test", "embeddinggemma", 10,
-      undefined, undefined, undefined, wrapped,
-    );
+    await expect(searchVec(
+      store.db, "provider failure", "embeddinggemma", 10,
+      undefined, undefined, undefined, provider,
+    )).rejects.toThrow(/remote down/);
 
-    expect(primary.embedCalls).toBe(1);
-    expect(fallback.embedCalls).toBe(1);
-    expect(results.length).toBeGreaterThan(0);
+    expect(provider.embedCalls).toBe(1);
   });
 
-  test("surfaces error when both primary AND fallback fail", async () => {
-    const primary = new AlwaysFailProvider("embeddinggemma");
-    const fallback = new AlwaysFailProvider("embeddinggemma");
-    const wrapped = new AutoFallbackEmbeddingProvider({
-      primary,
-      fallback,
-      warn: () => { /* swallow */ },
-    });
+  test("surfaces a commercial provider error without fallback", async () => {
+    const provider = new AlwaysFailProvider("embeddinggemma");
 
     await expect(
       searchVec(
         store.db, "doomed", "embeddinggemma", 10,
-        undefined, undefined, undefined, wrapped,
+        undefined, undefined, undefined, provider,
       ),
     ).rejects.toThrow(/backend unreachable/);
   });
@@ -241,7 +223,7 @@ describe("structuredSearch with EmbeddingProvider", () => {
   test("uses provider.embedBatch for vec/hyde sub-queries", async () => {
     const provider = new FixedProvider("embeddinggemma", FIXED_VEC);
 
-    // Deny access to the local llama-cpp — proves the provider path is exclusive.
+    // Deny access to the compatibility adapter to prove provider exclusivity.
     store.llm = new Proxy({}, {
       get(_target, prop) {
         throw new Error(
@@ -251,12 +233,12 @@ describe("structuredSearch with EmbeddingProvider", () => {
     }) as never;
 
     const queries: ExpandedQuery[] = [
-      { type: "vec", query: "what is the fallback chain about" },
-      { type: "hyde", query: "Fallback chains route around primary failure transparently." },
+      { type: "vec", query: "what is the commercial API failure policy" },
+      { type: "hyde", query: "Commercial provider failures remain explicit." },
     ];
 
     const results = await structuredSearch(store, queries, {
-      skipRerank: true, // reranker uses local llm — skip in this isolation test
+      skipRerank: true,
       embedProvider: provider,
     });
 
@@ -266,53 +248,32 @@ describe("structuredSearch with EmbeddingProvider", () => {
     expect(results.length).toBeGreaterThan(0);
   });
 
-  test("AutoFallback covers structuredSearch query batch", async () => {
-    const primary = new CircuitOpenProvider("embeddinggemma");
-    const fallback = new FixedProvider("embeddinggemma", FIXED_VEC);
-    const wrapped = new AutoFallbackEmbeddingProvider({
-      primary,
-      fallback,
-      warn: () => { /* swallow */ },
-    });
+  test("structuredSearch propagates an open provider circuit", async () => {
+    const provider = new CircuitOpenProvider("embeddinggemma");
 
     const queries: ExpandedQuery[] = [
-      { type: "vec", query: "fallback test" },
+      { type: "vec", query: "provider failure" },
     ];
 
-    const results = await structuredSearch(store, queries, {
+    await expect(structuredSearch(store, queries, {
       skipRerank: true,
-      embedProvider: wrapped,
-    });
+      embedProvider: provider,
+    })).rejects.toThrow(/remote down/);
 
-    expect(primary.embedBatchCalls).toBe(1);
-    expect(fallback.embedBatchCalls).toBe(1);
-    expect(results.length).toBeGreaterThan(0);
+    expect(provider.embedBatchCalls).toBe(1);
   });
 
-  test("structuredSearch degrades to empty results when both providers fail (batch path)", async () => {
-    // AutoFallback.embedBatch is contract-bound to return nulls on total
-    // failure (graceful degradation in batch mode — see autofallback.ts
-    // onTotalFail). structuredSearch then has no embeddings to query
-    // sqlite-vec with and returns []. This is the documented behavior;
-    // searchVec (single-embed path) is the one that surfaces a thrown
-    // error to the caller, see the test above.
-    const primary = new AlwaysFailProvider("embeddinggemma");
-    const fallback = new AlwaysFailProvider("embeddinggemma");
-    const wrapped = new AutoFallbackEmbeddingProvider({
-      primary,
-      fallback,
-      warn: () => { /* swallow */ },
-    });
+  test("structuredSearch surfaces a provider batch failure", async () => {
+    const provider = new AlwaysFailProvider("embeddinggemma");
 
     const queries: ExpandedQuery[] = [
       { type: "vec", query: "doomed" },
     ];
 
-    const results = await structuredSearch(store, queries, {
+    await expect(structuredSearch(store, queries, {
       skipRerank: true,
-      embedProvider: wrapped,
-    });
-    expect(results).toEqual([]);
+      embedProvider: provider,
+    })).rejects.toThrow(/backend unreachable/);
   });
 });
 
@@ -346,36 +307,26 @@ describe("vectorSearchQuery with EmbeddingProvider", () => {
     expect(results.length).toBeGreaterThan(0);
   });
 
-  test("AutoFallback rescues vectorSearchQuery from primary failure", async () => {
-    const primary = new CircuitOpenProvider("embeddinggemma");
-    const fallback = new FixedProvider("embeddinggemma", FIXED_VEC);
-    const wrapped = new AutoFallbackEmbeddingProvider({
-      primary,
-      fallback,
-      warn: () => { /* swallow */ },
-    });
+  test("vectorSearchQuery does not select a model fallback", async () => {
+    const provider = new CircuitOpenProvider("embeddinggemma");
 
     store.expandQuery = async () => [];
 
-    const results = await vectorSearchQuery(store, "fallback path", {
+    await expect(vectorSearchQuery(store, "provider failure", {
       minScore: 0,
-      embedProvider: wrapped,
-    });
+      embedProvider: provider,
+    })).rejects.toThrow(/remote down/);
 
-    expect(primary.embedCalls).toBeGreaterThanOrEqual(1);
-    expect(fallback.embedCalls).toBeGreaterThanOrEqual(1);
-    expect(results.length).toBeGreaterThan(0);
+    expect(provider.embedCalls).toBeGreaterThanOrEqual(1);
   });
 });
 
 // ─────────────────────────── Backward compat ────────────────────────────────
 
-describe("backward compat — no provider supplied", () => {
-  test("searchVec without provider uses precomputed embedding path (no llm needed)", async () => {
+describe("precomputed vector path", () => {
+  test("searchVec with a precomputed embedding needs no model operation", async () => {
     // When the caller passes `precomputedEmbedding`, searchVec must not
-    // touch any embedding backend at all — neither local nor provider.
-    // This is the cheapest backward-compat smoke test we can run without
-    // loading node-llama-cpp.
+    // touch any embedding backend.
     store.llm = new Proxy({}, {
       get(_target, prop) {
         throw new Error(`store.llm.${String(prop)} accessed unexpectedly`);

+ 2 - 2
test/eval.test.ts

@@ -156,7 +156,7 @@ describe("BM25 Search (FTS)", () => {
 // Vector Search Tests - Requires embedding model
 // =============================================================================
 
-describe.skipIf(!!process.env.CI)("Vector Search", () => {
+describe.skip("Vector Search requires a provisioned commercial embedding adapter", () => {
   let store: ReturnType<typeof createStore>;
   let db: Database;
   let hasEmbeddings = false;
@@ -268,7 +268,7 @@ describe.skipIf(!!process.env.CI)("Vector Search", () => {
 // Hybrid Search (RRF) Tests - Combines BM25 + Vector
 // =============================================================================
 
-describe.skipIf(!!process.env.CI)("Hybrid Search (RRF)", () => {
+describe.skip("Hybrid Search requires provisioned commercial model adapters", () => {
   let store: ReturnType<typeof createStore>;
   let db: Database;
   let hasVectors = false;

+ 34 - 38
test/llm.test.ts

@@ -1,10 +1,6 @@
 /**
- * llm.test.ts - Unit tests for the LLM abstraction layer (node-llama-cpp)
- *
- * Run with: bun test src/llm.test.ts
- *
- * These tests require the actual models to be downloaded. Run the embed or
- * rerank functions first to trigger model downloads.
+ * Compatibility-adapter tests for the commercial-only model policy.
+ * Historical local-runtime suites remain permanently skipped below.
  */
 
 import { describe, test, expect, beforeAll, afterAll, vi } from "vitest";
@@ -40,12 +36,12 @@ describe("Default LlamaCpp Singleton", () => {
 // =============================================================================
 
 describe("LlamaCpp.modelExists", () => {
-  test("returns exists:true for HuggingFace model URIs", async () => {
+  test("does not resolve remote model artifact identifiers", async () => {
     const llm = getDefaultLlamaCpp();
-    const result = await llm.modelExists("hf:org/repo/model.gguf");
+    const result = await llm.modelExists("remote-model-artifact");
 
-    expect(result.exists).toBe(true);
-    expect(result.name).toBe("hf:org/repo/model.gguf");
+    expect(result.exists).toBe(false);
+    expect(result.name).toBe("remote-model-artifact");
   });
 
   test("returns exists:false for non-existent local paths", async () => {
@@ -57,7 +53,7 @@ describe("LlamaCpp.modelExists", () => {
   });
 });
 
-describe("LlamaCpp expand context size config", () => {
+describe.skip("historical local expand context configuration", () => {
   const defaultExpandContextSize = 2048;
 
   test("uses default expand context size when no config or env is set", () => {
@@ -119,7 +115,7 @@ describe("LlamaCpp expand context size config", () => {
   });
 });
 
-describe("LlamaCpp model resolution (config > env > default)", () => {
+describe.skip("historical local model resolution", () => {
   const HARDCODED_EMBED = "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf";
   const HARDCODED_RERANK = "hf:ggml-org/Qwen3-Reranker-0.6B-Q8_0-GGUF/qwen3-reranker-0.6b-q8_0.gguf";
   const HARDCODED_GENERATE = "hf:tobil/qmd-query-expansion-1.7B-gguf/qmd-query-expansion-1.7B-q4_k_m.gguf";
@@ -167,15 +163,15 @@ describe("LlamaCpp model resolution (config > env > default)", () => {
 // QMD_DISABLE_LOCAL_LLM + remote-only auto-CPU (i-c28wngnd)
 // =============================================================================
 
-describe("isLocalLlmDisabled (QMD_DISABLE_LOCAL_LLM)", () => {
-  test("returns false when env var is unset", () => {
-    expect(isLocalLlmDisabled({})).toBe(false);
-    expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: undefined })).toBe(false);
+describe("local model runtime policy", () => {
+  test("remains disabled when env var is unset", () => {
+    expect(isLocalLlmDisabled({})).toBe(true);
+    expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: undefined })).toBe(true);
   });
 
-  test("returns false when env var is empty / whitespace", () => {
-    expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: "" })).toBe(false);
-    expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: "   " })).toBe(false);
+  test("remains disabled for empty or whitespace values", () => {
+    expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: "" })).toBe(true);
+    expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: "   " })).toBe(true);
   });
 
   test("returns true for canonical truthy values", () => {
@@ -184,16 +180,16 @@ describe("isLocalLlmDisabled (QMD_DISABLE_LOCAL_LLM)", () => {
     }
   });
 
-  test("returns false for canonical falsy values", () => {
+  test("cannot be re-enabled by legacy falsy values", () => {
     for (const v of ["0", "false", "no", "off", "FALSE", "No"]) {
-      expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: v })).toBe(false);
+      expect(isLocalLlmDisabled({ QMD_DISABLE_LOCAL_LLM: v })).toBe(true);
     }
   });
 });
 
-describe("resolveLlamaGpuMode (QMD_LLAMA_GPU + QMD_EMBED_ENDPOINT)", () => {
-  test("returns 'auto' for empty env (legacy local-only setup)", () => {
-    expect(resolveLlamaGpuMode({})).toBe("auto");
+describe("local model GPU policy", () => {
+  test("returns CPU-disabled mode for empty env", () => {
+    expect(resolveLlamaGpuMode({})).toBe("cpu");
   });
 
   test("explicit QMD_LLAMA_GPU=off|none|0|disabled forces CPU", () => {
@@ -202,9 +198,9 @@ describe("resolveLlamaGpuMode (QMD_LLAMA_GPU + QMD_EMBED_ENDPOINT)", () => {
     }
   });
 
-  test("explicit QMD_LLAMA_GPU=auto|on|true preserves probe", () => {
+  test("legacy GPU enable values cannot re-enable probing", () => {
     for (const v of ["auto", "on", "true", "Auto"]) {
-      expect(resolveLlamaGpuMode({ QMD_LLAMA_GPU: v })).toBe("auto");
+      expect(resolveLlamaGpuMode({ QMD_LLAMA_GPU: v })).toBe("cpu");
     }
   });
 
@@ -214,27 +210,27 @@ describe("resolveLlamaGpuMode (QMD_LLAMA_GPU + QMD_EMBED_ENDPOINT)", () => {
     ).toBe("cpu");
   });
 
-  test("explicit QMD_LLAMA_GPU=auto OVERRIDES QMD_EMBED_ENDPOINT auto-CPU", () => {
+  test("legacy GPU auto cannot override commercial endpoint policy", () => {
     expect(
       resolveLlamaGpuMode({
         QMD_LLAMA_GPU: "auto",
         QMD_EMBED_ENDPOINT: "http://models:8082",
       }),
-    ).toBe("auto");
+    ).toBe("cpu");
   });
 
-  test("empty QMD_EMBED_ENDPOINT does not trigger auto-CPU", () => {
-    expect(resolveLlamaGpuMode({ QMD_EMBED_ENDPOINT: "" })).toBe("auto");
-    expect(resolveLlamaGpuMode({ QMD_EMBED_ENDPOINT: "   " })).toBe("auto");
+  test("empty endpoint still leaves local GPU probing disabled", () => {
+    expect(resolveLlamaGpuMode({ QMD_EMBED_ENDPOINT: "" })).toBe("cpu");
+    expect(resolveLlamaGpuMode({ QMD_EMBED_ENDPOINT: "   " })).toBe("cpu");
   });
 
-  test("unknown QMD_LLAMA_GPU values fall back to 'auto' (preserve legacy probe)", () => {
-    expect(resolveLlamaGpuMode({ QMD_LLAMA_GPU: "vulkan" })).toBe("auto");
-    expect(resolveLlamaGpuMode({ QMD_LLAMA_GPU: "cuda" })).toBe("auto");
+  test("unknown GPU values cannot enable probing", () => {
+    expect(resolveLlamaGpuMode({ QMD_LLAMA_GPU: "vulkan" })).toBe("cpu");
+    expect(resolveLlamaGpuMode({ QMD_LLAMA_GPU: "cuda" })).toBe("cpu");
   });
 });
 
-describe("LlamaCpp.ensureLlama() + QMD_DISABLE_LOCAL_LLM", () => {
+describe.skip("historical local runtime initialization", () => {
   test("throws with actionable error when QMD_DISABLE_LOCAL_LLM=1", async () => {
     const prev = process.env.QMD_DISABLE_LOCAL_LLM;
     process.env.QMD_DISABLE_LOCAL_LLM = "1";
@@ -262,7 +258,7 @@ describe("LlamaCpp.ensureLlama() + QMD_DISABLE_LOCAL_LLM", () => {
   });
 });
 
-describe("LlamaCpp rerank deduping", () => {
+describe.skip("historical local reranking", () => {
   test("deduplicates identical document texts before scoring", async () => {
     const llm = new LlamaCpp({}) as any;
     llm._ciMode = false; // allow unit test even in CI (mocked, no real models)
@@ -298,7 +294,7 @@ describe("LlamaCpp rerank deduping", () => {
 // Integration Tests (require actual models)
 // =============================================================================
 
-describe.skipIf(!!process.env.CI)("LlamaCpp Integration", () => {
+describe.skip("historical local runtime integration", () => {
   // Use the singleton to avoid multiple Metal contexts
   const llm = getDefaultLlamaCpp();
 
@@ -701,7 +697,7 @@ describe.skipIf(!!process.env.CI)("LlamaCpp Integration", () => {
 // Session Management Tests
 // =============================================================================
 
-describe.skipIf(!!process.env.CI)("LLM Session Management", () => {
+describe.skip("historical local session management", () => {
   describe("withLLMSession", () => {
     test("session provides access to LLM operations", async () => {
       const result = await withLLMSession(async (session) => {

+ 2 - 2
test/mcp.test.ts

@@ -323,7 +323,7 @@ describe("MCP Server", () => {
   // searchVec (Vector similarity search)
   // ===========================================================================
 
-  describe.skipIf(!!process.env.CI)("searchVec (vector similarity)", () => {
+  describe.skip("searchVec requires a provisioned commercial embedding adapter", () => {
     test("returns results for semantic query", async () => {
       const results = await searchVec(testDb, "project documentation", DEFAULT_EMBED_MODEL, 10);
       expect(results.length).toBeGreaterThan(0);
@@ -349,7 +349,7 @@ describe("MCP Server", () => {
   // hybridQuery (query expansion + reranking)
   // ===========================================================================
 
-  describe.skipIf(!!process.env.CI)("hybridQuery (expansion + reranking)", () => {
+  describe.skip("hybridQuery requires provisioned commercial model adapters", () => {
     test("expands query with typed variations", async () => {
       const expanded = await expandQuery("api documentation", DEFAULT_QUERY_MODEL, testDb);
       // Returns ExpandedQuery[] — typed expansions, original excluded

+ 18 - 21
test/sdk.test.ts

@@ -21,8 +21,8 @@ import {
   type LexSearchOptions,
   type VectorSearchOptions,
   type ExpandQueryOptions,
+  type EmbeddingProvider,
 } from "../src/index.js";
-import { setDefaultLlamaCpp } from "../src/llm.js";
 
 // =============================================================================
 // Test Helpers
@@ -615,7 +615,7 @@ describe("search (unified API)", () => {
   });
 
   // Tests below use search({ query: ... }) which triggers LLM query expansion
-  describe.skipIf(!!process.env.CI)("with LLM query expansion", () => {
+  describe.skip("requires a provisioned commercial generation adapter", () => {
     test("search() with query and rerank:false returns results", async () => {
       const results = await store.search({ query: "authentication", rerank: false });
       expect(results.length).toBeGreaterThan(0);
@@ -926,32 +926,34 @@ describe("update", () => {
 });
 
 describe("embed", () => {
-  function createFakeTokenizer() {
-    return {
-      async tokenize(text: string) {
-        return new Array(Math.max(1, Math.ceil(text.length / 16))).fill(1);
-      },
-    };
-  }
-
-  function createFakeEmbedLlm() {
+  function createFakeCommercialProvider(): EmbeddingProvider & { embedBatchCalls: string[][] } {
     const embedBatchCalls: string[][] = [];
     return {
+      kind: "openai",
       embedBatchCalls,
+      getModelId: () => "commercial-test-embedding-v1",
+      getDimensions: () => 3,
+      healthcheck: async () => ({
+        ok: true,
+        model: "commercial-test-embedding-v1",
+        dimensions: 3,
+      }),
       async embed(_text: string) {
-        return { embedding: [0.1, 0.2, 0.3], model: "fake-embed" };
+        return { embedding: [0.1, 0.2, 0.3], model: "commercial-test-embedding-v1" };
       },
       async embedBatch(texts: string[]) {
         embedBatchCalls.push([...texts]);
         return texts.map((_text, index) => ({
           embedding: [index + 1, index + 2, index + 3],
-          model: "fake-embed",
+          model: "commercial-test-embedding-v1",
         }));
       },
+      async dispose() {},
     };
   }
 
   test("store.embed forwards batch limit options", async () => {
+    const fakeProvider = createFakeCommercialProvider();
     const store = await createStore({
       dbPath: freshDbPath(),
       config: {
@@ -959,12 +961,9 @@ describe("embed", () => {
           docs: { path: docsDir, pattern: "**/*.md" },
         },
       },
+      embedProvider: fakeProvider,
     });
 
-    const fakeLlm = createFakeEmbedLlm();
-    setDefaultLlamaCpp(createFakeTokenizer() as any);
-    store.internal.llm = fakeLlm as any;
-
     try {
       await store.update();
       const result = await store.embed({
@@ -972,12 +971,11 @@ describe("embed", () => {
         maxBatchBytes: 1024 * 1024,
       });
 
-      expect(fakeLlm.embedBatchCalls).toHaveLength(3);
-      expect(fakeLlm.embedBatchCalls.map(call => call.length)).toEqual([1, 1, 1]);
+      expect(fakeProvider.embedBatchCalls).toHaveLength(3);
+      expect(fakeProvider.embedBatchCalls.map(call => call.length)).toEqual([1, 1, 1]);
       expect(result.docsProcessed).toBe(3);
       expect(result.chunksEmbedded).toBe(3);
     } finally {
-      setDefaultLlamaCpp(null);
       await store.close();
     }
   });
@@ -992,7 +990,6 @@ describe("embed", () => {
       await expect(store.embed({ maxDocsPerBatch: 0 })).rejects.toThrow("maxDocsPerBatch");
       await expect(store.embed({ maxBatchBytes: 0 })).rejects.toThrow("maxBatchBytes");
     } finally {
-      setDefaultLlamaCpp(null);
       await store.close();
     }
   });

+ 2 - 2
test/store.test.ts

@@ -496,7 +496,7 @@ describe("Document Chunking", () => {
   });
 });
 
-describe.skipIf(!!process.env.CI)("Token-based Chunking", () => {
+describe.skip("Token-based Chunking requires a provisioned commercial tokenizer adapter", () => {
   test("chunkDocumentByTokens returns single chunk for small documents", async () => {
     const content = "This is a small document.";
     const chunks = await chunkDocumentByTokens(content, 900, 135);
@@ -2346,7 +2346,7 @@ describe("Integration", () => {
 // LlamaCpp Integration Tests (using real local models)
 // =============================================================================
 
-describe.skipIf(!!process.env.CI)("LlamaCpp Integration", () => {
+describe.skip("historical local model integration", () => {
   test("searchVec returns empty when no vector index", async () => {
     const store = await createTestStore();
     const collectionName = await createTestCollection();