refactor: three-layer provider registry with centralized resolution#80
Open
MayCXC wants to merge 26 commits intoMadAppGang:mainfrom
Open
refactor: three-layer provider registry with centralized resolution#80MayCXC wants to merge 26 commits intoMadAppGang:mainfrom
MayCXC wants to merge 26 commits intoMadAppGang:mainfrom
Conversation
4 tasks
4904627 to
7b2bea2
Compare
DialectManager had no state: constructor allocated all dialects, getAdapter() did a linear scan, needsTransformation() called getAdapter() again. Replace with a pure function that does the same scan without the class ceremony. All callers updated: composed-handler, cli, local-adapter, openrouter-api-format, and all test files.
78b1f5d to
b7ed52e
Compare
Add parseResponse() to APIFormat interface with default OpenAI implementation. Override in GeminiAPIFormat (candidates/parts) and AnthropicAPIFormat (content blocks). Enables non-streaming callers to parse responses without hardcoding wire formats. Add tokenStrategy to ProviderTransport interface. Each transport declares its own strategy (delta-aware, accumulate-both, local). ComposedHandler reads from transport, options override. Add getNonStreamingEndpoint() to ProviderTransport for Gemini (:generateContent vs :streamGenerateContent). Add unwrapResponse to ProviderTransport for CodeAssist envelope. ComposedHandler reads from transport as fallback.
Granular factory functions for constructing Layer 1 (APIFormat) and Layer 3 (ProviderTransport) independently without the full ComposedHandler. Enables non-streaming callers (MCP server, batch mode) to compose transport + format adapter directly. resolveAPIFormat(providerName, modelName) returns the wire format adapter. resolveTransport(provider, modelName, apiKey) returns the HTTP transport. Both return null for unknown providers.
…nsport RequestQueue: generic request queue with configurable concurrency, timeout, rate limiting, and retry. Provider-specific hooks for Gemini (quotaResetDelay parsing), OpenRouter (X-RateLimit headers), and local models (OOM retry). Replaces scattered per-transport queue implementations. BaseTransport: abstract base owning a RequestQueue. ApiKeyTransport adds Bearer auth + baseUrl/apiPath. OAuthTransport adds token-based auth with refresh. New transports can extend these instead of implementing ProviderTransport from scratch.
b7ed52e to
8b62997
Compare
All 10 transport implementations now extend BaseTransport, ApiKeyTransport, or OAuthTransport instead of implementing ProviderTransport from scratch. ApiKeyTransport: PoeProvider, OllamaProviderTransport, LiteLLMProviderTransport BaseTransport (custom auth): OpenAIProviderTransport, AnthropicProviderTransport, GeminiProviderTransport, OpenRouterProviderTransport, LocalTransport OAuthTransport: GeminiCodeAssistProviderTransport, VertexProviderTransport Gemini and OpenRouter now use RequestQueue with provider-specific hooks (geminiOnResponse, createOpenRouterHooks) instead of standalone queue singletons. tokenStrategy removed from ComposedHandlerOptions in all profiles (transport provides it). unwrapGeminiResponse removed from ComposedHandlerOptions (transport.unwrapResponse provides it). Net: -47 lines across 12 files.
Replace standalone hook functions (geminiOnResponse, createOpenRouterHooks) with protected method overrides on the transport subclasses: - GeminiProviderTransport.onResponse(): quotaResetDelay parsing - GeminiCodeAssistProviderTransport.onResponse(): same - OpenRouterProviderTransport.onResponse(): X-RateLimit headers - OpenRouterProviderTransport.calculateDelay(): rate limit spreading BaseTransport defines onResponse(), shouldRetry(), calculateDelay() with sensible defaults. Subclasses override for provider-specific behavior. No hook functions or closures. Removed geminiOnResponse, createOpenRouterHooks, oomShouldRetry from request-queue.ts (dead code, behavior now on transport classes).
Move matching logic from individual shouldHandle() implementations into a declarative DIALECTS table in the resolver. Each entry declares families and vendor prefixes. The resolver does the matching via matchesModelFamily, not the adapter classes. shouldHandle() still exists on the adapters (used by local-adapter and openrouter-api-format for inner adapter resolution) but the primary dispatch path is the centralized table.
ProviderProfile now declares createFormat() and createTransport() alongside createHandler(). The factory functions resolveAPIFormat() and resolveTransport() delegate to PROVIDER_PROFILES instead of maintaining duplicate lookup tables. Adding a provider: one entry in PROVIDER_PROFILES with three methods. No other tables to keep in sync. Net: -105 lines from eliminating the duplicate maps.
Replace ProviderProfile interface, profile objects, PROVIDER_PROFILES lookup table, and delegate factory functions with a single createHandlerForProvider switch statement. One place to add a provider. No indirection, no duplicate tables. Exported SUPPORTED_PROVIDERS set for test coverage verification. Net: -184 lines.
Remove ProfileContext indirection. createHandlerForProvider now takes (def, modelName, apiKey, targetModel, port, opts) where def is the ProviderDefinition from BUILTIN_PROVIDERS. Private resolveTransport() and resolveAPIFormat() switch on def.transport to select the right classes. One source of truth: BUILTIN_PROVIDERS declares transport type, the switch interprets it. proxy-server.ts looks up ProviderDefinition by resolved provider name and passes it directly. No RemoteProvider wrapping needed at the call site (toRemoteProvider is called internally by the transport resolver). Tests verify by iterating BUILTIN_PROVIDERS with getProviderByName.
…lect-manager All three resolvers (transport, API format, model dialect) now live in provider-profiles.ts. resolveModelDialect is a flat if/else chain using matchesModelFamily, same pattern as the other resolvers. createHandlerForProvider passes the resolved dialect to ComposedHandler via modelDialect option. ComposedHandler uses it as-is instead of calling a separate resolver. Deleted adapters/dialect-manager.ts. All callers updated to import from providers/provider-profiles.ts.
The resolver in provider-profiles.ts owns model matching. shouldHandle on adapter classes was dead code since the DIALECTS table (now a flat switch in resolveModelDialect) replaced it. Removed from: BaseAPIFormat abstract declaration, ModelDialect interface, DefaultAPIFormat, and all 14 adapter implementations. Cleaned up matchesModelFamily imports that were only used by shouldHandle. Net: -116 lines across 18 files.
…istry Replace 4 handler maps (openRouterHandlers, localProviderHandlers, remoteProviderHandlers, poeHandlers) and 4 helper functions with a single handlerCache + getHandler that delegates to createHandlerForProvider. proxy-server.ts no longer imports any transport or adapter classes. All construction goes through provider-profiles.ts. Added to provider-profiles.ts: - OpenRouter and Poe cases in resolveTransport/resolveAPIFormat - resolveEffective for publicKeyFallback (zen auth-less access) - createLocalHandler for ollama/lmstudio/vllm/mlx/URL models proxy-server.ts: 538 to 427 lines.
…ote-provider-registry, simplify claude-runner KimiCodingTransport: subclass of AnthropicProviderTransport with OAuth fallback in getHeaders(). Removes provider.name check from anthropic-compat.ts. Resolver creates the right subclass. Local adapter subclasses: LocalQwenFormatAdapter (no_think, sampling), LocalDeepSeekFormatAdapter, LocalLlamaFormatAdapter, LocalMistralFormatAdapter. Each overrides getSamplingParams(). Resolver selects subclass via matchesModelFamily. local-adapter.ts no longer branches on model name. remote-provider-registry.ts deleted. All callers replaced with getProviderByName/getProviderDefinitionByRemoteName from provider-definitions.ts. claude-runner.ts: removed hasNativeAnthropicMapping and onCleanup. Always set placeholder credentials (proxy handles all auth).
…ute maps, resolver def/apiKey BaseModelDialect: new abstract base for dialect-only classes (GrokModelDialect, DeepSeekModelDialect, QwenModelDialect, MiniMaxModelDialect, GLMModelDialect, XiaomiModelDialect). BaseAPIFormat now implements only APIFormat (wire format). GeminiModelDialect: extracted reasoning filter logic from GeminiAPIFormat. resolveModelDialect returns GeminiModelDialect for gemini models. GeminiAPIFormat is pure wire format. ZenTransport: extends OpenAIProviderTransport, defaults apiKey to "public". resolveTransport uses it for opencode-zen cases. Pre-computed maps in provider-definitions.ts: PROVIDERS_BY_NAME, PROVIDER_SHORTCUTS, LEGACY_PREFIX_PATTERNS, NATIVE_MODEL_PATTERNS, API_KEY_INFO, PROVIDER_DISPLAY_NAMES. Built eagerly at module load. model-parser uses them directly. ProviderResolution gains def and apiKey fields so proxy-server can pass them to createHandlerForProvider without re-resolving.
getShortcuts, getLegacyPrefixPatterns, getNativeModelPatterns, getShortestPrefix had zero callers (replaced by pre-computed maps). Updated tests to use maps directly.
Move ollama/lmstudio-specific logic from LocalTransport into subclasses. LocalTransport is now a generic base for any OpenAI-compatible local model server. OllamaTransport: num_ctx injection, /api/tags health check, /api/show context window detection. LMStudioTransport: /v1/models context window detection. Resolver selects the right subclass. No name checks in LocalTransport.
Grok XML tool format instruction: moved from openai-messages.ts and openrouter-api-format.ts into GrokModelDialect.prepareRequest(). Gemini output format instruction: moved from openrouter-api-format.ts into GeminiModelDialect.prepareRequest(). Gemini middleware registration: always register, let MiddlewareManager filter by shouldHandle(modelId) instead of branching in composed-handler. openrouter-api-format.ts no longer has any model-specific branches. openai-messages.ts no longer checks model identity. composed-handler.ts no longer checks for gemini.
getToolGuidanceHeader() is a protected method on LocalModelAdapter that subclasses override. Removes the last model-family branch from local-adapter.ts.
…rely resolveMiddlewares(modelId) in provider-profiles.ts selects which middleware to register. ComposedHandler receives them via options. shouldHandle removed from: ModelMiddleware interface, GeminiThoughtSignatureMiddleware, MiddlewareManager. MiddlewareManager no longer filters; all registered middleware is active (resolver already selected the right ones). Zero shouldHandle references remain in the codebase.
45f3a5f to
3504710
Compare
…URL parsing) Zen: added opencode-zen-minimax and opencode-zen-go-minimax definitions with transport: "anthropic". resolveEffective swaps definitions for minimax models. No model branching in resolvers. ZenTransport handles GPT endpoint internally. Renamed adapter to formatAdapter in ComposedHandlerOptions. Renamed explicitAdapter to formatAdapter in ComposedHandler. resolveModelDialect returns BaseModelDialect | null. Removed CodexAPIFormat, OpenAIAPIFormat, DefaultAPIFormat from dialect resolver. Models without dialect quirks get null. supportsVision default false in BaseModelDialect. Grok uses catalog lookup, Gemini always true. reasoningBlockDepth removed. inReasoningBlock reset fixed. getNonStreamingEndpoint uses URL parsing to strip alt param. preserveVendorPrefix on ProviderDefinition. processTextContent chaining via modelDialect param on stream parsers.
ea701a5 to
5546207
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Centralize all provider-specific logic in
provider-profiles.ts. Five resolvers, onecreateHandlerForProviderentry point. No provider branching in adapters, transports, stream parsers, composed-handler, or proxy-server. Rebased onto v6.2.2. Depends on #79.provider-profiles.ts (single source of truth)
Five resolvers + one composer:
resolveTransport(def, modelName, apiKey)switches ondef.transportresolveAPIFormat(def, modelName)switches ondef.transportresolveModelDialect(modelId)returnsBaseModelDialect | null(null = no quirks)resolveMiddlewares(modelId)selects middleware instancesresolveEffective(def, modelName, apiKey)handles definition swaps (zen+minimax) and publicKeyFallbackcreateHandlerForProvider(def, modelName, apiKey, targetModel, port, opts)composes all fiveClass hierarchy
BaseAPIFormat (wire format only): convertMessages, convertTools, buildPayload, getStreamFormat, parseResponse. Implementations: OpenAI, Anthropic, Gemini, Codex, Ollama, LiteLLM, OpenRouter.
BaseModelDialect (model quirks only): processTextContent, getContextWindow, supportsVision (default false), prepareRequest. Implementations: Grok (XML tool format injection), Gemini (reasoning filter, output format injection), DeepSeek, Qwen, MiniMax, GLM, Xiaomi.
BaseTransport owns RequestQueue with
onResponse()/shouldRetry()/calculateDelay()as overridable methods.ApiKeyTransport(Bearer),OAuthTransport(token refresh). Transport metadata: tokenStrategy, getNonStreamingEndpoint (URL-parsed, strips query params), unwrapResponse, preserveVendorPrefix.Transport subclasses
Adapter subclasses
Zen definition swap
proxy-server.ts
Single
handlerCache+getHandler. No transport or adapter imports. All construction viacreateHandlerForProvider.Removed
dialect-manager.ts,remote-provider-registry.tsshouldHandlefrom all adapters, ModelDialect interface, middleware interface, MiddlewareManager filterDialectManagerclass,ProfileContext,ProviderProfileinterface,PROVIDER_PROFILEStableunwrapGeminiResponseandtokenStrategyfrom ComposedHandlerOptionshasNativeAnthropicMappingfrom claude-runnerprocessTextContent chain
Stream parsers accept optional
modelDialectparameter. Format adapter runs first, then dialect if format didn't transform. No proxy objects.provider-definitions.ts
Pre-computed maps at module load: PROVIDERS_BY_NAME, PROVIDER_SHORTCUTS, LEGACY_PREFIX_PATTERNS, NATIVE_MODEL_PATTERNS, API_KEY_INFO, PROVIDER_DISPLAY_NAMES. ProviderResolution gains def and apiKey fields.
Test plan
Stats
69 files changed, +2120/-1867