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:
+16
-24
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user