ruvnet/ruflo · error · Error

${result.error ?? 'provider call failed'}

Error message

${result.error ?? 'provider call failed'}

What it means

Thrown inside the JsModelProvider callback wired to a WasmAgent: the v3 provider call (callAnthropicMessages) returned { success: false }, so the callback throws to signal failure to the WASM runtime. The message is result.error when present, else the literal 'provider call failed'. Because the provider is attached at agent-creation time only when ANTHROPIC_API_KEY / OPENROUTER_API_KEY / OLLAMA_API_KEY is set, this only fires in a keyed environment — every prompt is a real billable call.

Source

Thrown at v3/@claude-flow/cli/src/ruvector/agent-wasm.ts:172

 * Called once at agent-creation time; the provider stays attached for the
 * agent's lifetime.  No-op (returns false) when no provider keys are
 * configured so the echo-fallback path below is preserved for keyless
 * environments.
 */
async function attachJsModelProvider(agent: any, config: WasmAgentConfig): Promise<boolean> {
  const hasAny = !!(process.env.ANTHROPIC_API_KEY || process.env.OPENROUTER_API_KEY || process.env.OLLAMA_API_KEY);
  if (!hasAny) return false;
  const mod = await import('@ruvector/rvagent-wasm');
  const { callAnthropicMessages, resolveAnthropicModel } = await import('../mcp-tools/agent-execute-core.js');
  const model = resolveAnthropicModel(config.model);
  const systemPrompt = config.instructions || 'You are a helpful coding assistant running in a Ruflo WASM agent sandbox.';

  const provider = new mod.JsModelProvider(async (messagesJson: string) => {
    const messages: Array<{ role: string; content: string }> = JSON.parse(messagesJson);
    const lastUser = [...messages].reverse().find(m => m.role === 'user');
    const prompt = lastUser?.content ?? messagesJson;
    const result = await callAnthropicMessages({ prompt, systemPrompt, model, maxTokens: 2048 });
    if (!result.success) throw new Error(result.error ?? 'provider call failed');
    return JSON.stringify({ role: 'assistant', content: result.output ?? '' });
  });
  agent.set_model_provider(provider);
  return true;
}

/**
 * Send a prompt to a WASM agent.
 *
 * ADR-129 P1: JsModelProvider is now wired at creation time so the WASM
 * agent's internal conversation loop (multi-turn state, turn_count,
 * stop conditions) runs against a real LLM.  The echo-stub detection
 * block is kept as a fallback for keyless environments (CI, sandboxed
 * test runners) — behaviour is identical to the pre-P1 path when no
 * provider key is set.
 *
 * Billing note: every wasm_agent_prompt call with a provider key
 * configured makes a billable LLM call.  Use a keyless environment to

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Inspect the full result.error (the message interpolates it) — a 401 means rotate the key, 429 means back off, 'model not found' means fix the model string.
  2. Verify the env var the process actually sees: console.log(Boolean(process.env.ANTHROPIC_API_KEY)) — dotenv may not have loaded in this entrypoint.
  3. For transient failures (429, 5xx), wrap the prompt call in a retry with exponential backoff; the WASM runtime does not retry on its own.
  4. For local/offline runs, unset all three provider keys so the echo-stub fallback engages instead of failing through the provider path.

Example fix

// before — provider error surfaces as a thrown Error mid-prompt
const out = await promptWasmAgent(agentId, input);

// after — catch provider failures and fall back gracefully
try {
  const out = await promptWasmAgent(agentId, input);
} catch (e) {
  if (/provider call failed|rate.?limit|unauthorized|401|429/i.test(String(e))) {
    // surface a user-actionable message instead of crashing the agent loop
    throw new Error(`LLM provider unreachable: ${e}. Check ANTHROPIC_API_KEY and provider status.`);
  }
  throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

function hasProviderKey(): boolean {
  return !!(process.env.ANTHROPIC_API_KEY || process.env.OPENROUTER_API_KEY || process.env.OLLAMA_API_KEY);
}

// If you don't want billable calls (or provider failures), run keyless to get the echo stub.
// If you do, preflight the key with a cheap call:
async function preflightProvider(): Promise<boolean> {
  const { callAnthropicMessages } = await import('../mcp-tools/agent-execute-core.js');
  const r = await callAnthropicMessages({ prompt: 'ping', systemPrompt: '', model: resolveAnthropicModel(undefined), maxTokens: 1 });
  return r.success;
}

Try / catch

async function promptWithRetry(agentId: string, input: string, retries = 2): Promise<string> {
  for (let attempt = 0; ; attempt++) {
    try {
      return await promptWasmAgent(agentId, input);
    } catch (e) {
      const msg = String(e);
      const transient = /429|rate.?limit|timeout|econnreset|5\d\d/.test(msg);
      if (!transient || attempt >= retries) throw e;
      await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
    }
  }
}

Prevention

When it happens

Trigger: API key is set but invalid/expired (401); rate limit hit (429); requested model not available on the configured provider; RUFLO_PROVIDER points at a provider whose endpoint is unreachable; OpenRouter routing fails for an uncommon model; Ollama key set but ollama daemon not running; network proxy blocks the provider endpoint.

Common situations: ANTHROPIC_API_KEY rotated in the dashboard but the process still holds the old value; free-tier OpenRouter key hit its daily cap; model string like 'anthropic:claude-sonnet-4-6' resolved to a model the account doesn't have access to; running in CI behind a corporate firewall that blocks api.anthropic.com; transient provider outage during a long multi-turn agent run.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/a673bde4be58c49b. Report an issue: GitHub.