A small local proxy with:
- strict ordered fallback
- in-memory cooldown after failure
- automatic return to higher-priority providers after cooldown expires
- OpenAI-compatible
responsesandchat.completionsendpoints - Anthropic-compatible
messagesandmessages/count_tokensendpoints for Claude Code style clients
It is designed to be a thin local layer, not a full gateway.
cd /path/to/mini-fallback-proxy
chmod +x ./start.sh
./start.sh --config ~/.config/litellm.yaml--config is required.
start.sh will:
- install
uvlocally if it is missing - create or update the project
.venvwithuv sync - read bind settings from the config file
- launch the server with that config
Example file: config.example.yaml
app_settings:
host: 0.0.0.0
port: 8099
log_level: info
default_timeout: 60
normalize_upstream_model: true
router_settings:
allowed_fails: 1
cooldown_time: 300
providers:
- name: jucode
api_base: https://cf.jucode.top/v1
api_key: sk-your-jucode-key
endpoint_type: responses
order: 1
models:
- gpt-5.4
- gpt-5.4-mini
- gpt-5.3-codex
- name: rightcode
api_base: https://right.codes/codex/v1
api_key: sk-your-rightcode-key
endpoint_type: responses
order: 2
models:
- gpt-5.4
- gpt-5.4-mini
- gpt-5.3-codex
- name: yescode
api_base: https://co.yes.vg/v1
api_key: sk-your-yescode-key
endpoint_type: responses
order: 3
models:
- gpt-5.4
- gpt-5.4-mini
- gpt-5.3-codex
- name: anthropic-compatible
api_base: https://anthropic-compatible.example/v1
api_key: sk-your-provider-key
endpoint_type: anthropic
order: 4
models:
- deepseek-v4-flash:haiku
- deepseek-v4-pro:opus
- deepseek-v4-pro
- name: songsong-anthropic
api_base: https://ai.songsongcard.shop
api_key: sk-your-provider-key
endpoint_type: anthropic
order: 5
models: autoapp_settings
host: bind host for the local server. Default127.0.0.1. Use0.0.0.0if you want other machines on your LAN to access it.port: bind port for the local server. Default8099.log_level: uvicorn log level. Defaultinfo.default_timeout: request timeout in seconds when neither request body nor provider sets a timeout. Default60. For streaming requests, this proxy keepsconnect/write/poolbounded but disables the upstreamreadtimeout so long-thinking SSE streams are not cut off mid-response.stream_start_timeout: maximum seconds to wait for the first upstream SSE event before falling back. Default30, ordefault_timeoutif lower.sticky_ttl_seconds: optional session stickiness TTL in seconds. Default1800, so you do not need to set it unless you want to override it.normalize_upstream_model: iftrue,openai/gpt-5.4becomesgpt-5.4before forwarding upstream. Defaulttrue.hot_reload: iftrue, the proxy polls the config file and applies valid edits without restart. Defaulttrue.hot_reload_interval_seconds: config file polling interval. Default1.
host, port, and log_level are read by start.sh before uvicorn starts. Editing them is reflected in /debug/state after reload, but changing the actual listening socket or uvicorn log level still requires restarting the process.
router_settings
allowed_fails: allowed failures before cooldown. Cooldown starts only whenfail_count > allowed_fails. Default0.cooldown_time: cooldown length in seconds. Default300.allowed_retries: same-provider retries before falling back to the next provider. Default0, so existing fallback behavior is unchanged.retry_backoff_seconds: pause between same-provider retries. Default0.25.
providers[*]
name: required unique provider id. It is shown in logs/debug output and used for cooldown and sticky routing state.api_base: provider service root. MiniProxy builds the upstream request URL from this value andendpoint_type. If it already ends in/v1, MiniProxy appends the endpoint path directly; otherwise it inserts/v1first.api_url: optional exact upstream request URL. When set, MiniProxy sends requests to this URL exactly and does not append any endpoint path. This requiresendpoint_type.api_key: upstream API key.endpoint_type: optional endpoint family. Values areresponses,openai-compatible, oranthropic. When set, the provider is only used for that local endpoint family.order: provider priority. Lower number means higher priority.timeout: optional provider-specific timeout in seconds.headers: optional extra headers sent on every request to that upstream.models: non-empty list of models this provider supports, orauto.models_url: optional exact URL formodels: autodiscovery.
For pi-agent with OpenAI Responses / Codex relay upstream providers, set:
headers:
User-Agent: curl/8.7.1Most providers only need api_base, api_key, endpoint_type, order, and
models. Use api_url only for providers with a nonstandard request URL, and
use models_url only when model discovery is hosted somewhere different.
models: auto
When models is set to auto, the proxy loads model ids from the provider's models endpoint when the config is loaded, manually reloaded, or hot-reloaded. Startup waits for discovery to finish. Manual reload and hot reload run discovery off the main event loop, so a slow models endpoint can delay the config update but does not freeze in-flight proxy requests.
Parallel Discovery: When multiple providers use models: auto, the proxy fetches all model lists in parallel using asyncio.gather(), significantly reducing startup time compared to sequential requests.
providers:
- name: songsong-anthropic
api_base: https://ai.songsongcard.shop
api_key: sk-your-provider-key
endpoint_type: anthropic
models: autoBy default, discovery calls {api_base}/models when api_base already ends with /v1; otherwise it calls {api_base}/v1/models. Set models_url if a provider exposes model discovery somewhere else.
For endpoint_type: anthropic, discovery uses Anthropic-style headers: x-api-key and anthropic-version. Other endpoint types use OpenAI-style bearer auth. If the provider's model response includes supported_endpoint_types, the proxy filters discovered models to the configured endpoint_type when some models match. If metadata would filter out every discovered model, MiniProxy keeps the discovered IDs and logs a warning, treating the configured endpoint_type as the source of truth.
models: auto discovery never uses api_url as a fallback because api_url
can point at an exact request endpoint. If models is auto, configure either
api_base or models_url.
Allowed URL combinations:
api_base: infer both request URL and model discovery URL.api_base+api_url: send requests toapi_url, discover models fromapi_base.api_base+models_url: infer request URL fromapi_base, discover models frommodels_url.api_url+models_url: send requests toapi_url, discover models frommodels_url.api_urlonly: allowed only whenmodelsis an explicit list.
providers[*].models[*]
- String form, for same local and upstream model name:
gpt-5.4. - Anthropic role suffix form, for Claude Code role aliases:
models:
- deepseek-v4-flash:haiku
- deepseek-v4-pro:opusRole suffixes also match incoming Anthropic model ids by role substring. For example, deepseek-v4-pro:opus can serve requests for claude-opus-4-7, claude-opus-4-7[1M], or a future Claude Code Opus id containing opus.
- Generic alias suffix form, for exposing one local model id while forwarding another upstream model:
models:
- gpt-5.4-mini:gpt-5.5
- deepseek-v4-flash:claude-haiku-4-5-20251001The value before : is sent upstream. The value after : is exposed by this proxy in /v1/models and used for local routing. If the upstream model id itself contains :, use the mapping object form instead.
- Mapping form, for aliases or provider-specific upstream names:
models:
- model_name: gpt-5.4
model: openai/gpt-5.4
- model: kimi-k2
anthropic_role: opusmodel_name is the alias exposed by this local proxy. model is the upstream model value sent to that provider.
anthropic_role can be haiku, sonnet, or opus; it exposes the corresponding Claude-looking alias for /v1/messages while forwarding the real provider model upstream.
general_settings
- ignored by this project. It can stay in the file for compatibility with your existing LiteLLM config.
POST /v1/responsesPOST /v1/chat/completionsPOST /v1/messagesPOST /v1/messages/count_tokensPOST /responsesPOST /chat/completionsPOST /messagesPOST /messages/count_tokensGET /v1/modelsGET /modelsGET /GET /healthzGET /debug/statePOST /admin/reload
- For a requested model, the proxy tries only providers whose
modelslist includes that model, ordered by lowestorderfirst. endpoint_typerestricts a provider to one local route family:responsesfor/v1/responses,openai-compatiblefor/v1/chat/completions, andanthropicfor/v1/messagesplus/v1/messages/count_tokens.- Local OpenAI-style clients may use either
http://127.0.0.1:PORT/v1orhttp://127.0.0.1:PORTas the base URL. Claude Code should usehttp://127.0.0.1:PORTasANTHROPIC_BASE_URL. - Upstream provider bases may be configured with or without
/v1; for examplehttps://ai.songsongcard.shopplusendpoint_type: responsesroutes tohttps://ai.songsongcard.shop/v1/responses. - Upstream Anthropic provider bases should be the provider's documented Anthropic base, such as
https://api.deepseek.com/anthropic; the proxy routes messages tohttps://api.deepseek.com/anthropic/v1/messagesand token counts tohttps://api.deepseek.com/anthropic/v1/messages/count_tokens. api_urlbypasses URL inference and is used exactly as configured.- For
/v1/messages,:haiku,:sonnet, and:opusmodel suffixes expose Claude Code-safe aliases:claude-haiku-4-5-20251001,claude-sonnet-4-6, andclaude-opus-4-7. - If Claude Code sends a newer role model id that contains
haiku,sonnet, oropus, the proxy can still route it to providers configured with the matching role suffix. - Claude Code's
[1M]suffix is accepted for routing, then stripped before matching. For example,claude-opus-4-7[1M]routes asclaude-opus-4-7. - Anthropic role aliases are only local routing names. Upstream requests use the configured provider model, such as
deepseek-v4-pro. - Session stickiness is enabled by default for 30 minutes.
- Session key extraction order is:
x-fallback-sessionheader, then request bodyconversation_id,thread_id,previous_response_id, OpenAI Responsesprompt_cache_key,user, then the same session-like keys undermetadata. Claude Code's JSON-encodedmetadata.user_id.session_idis also accepted. - Stickiness is applied per
session + endpoint + model alias. - For an existing sticky session, the bound provider stays preferred until the sticky TTL expires or that provider has a counted failure, even if a higher-priority provider's cooldown has expired.
- Cooldown state is tracked per
provider + endpoint + model alias, not globally. - A provider enters cooldown only when its counted failure count becomes greater than
allowed_fails. allowed_retriesapplies before fallback for retryable failures. The original attempt plusallowed_retriesattempts may be sent to the same provider.- An exhausted retry cycle counts as one provider failure, not one failure per upstream attempt.
- Retries are limited to transport errors, timeouts,
408,429,5xx, and pre-body Responses stream-start failures. Auth/balance errors such as401,402, and403are not retried. - After cooldown expires, new requests automatically go back to the higher-priority provider.
- Request body
timeoutoverrides provider timeout, which overridesapp_settings.default_timeout. - For streaming requests, the selected timeout still applies to connect/write/pool, but upstream idle reads are left unbounded to avoid false reconnect loops.
- Streaming fallback is supported before the upstream stream starts. After the first upstream SSE event has been sent to the client, mid-stream failures cannot be transparently replayed on another provider.
- Failures and cooldown state live in memory only.
- Config hot reload is enabled by default. Valid changes to routes, providers, timeouts, cooldown settings, stickiness TTL, and model normalization are applied automatically.
- If a changed config is invalid, the proxy keeps serving with the last good config and records the error in
/debug/state. - Startup and successful reloads log a config summary with provider names, endpoint types, orders, model source (
explicitorauto), model counts, and model IDs. API keys are never logged. - For your current
jucodesetup,responsesworks with bare model names such asgpt-5.4. This proxy normalizesopenai/gpt-5.4togpt-5.4by default.
Built-in default failure policy:
401,402,403, timeouts, transport errors,408,429,5xx: fallback and count toward cooldown- model/endpoint/parameter capability mismatch: fallback but do not count toward cooldown
- request-invalid errors such as generic
400/422, context issues, content policy: do not count; default is no fallback - mid-stream disconnects: count toward cooldown but do not transparently replay on another provider
curl http://127.0.0.1:8099/
curl http://127.0.0.1:8099/healthz
curl http://127.0.0.1:8099/debug/state
curl -X POST http://127.0.0.1:8099/admin/reload