CLI Proxy API · DeepSeek V4 Pro

DeepSeek V4 Pro Failover Guide

How opencode-go and the official DeepSeek API were wired into a single failover pool on this Mac Mini so long-running goals never stall when the opencode-go quota runs out.

The Problem

What we were actually solving.

DeepSeek V4 Pro runs would start fine and then stop partway through. The root cause was not a crash: the deepseek-v4-pro model was served only by opencode-go. Our opencode-go subscription has a usage limit, so when a goal exhausted that quota mid-run, there was nothing to fall back to and the work halted.

The fix: let the official DeepSeek API also serve the same deepseek-v4-pro id, so the proxy can automatically spill over to it when opencode-go is quota- or cooldown-blocked. Same model name in, uninterrupted work out.

How It Works Now

One model id, two providers, priority-ordered failover.

flowchart TD A["Codex thread / client
model = deepseek-v4-pro"] --> B["CLI Proxy API
127.0.0.1:8317
launchd: com.cliproxyapi"] B --> C{"Router
strategy: fill-first
pick highest ready priority"} C -->|"priority 9 (preferred)"| D["opencode-go
publishes deepseek-v4-pro
key sk-K...z7un"] C -.->|"only if opencode-go is
quota / cooldown blocked"| E["Deepseek official
publishes deepseek-v4-pro
priority 5, key sk-b...e102"] D --> F["DeepSeek V4 Pro response"] E --> F style D fill:#123a1f,stroke:#3fbf6f,color:#eafff0 style E fill:#3a2a12,stroke:#e0952f,color:#fff0d9 style C fill:#152447,stroke:#5b8cff,color:#e8ecf6

Because both providers publish the same model id, the router treats them as one pool. fill-first drains opencode-go (priority 9) first, then automatically routes to the official DeepSeek API (priority 5) when opencode-go can't serve the request.

Before vs After

flowchart LR subgraph BEFORE["EARLIER (broken)"] direction TB b1["deepseek-v4-pro
served ONLY by opencode-go"] b1 --> b3["opencode-go hits quota
--> run stops, no fallback"] end subgraph AFTER["NOW (failover)"] direction TB a1["deepseek-v4-pro
shared by BOTH providers"] a2["opencode-go priority 9
official priority 5"] a1 --> a3["opencode-go quota hit
--> auto-route to official
--> run continues"] a2 --> a3 end style BEFORE fill:#3a1416,stroke:#e0555f,color:#ffe6e8 style AFTER fill:#123a1f,stroke:#3fbf6f,color:#eafff0

What Changed in the Config

All edits live in /Users/adeel/.cli-proxy-api/config.yaml.

A backup of the original was saved alongside it as config.yaml.bak.deepseek-opencode-primary-fallback.20260709042903.

  1. Set routing.strategy: fill-first so the router prefers the highest-priority ready provider and only spills over when it is blocked.
  2. Made opencode-go and official Deepseek both publish the shared id deepseek-v4-pro — this is what forms the single failover pool.
  3. Kept direct-pin aliases so each path can be tested in isolation: deepseek-v4-pro-oc (opencode-go) and deepseek-v4-pro-official (official).

Routing block

routing:
  strategy: fill-first

Official DeepSeek provider

- name: Deepseek
  base-url: https://api.deepseek.com
  api-key-entries:
    - api-key: sk-b...e102        # official DeepSeek key
  models:
    - name: deepseek-v4-pro
      alias: ''                   # shared id: deepseek-v4-pro
    - name: deepseek-v4-pro
      alias: deepseek-v4-pro-official   # direct-pin for testing
  priority: 5                     # fallback tier

opencode-go provider

- name: opencode-go
  base-url: https://opencode.ai/zen/go/v1
  api-key-entries:
    - api-key: sk-K...z7un         # opencode-go key
  models:
    - name: deepseek-v4-pro
      alias: deepseek-v4-pro       # shared id: deepseek-v4-pro
    - name: deepseek-v4-pro
      alias: deepseek-v4-pro-oc    # direct-pin for testing
  priority: 9                      # preferred tier

Live State & Verification

Service
com.cliproxyapi · running
Port
8317
Strategy
fill-first
Preferred
opencode-go (9)
Fallback
Deepseek official (5)
Last exit code
0

Model resolution (/v1/models)

Model idServed byRole
deepseek-v4-proopencode-goShared id · preferred
deepseek-v4-pro-officialDeepseekDirect-pin fallback
deepseek-v4-pro-ocopencode-goDirect-pin preferred

Runbook — Reproduce or Adjust

The commands used to edit, validate, restart, and verify.

View the config

sed -n '1,120p' /Users/adeel/.cli-proxy-api/config.yaml

Validate YAML

ruby -ryaml -e 'YAML.load_file(ARGV.fetch(0)); puts "yaml_ok"' \
  /Users/adeel/.cli-proxy-api/config.yaml

Restart the proxy

launchctl kickstart -k user/$(id -u)/com.cliproxyapi

Check the service

launchctl print user/$(id -u)/com.cliproxyapi \
  | rg 'state =|pid =|runs =|last exit code'

Check exposed DeepSeek models

curl -sS -H 'Authorization: Bearer sk-cliproxy-local' \
  http://127.0.0.1:8317/v1/models \
  | ruby -rjson -e 'd=JSON.parse(STDIN.read)["data"];
      puts d.select{|m| m["id"]=~/deepseek/i}.sort_by{|m| m["id"]}
            .map{|m| "#{m["id"]}=#{m["owned_by"]}"}'

Inspect routing logs

rg -n "deepseek-v4-pro" /Users/adeel/.cli-proxy-api/logs/stdout.log | tail -80

Honest Caveat

What we have proven is that routing, priorities, and the shared model id are wired correctly and the service is healthy. We have not yet forced opencode-go to actually hit its quota to watch the spillover live, because that means intentionally breaking a live provider path. A controlled hard-failover simulation can be run on request to confirm the automatic handoff end to end.