JuliusBrussee/caveman · error · Error

OpenCode ${providerID} baseURL changed after enable; refusin

Error message

OpenCode ${providerID} baseURL changed after enable; refusing destructive disable

What it means

Thrown while disabling or repairing the OpenCode integration when the live provider options.baseURL in opencode.json (usually ~/.config/opencode/opencode.json) is defined but no longer equals the route Caveman installed, which is recorded in the journal under owned.routes (keyed by provider id 'openai' or 'anthropic'). This is a merge guard: `caveman disable opencode` would otherwise reset baseURL to the pre-enable value and silently destroy your routing change. It only fires when the file hash already differs from after_sha256, i.e. someone edited the config after enable.

Source

Thrown at packages/cli/src/index.ts:7664

    const text = current.toString("utf8");
    if (text.includes(GEMINI_NATIVE_ENV_BEGIN) && !text.includes(block)) {
      throw new Error("Gemini routing block changed after enable; refusing destructive disable");
    }
    return Buffer.from(text.replace(`${block}\n\n`, "").replace(`\n\n${block}\n`, "\n").replace(`${block}\n`, "").replace(block, ""));
  }
  if (operation.kind === "opencode-plugin" || operation.kind === "pi-extension") {
    throw new Error(`${operation.file} changed after enable; refusing destructive disable`);
  }
  if (operation.kind === "opencode-config") {
    const root = parseJsonFileObject(operation.file, current);
    const providers = root.provider && typeof root.provider === "object" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};
    const routes = operation.owned?.routes && typeof operation.owned.routes === "object" && !Array.isArray(operation.owned.routes) ? operation.owned.routes as Record<string, unknown> : {};
    const previousRoutes = operation.owned?.previous_routes && typeof operation.owned.previous_routes === "object" && !Array.isArray(operation.owned.previous_routes) ? operation.owned.previous_routes as Record<string, unknown> : {};
    for (const providerID of ["openai", "anthropic"]) {
      const provider = providers[providerID] && typeof providers[providerID] === "object" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};
      const options = provider.options && typeof provider.options === "object" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};
      if (options.baseURL !== undefined && options.baseURL !== routes[providerID]) {
        throw new Error(`OpenCode ${providerID} baseURL changed after enable; refusing destructive disable`);
      }
      if (previousRoutes[providerID] === null || previousRoutes[providerID] === undefined) delete options.baseURL;
      else options.baseURL = previousRoutes[providerID];
      if (Object.keys(options).length > 0) provider.options = options;
      else delete provider.options;
      if (Object.keys(provider).length > 0) providers[providerID] = provider;
      else delete providers[providerID];
    }
    if (Object.keys(providers).length > 0) root.provider = providers;
    else delete root.provider;
    const mcp = root.mcp && typeof root.mcp === "object" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};
    if (mcp.caveman !== undefined && JSON.stringify(mcp.caveman) !== JSON.stringify(operation.owned?.installed_mcp)) {
      throw new Error("OpenCode caveman MCP entry changed after enable; refusing destructive disable");
    }
    if (mcp.caveman !== undefined) {
      if (operation.owned?.previous_mcp === null || operation.owned?.previous_mcp === undefined) delete mcp.caveman;
      else mcp.caveman = operation.owned.previous_mcp;
    }

View on GitHub (pinned to 2f49f0e1a3)

Solutions

  1. Set the drifted provider's options.baseURL back to the exact value in owned.routes of ~/.caveman/integrations/opencode.json (or delete the options.baseURL key entirely), then rerun `caveman disable opencode`
  2. If you intend to keep the new baseURL: port any other config you care about out of opencode.json, then manually remove the caveman provider routing and mcp.caveman entry and delete the journal file
  3. Re-run `caveman enable opencode` to re-journal the current routing, then disable before editing again

Example fix

// before: ~/.config/opencode/opencode.json was edited after enable
{ "provider": { "openai": { "options": { "baseURL": "http://localhost:4000/v1" } } } }

// after: baseURL matches the journaled route (owned.routes.openai), disable proceeds
{ "provider": { "openai": { "options": { "baseURL": "http://127.0.0.1:3777/w/opencode/v1" } } } }
Defensive patterns

Strategy: validation

Validate before calling

// Verify provider baseURL still matches the journaled route before disabling
import { readFileSync } from 'node:fs';
const home = process.env.CAVEMAN_HOME ?? `${process.env.HOME}/.caveman`;
const journal = JSON.parse(readFileSync(`${home}/integrations/opencode.json`, 'utf8'));
const cfg = JSON.parse(readFileSync(process.env.OPENCODE_CONFIG ?? `${process.env.HOME}/.config/opencode/opencode.json`, 'utf8'));
for (const op of journal.operations) {
  for (const id of ['openai', 'anthropic']) {
    const live = cfg.provider?.[id]?.options?.baseURL;
    if (live !== undefined && live !== op.owned?.routes?.[id]) {
      throw new Error(`refusing: ${id} baseURL drifted`);
    }
  }
}

Try / catch

catch (err) { if (err.message.includes('baseURL changed after enable')) { /* reset options.baseURL to journal value or delete it, then retry once */ } else throw err; }

Prevention

When it happens

Trigger: `caveman disable opencode` (or `caveman doctor opencode --fix`) after manually changing provider.openai.options.baseURL or provider.anthropic.options.baseURL in opencode.json to point at a different endpoint (e.g. a direct OpenAI/Anthropic URL, a different proxy port) while a Caveman route was installed.

Common situations: Temporarily switching opencode to a direct provider URL while Caveman routing was enabled; another routing tool (LiteLLM, OpenRouter config) rewriting provider options; editing baseURL to debug, then later trying to disable Caveman cleanly.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@2f49f0e1a3 (2026-08-18). Data as JSON: /api/errors/a6e6e6f7fcae04b7. Report an issue: GitHub.