From c8c2e5016942d21c973d3b363c36c5c26b549bad Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Tue, 24 Mar 2026 05:18:10 +0000 Subject: [PATCH 1/2] refactor: replace DialectManager class with resolveModelDialect function 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. --- packages/cli/src/adapters/dialect-manager.ts | 65 ++++++------------- packages/cli/src/adapters/index.ts | 12 +--- .../cli/src/adapters/litellm-api-format.ts | 2 +- packages/cli/src/adapters/local-adapter.ts | 5 +- .../cli/src/adapters/openrouter-api-format.ts | 5 +- packages/cli/src/cli.ts | 7 +- packages/cli/src/e2e-glm-adapter.test.ts | 42 +++++------- packages/cli/src/e2e-model-catalog.test.ts | 22 +++---- packages/cli/src/format-translation.test.ts | 22 +++---- packages/cli/src/handlers/composed-handler.ts | 20 +++--- .../src/providers/provider-routing.test.ts | 52 +++++++-------- 11 files changed, 100 insertions(+), 154 deletions(-) diff --git a/packages/cli/src/adapters/dialect-manager.ts b/packages/cli/src/adapters/dialect-manager.ts index 99a5ca0..a7aca5a 100644 --- a/packages/cli/src/adapters/dialect-manager.ts +++ b/packages/cli/src/adapters/dialect-manager.ts @@ -1,11 +1,8 @@ /** - * DialectManager — selects the appropriate Layer 2 ModelDialect for a given model. + * resolveModelDialect — select the appropriate ModelDialect for a given model. * - * This allows ComposedHandler to apply model-specific quirks independent of - * which Layer 1 APIFormat or Layer 3 ProviderTransport are used: - * - Grok: XML function calls - * - Gemini: Thought signatures in reasoning_details - * - DeepSeek, GLM, etc.: thinking param stripping / mapping + * Pure function. Returns the first dialect whose shouldHandle() matches, + * or DefaultAPIFormat as fallback. */ import { BaseAPIFormat, DefaultAPIFormat } from "./base-api-format.js"; @@ -19,46 +16,22 @@ import { DeepSeekModelDialect } from "./deepseek-model-dialect.js"; import { GLMModelDialect } from "./glm-model-dialect.js"; import { XiaomiModelDialect } from "./xiaomi-model-dialect.js"; -export class DialectManager { - private adapters: BaseAPIFormat[]; - private defaultAdapter: DefaultAPIFormat; +const DIALECTS: Array BaseAPIFormat> = [ + GrokModelDialect, + GeminiAPIFormat, + CodexAPIFormat, // Must precede OpenAIAPIFormat + OpenAIAPIFormat, + QwenModelDialect, + MiniMaxModelDialect, + DeepSeekModelDialect, + GLMModelDialect, + XiaomiModelDialect, +]; - constructor(modelId: string) { - // Register all available dialects/formats - this.adapters = [ - new GrokModelDialect(modelId), - new GeminiAPIFormat(modelId), - new CodexAPIFormat(modelId), // Must be before OpenAIAPIFormat (codex matches first) - new OpenAIAPIFormat(modelId), - new QwenModelDialect(modelId), - new MiniMaxModelDialect(modelId), - new DeepSeekModelDialect(modelId), - new GLMModelDialect(modelId), - new XiaomiModelDialect(modelId), - ]; - this.defaultAdapter = new DefaultAPIFormat(modelId); - } - - /** - * Get the appropriate dialect/format for the current model - */ - getAdapter(): BaseAPIFormat { - for (const adapter of this.adapters) { - if (adapter.shouldHandle(this.defaultAdapter["modelId"])) { - return adapter; - } - } - return this.defaultAdapter; - } - - /** - * Check if current model needs special handling - */ - needsTransformation(): boolean { - return this.getAdapter() !== this.defaultAdapter; +export function resolveModelDialect(modelId: string): BaseAPIFormat { + for (const Dialect of DIALECTS) { + const d = new Dialect(modelId); + if (d.shouldHandle(modelId)) return d; } + return new DefaultAPIFormat(modelId); } - -// Backward-compatible alias -/** @deprecated Use DialectManager */ -export { DialectManager as AdapterManager }; diff --git a/packages/cli/src/adapters/index.ts b/packages/cli/src/adapters/index.ts index d184b63..59cb555 100644 --- a/packages/cli/src/adapters/index.ts +++ b/packages/cli/src/adapters/index.ts @@ -1,16 +1,8 @@ /** - * Model format and dialect implementations + * Model format and dialect exports */ export { BaseAPIFormat, DefaultAPIFormat } from "./base-api-format.js"; export type { ToolCall, AdapterResult } from "./base-api-format.js"; export { GrokModelDialect } from "./grok-model-dialect.js"; -export { DialectManager } from "./dialect-manager.js"; - -// Backward-compatible aliases -export { - BaseAPIFormat as BaseModelAdapter, - DefaultAPIFormat as DefaultAdapter, -} from "./base-api-format.js"; -export { GrokModelDialect as GrokAdapter } from "./grok-model-dialect.js"; -export { DialectManager as AdapterManager } from "./dialect-manager.js"; +export { resolveModelDialect } from "./dialect-manager.js"; diff --git a/packages/cli/src/adapters/litellm-api-format.ts b/packages/cli/src/adapters/litellm-api-format.ts index 91e70d5..7f3e34e 100644 --- a/packages/cli/src/adapters/litellm-api-format.ts +++ b/packages/cli/src/adapters/litellm-api-format.ts @@ -37,7 +37,7 @@ export class LiteLLMAPIFormat extends DefaultAPIFormat { } shouldHandle(modelId: string): boolean { - return false; // Always used explicitly, not via DialectManager matching + return false; // Always used explicitly, not via resolveModelDialect matching } supportsVision(): boolean { diff --git a/packages/cli/src/adapters/local-adapter.ts b/packages/cli/src/adapters/local-adapter.ts index c45073b..ea213f8 100644 --- a/packages/cli/src/adapters/local-adapter.ts +++ b/packages/cli/src/adapters/local-adapter.ts @@ -12,7 +12,7 @@ */ import { BaseAPIFormat, type AdapterResult } from "./base-api-format.js"; -import { DialectManager } from "./dialect-manager.js"; +import { resolveModelDialect } from "./dialect-manager.js"; import { log } from "../logger.js"; interface SamplingParams { @@ -31,8 +31,7 @@ export class LocalModelAdapter extends BaseAPIFormat { super(modelId); this.providerName = providerName; - const manager = new DialectManager(modelId); - this.innerAdapter = manager.getAdapter(); + this.innerAdapter = resolveModelDialect(modelId); } // ─── Text processing delegates to inner adapter ─────────────────── diff --git a/packages/cli/src/adapters/openrouter-api-format.ts b/packages/cli/src/adapters/openrouter-api-format.ts index c78e9db..1288baf 100644 --- a/packages/cli/src/adapters/openrouter-api-format.ts +++ b/packages/cli/src/adapters/openrouter-api-format.ts @@ -11,7 +11,7 @@ */ import { BaseAPIFormat, type AdapterResult } from "./base-api-format.js"; -import { DialectManager } from "./dialect-manager.js"; +import { resolveModelDialect } from "./dialect-manager.js"; import { removeUriFormat } from "../transform.js"; import { log } from "../logger.js"; @@ -22,8 +22,7 @@ export class OpenRouterAPIFormat extends BaseAPIFormat { super(modelId); // Get model-specific dialect (GrokModelDialect, GeminiAPIFormat, etc.) - const manager = new DialectManager(modelId); - this.innerAdapter = manager.getAdapter(); + this.innerAdapter = resolveModelDialect(modelId); } /** Synchronous reasoning support check via model ID patterns */ diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index bc86abc..19bd472 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1430,10 +1430,9 @@ async function probeModelRouting(models: string[], jsonOutput: boolean): Promise declaredStreamFormat = "openai-sse"; } - // Get model dialect via DialectManager - const { DialectManager } = await import("./adapters/dialect-manager.js"); - const adapterManager = new DialectManager(modelName); - const modelTranslator = adapterManager.getAdapter(); + // Get model dialect via resolveModelDialect + const { resolveModelDialect } = await import("./adapters/dialect-manager.js"); + const modelTranslator = resolveModelDialect(modelName); const modelTranslatorName = modelTranslator.getName(); // Transport overrides (aggregators that normalize responses to openai-sse) diff --git a/packages/cli/src/e2e-glm-adapter.test.ts b/packages/cli/src/e2e-glm-adapter.test.ts index 657930f..b0a61be 100644 --- a/packages/cli/src/e2e-glm-adapter.test.ts +++ b/packages/cli/src/e2e-glm-adapter.test.ts @@ -3,7 +3,7 @@ * * Validates: * 1. GLMModelDialect model detection, context windows, and vision support - * 2. DialectManager correctly selects GLMModelDialect for GLM models + * 2. resolveModelDialect correctly selects GLMModelDialect for GLM models * 3. ComposedHandler three-layer architecture — model dialect provides model-specific * overrides (context window, vision, prepareRequest) even when a provider format * (LiteLLMAPIFormat, OpenRouterAPIFormat) is set as the explicit adapter @@ -11,7 +11,7 @@ import { describe, test, expect } from "bun:test"; import { GLMModelDialect } from "./adapters/glm-model-dialect.js"; -import { DialectManager } from "./adapters/dialect-manager.js"; +import { resolveModelDialect } from "./adapters/dialect-manager.js"; import { LiteLLMAPIFormat } from "./adapters/litellm-api-format.js"; import { DefaultAPIFormat } from "./adapters/base-api-format.js"; @@ -134,50 +134,46 @@ describe("GLMModelDialect — processTextContent", () => { }); }); -// ─── Group 2: DialectManager selects GLMModelDialect ───────────────────────── +// ─── Group 2: resolveModelDialect selects GLMModelDialect ───────────────────── -describe("DialectManager — GLM routing", () => { +describe("resolveModelDialect — GLM routing", () => { test("selects GLMModelDialect for glm-5", () => { - const manager = new DialectManager("glm-5"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("glm-5"); expect(adapter.getName()).toBe("GLMModelDialect"); }); test("selects GLMModelDialect for glm-4-long", () => { - const manager = new DialectManager("glm-4-long"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("glm-4-long"); expect(adapter.getName()).toBe("GLMModelDialect"); }); test("does NOT select GLMModelDialect for gpt-4o", () => { - const manager = new DialectManager("gpt-4o"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("gpt-4o"); expect(adapter.getName()).not.toBe("GLMModelDialect"); }); test("needsTransformation returns true for GLM models", () => { - const manager = new DialectManager("glm-5"); - expect(manager.needsTransformation()).toBe(true); + const adapter = resolveModelDialect("glm-5"); + expect(adapter.getName() !== "DefaultAPIFormat").toBe(true); }); }); // ─── Group 3: Three-layer adapter architecture ─────────────────────────────── // // When a format adapter (LiteLLMAPIFormat) is the explicit adapter, the model -// dialect (GLMModelDialect) should still be resolved by DialectManager for +// dialect (GLMModelDialect) should still be resolved by resolveModelDialect for // model-specific concerns. describe("Three-layer adapter — model dialect overrides format adapter", () => { - test("DialectManager resolves GLMModelDialect even when LiteLLMAPIFormat would be used", () => { + test("resolveModelDialect resolves GLMModelDialect even when LiteLLMAPIFormat would be used", () => { // Simulate what ComposedHandler does: // 1. Explicit adapter = LiteLLMAPIFormat (L1 wire format) - // 2. DialectManager.getAdapter() = GLMModelDialect (L2 model quirks) + // 2. resolveModelDialect() = GLMModelDialect (L2 model quirks) const litellmAdapter = new LiteLLMAPIFormat("glm-5", "https://example.com"); - const adapterManager = new DialectManager("glm-5"); - const modelAdapter = adapterManager.getAdapter(); + const modelAdapter = resolveModelDialect("glm-5"); // Format adapter handles wire format / transport expect(litellmAdapter.getName()).toBe("LiteLLMAPIFormat"); @@ -196,24 +192,21 @@ describe("Three-layer adapter — model dialect overrides format adapter", () => }); test("model dialect provides correct context window for glm-4-long via LiteLLM", () => { - const adapterManager = new DialectManager("glm-4-long"); - const modelAdapter = adapterManager.getAdapter(); + const modelAdapter = resolveModelDialect("glm-4-long"); expect(modelAdapter.getName()).toBe("GLMModelDialect"); expect(modelAdapter.getContextWindow()).toBe(1_000_000); }); test("model dialect correctly reports no vision for glm-4-flash via LiteLLM", () => { - const adapterManager = new DialectManager("glm-4-flash"); - const modelAdapter = adapterManager.getAdapter(); + const modelAdapter = resolveModelDialect("glm-4-flash"); expect(modelAdapter.getName()).toBe("GLMModelDialect"); expect(modelAdapter.supportsVision()).toBe(false); }); test("non-GLM model via LiteLLM falls back to DefaultAPIFormat", () => { - const adapterManager = new DialectManager("some-unknown-model"); - const modelAdapter = adapterManager.getAdapter(); + const modelAdapter = resolveModelDialect("some-unknown-model"); // Should be DefaultAPIFormat, not GLMModelDialect expect(modelAdapter.getName()).toBe("DefaultAPIFormat"); @@ -221,8 +214,7 @@ describe("Three-layer adapter — model dialect overrides format adapter", () => test("model dialect strips thinking, format adapter does not", () => { const litellmAdapter = new LiteLLMAPIFormat("glm-5", "https://example.com"); - const adapterManager = new DialectManager("glm-5"); - const modelAdapter = adapterManager.getAdapter(); + const modelAdapter = resolveModelDialect("glm-5"); // Format adapter does not strip thinking (no override) const request1 = { model: "glm-5", thinking: { budget: 10000 }, messages: [] }; diff --git a/packages/cli/src/e2e-model-catalog.test.ts b/packages/cli/src/e2e-model-catalog.test.ts index 10399b4..4bb84e8 100644 --- a/packages/cli/src/e2e-model-catalog.test.ts +++ b/packages/cli/src/e2e-model-catalog.test.ts @@ -15,7 +15,7 @@ import { lookupModel } from "./adapters/model-catalog.js"; import { MiniMaxModelDialect } from "./adapters/minimax-model-dialect.js"; import { GLMModelDialect } from "./adapters/glm-model-dialect.js"; import { GrokModelDialect } from "./adapters/grok-model-dialect.js"; -import { DialectManager } from "./adapters/dialect-manager.js"; +import { resolveModelDialect } from "./adapters/dialect-manager.js"; import { AnthropicAPIFormat } from "./adapters/anthropic-api-format.js"; const MINIMAX_API_KEY = @@ -181,40 +181,34 @@ describe("Group 2: GrokModelDialect — catalog integration", () => { }); }); -describe("Group 2: DialectManager — correct dialect selection", () => { +describe("Group 2: resolveModelDialect — correct dialect selection", () => { test("selects MiniMaxModelDialect for MiniMax-M2.7", () => { - const manager = new DialectManager("MiniMax-M2.7"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("MiniMax-M2.7"); expect(adapter.getName()).toBe("MiniMaxModelDialect"); }); test("selects GLMModelDialect for glm-5", () => { - const manager = new DialectManager("glm-5"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("glm-5"); expect(adapter.getName()).toBe("GLMModelDialect"); }); test("selects GrokModelDialect for grok-4", () => { - const manager = new DialectManager("grok-4"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("grok-4"); expect(adapter.getName()).toBe("GrokModelDialect"); }); test("selects GrokModelDialect for x-ai/grok-4-fast", () => { - const manager = new DialectManager("x-ai/grok-4-fast"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("x-ai/grok-4-fast"); expect(adapter.getName()).toBe("GrokModelDialect"); }); test("selects MiniMaxModelDialect for minimax-m2.5", () => { - const manager = new DialectManager("minimax-m2.5"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("minimax-m2.5"); expect(adapter.getName()).toBe("MiniMaxModelDialect"); }); test("returns DefaultAPIFormat for unknown model", () => { - const manager = new DialectManager("totally-unknown-model-xyz"); - const adapter = manager.getAdapter(); + const adapter = resolveModelDialect("totally-unknown-model-xyz"); expect(adapter.getName()).toBe("DefaultAPIFormat"); }); }); diff --git a/packages/cli/src/format-translation.test.ts b/packages/cli/src/format-translation.test.ts index bdc4e85..c6cdb35 100644 --- a/packages/cli/src/format-translation.test.ts +++ b/packages/cli/src/format-translation.test.ts @@ -582,15 +582,15 @@ describe("Model Adapter Quirks", () => { expect(request.thinking).toBeUndefined(); }); - test("AdapterManager selects correct adapter for model IDs", async () => { - const { DialectManager } = await import("./adapters/dialect-manager.js"); + test("resolveModelDialect selects correct adapter for model IDs", async () => { + const { resolveModelDialect } = await import("./adapters/dialect-manager.js"); - expect(new DialectManager("glm-5").getAdapter().getName()).toBe("GLMModelDialect"); - expect(new DialectManager("grok-3").getAdapter().getName()).toBe("GrokModelDialect"); - expect(new DialectManager("minimax-m2.5").getAdapter().getName()).toBe("MiniMaxModelDialect"); - expect(new DialectManager("qwen3.5-plus").getAdapter().getName()).toBe("QwenModelDialect"); - expect(new DialectManager("deepseek-r1").getAdapter().getName()).toBe("DeepSeekModelDialect"); - expect(new DialectManager("unknown-model").getAdapter().getName()).toBe("DefaultAPIFormat"); + expect(resolveModelDialect("glm-5").getName()).toBe("GLMModelDialect"); + expect(resolveModelDialect("grok-3").getName()).toBe("GrokModelDialect"); + expect(resolveModelDialect("minimax-m2.5").getName()).toBe("MiniMaxModelDialect"); + expect(resolveModelDialect("qwen3.5-plus").getName()).toBe("QwenModelDialect"); + expect(resolveModelDialect("deepseek-r1").getName()).toBe("DeepSeekModelDialect"); + expect(resolveModelDialect("unknown-model").getName()).toBe("DefaultAPIFormat"); }); }); @@ -656,9 +656,9 @@ describe("CodexAdapter", () => { expect(new CodexAPIFormat("codex-mini").getName()).toBe("CodexAPIFormat"); }); - test("AdapterManager selects CodexAPIFormat for codex-mini", async () => { - const { DialectManager } = await import("./adapters/dialect-manager.js"); - expect(new DialectManager("codex-mini").getAdapter().getName()).toBe("CodexAPIFormat"); + test("resolveModelDialect selects CodexAPIFormat for codex-mini", async () => { + const { resolveModelDialect } = await import("./adapters/dialect-manager.js"); + expect(resolveModelDialect("codex-mini").getName()).toBe("CodexAPIFormat"); }); }); diff --git a/packages/cli/src/handlers/composed-handler.ts b/packages/cli/src/handlers/composed-handler.ts index 2b7c2de..b8908b8 100644 --- a/packages/cli/src/handlers/composed-handler.ts +++ b/packages/cli/src/handlers/composed-handler.ts @@ -22,7 +22,7 @@ import type { ProviderTransport } from "../providers/transport/types.js"; import type { BaseAPIFormat } from "../adapters/base-api-format.js"; // Alias for readability within this file type BaseModelAdapter = BaseAPIFormat; -import { DialectManager } from "../adapters/dialect-manager.js"; +import { resolveModelDialect } from "../adapters/dialect-manager.js"; import { MiddlewareManager, GeminiThoughtSignatureMiddleware } from "../middleware/index.js"; import { TokenTracker } from "./shared/token-tracker.js"; import { transformOpenAIToClaude } from "../transform.js"; @@ -67,10 +67,11 @@ export interface ComposedHandlerOptions { export class ComposedHandler implements ModelHandler { private provider: ProviderTransport; - private adapterManager: DialectManager; private explicitAdapter?: BaseModelAdapter; /** Model-specific adapter (GLM, Grok, etc.) — handles model quirks independent of provider */ private modelAdapter?: BaseModelAdapter; + /** Auto-resolved dialect for this model */ + private resolvedDialect: BaseModelAdapter; private middlewareManager: MiddlewareManager; private tokenTracker: TokenTracker; private targetModel: string; @@ -92,14 +93,11 @@ export class ComposedHandler implements ModelHandler { this.explicitAdapter = options.adapter; this.isInteractive = options.isInteractive ?? false; - // Initialize dialect manager for automatic dialect/format selection - this.adapterManager = new DialectManager(targetModel); - - // Always resolve model-specific adapter (GLM, Grok, DeepSeek, etc.) - // This handles model quirks independent of provider transport (LiteLLM, OpenRouter, etc.) - const resolvedModelAdapter = this.adapterManager.getAdapter(); - if (resolvedModelAdapter.getName() !== "DefaultAPIFormat") { - this.modelAdapter = resolvedModelAdapter; + // Resolve model dialect (GLM, Grok, DeepSeek, etc.) + // Handles model quirks independent of provider transport + this.resolvedDialect = resolveModelDialect(targetModel); + if (this.resolvedDialect.getName() !== "DefaultAPIFormat") { + this.modelAdapter = this.resolvedDialect; } // Initialize middleware (only register model-specific middleware when applicable) @@ -122,7 +120,7 @@ export class ComposedHandler implements ModelHandler { /** Provider adapter — handles transport format (messages, tools, payload) */ private getAdapter(): BaseModelAdapter { - return this.explicitAdapter || this.adapterManager.getAdapter(); + return this.explicitAdapter || this.resolvedDialect; } /** Model context window — model adapter wins over provider adapter */ diff --git a/packages/cli/src/providers/provider-routing.test.ts b/packages/cli/src/providers/provider-routing.test.ts index 7fdf9c1..e66f4c7 100644 --- a/packages/cli/src/providers/provider-routing.test.ts +++ b/packages/cli/src/providers/provider-routing.test.ts @@ -10,7 +10,7 @@ import { describe, test, expect } from "bun:test"; import { parseModelSpec } from "./model-parser.js"; import { BUILTIN_PROVIDERS, getShortcuts } from "./provider-definitions.js"; -import { DialectManager } from "../adapters/dialect-manager.js"; +import { resolveModelDialect } from "../adapters/dialect-manager.js"; import { GrokModelDialect } from "../adapters/grok-model-dialect.js"; import { GeminiAPIFormat } from "../adapters/gemini-api-format.js"; import { QwenModelDialect } from "../adapters/qwen-model-dialect.js"; @@ -161,127 +161,127 @@ describe("parseModelSpec — native model auto-detection", () => { // Section 2: Adapter selection // --------------------------------------------------------------------------- -describe("DialectManager — correct dialect selection", () => { +describe("resolveModelDialect — correct dialect selection", () => { test("grok-beta → GrokModelDialect", () => { - const adapter = new DialectManager("grok-beta").getAdapter(); + const adapter = resolveModelDialect("grok-beta"); expect(adapter).toBeInstanceOf(GrokModelDialect); }); test("x-ai/grok-beta → GrokModelDialect", () => { - const adapter = new DialectManager("x-ai/grok-beta").getAdapter(); + const adapter = resolveModelDialect("x-ai/grok-beta"); expect(adapter).toBeInstanceOf(GrokModelDialect); }); test("gemini-2.0-flash → GeminiAPIFormat", () => { - const adapter = new DialectManager("gemini-2.0-flash").getAdapter(); + const adapter = resolveModelDialect("gemini-2.0-flash"); expect(adapter).toBeInstanceOf(GeminiAPIFormat); }); test("google/gemini-2.5-pro → GeminiAPIFormat", () => { - const adapter = new DialectManager("google/gemini-2.5-pro").getAdapter(); + const adapter = resolveModelDialect("google/gemini-2.5-pro"); expect(adapter).toBeInstanceOf(GeminiAPIFormat); }); test("deepseek-r1 → DeepSeekModelDialect", () => { - const adapter = new DialectManager("deepseek-r1").getAdapter(); + const adapter = resolveModelDialect("deepseek-r1"); expect(adapter).toBeInstanceOf(DeepSeekModelDialect); }); test("glm-5 → GLMModelDialect", () => { - const adapter = new DialectManager("glm-5").getAdapter(); + const adapter = resolveModelDialect("glm-5"); expect(adapter).toBeInstanceOf(GLMModelDialect); }); test("zhipu/glm-4 → GLMModelDialect", () => { - const adapter = new DialectManager("zhipu/glm-4").getAdapter(); + const adapter = resolveModelDialect("zhipu/glm-4"); expect(adapter).toBeInstanceOf(GLMModelDialect); }); test("minimax-m2.5 → MiniMaxModelDialect", () => { - const adapter = new DialectManager("minimax-m2.5").getAdapter(); + const adapter = resolveModelDialect("minimax-m2.5"); expect(adapter).toBeInstanceOf(MiniMaxModelDialect); }); test("qwen3-coder → QwenModelDialect", () => { - const adapter = new DialectManager("qwen3-coder").getAdapter(); + const adapter = resolveModelDialect("qwen3-coder"); expect(adapter).toBeInstanceOf(QwenModelDialect); }); test("xiaomi/mimo-vl-2b → XiaomiModelDialect", () => { - const adapter = new DialectManager("xiaomi/mimo-vl-2b").getAdapter(); + const adapter = resolveModelDialect("xiaomi/mimo-vl-2b"); expect(adapter).toBeInstanceOf(XiaomiModelDialect); }); test("codex-mini → CodexAPIFormat", () => { - const adapter = new DialectManager("codex-mini").getAdapter(); + const adapter = resolveModelDialect("codex-mini"); expect(adapter).toBeInstanceOf(CodexAPIFormat); }); test("gpt-4o → DefaultAPIFormat (GPT models use default OpenAI format)", () => { - const adapter = new DialectManager("gpt-4o").getAdapter(); + const adapter = resolveModelDialect("gpt-4o"); expect(adapter).toBeInstanceOf(DefaultAPIFormat); }); test("o3-mini → OpenAIAPIFormat (o-series needs reasoning_effort mapping)", () => { - const adapter = new DialectManager("o3-mini").getAdapter(); + const adapter = resolveModelDialect("o3-mini"); expect(adapter).toBeInstanceOf(OpenAIAPIFormat); }); test("unknown-model → DefaultAPIFormat", () => { - const adapter = new DialectManager("unknown-model").getAdapter(); + const adapter = resolveModelDialect("unknown-model"); expect(adapter).toBeInstanceOf(DefaultAPIFormat); }); }); -describe("DialectManager — false positive prevention", () => { +describe("resolveModelDialect — false positive prevention", () => { test("qwen-grok-hybrid → QwenModelDialect (NOT GrokModelDialect)", () => { - const adapter = new DialectManager("qwen-grok-hybrid").getAdapter(); + const adapter = resolveModelDialect("qwen-grok-hybrid"); expect(adapter).toBeInstanceOf(QwenModelDialect); expect(adapter).not.toBeInstanceOf(GrokModelDialect); }); test("deepseek-glm-test → DeepSeekModelDialect (NOT GLMModelDialect)", () => { - const adapter = new DialectManager("deepseek-glm-test").getAdapter(); + const adapter = resolveModelDialect("deepseek-glm-test"); expect(adapter).toBeInstanceOf(DeepSeekModelDialect); expect(adapter).not.toBeInstanceOf(GLMModelDialect); }); test("my-grok-clone → DefaultAPIFormat (not GrokModelDialect — grok is mid-string)", () => { - const adapter = new DialectManager("my-grok-clone").getAdapter(); + const adapter = resolveModelDialect("my-grok-clone"); expect(adapter).not.toBeInstanceOf(GrokModelDialect); // Should fall to default since none of the specific families match expect(adapter).toBeInstanceOf(DefaultAPIFormat); }); test("my-minimax-clone → DefaultAPIFormat (not MiniMaxModelDialect)", () => { - const adapter = new DialectManager("my-minimax-clone").getAdapter(); + const adapter = resolveModelDialect("my-minimax-clone"); expect(adapter).not.toBeInstanceOf(MiniMaxModelDialect); expect(adapter).toBeInstanceOf(DefaultAPIFormat); }); test("test-deepseek-model → DefaultAPIFormat (not DeepSeekModelDialect — deepseek is mid-string)", () => { - const adapter = new DialectManager("test-deepseek-model").getAdapter(); + const adapter = resolveModelDialect("test-deepseek-model"); expect(adapter).not.toBeInstanceOf(DeepSeekModelDialect); expect(adapter).toBeInstanceOf(DefaultAPIFormat); }); test("vendor/grok-beta uses GrokModelDialect (vendor prefix is fine)", () => { - const adapter = new DialectManager("vendor/grok-beta").getAdapter(); + const adapter = resolveModelDialect("vendor/grok-beta"); expect(adapter).toBeInstanceOf(GrokModelDialect); }); test("vendor/deepseek-r1 uses DeepSeekModelDialect (vendor prefix)", () => { - const adapter = new DialectManager("vendor/deepseek-r1").getAdapter(); + const adapter = resolveModelDialect("vendor/deepseek-r1"); expect(adapter).toBeInstanceOf(DeepSeekModelDialect); }); test("vendor/minimax-m2.5 uses MiniMaxModelDialect (vendor prefix)", () => { - const adapter = new DialectManager("vendor/minimax-m2.5").getAdapter(); + const adapter = resolveModelDialect("vendor/minimax-m2.5"); expect(adapter).toBeInstanceOf(MiniMaxModelDialect); }); test("openrouter/x-ai/grok-beta uses GrokModelDialect (double vendor prefix)", () => { - const adapter = new DialectManager("openrouter/x-ai/grok-beta").getAdapter(); + const adapter = resolveModelDialect("openrouter/x-ai/grok-beta"); expect(adapter).toBeInstanceOf(GrokModelDialect); }); }); From 78b1f5d35f9ff5d4552aac824e22363869fbe9fb Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Tue, 24 Mar 2026 05:20:06 +0000 Subject: [PATCH 2/2] docs: update CLAUDE.md for resolveModelDialect --- CLAUDE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 568585f..2a3bd19 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -95,7 +95,7 @@ Each converter declares its stream format via `getStreamFormat()`. Translates model-specific dialect differences (context windows, thinking→reasoning_effort, vision rules). - **Interface**: `adapters/model-translator.ts` - **Implementations**: GLMAdapter, GrokAdapter, MiniMaxAdapter, DeepSeekAdapter, QwenAdapter, CodexAdapter -- **Selection**: `AdapterManager` auto-selects based on model ID +- **Selection**: `resolveModelDialect(modelId)` selects based on model ID ### Layer 3: ProviderTransport — HTTP transport Handles auth, endpoints, headers, rate limiting. Optionally overrides stream format for aggregators. @@ -113,7 +113,7 @@ transport.overrideStreamFormat() ?? modelAdapter.getStreamFormat() ?? providerAd ``` **Adding a new provider**: Add one entry to `PROVIDER_PROFILES` table in `providers/provider-profiles.ts`. -**Adding a new model**: Create a ModelTranslator adapter, register in `adapters/adapter-manager.ts`. +**Adding a new model**: Create a ModelDialect adapter, add it to the `DIALECTS` array in `adapters/dialect-manager.ts`. **Verifying wiring**: `claudish --probe ` shows the full adapter composition. ### Stream Parsers