JuliusBrussee/caveman · error

cave_claude_provider_unsupported

cave_claude_provider_unsupported

Error message

cave_claude_provider_unsupported

What it means

resolveClaudeModel resolves the Claude model from the agent definition, CAVE_MODEL env var, .caveman/provider.json, or a default when ANTHROPIC_API_KEY exists. The Claude runtime only accepts models whose provider prefix is exactly "anthropic/" (slash after at least one character). Any other provider prefix triggers this fail-closed guard because this executor only supports Anthropic models.

Solutions

  1. Prefix the model with "anthropic/", e.g. CAVE_MODEL=anthropic/claude-haiku-4-5 or model: "anthropic/claude-sonnet-4-5" in the definition.
  2. If you need a non-Anthropic provider, use a different runtime/adapter (Pi runtime or adapters.ts bridges) instead of the Claude executor.
  3. Check .caveman/provider.json and CAVE_MODEL for a stale provider prefix left by another tool.

Example fix

// before
process.env.CAVE_MODEL = "openai/gpt-4o";
// after
process.env.CAVE_MODEL = "anthropic/claude-haiku-4-5";
Defensive patterns

Strategy: validation

Validate before calling

function isAnthropicModel(model) {
  const i = model.indexOf("/");
  return i >= 1 && model.slice(0, i) === "anthropic";
}
const model = definition.model ?? process.env.CAVE_MODEL;
if (model !== undefined && !isAnthropicModel(model)) {
  throw new Error(`refusing to run: ${model} is not an anthropic/ model`);
}

Type guard

function isAnthropicModel(v) { return typeof v === "string" && v.indexOf("/") >= 1 && v.slice(0, v.indexOf("/")) === "anthropic"; }

Try / catch

try {
  await runClaudeAgent(options);
} catch (e) {
  if (e.message === "cave_claude_provider_unsupported") {
    console.error(`Model must be prefixed "anthropic/", got: ${options.model ?? process.env.CAVE_MODEL}`);
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a model string like "openai/gpt-4o", "bedrock/...", "openrouter/..." via definition.model, CAVE_MODEL, or .caveman/provider.json; or a string with no "/" at all (indexOf returns -1) or one starting with "/" (separator < 1).

Common situations: Reusing a model id written for a multi-provider gateway (e.g. OpenRouter-style "anthropic/claude-..." is fine but "google/gemini..." is not); copying a provider.json from a project configured for another provider; forgetting the "anthropic/" prefix entirely.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/dcae21f662ee14dd. Report an issue: GitHub.

Appendix: source

Thrown at packages/agent/src/claude-runtime.ts:471

  const target = name.toLowerCase();
  const kept = (raw ?? "").split(/\r\n|\n|\r/).filter((line) => {
    if (!line.trim()) return false;
    const colon = line.indexOf(":");
    return (colon < 0 ? line : line.slice(0, colon)).trim().toLowerCase() !== target;
  });
  kept.push(`${name}: ${value}`);
  return kept.join("\n");
}

function resolveClaudeModel(definition: AgentDefinition, rootDir: string): string {
  const configured = typeof definition.model === "string"
    ? definition.model
    : process.env.CAVE_MODEL ?? localModel(rootDir) ??
      (process.env.ANTHROPIC_API_KEY ? "anthropic/claude-haiku-4-5" : undefined);
  if (configured === undefined) throw new Error("cave_claude_model_required");
  const separator = configured.indexOf("/");
  if (separator < 1 || configured.slice(0, separator) !== "anthropic") {
    throw new Error("cave_claude_provider_unsupported");
  }
  return normalizeClaudeModel(configured.slice(separator + 1));
}

function localModel(rootDir: string): string | undefined {
  try {
    const parsed = JSON.parse(readFileSync(resolve(rootDir, ".caveman/provider.json"), "utf8")) as {
      model?: unknown;
    };
    return typeof parsed.model === "string" ? parsed.model : undefined;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
    throw new Error("caveman agent: invalid .caveman/provider.json");
  }
}

function normalizeClaudeModel(model: string): string {
  return model.startsWith("anthropic/") ? model.slice("anthropic/".length) : model;

View on GitHub (pinned to 3ee70a1026)