Skip to content

refactor: three-layer provider registry with centralized resolution#80

Open
MayCXC wants to merge 26 commits intoMadAppGang:mainfrom
MayCXC:refactor/transport-hierarchy
Open

refactor: three-layer provider registry with centralized resolution#80
MayCXC wants to merge 26 commits intoMadAppGang:mainfrom
MayCXC:refactor/transport-hierarchy

Conversation

@MayCXC
Copy link
Copy Markdown

@MayCXC MayCXC commented Mar 19, 2026

Summary

Centralize all provider-specific logic in provider-profiles.ts. Five resolvers, one createHandlerForProvider entry 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 on def.transport
  • resolveAPIFormat(def, modelName) switches on def.transport
  • resolveModelDialect(modelId) returns BaseModelDialect | null (null = no quirks)
  • resolveMiddlewares(modelId) selects middleware instances
  • resolveEffective(def, modelName, apiKey) handles definition swaps (zen+minimax) and publicKeyFallback
  • createHandlerForProvider(def, modelName, apiKey, targetModel, port, opts) composes all five

Class 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

  • ZenTransport: extends OpenAI, public key fallback, GPT endpoint override
  • KimiCodingTransport: extends Anthropic, OAuth fallback in getHeaders()
  • OllamaTransport: extends Local, num_ctx injection, /api/tags health check, /api/show context window
  • LMStudioTransport: extends Local, /v1/models context window detection

Adapter subclasses

  • GeminiModelDialect: extracted reasoning filter from GeminiAPIFormat
  • LocalQwen/DeepSeek/Llama/MistralFormatAdapter: model-family sampling params and tool guidance

Zen definition swap

  • opencode-zen-minimax and opencode-zen-go-minimax definitions with transport: "anthropic"
  • resolveEffective swaps definitions for minimax models
  • Resolvers see the correct transport type, no model branching needed

proxy-server.ts

Single handlerCache + getHandler. No transport or adapter imports. All construction via createHandlerForProvider.

Removed

  • dialect-manager.ts, remote-provider-registry.ts
  • shouldHandle from all adapters, ModelDialect interface, middleware interface, MiddlewareManager filter
  • DialectManager class, ProfileContext, ProviderProfile interface, PROVIDER_PROFILES table
  • Standalone hook functions (geminiOnResponse, createOpenRouterHooks)
  • unwrapGeminiResponse and tokenStrategy from ComposedHandlerOptions
  • hasNativeAnthropicMapping from claude-runner
  • Dead getter functions from provider-definitions
  • Model-family branches from local-adapter, openai-messages, openrouter-api-format
  • Gemini middleware check from composed-handler (middleware manager no longer filters)

processTextContent chain

Stream parsers accept optional modelDialect parameter. 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

  • 138/138 format-translation + provider-routing + glm-adapter tests pass
  • Full suite: 521 pass, 5 skip, 1 pre-existing fail (mtm binary)
  • Gemini MCP non-streaming: JSON parse fix (getNonStreamingEndpoint strips ?alt=sse via URL parsing)
  • GitHub Models MCP non-streaming: stream_options stripped when stream=false

Stats

69 files changed, +2120/-1867

MayCXC added 2 commits March 24, 2026 05:18
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.
@MayCXC MayCXC force-pushed the refactor/transport-hierarchy branch 2 times, most recently from 78b1f5d to b7ed52e Compare March 24, 2026 05:23
MayCXC added 4 commits March 24, 2026 05:35
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.
@MayCXC MayCXC force-pushed the refactor/transport-hierarchy branch from b7ed52e to 8b62997 Compare March 24, 2026 05:39
@MayCXC MayCXC changed the title refactor: transport class hierarchy, adapter split, and provider unification refactor: parseResponse, transport metadata, factory functions, RequestQueue, BaseTransport Mar 24, 2026
MayCXC added 14 commits March 24, 2026 05:54
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.
@MayCXC MayCXC changed the title refactor: parseResponse, transport metadata, factory functions, RequestQueue, BaseTransport refactor: three-layer provider registry with centralized resolution Mar 24, 2026
MayCXC added 4 commits March 24, 2026 08:36
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.
@MayCXC MayCXC force-pushed the refactor/transport-hierarchy branch 3 times, most recently from 45f3a5f to 3504710 Compare March 24, 2026 15:59
…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.
@MayCXC MayCXC force-pushed the refactor/transport-hierarchy branch from ea701a5 to 5546207 Compare March 24, 2026 16:35
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