feat: multi-backend proxy with auto rate-limit jailing

Refactor from monolithic proxy to multi-backend architecture
(Backend interface: OpenCode Zen + Kilo gateway).

- Add auto rate-limit jailing: models returning HTTP 429 are
  hidden from /v1/models immediately and re-probed every 30 min
- Backend interface supports model aliases, custom headers, and
  per-backend routing
- Full Anthropic Messages API support with OpenAI format conversion
- 9 free models across two backends with Claude name aliases
This commit is contained in:
Renato
2026-06-26 19:08:01 +02:00
parent 0a4289cafc
commit 05a52d29e2
9 changed files with 652 additions and 245 deletions
+16 -24
View File
@@ -104,32 +104,20 @@ type OpenAIErrorDetail struct {
// ---------- Proxy forwarding logic ----------
// UpstreamHeaders builds the required headers for OpenCode Zen.
func UpstreamHeaders(requestID, sessionID string) map[string]string {
return map[string]string{
"Authorization": "Bearer public",
"User-Agent": "opencode/1.15.0 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.13",
"x-opencode-client": "cli",
"x-opencode-project": "global",
"x-opencode-request": requestID,
"x-opencode-session": sessionID,
"Content-Type": "application/json",
"Accept": "text/event-stream, application/json",
}
}
// ForwardChatCompletion sends the OpenAI request to OpenCode Zen and streams or collects the response.
// ForwardChatCompletion routes the OpenAI request to the matching backend and
// streams or collects the response.
func (p *Proxy) ForwardChatCompletion(w http.ResponseWriter, r *http.Request, body []byte) error {
var req OpenAIChatRequest
if err := json.Unmarshal(body, &req); err != nil {
return fmt.Errorf("invalid request body: %w", err)
}
// Resolve model name
resolved, ok := p.ResolveModel(req.Model)
if !ok {
// Unknown model — log but still forward (might be a new model)
fmt.Printf("[proxy] unknown model '%s', forwarding as-is\n", req.Model)
// Route to the backend that owns this model (default backend = pass-through).
backend, resolved, known := p.resolveModel(req.Model)
if !known {
fmt.Printf("[proxy] unknown model '%s', forwarding as-is via %s\n", req.Model, backend.Name())
} else {
fmt.Printf("[proxy] model '%s' -> '%s' via %s\n", req.Model, resolved, backend.Name())
}
// Update the body with resolved model
@@ -144,7 +132,7 @@ func (p *Proxy) ForwardChatCompletion(w http.ResponseWriter, r *http.Request, bo
// Generate request/session IDs for upstream
requestID := RequestID()
// Get session ID from the client's proxy API key (or default)
// Session ID keyed by the client's proxy API key (or default)
userKey := "default"
if p.apiKey != "" {
userKey = r.Header.Get("Authorization")
@@ -154,13 +142,13 @@ func (p *Proxy) ForwardChatCompletion(w http.ResponseWriter, r *http.Request, bo
}
sessionID := p.SessionID(userKey)
// Build upstream request
upstreamReq, err := http.NewRequestWithContext(r.Context(), "POST", ZenChatURL(), bytes.NewReader(body))
// Build upstream request to the chosen backend
upstreamReq, err := http.NewRequestWithContext(r.Context(), "POST", backend.ChatURL(), bytes.NewReader(body))
if err != nil {
return fmt.Errorf("failed to create upstream request: %w", err)
}
for k, v := range UpstreamHeaders(requestID, sessionID) {
for k, v := range backend.Headers(requestID, sessionID) {
upstreamReq.Header.Set(k, v)
}
@@ -174,6 +162,10 @@ func (p *Proxy) ForwardChatCompletion(w http.ResponseWriter, r *http.Request, bo
// Handle non-200 responses
if resp.StatusCode != 200 {
fmt.Printf("[proxy] upstream status=%d for model='%s' via %s\n", resp.StatusCode, resolved, backend.Name())
if resp.StatusCode == http.StatusTooManyRequests {
p.markRateLimited(resolved)
}
bodyBytes, _ := io.ReadAll(resp.Body)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)