Skip to content

[Bug]: ForegroundFallbackManager model switch broken in v2 - promptAsync is no-op #1125

Description

@BaconDroid

Description

In v2 mode, the v2/client-shim.ts makes both session.prompt and session.promptAsync no-ops. This silently breaks the ForegroundFallbackManager.execFallback mechanism, which relies on promptAsync to replay the user message with the fallback model.

Impact

When a provider hits a rate limit (e.g., OpenAI quota exceeded), the fallback chain is configured correctly, isFailoverError matches, shouldTriggerFallback returns true, and execFallback calculates the correct next model — but the actual model switch never happens because promptAsync is ignored.

The user sees the error repeated N times (where N = maxRetries) with no model switch, even though fallback chains are properly configured in presets.

Root Cause

src/v2/client-shim.ts (line ~45940 in dist):

promptAsync: async (args) => {
  log("[v2][shim] session.promptAsync ignored", { id: args?.sessionID });
  return {};
}

The ForegroundFallbackManager.execFallback (line ~27448) calls:

await sessionClient.promptAsync(promptBody);

This silently succeeds but does nothing. The manager logs "switched to fallback model" but no prompt is actually sent.

Reproduction

  1. Configure a preset with a fallback chain (e.g., openai/gpt-5.6-terra -> opencode-go/mimo-v2.5)
  2. Exhaust the primary provider quota
  3. Observe: error repeats, no model switch occurs, promptAsync no-op logs appear

Local Workaround

Patch the v2 shim to forward promptAsync via the HTTP API:

promptAsync: async (args) => {
  const id = args?.path?.id ?? args?.sessionID;
  log("[v2][shim] session.promptAsync via HTTP fallback", { id });
  try {
    const serverUrl = "http://localhost:4096";
    const resp = await fetch(`${serverUrl}/session/${id}/prompt`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(args?.body ?? args)
    });
    return resp.ok ? {} : { error: resp.statusText };
  } catch (err) {
    log("[v2][shim] promptAsync HTTP failed", { error: err instanceof Error ? err.message : String(err) });
    return {};
  }
}

Suggested Fix

Two options:

  1. Short term: Forward promptAsync to the OpenCode HTTP API (as shown in the workaround above) instead of no-op'ing it. The execFallback call path would work as-is.

  2. Long term: Expose a dedicated model-switch API in v2 (e.g., session.setModel(sessionId, modelRef)) so the fallback manager can switch models without replaying the entire message. This avoids the semantic mismatch of re-sending user messages for what is essentially a model routing change.

Related

Environment

  • oh-my-opencode-slim: v2.2.17
  • OpenCode: v2 runtime
  • Auth plugin: custom multi-account auth rotation plugin (not directly related but compounds the issue — rotation absorbs 429s until all accounts are exhausted, then the broken fallback fails to switch models)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions