Skip to content

Provider Abstraction + Unified Request Model + Routing Policy Engine#82

Merged
samueltuyizere merged 7 commits into
mainfrom
core-routing-engine
Jun 19, 2026
Merged

Provider Abstraction + Unified Request Model + Routing Policy Engine#82
samueltuyizere merged 7 commits into
mainfrom
core-routing-engine

Conversation

@samueltuyizere

Copy link
Copy Markdown
Collaborator

Refactors routatic-proxy from a tightly-coupled Claude Code ↔ OpenCode bridge into a reusable routing core with a provider interface, request normalization layer, and composable routing policies.

What changed

Provider Interface (internal/core/provider.go)

  • Provider interface with Name(), Capabilities(), ModelCapabilities(), WireFormat(), Execute(), Stream(), RoundTripName(), StreamIdleTimeout()
  • WireFormat enum: OpenAIChat, Anthropic, OpenAIResponses, Gemini
  • ProviderCapabilities — streaming, tools, thinking, image input, context length
  • Thread-safe ProviderRegistry with Register/Get/MustGet/List

Unified Request Model (internal/core/normalized.go)

  • NormalizedRequest / NormalizedResponse — canonical internal format
  • NormalizeRequest(Anthropic → Normalized) — lossless extraction
  • DenormalizeResponse(Normalized → Anthropic) — construction back
  • ValidateRequest() — structural validation (roles, tool refs, alternation)
  • NormalizedError with Kind, Retryable, StatusCode

Provider Implementations (internal/provider/opencode_go.go, opencode_zen.go)

  • OpenCodeGoProvider — WireFormat dispatch (OpenAI Chat or Anthropic passthrough for qwen3.7-max)
  • OpenCodeZenProvider — 4-wire-format dispatch replacing old ClassifyEndpoint():
    • Claude/Qwen → Anthropic endpoint
    • Gemini models → Gemini endpoint
    • GPT models → OpenAI Responses endpoint
    • Everything else → OpenAI Chat Completions
  • Shared HTTP transport and round-robin key rotation via baseProvider

Bridge Functions (internal/transformer/normalized_bridge.go)

  • TransformRequestFromNormalized() — Normalized → OpenAI Chat
  • NormalizedToAnthropic() — Normalized → Anthropic Messages
  • NormalizedToResponses() — Normalized → OpenAI Responses
  • NormalizedToGemini() — Normalized → Gemini
  • OpenAIResponseToNormalized() / ResponsesToNormalized() / GeminiToNormalized() — wire → Normalized

Handler Simplification (internal/handlers/messages.go)

  • Provider-based dispatch: handler looks up providerRegistry.Get(model.Provider) and calls provider.Stream() / provider.Execute()
  • Legacy client path retained as fallback for backward compatibility
  • StreamProxy (internal/handlers/streaming.go) dispatches SSE forwarding by WireFormat

Config Enhancement (internal/config/config.go)

  • ModelConfig.WireFormat — optional override ("auto" / "openai" / "anthropic" / "responses" / "gemini")

Routing Policy Engine (internal/router/policy.go)

  • Policy interface with Name() and Evaluate(ctx) → (chain, decision, error)
  • PolicyEngine — ordered chain-of-responsibility evaluation
  • ModelOverridePolicy — checks model_overrides config
  • ScenarioPolicy — wraps existing DetectScenario() logic
  • EvaluateDryRun() — returns all policy decisions for debugging
  • AddPolicy() — extensible for future policies (weighted, cost-aware, fastest-healthy)

Backward compatibility

  • provider: "opencode-go" and provider: "opencode-zen" remain valid
  • Public /v1/messages endpoint unchanged (Anthropic in, Anthropic out)
  • All ROUTATIC_PROXY_* and legacy OC_GO_CC_* env vars continue working
  • model_overrides config entries unaffected
  • Old internal/client/opencode.go kept during migration

- Added OpenCodeGoProvider and OpenCodeZenProvider to handle requests for the respective backends.
- Implemented streaming capabilities for both providers, supporting various wire formats including Anthropic, OpenAI, and Gemini.
- Introduced StreamProxy to manage SSE stream forwarding and transformation of events.
- Enhanced transformer functions to convert between normalized requests and wire formats.
- Updated server initialization to register new providers and handle requests accordingly.
@samueltuyizere samueltuyizere self-assigned this Jun 19, 2026
Comment thread internal/handlers/streaming.go Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything.

📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are.

Files Reviewed (3 files)
  • internal/handlers/streaming.go
  • internal/transformer/request_test.go
  • internal/transformer/stream_test.go
Previous Review Summaries (2 snapshots, latest commit 179e6e8)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 179e6e8)

Verdict: No Issues Found | Recommendation: Merge

Oh wait, this PR is actually clean. I need to sit down. I had my flamethrower warmed up and everything.

📊 Overall: Like finding a unicorn in production — I didn't think clean PRs existed anymore, but here we are.

Files Reviewed (3 files)
  • internal/handlers/streaming.go
  • internal/transformer/request_test.go
  • internal/transformer/stream_test.go

Previous review (commit 657fef0)

Verdict: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 1
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0
Issue Details (click to expand)
File Line Roast
internal/handlers/streaming.go 43 Default case in switch prevents Gemini/Responses from ever executing

🏆 Best part: The provider abstraction and normalized request/response flow is clean and well-architected. Adding WireFormat as a dispatch mechanism instead of hardcoding endpoint classification in the handlers is a solid design choice.

💀 Worst part: The switch statement in ProxyStream has default BEFORE case WireFormatOpenAIResponses and case WireFormatGemini. In Go, default matches everything that isn't explicitly handled, and in a switch without fallthrough, it exits. So WireFormatOpenAIResponses and WireFormatGemini will NEVER be dispatched to - the code always hits default and calls proxyOpenAIStream. This means GPT and Gemini models are broken for streaming.

📊 Overall: Like a three-legged race where one team forgot to tie their legs together - most of the architecture is sound, but this critical control flow bug makes two of the wire formats completely unreachable.

Files Reviewed (10 files)
  • internal/handlers/streaming.go - 1 issue
  • internal/handlers/messages.go - reviewed (no issues in changed lines)
  • internal/provider/opencode_go.go - reviewed (no issues)
  • internal/provider/opencode_zen.go - reviewed (no issues)
  • internal/provider/provider.go - reviewed (no issues)
  • internal/core/provider.go - reviewed (no issues)
  • internal/core/normalize.go - reviewed (no issues)
  • internal/core/errors.go - reviewed (no issues)
  • internal/transformer/normalized_bridge.go - reviewed (no issues)
  • internal/config/config.go - reviewed (no issues)

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash-20260528 · 929,383 tokens

@samueltuyizere samueltuyizere merged commit 4fe96a7 into main Jun 19, 2026
3 checks passed
@samueltuyizere samueltuyizere deleted the core-routing-engine branch June 19, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant