{"record":{"id":"a6e6e6f7fcae04b7","repo":"JuliusBrussee/caveman","slug":"opencode-providerid-baseurl-changed-after-enabl","errorCode":null,"errorMessage":"OpenCode ${providerID} baseURL changed after enable; refusing destructive disable","messagePattern":"OpenCode (.+?) baseURL changed after enable; refusing destructive disable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":7664,"sourceCode":"    const text = current.toString(\"utf8\");\n    if (text.includes(GEMINI_NATIVE_ENV_BEGIN) && !text.includes(block)) {\n      throw new Error(\"Gemini routing block changed after enable; refusing destructive disable\");\n    }\n    return Buffer.from(text.replace(`${block}\\n\\n`, \"\").replace(`\\n\\n${block}\\n`, \"\\n\").replace(`${block}\\n`, \"\").replace(block, \"\"));\n  }\n  if (operation.kind === \"opencode-plugin\" || operation.kind === \"pi-extension\") {\n    throw new Error(`${operation.file} changed after enable; refusing destructive disable`);\n  }\n  if (operation.kind === \"opencode-config\") {\n    const root = parseJsonFileObject(operation.file, current);\n    const providers = root.provider && typeof root.provider === \"object\" && !Array.isArray(root.provider) ? root.provider as Record<string, unknown> : {};\n    const routes = operation.owned?.routes && typeof operation.owned.routes === \"object\" && !Array.isArray(operation.owned.routes) ? operation.owned.routes as Record<string, unknown> : {};\n    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> : {};\n    for (const providerID of [\"openai\", \"anthropic\"]) {\n      const provider = providers[providerID] && typeof providers[providerID] === \"object\" && !Array.isArray(providers[providerID]) ? providers[providerID] as Record<string, unknown> : {};\n      const options = provider.options && typeof provider.options === \"object\" && !Array.isArray(provider.options) ? provider.options as Record<string, unknown> : {};\n      if (options.baseURL !== undefined && options.baseURL !== routes[providerID]) {\n        throw new Error(`OpenCode ${providerID} baseURL changed after enable; refusing destructive disable`);\n      }\n      if (previousRoutes[providerID] === null || previousRoutes[providerID] === undefined) delete options.baseURL;\n      else options.baseURL = previousRoutes[providerID];\n      if (Object.keys(options).length > 0) provider.options = options;\n      else delete provider.options;\n      if (Object.keys(provider).length > 0) providers[providerID] = provider;\n      else delete providers[providerID];\n    }\n    if (Object.keys(providers).length > 0) root.provider = providers;\n    else delete root.provider;\n    const mcp = root.mcp && typeof root.mcp === \"object\" && !Array.isArray(root.mcp) ? root.mcp as Record<string, unknown> : {};\n    if (mcp.caveman !== undefined && JSON.stringify(mcp.caveman) !== JSON.stringify(operation.owned?.installed_mcp)) {\n      throw new Error(\"OpenCode caveman MCP entry changed after enable; refusing destructive disable\");\n    }\n    if (mcp.caveman !== undefined) {\n      if (operation.owned?.previous_mcp === null || operation.owned?.previous_mcp === undefined) delete mcp.caveman;\n      else mcp.caveman = operation.owned.previous_mcp;\n    }","sourceCodeStart":7646,"sourceCodeEnd":7682,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/2f49f0e1a352aa810e70056b7930aeb0b3d219b4/packages/cli/src/index.ts#L7646-L7682","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","solutions":["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`","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","Re-run `caveman enable opencode` to re-journal the current routing, then disable before editing again"],"exampleFix":"// before: ~/.config/opencode/opencode.json was edited after enable\n{ \"provider\": { \"openai\": { \"options\": { \"baseURL\": \"http://localhost:4000/v1\" } } } }\n\n// after: baseURL matches the journaled route (owned.routes.openai), disable proceeds\n{ \"provider\": { \"openai\": { \"options\": { \"baseURL\": \"http://127.0.0.1:3777/w/opencode/v1\" } } } }","handlingStrategy":"validation","validationCode":"// Verify provider baseURL still matches the journaled route before disabling\nimport { readFileSync } from 'node:fs';\nconst home = process.env.CAVEMAN_HOME ?? `${process.env.HOME}/.caveman`;\nconst journal = JSON.parse(readFileSync(`${home}/integrations/opencode.json`, 'utf8'));\nconst cfg = JSON.parse(readFileSync(process.env.OPENCODE_CONFIG ?? `${process.env.HOME}/.config/opencode/opencode.json`, 'utf8'));\nfor (const op of journal.operations) {\n  for (const id of ['openai', 'anthropic']) {\n    const live = cfg.provider?.[id]?.options?.baseURL;\n    if (live !== undefined && live !== op.owned?.routes?.[id]) {\n      throw new Error(`refusing: ${id} baseURL drifted`);\n    }\n  }\n}","typeGuard":null,"tryCatchPattern":"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; }","preventionTips":["Do not edit provider.options.baseURL in opencode.json while caveman routing is enabled — disable first, change, re-enable","Keep custom provider endpoints under a different provider key than openai/anthropic","Check `caveman doctor opencode` output before scripted disables"],"tags":["opencode","baseurl","provider-routing","config-drift","disable"],"backgroundTag":"config-drift-detected","analyzedSha":"2f49f0e1a352aa810e70056b7930aeb0b3d219b4","analyzedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}