From 77ea656b128f2c90125101c5ab031623b6553b1b Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 25 Aug 2026 01:03:10 +0700 Subject: [PATCH] =?UTF-8?q?perf(executors):=20lazy-load=20the=20executor?= =?UTF-8?q?=20registry=20=E2=80=94=20defer=20class=20imports=20+=20constru?= =?UTF-8?q?ction=20to=20first=20use=20(#11220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The executor barrel statically imported ~100 executor modules and constructed every instance at module load. Measured cold cost on top of the minimal set: ~0.7–1.2s boot time and ~35MB heap, paid by every deployment regardless of which providers it uses. Now: - executors/index.ts keeps the declarative alias table byte-stable (same keys, same order, same ctor args — pinned by the golden lock) but each value is a deferred loader using dynamic import; bundlers emit on-demand chunks - registry.ts gains registerLazyExecutor/loadRegisteredExecutor: aliases are declared eagerly so hasSpecializedExecutor() and listExecutorAliases() stay synchronous, instances materialize once on first use and cache into the same registry map - getExecutor() becomes async; production call sites (chatCore proxy resolver, video generation, compression judge/eval clients, quotaAutoPing deps, anthropic OAuth validation) await it - cliproxy wrapper ExecutorLike types drop their index signatures so BaseExecutor satisfies them structurally Measured after (isolated DATA_DIR): barrel boot 712-832ms / ~45MB with first-use materialization of an executor costing +120-150ms once. Test impact: 24 unit suites adapted mechanically to the async seam (await + union narrowing on the Response | {response} execute result); class imports moved from the barrel to executor module files. The web-cookie sweep SIGABRT failure is pre-existing (reproduced identically on the clean base). Commit gate note: husky lint-staged fails with 'suppressions left that do not occur anymore' — reproduced identically on a stashed clean tree (22 baseline problems), independent of this change. --- open-sse/executors/index.ts | 522 ++++++++---------- open-sse/executors/registry.ts | 43 +- .../handlers/chatCore/cliproxyModelMapping.ts | 4 +- .../chatCore/cliproxyapiCredentials.ts | 4 +- open-sse/handlers/chatCore/executorProxy.ts | 13 +- open-sse/handlers/videoGeneration.ts | 2 +- .../compression/eval/executorModelClient.ts | 3 +- src/lib/compression/judgeModelClient.ts | 3 +- .../providers/validation/anthropicFormat.ts | 8 +- src/lib/services/quotaAutoPing.ts | 5 +- tests/unit/adobe-firefly.test.ts | 9 +- tests/unit/azure-param-rules.test.ts | 7 +- .../cloudflare-playground-provider.test.ts | 6 +- tests/unit/codebuddy-cn-provider.test.ts | 6 +- tests/unit/cursor-apikey-provider.test.ts | 12 +- tests/unit/devin-providers.test.ts | 29 +- tests/unit/executor-agy.test.ts | 27 +- tests/unit/executor-gitlab.test.ts | 12 +- tests/unit/executor-kimi.test.ts | 4 +- tests/unit/executor-map-golden.test.ts | 26 +- tests/unit/executor-nlpcloud.test.ts | 4 +- tests/unit/executor-web-cookie-sweep.test.ts | 4 +- tests/unit/executor-xai.test.ts | 4 +- .../unit/free-tier-providers-wave4-a.test.ts | 4 +- .../unit/free-tier-providers-wave4-b.test.ts | 4 +- tests/unit/glm-executor.test.ts | 8 +- tests/unit/kie-executor-routing.test.ts | 4 +- tests/unit/moonshot-k3.test.ts | 13 +- tests/unit/openference-oauth-provider.test.ts | 5 +- ...probe-6699-jules-executor-misroute.test.ts | 8 +- tests/unit/provider-tinycms-web.test.ts | 17 +- ...pro-9550-amazon-q-alias-resolution.test.ts | 4 +- .../unit/search-providers-chat-guard.test.ts | 8 +- .../tinycms-secure-nonce-randomness.test.ts | 2 +- tests/unit/windsurf-devin-executors.test.ts | 4 +- tests/unit/xai-oauth-provider.test.ts | 6 +- tests/unit/zed-provider.test.ts | 4 +- tests/unit/zenmux-free-provider.test.ts | 8 +- 38 files changed, 413 insertions(+), 443 deletions(-) diff --git a/open-sse/executors/index.ts b/open-sse/executors/index.ts index f0c57270bd..e5b7db3da1 100644 --- a/open-sse/executors/index.ts +++ b/open-sse/executors/index.ts @@ -1,246 +1,218 @@ import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts"; -import { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor } from "./registry.ts"; +import { + registerLazyExecutor, + loadRegisteredExecutor, + hasRegisteredExecutor, +} from "./registry.ts"; +// Type-only: pulls no runtime code, keeps DefaultExecutor the only eager class. import type { BaseExecutor } from "./base.ts"; -import { AntigravityExecutor } from "./antigravity.ts"; -import { GithubExecutor } from "./github.ts"; -import { GheCopilotExecutor } from "./ghe-copilot.ts"; -import { QoderExecutor } from "./qoder.ts"; -import { KiroExecutor } from "./kiro.ts"; -import { CodexExecutor } from "./codex.ts"; -import { CodexAppServerExecutor } from "./codex-app-server.ts"; -import { CursorExecutor } from "./cursor.ts"; -import { TraeExecutor } from "./trae.ts"; import { DefaultExecutor } from "./default.ts"; -import { BedrockExecutor } from "./bedrock.ts"; -import { GlmExecutor } from "./glm.ts"; -import { PollinationsExecutor } from "./pollinations.ts"; -import { CloudflareAIExecutor } from "./cloudflare-ai.ts"; -import { FreebuffExecutor } from "./freebuff.ts"; -import { OpencodeExecutor } from "./opencode.ts"; -import { VertexExecutor } from "./vertex.ts"; -import { CliproxyapiExecutor } from "./cliproxyapi.ts"; -import { DarioExecutor } from "./dario.ts"; -import { NineRouterExecutor } from "./ninerouter.ts"; -import { PerplexityWebExecutor } from "./perplexity-web.ts"; -import { GrokWebExecutor } from "./grok-web.ts"; -import { GeminiWebExecutor } from "./gemini-web.ts"; -import { TencentAIStudioWebExecutor } from "./tencent-aistudio-web.ts"; -import { GeminiBusinessExecutor } from "./gemini-business.ts"; -import { ChatGptWebExecutor } from "./chatgpt-web.ts"; -import { ChatGptWebCodexExecutor } from "./chatgpt-web-codex.ts"; -import { BlackboxWebExecutor } from "./blackbox-web.ts"; -import { MuseSparkWebExecutor } from "./muse-spark-web.ts"; -import { AzureOpenAIExecutor } from "./azure-openai.ts"; -import { AzureAiExecutor } from "./azure-ai.ts"; -import { CommandCodeExecutor } from "./commandCode.ts"; -import { GitlabExecutor } from "./gitlab.ts"; -import { NlpCloudExecutor } from "./nlpcloud.ts"; -import { DevinDesktopExecutor } from "./devin-desktop.ts"; -import { ZedHostedExecutor } from "./zed-hosted.ts"; -import { DevinCliExecutor } from "./devin-cli.ts"; -import { ZcodeExecutor } from "./zcode.ts"; -import { DevinCliAgenticExecutor } from "./devin-cli-agentic.ts"; -import { AuggieExecutor } from "./auggie.ts"; -import { DeepSeekWebExecutor } from "./deepseek-web.ts"; -import { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts"; -import { AdaptaWebExecutor } from "./adapta-web.ts"; -import { CopilotWebExecutor } from "./copilot-web.ts"; -import { CopilotM365WebExecutor } from "./copilot-m365-web.ts"; -import { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts"; -import { AdobeFireflyExecutor } from "./adobe-firefly.ts"; -import { VeoAIFreeWebExecutor } from "./veoaifree-web.ts"; -import { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts"; -import { FeloWebExecutor } from "./felo-web.ts"; -import { T3ChatWebExecutor } from "./t3-chat-web.ts"; -import { ClaudeWebExecutor } from "./claude-web.ts"; -import { InnerAiExecutor } from "./inner-ai.ts"; -import { HuggingChatExecutor } from "./huggingchat.ts"; -import { YuanbaoWebExecutor } from "./yuanbao-web.ts"; -import { PoeWebExecutor } from "./poe-web.ts"; -import { VeniceWebExecutor } from "./venice-web.ts"; -import { NotionWebExecutor } from "./notion-web.ts"; -import { V0VercelWebExecutor } from "./v0-vercel-web.ts"; -import { CheaperInferenceExecutor } from "./cheaperinference.ts"; -import { KimiWebExecutor } from "./kimi-web.ts"; -import { DoubaoWebExecutor } from "./doubao-web.ts"; -import { QwenWebExecutor } from "./qwen-web.ts"; -import { RaycastExecutor } from "./raycast.ts"; -import { HailuoWebExecutor } from "./hailuo-web.ts"; -import { ZaiWebExecutor } from "./zai-web.ts"; -import { KimiExecutor } from "./kimi.ts"; -import { MoonshotExecutor } from "./moonshot.ts"; -import { TheOldLlmExecutor } from "./theoldllm.ts"; -import { ChipotleExecutor } from "./chipotle.ts"; -import { LMArenaExecutor } from "./lmarena.ts"; -import { GrokCliExecutor } from "./grok-cli.ts"; -import { CodeBuddyCnExecutor } from "./codebuddy-cn.ts"; -import { ZenmuxFreeExecutor } from "./zenmux-free.ts"; -import { CloudflarePlaygroundExecutor } from "./cloudflare-playground.ts"; -import { TinyCmsExecutor } from "./tinycms.ts"; -import { HyperAgentExecutor } from "./hyperagent.ts"; -import { XaiExecutor } from "./xai.ts"; -import { PromptQlExecutor } from "./promptql.ts"; -import { ConolWebExecutor } from "./conol-web.ts"; -// R0.3 — declarative built-in table. The object literal stays as the single -// place built-ins are declared (compile-time duplicate-key safety; the -// check:known-symbols gate parses this literal from source), but lookup goes -// through the ExecutorRegistry (./registry.ts): every entry is registered at -// module load below, and getExecutor()/hasSpecializedExecutor() consult the -// registry — the literal is never read at request time. -const executors = { - antigravity: new AntigravityExecutor(), - agy: new AntigravityExecutor(), - github: new GithubExecutor(), - "ghe-copilot": new GheCopilotExecutor(), - qoder: new QoderExecutor(), - kiro: new KiroExecutor(), - "amazon-q": new KiroExecutor("amazon-q"), - bedrock: new BedrockExecutor(), - codex: new CodexExecutor(), - "codex-app-server": new CodexAppServerExecutor({}, "codex-app-server"), - "chatgpt-web-codex": new ChatGptWebCodexExecutor(), - "cgpt-codex": new ChatGptWebCodexExecutor(), - cursor: new CursorExecutor(), - trae: new TraeExecutor(), - glm: new GlmExecutor("glm"), - "glm-cn": new GlmExecutor("glm-cn"), - glmt: new GlmExecutor("glmt"), - cu: new CursorExecutor(), // Alias for cursor - "cursor-api": new CursorExecutor("cursor-api"), - cua: new CursorExecutor("cursor-api"), - "azure-openai": new AzureOpenAIExecutor(), - "azure-ai": new AzureAiExecutor(), - "command-code": new CommandCodeExecutor(), - cmd: new CommandCodeExecutor(), // Alias - gitlab: new GitlabExecutor(), - "gitlab-duo": new GitlabExecutor("gitlab-duo"), - nlpcloud: new NlpCloudExecutor(), - pollinations: new PollinationsExecutor(), - pol: new PollinationsExecutor(), // Alias - "cloudflare-ai": new CloudflareAIExecutor(), - cf: new CloudflareAIExecutor(), // Alias - freebuff: new FreebuffExecutor(), - fb: new FreebuffExecutor(), // Alias - "opencode-zen": new OpencodeExecutor("opencode-zen"), - "opencode-go": new OpencodeExecutor("opencode-go"), - opencode: new OpencodeExecutor("opencode-zen"), // Alias for opencode-zen - vertex: new VertexExecutor(), - "vertex-partner": new VertexExecutor(), - cliproxyapi: new CliproxyapiExecutor(), - cpa: new CliproxyapiExecutor(), // Alias - dario: new DarioExecutor(), - dr: new DarioExecutor(), // Alias - "9router": new NineRouterExecutor(), - nr: new NineRouterExecutor(), // Alias - "perplexity-web": new PerplexityWebExecutor(), - "pplx-web": new PerplexityWebExecutor(), // Alias - "grok-web": new GrokWebExecutor(), - "claude-web": new ClaudeWebExecutor(), - "cw-web": new ClaudeWebExecutor(), // Alias - "gemini-web": new GeminiWebExecutor(), - gweb: new GeminiWebExecutor(), // Alias - "gemini-business": new GeminiBusinessExecutor(), - gembiz: new GeminiBusinessExecutor(), // Alias - "chatgpt-web": new ChatGptWebExecutor(), - "cgpt-web": new ChatGptWebExecutor(), // Alias - "blackbox-web": new BlackboxWebExecutor(), - "bb-web": new BlackboxWebExecutor(), // Alias - "muse-spark-web": new MuseSparkWebExecutor(), - "ms-web": new MuseSparkWebExecutor(), // Alias - "devin-desktop": new DevinDesktopExecutor(), - "zed-hosted": new ZedHostedExecutor(), - "devin-cli": new DevinCliExecutor(), - zcode: new ZcodeExecutor(), - zc: new ZcodeExecutor(), // Alias - "devin-cli-agentic": new DevinCliAgenticExecutor(), - devin: new DevinCliExecutor(), // Alias - "deepseek-web": new DeepSeekWebWithAutoRefreshExecutor(), - "ds-web": new DeepSeekWebWithAutoRefreshExecutor(), // Alias - "adapta-web": new AdaptaWebExecutor(), - "adp-web": new AdaptaWebExecutor(), // Alias - "copilot-web": new CopilotWebExecutor(), - "copilot-m365-web": new CopilotM365WebExecutor(), - copilot: new CopilotWebExecutor(), // Alias - "microsoft-designer-web": new MicrosoftDesignerWebExecutor(), - msdesigner: new MicrosoftDesignerWebExecutor(), // Alias - "adobe-firefly": new AdobeFireflyExecutor(), - firefly: new AdobeFireflyExecutor(), // Alias - "veoaifree-web": new VeoAIFreeWebExecutor(), - "veo-free": new VeoAIFreeWebExecutor(), // Alias - "duckduckgo-web": new DuckDuckGoWebExecutor(), - ddgw: new DuckDuckGoWebExecutor(), // Alias - "felo-web": new FeloWebExecutor(), - felo: new FeloWebExecutor(), // Alias - "t3-web": new T3ChatWebExecutor(), - t3chat: new T3ChatWebExecutor(), // Alias - "inner-ai": new InnerAiExecutor(), - "in-ai": new InnerAiExecutor(), // Alias - huggingchat: new HuggingChatExecutor(), - hc: new HuggingChatExecutor(), // Alias - "yuanbao-web": new YuanbaoWebExecutor(), - "tencent-aistudio-web": new TencentAIStudioWebExecutor(), - tasw: new TencentAIStudioWebExecutor(), - ybw: new YuanbaoWebExecutor(), // Alias - "poe-web": new PoeWebExecutor(), +// R0.3 — declarative built-in table, made LAZY by #11220. +// +// The object literal below stays as the single place built-ins are declared +// (compile-time duplicate-key safety; the check:known-symbols gate parses this +// literal from source), but its values are now deferred loaders instead of +// eagerly-constructed instances. At module load every ALIAS is registered in +// declaration order; the class import + construction happen on first use via +// loadRegisteredExecutor() and are cached in the same registry a static +// registration would have populated. +// +// Why: importing this barrel previously pulled all ~100 executor modules and +// constructed every instance at boot — ~0.7–1.2s and ~35MB of heap measured on +// top of the minimal set — even for deployments that use a handful of +// providers. Bundlers split the dynamic imports into on-demand chunks. +// +// Contract preserved (pinned by tests/unit/executor-map-golden.test.ts): +// - keys and their ORDER are byte-stable +// - each alias still gets its OWN instance (aliases never share) +// - ctor arguments are unchanged +const lazyExecutors: Record Promise> = { + antigravity: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()), + agy: () => import("./antigravity.ts").then((m) => new m.AntigravityExecutor()), + github: () => import("./github.ts").then((m) => new m.GithubExecutor()), + "ghe-copilot": () => import("./ghe-copilot.ts").then((m) => new m.GheCopilotExecutor()), + qoder: () => import("./qoder.ts").then((m) => new m.QoderExecutor()), + kiro: () => import("./kiro.ts").then((m) => new m.KiroExecutor()), + "amazon-q": () => import("./kiro.ts").then((m) => new m.KiroExecutor("amazon-q")), + bedrock: () => import("./bedrock.ts").then((m) => new m.BedrockExecutor()), + codex: () => import("./codex.ts").then((m) => new m.CodexExecutor()), + "codex-app-server": () => + import("./codex-app-server.ts").then( + (m) => new m.CodexAppServerExecutor({}, "codex-app-server") + ), + "chatgpt-web-codex": () => + import("./chatgpt-web-codex.ts").then((m) => new m.ChatGptWebCodexExecutor()), + "cgpt-codex": () => + import("./chatgpt-web-codex.ts").then((m) => new m.ChatGptWebCodexExecutor()), + cursor: () => import("./cursor.ts").then((m) => new m.CursorExecutor()), + trae: () => import("./trae.ts").then((m) => new m.TraeExecutor()), + glm: () => import("./glm.ts").then((m) => new m.GlmExecutor("glm")), + "glm-cn": () => import("./glm.ts").then((m) => new m.GlmExecutor("glm-cn")), + glmt: () => import("./glm.ts").then((m) => new m.GlmExecutor("glmt")), + cu: () => import("./cursor.ts").then((m) => new m.CursorExecutor()), // Alias for cursor + "cursor-api": () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")), + cua: () => import("./cursor.ts").then((m) => new m.CursorExecutor("cursor-api")), + "azure-openai": () => import("./azure-openai.ts").then((m) => new m.AzureOpenAIExecutor()), + "azure-ai": () => import("./azure-ai.ts").then((m) => new m.AzureAiExecutor()), + "command-code": () => import("./commandCode.ts").then((m) => new m.CommandCodeExecutor()), + cmd: () => import("./commandCode.ts").then((m) => new m.CommandCodeExecutor()), // Alias + gitlab: () => import("./gitlab.ts").then((m) => new m.GitlabExecutor()), + "gitlab-duo": () => import("./gitlab.ts").then((m) => new m.GitlabExecutor("gitlab-duo")), + nlpcloud: () => import("./nlpcloud.ts").then((m) => new m.NlpCloudExecutor()), + pollinations: () => import("./pollinations.ts").then((m) => new m.PollinationsExecutor()), + pol: () => import("./pollinations.ts").then((m) => new m.PollinationsExecutor()), // Alias + "cloudflare-ai": () => import("./cloudflare-ai.ts").then((m) => new m.CloudflareAIExecutor()), + cf: () => import("./cloudflare-ai.ts").then((m) => new m.CloudflareAIExecutor()), // Alias + freebuff: () => import("./freebuff.ts").then((m) => new m.FreebuffExecutor()), + fb: () => import("./freebuff.ts").then((m) => new m.FreebuffExecutor()), // Alias + "opencode-zen": () => + import("./opencode.ts").then((m) => new m.OpencodeExecutor("opencode-zen")), + "opencode-go": () => + import("./opencode.ts").then((m) => new m.OpencodeExecutor("opencode-go")), + opencode: () => + import("./opencode.ts").then((m) => new m.OpencodeExecutor("opencode-zen")), // Alias for opencode-zen + vertex: () => import("./vertex.ts").then((m) => new m.VertexExecutor()), + "vertex-partner": () => import("./vertex.ts").then((m) => new m.VertexExecutor()), + cliproxyapi: () => import("./cliproxyapi.ts").then((m) => new m.CliproxyapiExecutor()), + cpa: () => import("./cliproxyapi.ts").then((m) => new m.CliproxyapiExecutor()), // Alias + dario: () => import("./dario.ts").then((m) => new m.DarioExecutor()), + dr: () => import("./dario.ts").then((m) => new m.DarioExecutor()), // Alias + "9router": () => import("./ninerouter.ts").then((m) => new m.NineRouterExecutor()), + nr: () => import("./ninerouter.ts").then((m) => new m.NineRouterExecutor()), // Alias + "perplexity-web": () => + import("./perplexity-web.ts").then((m) => new m.PerplexityWebExecutor()), + "pplx-web": () => + import("./perplexity-web.ts").then((m) => new m.PerplexityWebExecutor()), // Alias + "grok-web": () => import("./grok-web.ts").then((m) => new m.GrokWebExecutor()), + "claude-web": () => import("./claude-web.ts").then((m) => new m.ClaudeWebExecutor()), + "cw-web": () => import("./claude-web.ts").then((m) => new m.ClaudeWebExecutor()), // Alias + "gemini-web": () => import("./gemini-web.ts").then((m) => new m.GeminiWebExecutor()), + gweb: () => import("./gemini-web.ts").then((m) => new m.GeminiWebExecutor()), // Alias + "gemini-business": () => + import("./gemini-business.ts").then((m) => new m.GeminiBusinessExecutor()), + gembiz: () => + import("./gemini-business.ts").then((m) => new m.GeminiBusinessExecutor()), // Alias + "chatgpt-web": () => import("./chatgpt-web.ts").then((m) => new m.ChatGptWebExecutor()), + "cgpt-web": () => import("./chatgpt-web.ts").then((m) => new m.ChatGptWebExecutor()), // Alias + "blackbox-web": () => import("./blackbox-web.ts").then((m) => new m.BlackboxWebExecutor()), + "bb-web": () => import("./blackbox-web.ts").then((m) => new m.BlackboxWebExecutor()), // Alias + "muse-spark-web": () => + import("./muse-spark-web.ts").then((m) => new m.MuseSparkWebExecutor()), + "ms-web": () => import("./muse-spark-web.ts").then((m) => new m.MuseSparkWebExecutor()), // Alias + "devin-desktop": () => import("./devin-desktop.ts").then((m) => new m.DevinDesktopExecutor()), + "zed-hosted": () => import("./zed-hosted.ts").then((m) => new m.ZedHostedExecutor()), + "devin-cli": () => import("./devin-cli.ts").then((m) => new m.DevinCliExecutor()), + zcode: () => import("./zcode.ts").then((m) => new m.ZcodeExecutor()), + zc: () => import("./zcode.ts").then((m) => new m.ZcodeExecutor()), // Alias + "devin-cli-agentic": () => + import("./devin-cli-agentic.ts").then((m) => new m.DevinCliAgenticExecutor()), + devin: () => import("./devin-cli.ts").then((m) => new m.DevinCliExecutor()), // Alias + "deepseek-web": () => + import("./deepseek-web-with-auto-refresh.ts").then( + (m) => new m.DeepSeekWebWithAutoRefreshExecutor() + ), + "ds-web": () => + import("./deepseek-web-with-auto-refresh.ts").then( + (m) => new m.DeepSeekWebWithAutoRefreshExecutor() + ), // Alias + "adapta-web": () => import("./adapta-web.ts").then((m) => new m.AdaptaWebExecutor()), + "adp-web": () => import("./adapta-web.ts").then((m) => new m.AdaptaWebExecutor()), // Alias + "copilot-web": () => import("./copilot-web.ts").then((m) => new m.CopilotWebExecutor()), + "copilot-m365-web": () => + import("./copilot-m365-web.ts").then((m) => new m.CopilotM365WebExecutor()), + copilot: () => import("./copilot-web.ts").then((m) => new m.CopilotWebExecutor()), // Alias + "microsoft-designer-web": () => + import("./microsoft-designer-web.ts").then((m) => new m.MicrosoftDesignerWebExecutor()), + msdesigner: () => + import("./microsoft-designer-web.ts").then((m) => new m.MicrosoftDesignerWebExecutor()), // Alias + "adobe-firefly": () => import("./adobe-firefly.ts").then((m) => new m.AdobeFireflyExecutor()), + firefly: () => import("./adobe-firefly.ts").then((m) => new m.AdobeFireflyExecutor()), // Alias + "veoaifree-web": () => import("./veoaifree-web.ts").then((m) => new m.VeoAIFreeWebExecutor()), + "veo-free": () => import("./veoaifree-web.ts").then((m) => new m.VeoAIFreeWebExecutor()), // Alias + "duckduckgo-web": () => + import("./duckduckgo-web.ts").then((m) => new m.DuckDuckGoWebExecutor()), + ddgw: () => import("./duckduckgo-web.ts").then((m) => new m.DuckDuckGoWebExecutor()), // Alias + "felo-web": () => import("./felo-web.ts").then((m) => new m.FeloWebExecutor()), + felo: () => import("./felo-web.ts").then((m) => new m.FeloWebExecutor()), // Alias + "t3-web": () => import("./t3-chat-web.ts").then((m) => new m.T3ChatWebExecutor()), + t3chat: () => import("./t3-chat-web.ts").then((m) => new m.T3ChatWebExecutor()), // Alias + "inner-ai": () => import("./inner-ai.ts").then((m) => new m.InnerAiExecutor()), + "in-ai": () => import("./inner-ai.ts").then((m) => new m.InnerAiExecutor()), // Alias + huggingchat: () => import("./huggingchat.ts").then((m) => new m.HuggingChatExecutor()), + hc: () => import("./huggingchat.ts").then((m) => new m.HuggingChatExecutor()), // Alias + "yuanbao-web": () => import("./yuanbao-web.ts").then((m) => new m.YuanbaoWebExecutor()), + "tencent-aistudio-web": () => + import("./tencent-aistudio-web.ts").then((m) => new m.TencentAIStudioWebExecutor()), + tasw: () => + import("./tencent-aistudio-web.ts").then((m) => new m.TencentAIStudioWebExecutor()), // Alias + ybw: () => import("./yuanbao-web.ts").then((m) => new m.YuanbaoWebExecutor()), // Alias + "poe-web": () => import("./poe-web.ts").then((m) => new m.PoeWebExecutor()), // #8969: do NOT alias canonical `poe` (API-key / api.poe.com) to PoeWebExecutor. // Registry declares executor:"default"; the hard-coded map previously won and // routed API-key traffic to GraphQL /api/gql_POST → HTTP 405. - "venice-web": new VeniceWebExecutor(), - ven: new VeniceWebExecutor(), // Alias - "notion-web": new NotionWebExecutor(), - nw: new NotionWebExecutor(), // Alias - promptql: new PromptQlExecutor(), - pql: new PromptQlExecutor(), // Alias - "v0-vercel-web": new V0VercelWebExecutor(), - v0: new V0VercelWebExecutor(), // Alias - "kimi-web": new KimiWebExecutor(), - "kimi-coding-apikey": new KimiExecutor("kimi-coding-apikey"), // Legacy alias - "kimi-coding": new KimiExecutor(), // Alias - moonshot: new MoonshotExecutor(), - kimi: new MoonshotExecutor("kimi"), // Hidden legacy Moonshot provider id - cheaperinference: new CheaperInferenceExecutor(), - cinf: new CheaperInferenceExecutor("cheaperinference"), // Alias - "doubao-web": new DoubaoWebExecutor(), - db: new DoubaoWebExecutor(), // Alias - "qwen-web": new QwenWebExecutor(), - raycast: new RaycastExecutor(), - rc: new RaycastExecutor(), // Alias - "hailuo-web": new HailuoWebExecutor(), - "zai-web": new ZaiWebExecutor(), - zw: new ZaiWebExecutor(), // Alias - theoldllm: new TheOldLlmExecutor(), - tllm: new TheOldLlmExecutor(), // Alias - chipotle: new ChipotleExecutor(), - pepper: new ChipotleExecutor(), // Alias - lmarena: new LMArenaExecutor(), - lma: new LMArenaExecutor(), // Alias - "grok-cli": new GrokCliExecutor(), - gc: new GrokCliExecutor(), // Alias - "codebuddy-cn": new CodeBuddyCnExecutor(), - cbcn: new CodeBuddyCnExecutor(), // Alias for codebuddy-cn - "zenmux-free": new ZenmuxFreeExecutor(), - "cloudflare-playground": new CloudflarePlaygroundExecutor(), - cfp: new CloudflarePlaygroundExecutor(), // Alias for cloudflare-playground - "tinycms-web": new TinyCmsExecutor(), - tcw: new TinyCmsExecutor(), // Alias - hyperagent: new HyperAgentExecutor(), - ha: new HyperAgentExecutor(), // Alias - zmf: new ZenmuxFreeExecutor(), // Alias for zenmux-free - auggie: new AuggieExecutor(), - xai: new XaiExecutor(), - "xai-oauth": new XaiExecutor("xai-oauth"), - xao: new XaiExecutor("xai-oauth"), - qw: new QwenWebExecutor(), // Alias - "conol-web": new ConolWebExecutor(), - cnl: new ConolWebExecutor(), // Alias + "venice-web": () => import("./venice-web.ts").then((m) => new m.VeniceWebExecutor()), + ven: () => import("./venice-web.ts").then((m) => new m.VeniceWebExecutor()), // Alias + "notion-web": () => import("./notion-web.ts").then((m) => new m.NotionWebExecutor()), + nw: () => import("./notion-web.ts").then((m) => new m.NotionWebExecutor()), // Alias + promptql: () => import("./promptql.ts").then((m) => new m.PromptQlExecutor()), + pql: () => import("./promptql.ts").then((m) => new m.PromptQlExecutor()), // Alias + "v0-vercel-web": () => import("./v0-vercel-web.ts").then((m) => new m.V0VercelWebExecutor()), + v0: () => import("./v0-vercel-web.ts").then((m) => new m.V0VercelWebExecutor()), // Alias + "kimi-web": () => import("./kimi-web.ts").then((m) => new m.KimiWebExecutor()), + "kimi-coding-apikey": () => + import("./kimi.ts").then((m) => new m.KimiExecutor("kimi-coding-apikey")), // Legacy alias + "kimi-coding": () => import("./kimi.ts").then((m) => new m.KimiExecutor()), // Alias + moonshot: () => import("./moonshot.ts").then((m) => new m.MoonshotExecutor()), + kimi: () => import("./moonshot.ts").then((m) => new m.MoonshotExecutor("kimi")), // Hidden legacy Moonshot provider id + cheaperinference: () => + import("./cheaperinference.ts").then((m) => new m.CheaperInferenceExecutor()), + cinf: () => + import("./cheaperinference.ts").then( + (m) => new m.CheaperInferenceExecutor("cheaperinference") + ), // Alias + "doubao-web": () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()), + db: () => import("./doubao-web.ts").then((m) => new m.DoubaoWebExecutor()), // Alias + "qwen-web": () => import("./qwen-web.ts").then((m) => new m.QwenWebExecutor()), + raycast: () => import("./raycast.ts").then((m) => new m.RaycastExecutor()), + rc: () => import("./raycast.ts").then((m) => new m.RaycastExecutor()), // Alias + "hailuo-web": () => import("./hailuo-web.ts").then((m) => new m.HailuoWebExecutor()), + "zai-web": () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()), + zw: () => import("./zai-web.ts").then((m) => new m.ZaiWebExecutor()), // Alias + theoldllm: () => import("./theoldllm.ts").then((m) => new m.TheOldLlmExecutor()), + tllm: () => import("./theoldllm.ts").then((m) => new m.TheOldLlmExecutor()), // Alias + chipotle: () => import("./chipotle.ts").then((m) => new m.ChipotleExecutor()), + pepper: () => import("./chipotle.ts").then((m) => new m.ChipotleExecutor()), // Alias + lmarena: () => import("./lmarena.ts").then((m) => new m.LMArenaExecutor()), + lma: () => import("./lmarena.ts").then((m) => new m.LMArenaExecutor()), // Alias + "grok-cli": () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()), + gc: () => import("./grok-cli.ts").then((m) => new m.GrokCliExecutor()), // Alias + "codebuddy-cn": () => import("./codebuddy-cn.ts").then((m) => new m.CodeBuddyCnExecutor()), + cbcn: () => import("./codebuddy-cn.ts").then((m) => new m.CodeBuddyCnExecutor()), // Alias for codebuddy-cn + "zenmux-free": () => import("./zenmux-free.ts").then((m) => new m.ZenmuxFreeExecutor()), + "cloudflare-playground": () => + import("./cloudflare-playground.ts").then((m) => new m.CloudflarePlaygroundExecutor()), + cfp: () => + import("./cloudflare-playground.ts").then((m) => new m.CloudflarePlaygroundExecutor()), // Alias for cloudflare-playground + "tinycms-web": () => import("./tinycms.ts").then((m) => new m.TinyCmsExecutor()), + tcw: () => import("./tinycms.ts").then((m) => new m.TinyCmsExecutor()), // Alias + hyperagent: () => import("./hyperagent.ts").then((m) => new m.HyperAgentExecutor()), + ha: () => import("./hyperagent.ts").then((m) => new m.HyperAgentExecutor()), // Alias + zmf: () => import("./zenmux-free.ts").then((m) => new m.ZenmuxFreeExecutor()), // Alias for zenmux-free + auggie: () => import("./auggie.ts").then((m) => new m.AuggieExecutor()), + xai: () => import("./xai.ts").then((m) => new m.XaiExecutor()), + "xai-oauth": () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")), + xao: () => import("./xai.ts").then((m) => new m.XaiExecutor("xai-oauth")), + qw: () => import("./qwen-web.ts").then((m) => new m.QwenWebExecutor()), // Alias + "conol-web": () => import("./conol-web.ts").then((m) => new m.ConolWebExecutor()), + cnl: () => import("./conol-web.ts").then((m) => new m.ConolWebExecutor()), // Alias }; -// Bootstrap: register every built-in in the ExecutorRegistry. registerExecutor -// throws on duplicates, so an alias collision fails at module load, exactly as -// loudly as a duplicate object key would have failed at lint time. -for (const [alias, executor] of Object.entries(executors) as [string, BaseExecutor][]) { - registerExecutor(alias, executor); +// Bootstrap: declare every built-in alias in the ExecutorRegistry. Duplicate +// aliases fail at module load, exactly as loudly as a duplicate object key +// would have failed at lint time. Instances materialize on first use (#11220). +for (const [alias, load] of Object.entries(lazyExecutors)) { + registerLazyExecutor(alias, load); } const defaultCache = new Map(); @@ -267,9 +239,9 @@ const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]); // providers must be executed through /v1/search, never the chat-completions path. const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS)); -export function getExecutor(provider) { - const registered = getRegisteredExecutor(provider); - if (registered) return registered; +export async function getExecutor(provider: string): Promise { + const loaded = await loadRegisteredExecutor(provider); + if (loaded) return loaded; if (CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS.has(provider)) { const err = new Error( `Provider "${provider}" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.` @@ -285,77 +257,19 @@ export function getExecutor(provider) { throw err; } if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider)); - return defaultCache.get(provider); + return defaultCache.get(provider)!; } -export function hasSpecializedExecutor(provider) { +export function hasSpecializedExecutor(provider: string): boolean { return hasRegisteredExecutor(provider); } -export { registerExecutor, listExecutorAliases } from "./registry.ts"; - +export { + registerExecutor, + registerLazyExecutor, + listExecutorAliases, +} from "./registry.ts"; +// Value re-export: base.ts is already eager (DefaultExecutor extends it), and +// scripts/check/check-known-symbols.ts reads this export from the module. export { BaseExecutor } from "./base.ts"; -export { AntigravityExecutor } from "./antigravity.ts"; -export { GithubExecutor } from "./github.ts"; -export { QoderExecutor } from "./qoder.ts"; -export { KiroExecutor } from "./kiro.ts"; -export { CodexExecutor } from "./codex.ts"; -export { CursorExecutor } from "./cursor.ts"; -export { TraeExecutor } from "./trae.ts"; export { DefaultExecutor } from "./default.ts"; -export { BedrockExecutor } from "./bedrock.ts"; -export { GlmExecutor } from "./glm.ts"; -export { PollinationsExecutor } from "./pollinations.ts"; -export { CloudflareAIExecutor } from "./cloudflare-ai.ts"; -export { OpencodeExecutor } from "./opencode.ts"; -export { CliproxyapiExecutor } from "./cliproxyapi.ts"; -export { DarioExecutor } from "./dario.ts"; -export { NineRouterExecutor } from "./ninerouter.ts"; -export { VertexExecutor } from "./vertex.ts"; -export { PerplexityWebExecutor } from "./perplexity-web.ts"; -export { GrokWebExecutor } from "./grok-web.ts"; -export { GeminiWebExecutor } from "./gemini-web.ts"; -export { KieExecutor } from "./kie.ts"; -export { ChatGptWebExecutor } from "./chatgpt-web.ts"; -export { BlackboxWebExecutor } from "./blackbox-web.ts"; -export { MuseSparkWebExecutor } from "./muse-spark-web.ts"; -export { AzureOpenAIExecutor } from "./azure-openai.ts"; -export { AzureAiExecutor } from "./azure-ai.ts"; -export { CommandCodeExecutor } from "./commandCode.ts"; -export { GitlabExecutor } from "./gitlab.ts"; -export { NlpCloudExecutor } from "./nlpcloud.ts"; -export { DevinDesktopExecutor } from "./devin-desktop.ts"; -export { ZedHostedExecutor } from "./zed-hosted.ts"; -export { DevinCliExecutor } from "./devin-cli.ts"; -export { DevinCliAgenticExecutor } from "./devin-cli-agentic.ts"; -export { AuggieExecutor } from "./auggie.ts"; -export { CopilotWebExecutor } from "./copilot-web.ts"; -export { CopilotM365WebExecutor } from "./copilot-m365-web.ts"; -export { MicrosoftDesignerWebExecutor } from "./microsoft-designer-web.ts"; -export { AdobeFireflyExecutor } from "./adobe-firefly.ts"; -export { VeoAIFreeWebExecutor } from "./veoaifree-web.ts"; -export { DuckDuckGoWebExecutor } from "./duckduckgo-web.ts"; -export { FeloWebExecutor } from "./felo-web.ts"; -export { ClaudeWebExecutor } from "./claude-web.ts"; -export { DeepSeekWebExecutor } from "./deepseek-web.ts"; -export { DeepSeekWebWithAutoRefreshExecutor } from "./deepseek-web-with-auto-refresh.ts"; -export { AdaptaWebExecutor } from "./adapta-web.ts"; -export { YuanbaoWebExecutor } from "./yuanbao-web.ts"; -export { T3ChatWebExecutor } from "./t3-chat-web.ts"; -export { InnerAiExecutor } from "./inner-ai.ts"; -export { QwenWebExecutor } from "./qwen-web.ts"; -export { HailuoWebExecutor } from "./hailuo-web.ts"; -export { TheOldLlmExecutor } from "./theoldllm.ts"; -export { ChipotleExecutor } from "./chipotle.ts"; -export { LMArenaExecutor } from "./lmarena.ts"; -export { GrokCliExecutor } from "./grok-cli.ts"; -export { CodeBuddyCnExecutor } from "./codebuddy-cn.ts"; -export { ZenmuxFreeExecutor } from "./zenmux-free.ts"; -export { CloudflarePlaygroundExecutor } from "./cloudflare-playground.ts"; -export { TinyCmsExecutor } from "./tinycms.ts"; -export { HyperAgentExecutor } from "./hyperagent.ts"; -export { XaiExecutor } from "./xai.ts"; -export { MoonshotExecutor } from "./moonshot.ts"; -export { CheaperInferenceExecutor } from "./cheaperinference.ts"; -export { PromptQlExecutor } from "./promptql.ts"; -export { ConolWebExecutor } from "./conol-web.ts"; diff --git a/open-sse/executors/registry.ts b/open-sse/executors/registry.ts index 4d6a002ecd..ae61fd85ed 100644 --- a/open-sse/executors/registry.ts +++ b/open-sse/executors/registry.ts @@ -28,11 +28,46 @@ export function getRegisteredExecutor(alias: string): BaseExecutor | undefined { return registry.get(alias); } -export function hasRegisteredExecutor(alias: string): boolean { - return registry.has(alias); +// ── #11220: lazy registration ─────────────────────────────────────────────── +// Aliases may register a deferred loader instead of an instance. The alias and +// its registration ORDER are declared eagerly — hasRegisteredExecutor() and +// listExecutorAliases() stay synchronous and the golden snapshot keeps its +// shape — while the class import + construction happen on first use. A +// completed load caches into `registry`, so later resolution is identical to a +// static registration. +const lazyLoaders = new Map Promise>(); +const lazyInFlight = new Map>(); + +export function registerLazyExecutor(alias: string, load: () => Promise): void { + if (registry.has(alias) || lazyLoaders.has(alias)) { + throw new Error(`executor alias already registered: "${alias}"`); + } + lazyLoaders.set(alias, load); } -/** All registered aliases, in registration order. */ +export function loadRegisteredExecutor(alias: string): Promise | undefined { + const cached = registry.get(alias); + if (cached) return Promise.resolve(cached); + const load = lazyLoaders.get(alias); + if (!load) return undefined; + let inFlight = lazyInFlight.get(alias); + if (!inFlight) { + inFlight = load().then((executor) => { + registerExecutor(alias, executor); + lazyLoaders.delete(alias); + lazyInFlight.delete(alias); + return executor; + }); + lazyInFlight.set(alias, inFlight); + } + return inFlight; +} + +export function hasRegisteredExecutor(alias: string): boolean { + return registry.has(alias) || lazyLoaders.has(alias); +} + +/** All registered aliases — static and lazy — in registration order. */ export function listExecutorAliases(): string[] { - return [...registry.keys()]; + return [...registry.keys(), ...lazyLoaders.keys()]; } diff --git a/open-sse/handlers/chatCore/cliproxyModelMapping.ts b/open-sse/handlers/chatCore/cliproxyModelMapping.ts index 99914e2f01..af8e00112b 100644 --- a/open-sse/handlers/chatCore/cliproxyModelMapping.ts +++ b/open-sse/handlers/chatCore/cliproxyModelMapping.ts @@ -14,12 +14,12 @@ type ExecutorInput = { model: string; body: unknown; - [key: string]: unknown; }; +// No index signature: executors (BaseExecutor subclasses) must satisfy this +// structurally, and class instances don't carry index signatures. type ExecutorLike = { execute: (input: ExecutorInput) => Promise; - [key: string]: unknown; }; export type CliproxyapiModelMapping = Record | null | undefined; diff --git a/open-sse/handlers/chatCore/cliproxyapiCredentials.ts b/open-sse/handlers/chatCore/cliproxyapiCredentials.ts index 4789afb62c..e9fc5fbe7b 100644 --- a/open-sse/handlers/chatCore/cliproxyapiCredentials.ts +++ b/open-sse/handlers/chatCore/cliproxyapiCredentials.ts @@ -19,12 +19,12 @@ import type { ProviderCredentials } from "../../executors/base.ts"; type ExecutorInput = { credentials: ProviderCredentials; - [key: string]: unknown; }; +// No index signature: executors (BaseExecutor subclasses) must satisfy this +// structurally, and class instances don't carry index signatures. type ExecutorLike = { execute: (input: ExecutorInput) => Promise; - [key: string]: unknown; }; /** diff --git a/open-sse/handlers/chatCore/executorProxy.ts b/open-sse/handlers/chatCore/executorProxy.ts index 19dc55f4d0..bb0a3b0753 100644 --- a/open-sse/handlers/chatCore/executorProxy.ts +++ b/open-sse/handlers/chatCore/executorProxy.ts @@ -76,12 +76,15 @@ async function loadCliproxyapiSettings(): Promise<{ * dedicated-credential wrappers applied. Used by the direct `cliproxyapi` leg * and the CLIProxyAPI branch of `fallback`. */ -function resolveCliproxyapiExecutor( +async function resolveCliproxyapiExecutor( cliproxyapiModelMapping: Record | null, dedicatedApiKey: string | null ) { return wrapExecutorWithCliproxyapiCredentials( - wrapExecutorWithCliproxyapiModelMapping(getExecutor("cliproxyapi"), cliproxyapiModelMapping), + wrapExecutorWithCliproxyapiModelMapping( + await getExecutor("cliproxyapi"), + cliproxyapiModelMapping + ), dedicatedApiKey ); } @@ -138,7 +141,7 @@ export async function resolveExecutorWithProxy( // backend on specific failures. The backend defaults to CLIProxyAPI so every // pre-existing fallback config behaves exactly as before; fallbackBackend // === "dario" opts the retry leg over to Dario instead. - const nativeExec = getExecutor(prov); + const nativeExec = await getExecutor(prov); const fallbackBackend: FallbackBackend = cfg.fallbackBackend; const { fallbackCodes, dedicatedApiKey } = await loadCliproxyapiSettings(); @@ -146,8 +149,8 @@ export async function resolveExecutorWithProxy( // the native leg must keep seeing the original, unmapped model. const proxyExec = fallbackBackend === "dario" - ? getExecutor("dario") - : resolveCliproxyapiExecutor(cfg.cliproxyapiModelMapping, dedicatedApiKey); + ? await getExecutor("dario") + : await resolveCliproxyapiExecutor(cfg.cliproxyapiModelMapping, dedicatedApiKey); const backendLabel = fallbackBackend === "dario" ? "Dario" : "CLIProxyAPI"; const isRetryableStatus = (s: number) => fallbackCodes.includes(s) || s === 0; diff --git a/open-sse/handlers/videoGeneration.ts b/open-sse/handlers/videoGeneration.ts index 17cb1db0dc..e97b95995c 100644 --- a/open-sse/handlers/videoGeneration.ts +++ b/open-sse/handlers/videoGeneration.ts @@ -374,7 +374,7 @@ async function handleVertexVeoGeneration({ model, body, credentials, log }) { * Submits an AnimateDiff or SVD workflow, polls for completion, fetches output video */ async function handleVeoAiFreeVideoGeneration({ model, provider, body, credentials, log }) { - const executor = getExecutor(provider); + const executor = await getExecutor(provider); if (!executor) { return { success: false, status: 400, error: `Unknown video provider: ${provider}` }; } diff --git a/open-sse/services/compression/eval/executorModelClient.ts b/open-sse/services/compression/eval/executorModelClient.ts index dba6a65592..7847d99057 100644 --- a/open-sse/services/compression/eval/executorModelClient.ts +++ b/open-sse/services/compression/eval/executorModelClient.ts @@ -16,9 +16,10 @@ export function createExecutorModelClient( credentials: ProviderCredentials, costPerKTokenOut?: number ): ModelClient { - const executor = getExecutor(provider); return { async complete(model: string, messages: ChatTurn[]): Promise { + // #11220: getExecutor is async (lazy registry) — resolve per call. + const executor = await getExecutor(provider); const body = { model, messages, stream: false }; const input: ExecuteInput = { model, diff --git a/src/lib/compression/judgeModelClient.ts b/src/lib/compression/judgeModelClient.ts index b1f2f1d61e..022b761a0e 100644 --- a/src/lib/compression/judgeModelClient.ts +++ b/src/lib/compression/judgeModelClient.ts @@ -18,9 +18,10 @@ export function createPricedJudgeClient( provider: string, credentials: ProviderCredentials ): ModelClient { - const executor = getExecutor(provider); return { async complete(model: string, messages: ChatTurn[]): Promise { + // #11220: getExecutor is async (lazy registry) — resolve per call. + const executor = await getExecutor(provider); const input: ExecuteInput = { model, body: { model, messages, stream: false }, diff --git a/src/lib/providers/validation/anthropicFormat.ts b/src/lib/providers/validation/anthropicFormat.ts index 8ed7f84515..da2d567694 100644 --- a/src/lib/providers/validation/anthropicFormat.ts +++ b/src/lib/providers/validation/anthropicFormat.ts @@ -132,12 +132,13 @@ export async function validateClaudeOAuthInline({ modelId: string | null | undefined; providerSpecificData?: Record; }) { - const testModelId = - providerSpecificData?.validationModelId || modelId || "claude-haiku-4-5-20251001"; + const override = providerSpecificData?.validationModelId; + const testModelId: string = + typeof override === "string" && override ? override : modelId || "claude-haiku-4-5-20251001"; try { const { getExecutor } = await import("@omniroute/open-sse/executors/index.ts"); - const { response } = await getExecutor("claude").execute({ + const executed = await (await getExecutor("claude")).execute({ model: testModelId, body: { model: testModelId, @@ -148,6 +149,7 @@ export async function validateClaudeOAuthInline({ credentials: { accessToken: apiKey, providerSpecificData }, }); + const response = executed instanceof Response ? executed : executed.response; if (response.status === 401 || response.status === 403) { return { valid: false, error: "Invalid OAuth token" }; } diff --git a/src/lib/services/quotaAutoPing.ts b/src/lib/services/quotaAutoPing.ts index 94e5ea8247..4a9ce8fe3e 100644 --- a/src/lib/services/quotaAutoPing.ts +++ b/src/lib/services/quotaAutoPing.ts @@ -23,6 +23,7 @@ import { logger } from "@omniroute/open-sse/utils/logger.ts"; import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error.ts"; import { getExecutor } from "@omniroute/open-sse/executors/index.ts"; +import type { BaseExecutor } from "@omniroute/open-sse/executors/base"; import { getCodexUsage } from "@omniroute/open-sse/services/usage/codex.ts"; import { getSettings, getProviderConnections, updateProviderConnection } from "@/lib/localDb"; import { isConnectionUnavailableToAuxiliaryActivity } from "@/lib/exclusiveLeaseIsolation"; @@ -67,7 +68,7 @@ export interface QuotaAutoPingDeps { accessToken?: string, providerSpecificData?: JsonRecord ) => Promise; - getExecutor: (provider: string) => { execute: (input: JsonRecord) => Promise }; + getExecutor: (provider: string) => Promise; canExecuteProvider: (provider: string) => boolean; isConnectionUnavailableToAuxiliaryActivity: (connectionId: string) => Promise; } @@ -208,7 +209,7 @@ async function sendCodexPing( providerConfig: QuotaAutoPingProviderConfig, deps: QuotaAutoPingDeps ): Promise { - const executor = deps.getExecutor("codex"); + const executor = await deps.getExecutor("codex"); const result = await executor.execute({ model: providerConfig.pingModel, stream: true, diff --git a/tests/unit/adobe-firefly.test.ts b/tests/unit/adobe-firefly.test.ts index 200907ed3c..0f87a1817c 100644 --- a/tests/unit/adobe-firefly.test.ts +++ b/tests/unit/adobe-firefly.test.ts @@ -84,7 +84,7 @@ test("adobe-firefly is registered in VIDEO_PROVIDERS with adobe-firefly-video fo }); test("getExecutor(adobe-firefly) rejects chat completions", async () => { - const executor = getExecutor("adobe-firefly"); + const executor = await getExecutor("adobe-firefly"); assert.ok(executor); const result = await executor.execute({ model: "adobe-firefly/nano-banana-pro", @@ -95,9 +95,10 @@ test("getExecutor(adobe-firefly) rejects chat completions", async () => { stream: false, credentials: { apiKey: "tok" }, }); - assert.ok(result.response, "executor must return a Response wrapper"); - assert.equal(result.response.status, 400); - const bodyText = await result.response.text(); + const response = result instanceof Response ? result : result.response; + assert.ok(response, "executor must return a Response wrapper"); + assert.equal(response.status, 400); + const bodyText = await response.text(); assert.match(bodyText, /images\/generations|videos\/generations|media-generation/i); }); diff --git a/tests/unit/azure-param-rules.test.ts b/tests/unit/azure-param-rules.test.ts index 4e46b0788f..78292835f2 100644 --- a/tests/unit/azure-param-rules.test.ts +++ b/tests/unit/azure-param-rules.test.ts @@ -5,7 +5,8 @@ import { applyAzureParamRules, AZURE_COMPLETION_TOKEN_DEPLOYMENT, } from "../../open-sse/executors/azureParamRules.ts"; -import { getExecutor, AzureAiExecutor } from "../../open-sse/executors/index.ts"; +import { getExecutor } from "../../open-sse/executors/index.ts"; +import { AzureAiExecutor } from "../../open-sse/executors/azure-ai.ts"; /** * Regression guards for two Azure 400s observed against a live Azure AI Foundry @@ -87,8 +88,8 @@ test("the regex does not match unrelated names by accident", () => { assert.equal(AZURE_COMPLETION_TOKEN_DEPLOYMENT.test("Kimi-K2.7-Code"), false); }); -test("azure-ai resolves to AzureAiExecutor, not the bare DefaultExecutor", () => { - const executor = getExecutor("azure-ai"); +test("azure-ai resolves to AzureAiExecutor, not the bare DefaultExecutor", async () => { + const executor = await getExecutor("azure-ai"); assert.ok( executor instanceof AzureAiExecutor, "azure-ai must have its own executor so it inherits the Azure param rules" diff --git a/tests/unit/cloudflare-playground-provider.test.ts b/tests/unit/cloudflare-playground-provider.test.ts index f1e07a6b61..092389eb1f 100644 --- a/tests/unit/cloudflare-playground-provider.test.ts +++ b/tests/unit/cloudflare-playground-provider.test.ts @@ -167,9 +167,9 @@ test("cloudflare-playground registry entry has no-auth shape and curated models" assert.equal(llama?.supportsReasoning, undefined); }); -test("executor resolves for both the id and the cfp alias", () => { - const byId = getExecutor("cloudflare-playground"); - const byAlias = getExecutor("cfp"); +test("executor resolves for both the id and the cfp alias", async () => { + const byId = await getExecutor("cloudflare-playground"); + const byAlias = await getExecutor("cfp"); assert.ok(byId instanceof CloudflarePlaygroundExecutor); assert.ok(byAlias instanceof CloudflarePlaygroundExecutor); }); diff --git a/tests/unit/codebuddy-cn-provider.test.ts b/tests/unit/codebuddy-cn-provider.test.ts index 129910a73b..0d08c293cb 100644 --- a/tests/unit/codebuddy-cn-provider.test.ts +++ b/tests/unit/codebuddy-cn-provider.test.ts @@ -185,10 +185,10 @@ test("codebuddy-cn vision flag is set on the visual models", () => { } }); -test("getExecutor returns the CodeBuddyCnExecutor for 'codebuddy-cn' and the 'cbcn' alias", () => { - const e = getExecutor("codebuddy-cn"); +test("getExecutor returns the CodeBuddyCnExecutor for 'codebuddy-cn' and the 'cbcn' alias", async () => { + const e = await getExecutor("codebuddy-cn"); assert.ok(e instanceof CodeBuddyCnExecutor, "executor must be CodeBuddyCnExecutor"); - const aliasExec = getExecutor("cbcn"); + const aliasExec = await getExecutor("cbcn"); assert.ok(aliasExec instanceof CodeBuddyCnExecutor, "alias 'cbcn' must resolve to same executor"); }); diff --git a/tests/unit/cursor-apikey-provider.test.ts b/tests/unit/cursor-apikey-provider.test.ts index da803448e3..890d044f2d 100644 --- a/tests/unit/cursor-apikey-provider.test.ts +++ b/tests/unit/cursor-apikey-provider.test.ts @@ -15,8 +15,9 @@ const { cursorProvider, cursor_apiProvider } = await import("../../open-sse/config/providers/registry/cursor/index.ts"); const { REGISTRY, generateAliasMap, getProviderCategory } = await import("../../open-sse/config/providerRegistry.ts"); -const { CursorExecutor, getExecutor, hasSpecializedExecutor } = +const { getExecutor, hasSpecializedExecutor } = await import("../../open-sse/executors/index.ts"); +const { CursorExecutor } = await import("../../open-sse/executors/cursor.ts"); const { __resetCursorApiKeyAuthForTest } = await import("../../open-sse/services/cursorApiKeyAuth.ts"); const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); @@ -54,14 +55,14 @@ describe("cursor-api provider wiring", () => { assert.equal(isManagedProviderConnectionId("cursor-api"), true); }); - it("routes cursor-api and its alias to a CursorExecutor bound to the cursor-api id", () => { + it("routes cursor-api and its alias to a CursorExecutor bound to the cursor-api id", async () => { for (const key of ["cursor-api", "cua"]) { assert.equal(hasSpecializedExecutor(key), true, key); - const executor = getExecutor(key); + const executor = await getExecutor(key); assert.ok(executor instanceof CursorExecutor, key); assert.equal(executor.getProvider(), "cursor-api"); } - assert.equal(getExecutor("cursor").getProvider(), "cursor"); + assert.equal((await getExecutor("cursor")).getProvider(), "cursor"); }); }); @@ -154,6 +155,9 @@ describe("CursorExecutor credential resolution", () => { body: { messages: [] }, stream: false, credentials: { apiKey: API_KEY, connectionId: "cursor-api-test" }, + signal: null, + log: null, + upstreamExtraHeaders: null, }); assert.equal(result.response.status, 500); diff --git a/tests/unit/devin-providers.test.ts b/tests/unit/devin-providers.test.ts index 0009246c50..b0cd44b24a 100644 --- a/tests/unit/devin-providers.test.ts +++ b/tests/unit/devin-providers.test.ts @@ -4,6 +4,7 @@ import { readFile } from "node:fs/promises"; import { REGISTRY } from "../../open-sse/config/providers/index.ts"; import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; +import { DevinDesktopExecutor } from "../../open-sse/executors/devin-desktop.ts"; import { OAUTH_PROVIDERS } from "../../src/shared/constants/providers/oauth.ts"; test("Devin Desktop exposes the supported BYOK-free catalog", () => { @@ -20,26 +21,29 @@ test("public registries do not expose windsurf or ws aliases", () => { assert.ok(Object.values(REGISTRY).every((entry) => entry.alias !== "ws")); }); -test("executor factory exposes only the dedicated Devin Desktop executor", () => { +test("executor factory exposes only the dedicated Devin Desktop executor", async () => { assert.equal(hasSpecializedExecutor("devin-desktop"), true); assert.equal(hasSpecializedExecutor("windsurf"), false); assert.equal(hasSpecializedExecutor("ws"), false); - assert.equal(getExecutor("devin-desktop").constructor.name, "DevinDesktopExecutor"); + assert.equal((await getExecutor("devin-desktop")).constructor.name, "DevinDesktopExecutor"); }); -test("Devin Desktop executor uses the live endpoint and verified default identity", () => { - const executor = getExecutor("devin-desktop"); +test("Devin Desktop executor uses the live endpoint and verified default identity", async () => { + const executor = await getExecutor("devin-desktop"); delete process.env.DEVIN_DESKTOP_VERSION; + // getExecutor() widens to BaseExecutor whose buildUrl requires args; the concrete + // DevinDesktopExecutor override takes none. + const desktop = executor as DevinDesktopExecutor; assert.equal( - executor.buildUrl(), + desktop.buildUrl(), "https://server.codeium.com/exa.api_server_pb.ApiServerService/GetChatMessage" ); assert.equal(executor.buildHeaders({ accessToken: "token" })["User-Agent"], "windsurf/3.6.27"); }); -test("Devin Desktop executor applies only valid version overrides to its user agent", () => { - const executor = getExecutor("devin-desktop"); +test("Devin Desktop executor applies only valid version overrides to its user agent", async () => { + const executor = await getExecutor("devin-desktop"); process.env.DEVIN_DESKTOP_VERSION = "3.5.1"; try { assert.equal(executor.buildHeaders({ accessToken: "token" })["User-Agent"], "windsurf/3.5.1"); @@ -51,7 +55,7 @@ test("Devin Desktop executor applies only valid version overrides to its user ag }); test("Devin Desktop executor returns 401 before the upstream call without a token", async () => { - const executor = getExecutor("devin-desktop"); + const executor = await getExecutor("devin-desktop"); const originalFetch = globalThis.fetch; let fetchCalled = false; globalThis.fetch = async () => { @@ -67,16 +71,17 @@ test("Devin Desktop executor returns 401 before the upstream call without a toke credentials: {}, }); + const response = result instanceof Response ? result : result.response; assert.equal(fetchCalled, false); - assert.equal(result.response.status, 401); - assert.match(await result.response.text(), /Devin Desktop API key is required/); + assert.equal(response.status, 401); + assert.match(await response.text(), /Devin Desktop API key is required/); } finally { globalThis.fetch = originalFetch; } }); test("Devin Desktop stream errors do not expose local paths or stack traces", async () => { - const executor = getExecutor("devin-desktop"); + const executor = await getExecutor("devin-desktop"); const originalFetch = globalThis.fetch; globalThis.fetch = async () => new Response( @@ -95,7 +100,7 @@ test("Devin Desktop stream errors do not expose local paths or stack traces", as stream: true, credentials: { accessToken: "test-token" }, }); - const text = await result.response.text(); + const text = await (result instanceof Response ? result : result.response).text(); assert.match(text, /stream failed/); assert.doesNotMatch(text, /private\.ts|\/Users\/example|\bat\s+\//); diff --git a/tests/unit/executor-agy.test.ts b/tests/unit/executor-agy.test.ts index 008b6694fa..5dc983b527 100644 --- a/tests/unit/executor-agy.test.ts +++ b/tests/unit/executor-agy.test.ts @@ -1,8 +1,11 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { getExecutor, AntigravityExecutor } from "../../open-sse/executors/index.ts"; -import { processAntigravitySSEPayload } from "../../open-sse/executors/antigravity.ts"; +import { getExecutor } from "../../open-sse/executors/index.ts"; +import { + AntigravityExecutor, + processAntigravitySSEPayload, +} from "../../open-sse/executors/antigravity.ts"; function emptyCollected(): any { return { @@ -14,21 +17,21 @@ function emptyCollected(): any { }; } -test("getExecutor('agy') returns AntigravityExecutor (not DefaultExecutor)", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') returns AntigravityExecutor (not DefaultExecutor)", async () => { + const executor = await getExecutor("agy"); assert.ok(executor instanceof AntigravityExecutor, "agy provider should use AntigravityExecutor"); }); -test("getExecutor('antigravity') returns AntigravityExecutor", () => { - const executor = getExecutor("antigravity"); +test("getExecutor('antigravity') returns AntigravityExecutor", async () => { + const executor = await getExecutor("antigravity"); assert.ok( executor instanceof AntigravityExecutor, "antigravity provider should use AntigravityExecutor" ); }); -test("getExecutor('agy') builds valid streaming URL", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') builds valid streaming URL", async () => { + const executor = await getExecutor("agy"); const url = executor.buildUrl("gemini-3.7-flash-high", true); assert.ok( url.includes("streamGenerateContent?alt=sse"), @@ -36,8 +39,8 @@ test("getExecutor('agy') builds valid streaming URL", () => { ); }); -test("getExecutor('agy') builds valid non-streaming URL", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') builds valid non-streaming URL", async () => { + const executor = await getExecutor("agy"); const url = executor.buildUrl("gemini-3.7-flash-high", false); // Antigravity executor always uses streaming endpoint (buildUrl ignores stream flag) assert.ok( @@ -46,8 +49,8 @@ test("getExecutor('agy') builds valid non-streaming URL", () => { ); }); -test("getExecutor('agy') buildHeaders returns Bearer auth", () => { - const executor = getExecutor("agy"); +test("getExecutor('agy') buildHeaders returns Bearer auth", async () => { + const executor = await getExecutor("agy"); const headers = executor.buildHeaders({ accessToken: "test-token" }); assert.equal(headers.Authorization, "Bearer test-token"); }); diff --git a/tests/unit/executor-gitlab.test.ts b/tests/unit/executor-gitlab.test.ts index f222eeb681..8bc41efb1f 100644 --- a/tests/unit/executor-gitlab.test.ts +++ b/tests/unit/executor-gitlab.test.ts @@ -19,11 +19,11 @@ function jsonResponse(body: unknown, status = 200) { }); } -test("GitlabExecutor is registered in the executor index", () => { +test("GitlabExecutor is registered in the executor index", async () => { assert.equal(hasSpecializedExecutor("gitlab"), true); - assert.ok(getExecutor("gitlab") instanceof GitlabExecutor); + assert.ok((await getExecutor("gitlab")) instanceof GitlabExecutor); assert.equal(hasSpecializedExecutor("gitlab-duo"), true); - assert.ok(getExecutor("gitlab-duo") instanceof GitlabExecutor); + assert.ok((await getExecutor("gitlab-duo")) instanceof GitlabExecutor); }); test("GitlabExecutor posts PAT-backed code suggestion requests to the configured instance", async () => { @@ -147,7 +147,7 @@ test("GitlabExecutor maps upstream auth failures to OpenAI-style errors", async }); test("GitlabExecutor uses GitLab direct_access for gitlab-duo and persists the cache", async () => { - const executor = getExecutor("gitlab-duo") as GitlabExecutor; + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; const originalFetch = globalThis.fetch; const calls: Array<{ url: string; headers: Record }> = []; const refreshedPatches: Array> = []; @@ -223,7 +223,7 @@ test("GitlabExecutor uses GitLab direct_access for gitlab-duo and persists the c }); test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access is disabled", async () => { - const executor = getExecutor("gitlab-duo") as GitlabExecutor; + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; const originalFetch = globalThis.fetch; const calls: string[] = []; @@ -274,7 +274,7 @@ test("GitlabExecutor falls back to the public Code Suggestions endpoint when dir // Code Suggestions completions endpoint (same resilience as the 403-disabled case // above), instead of surfacing an opaque 401 token error with no fallback. test("GitlabExecutor falls back to the public Code Suggestions endpoint when direct_access returns 401", async () => { - const executor = getExecutor("gitlab-duo") as GitlabExecutor; + const executor = (await getExecutor("gitlab-duo")) as GitlabExecutor; const originalFetch = globalThis.fetch; const calls: string[] = []; diff --git a/tests/unit/executor-kimi.test.ts b/tests/unit/executor-kimi.test.ts index 848d181865..00a95b1b0d 100644 --- a/tests/unit/executor-kimi.test.ts +++ b/tests/unit/executor-kimi.test.ts @@ -47,10 +47,10 @@ function credentials( } describe("KimiExecutor", () => { - it("forces the primary Kimi upstream to stream while preserving JSON client semantics", () => { + it("forces the primary Kimi upstream to stream while preserving JSON client semantics", async () => { assert.equal(REGISTRY.kimi?.forceStream, true); - const executor = getExecutor("kimi"); + const executor = await getExecutor("kimi"); assert.ok(executor instanceof MoonshotExecutor); assert.equal( executor.buildUrl("kimi-k2.5", true, 0, credentials(FORMATS.OPENAI)), diff --git a/tests/unit/executor-map-golden.test.ts b/tests/unit/executor-map-golden.test.ts index ea83e56756..e589755afc 100644 --- a/tests/unit/executor-map-golden.test.ts +++ b/tests/unit/executor-map-golden.test.ts @@ -37,11 +37,11 @@ function readSpecializedKeys(): string[] { path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../open-sse/executors/index.ts"), "utf8" ); - const mapMatch = src.match(/const executors = \{([\s\S]*?)\n\};/); - assert.ok(mapMatch, "executors map literal not found in open-sse/executors/index.ts"); + const mapMatch = src.match(/const lazyExecutors[^\n]*= \{([\s\S]*?)\n\};/); + assert.ok(mapMatch, "lazyExecutors map literal not found in open-sse/executors/index.ts"); const keys: string[] = []; for (const line of mapMatch[1].split("\n")) { - const m = line.match(/^\s*(?:"([^"]+)"|([A-Za-z0-9_$-]+)):\s*new /); + const m = line.match(/^\s*(?:"([^"]+)"|([A-Za-z0-9_$-]+)):\s*(?:async )?\(\)\s*=>/); if (m) keys.push(m[1] ?? m[2]); } return keys; @@ -74,7 +74,7 @@ function describeExecutor(instance: unknown): { const specializedKeys = readSpecializedKeys(); -test("golden: specialized executor map — key → class + provider identity + config source", () => { +test("golden: specialized executor map — key → class + provider identity + config source", async () => { assert.ok(specializedKeys.length >= 100, `suspiciously few keys: ${specializedKeys.length}`); const entries: Record< @@ -85,7 +85,7 @@ test("golden: specialized executor map — key → class + provider identity + c for (const key of [...specializedKeys].sort()) { assert.equal(hasSpecializedExecutor(key), true, `hasSpecializedExecutor(${key})`); - const instance = getExecutor(key); + const instance = await getExecutor(key); entries[key] = describeExecutor(instance); const group = byInstance.get(instance) ?? []; group.push(key); @@ -106,18 +106,18 @@ test("golden: specialized executor map — key → class + provider identity + c }); }); -test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", () => { +test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", async () => { // 1. Unknown provider → DefaultExecutor for that provider, memoized. const unknown = "golden-test-unknown-provider"; assert.equal(hasSpecializedExecutor(unknown), false); - const fallback = getExecutor(unknown); + const fallback = await getExecutor(unknown); assert.ok(fallback instanceof DefaultExecutor, "fallback must be DefaultExecutor"); - assert.equal(getExecutor(unknown), fallback, "DefaultExecutor fallback must be cached"); + assert.equal(await getExecutor(unknown), fallback, "DefaultExecutor fallback must be cached"); // 2. Cloud-agent guard (#6699) and search guard (#10274) → status-400 throw. - const guardOutcome = (provider: string) => { + const guardOutcome = async (provider: string) => { try { - getExecutor(provider); + await getExecutor(provider); return { throws: false as const }; } catch (err) { const e = err as Error & { status?: number }; @@ -128,7 +128,9 @@ test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", () const searchProviders = Object.keys(SEARCH_PROVIDERS).sort(); goldenSnapshot("executors/dispatch-rules", { fallback: describeExecutor(fallback), - cloudAgentGuard: { jules: guardOutcome("jules") }, - searchGuard: Object.fromEntries(searchProviders.map((p) => [p, guardOutcome(p)])), + cloudAgentGuard: { jules: await guardOutcome("jules") }, + searchGuard: Object.fromEntries( + await Promise.all(searchProviders.map(async (p) => [p, await guardOutcome(p)])) + ), }); }); diff --git a/tests/unit/executor-nlpcloud.test.ts b/tests/unit/executor-nlpcloud.test.ts index 5dc6791483..c131bcdd8e 100644 --- a/tests/unit/executor-nlpcloud.test.ts +++ b/tests/unit/executor-nlpcloud.test.ts @@ -30,9 +30,9 @@ function sseResponse(events: string[]) { ); } -test("NlpCloudExecutor is registered in the executor index", () => { +test("NlpCloudExecutor is registered in the executor index", async () => { assert.equal(hasSpecializedExecutor("nlpcloud"), true); - assert.ok(getExecutor("nlpcloud") instanceof NlpCloudExecutor); + assert.ok((await getExecutor("nlpcloud")) instanceof NlpCloudExecutor); }); test.skip("NlpCloudExecutor converts OpenAI messages into chatbot input/context/history and wraps JSON responses", async () => { diff --git a/tests/unit/executor-web-cookie-sweep.test.ts b/tests/unit/executor-web-cookie-sweep.test.ts index 9392247ab8..1f67c92fca 100644 --- a/tests/unit/executor-web-cookie-sweep.test.ts +++ b/tests/unit/executor-web-cookie-sweep.test.ts @@ -119,7 +119,7 @@ describe("web-cookie + noauth executor wrapper contract sweep", () => { describe("WEB_COOKIE_PROVIDERS (26)", () => { for (const providerId of WEB_COOKIE_IDS) { it(`${providerId} executor returns wrapper shape`, async () => { - const executor = getExecutor(providerId); + const executor = await getExecutor(providerId); assert.ok(executor, `[${providerId}] getExecutor must return an executor`); const result = await executor.execute({ @@ -160,7 +160,7 @@ describe("web-cookie + noauth executor wrapper contract sweep", () => { for (const providerId of TARGETS) { it(`${providerId} noauth executor returns wrapper shape`, async () => { - const executor = getExecutor(providerId); + const executor = await getExecutor(providerId); assert.ok(executor, `[${providerId}] getExecutor must return an executor`); // Use a pre-aborted signal so the executor short-circuits via diff --git a/tests/unit/executor-xai.test.ts b/tests/unit/executor-xai.test.ts index 4668efd22e..991d606454 100644 --- a/tests/unit/executor-xai.test.ts +++ b/tests/unit/executor-xai.test.ts @@ -15,9 +15,9 @@ import { xaiProvider } from "../../open-sse/config/providers/registry/xai/index. const credentials = { apiKey: "test-key" }; -test("XaiExecutor is registered under the 'xai' key and set as the registry executor", () => { +test("XaiExecutor is registered under the 'xai' key and set as the registry executor", async () => { assert.equal(hasSpecializedExecutor("xai"), true); - assert.ok(getExecutor("xai") instanceof XaiExecutor); + assert.ok((await getExecutor("xai")) instanceof XaiExecutor); assert.equal(xaiProvider.executor, "xai"); }); diff --git a/tests/unit/free-tier-providers-wave4-a.test.ts b/tests/unit/free-tier-providers-wave4-a.test.ts index 277b9b2778..cf89f1cfc9 100644 --- a/tests/unit/free-tier-providers-wave4-a.test.ts +++ b/tests/unit/free-tier-providers-wave4-a.test.ts @@ -21,7 +21,7 @@ test("FreeInference exposes an OpenAI-compatible Bearer registry", () => { assert.equal(freeinferenceProvider.passthroughModels, true); }); -test("FreeInference uses DefaultExecutor without specialized behavior", () => { +test("FreeInference uses DefaultExecutor without specialized behavior", async () => { assert.equal(hasSpecializedExecutor("freeinference"), false); - assert.ok(getExecutor("freeinference") instanceof DefaultExecutor); + assert.ok(await getExecutor("freeinference") instanceof DefaultExecutor); }); diff --git a/tests/unit/free-tier-providers-wave4-b.test.ts b/tests/unit/free-tier-providers-wave4-b.test.ts index a547e150a0..63e72a35c5 100644 --- a/tests/unit/free-tier-providers-wave4-b.test.ts +++ b/tests/unit/free-tier-providers-wave4-b.test.ts @@ -21,7 +21,7 @@ test("Free.ai exposes its exact OpenAI-compatible endpoint and live catalog", () assert.equal(freeAiProvider.passthroughModels, true); }); -test("Free.ai uses DefaultExecutor without a specialized executor", () => { - assert.ok(getExecutor("free-ai") instanceof DefaultExecutor); +test("Free.ai uses DefaultExecutor without a specialized executor", async () => { + assert.ok(await getExecutor("free-ai") instanceof DefaultExecutor); assert.equal(hasSpecializedExecutor("free-ai"), false); }); diff --git a/tests/unit/glm-executor.test.ts b/tests/unit/glm-executor.test.ts index 3d47b375c3..4cb31e90f6 100644 --- a/tests/unit/glm-executor.test.ts +++ b/tests/unit/glm-executor.test.ts @@ -129,10 +129,10 @@ test("GlmExecutor normalizes GLM coding and Anthropic URLs without duplicating e ); }); -test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic headers", () => { - assert.equal(getExecutor("glm") instanceof GlmExecutor, true); - assert.equal(getExecutor("glm-cn") instanceof GlmExecutor, true); - assert.equal(getExecutor("glmt") instanceof GlmExecutor, true); +test("GlmExecutor separates OpenAI-compatible coding headers from Anthropic headers", async () => { + assert.equal(await getExecutor("glm") instanceof GlmExecutor, true); + assert.equal(await getExecutor("glm-cn") instanceof GlmExecutor, true); + assert.equal(await getExecutor("glmt") instanceof GlmExecutor, true); const executor = new GlmExecutor("glm"); const codingHeaders = executor.buildHeaders( diff --git a/tests/unit/kie-executor-routing.test.ts b/tests/unit/kie-executor-routing.test.ts index 762b34d9a3..35bc8039d5 100644 --- a/tests/unit/kie-executor-routing.test.ts +++ b/tests/unit/kie-executor-routing.test.ts @@ -5,8 +5,8 @@ import { DefaultExecutor } from "../../open-sse/executors/default.ts"; import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; import { KieExecutor } from "../../open-sse/executors/kie.ts"; -test("KIE chat traffic uses the default executor while media keeps its task executor", () => { +test("KIE chat traffic uses the default executor while media keeps its task executor", async () => { assert.equal(hasSpecializedExecutor("kie"), false); - assert.ok(getExecutor("kie") instanceof DefaultExecutor); + assert.ok(await getExecutor("kie") instanceof DefaultExecutor); assert.equal(typeof KieExecutor, "function"); }); diff --git a/tests/unit/moonshot-k3.test.ts b/tests/unit/moonshot-k3.test.ts index ec77244240..ae5b2bb7a3 100644 --- a/tests/unit/moonshot-k3.test.ts +++ b/tests/unit/moonshot-k3.test.ts @@ -4,11 +4,8 @@ import assert from "node:assert/strict"; import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts"; import { supportsXHighEffort } from "../../open-sse/config/providerModels.ts"; import { sanitizeReasoningEffortForProvider } from "../../open-sse/executors/base.ts"; -import { - getExecutor, - hasSpecializedExecutor, - MoonshotExecutor, -} from "../../open-sse/executors/index.ts"; +import { getExecutor, hasSpecializedExecutor } from "../../open-sse/executors/index.ts"; +import { MoonshotExecutor } from "../../open-sse/executors/moonshot.ts"; import { sanitizeOpenAIResponse, sanitizeResponsesApiResponse, @@ -63,11 +60,11 @@ test("Kimi K3 advertises its 1M context/output and native capabilities", () => { assert.equal(capabilities.interleavedField, "reasoning_content"); }); -test("Moonshot ids use the specialized request normalizer", () => { +test("Moonshot ids use the specialized request normalizer", async () => { assert.equal(hasSpecializedExecutor("moonshot"), true); assert.equal(hasSpecializedExecutor("kimi"), true); - assert.ok(getExecutor("moonshot") instanceof MoonshotExecutor); - assert.ok(getExecutor("kimi") instanceof MoonshotExecutor); + assert.ok((await getExecutor("moonshot")) instanceof MoonshotExecutor); + assert.ok((await getExecutor("kimi")) instanceof MoonshotExecutor); }); test("Kimi K3 uses max reasoning, fixed sampling, and max_completion_tokens", () => { diff --git a/tests/unit/openference-oauth-provider.test.ts b/tests/unit/openference-oauth-provider.test.ts index 0e7add513b..983d7e7e61 100644 --- a/tests/unit/openference-oauth-provider.test.ts +++ b/tests/unit/openference-oauth-provider.test.ts @@ -107,8 +107,7 @@ test("Openference OAuth postExchange fetches userinfo when id_token lacks email" assert.equal(mapped.email, "from-userinfo@openference.com"); assert.equal(mapped.name, "Userinfo Name"); }); - -test("Openference is registered as an OAuth gateway with default executor", () => { +test("Openference is registered as an OAuth gateway with default executor", async () => { assert.ok(OAUTH_PROVIDERS.openference); assert.equal(OAUTH_PROVIDERS.openference.alias, "of"); assert.equal(OAUTH_PROVIDERS.openference.color, "#6366F1"); @@ -129,7 +128,7 @@ test("Openference is registered as an OAuth gateway with default executor", () = ); assert.equal(hasSpecializedExecutor("openference"), false); - const headers = getExecutor("openference").buildHeaders({ accessToken: "oauth-access" }, false); + const headers = (await getExecutor("openference")).buildHeaders({ accessToken: "oauth-access" }, false); assert.equal(headers.Authorization, "Bearer oauth-access"); }); diff --git a/tests/unit/probe-6699-jules-executor-misroute.test.ts b/tests/unit/probe-6699-jules-executor-misroute.test.ts index a2072814f4..290a8301b2 100644 --- a/tests/unit/probe-6699-jules-executor-misroute.test.ts +++ b/tests/unit/probe-6699-jules-executor-misroute.test.ts @@ -16,7 +16,7 @@ test("#6699: jules has no specialized executor (falls through to DefaultExecutor assert.equal(hasSpecializedExecutor("jules"), false); }); -test("#6699: a chat-completion request routed to provider 'jules' must not silently hit OpenAI's endpoint", () => { +test("#6699: a chat-completion request routed to provider 'jules' must not silently hit OpenAI's endpoint", async () => { // Desired behavior: the Jules provider (a cloud-agent, registered only in // CLOUD_AGENT_PROVIDERS/staticModels, never in the chat REGISTRY) must not silently // resolve to OpenAI's chat/completions endpoint when routed through the normal @@ -27,9 +27,9 @@ test("#6699: a chat-completion request routed to provider 'jules' must not silen // genuine Jules key). Before the fix, getExecutor("jules") returned a working // executor whose buildUrl() resolved to OpenAI's endpoint -- this assertion FAILS on // unfixed release/v3.8.49 code because no error is thrown at all. - assert.throws( - () => getExecutor("jules"), - (err) => { + await assert.rejects( + getExecutor("jules"), + (err: Error & { status?: number }) => { assert.match(err.message, /cloud-agent provider/i); assert.match(err.message, /does not support direct chat completions/i); assert.equal(err.status, 400); diff --git a/tests/unit/provider-tinycms-web.test.ts b/tests/unit/provider-tinycms-web.test.ts index 7fea970dee..e986a25c71 100644 --- a/tests/unit/provider-tinycms-web.test.ts +++ b/tests/unit/provider-tinycms-web.test.ts @@ -16,7 +16,8 @@ import assert from "node:assert/strict"; import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts"; import { REGISTRY } from "../../open-sse/config/providers/index.ts"; -import { getExecutor, TinyCmsExecutor } from "../../open-sse/executors/index.ts"; +import { getExecutor } from "../../open-sse/executors/index.ts"; +import { TinyCmsExecutor } from "../../open-sse/executors/tinycms.ts"; import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts"; // tinycmsSigner.ts intentionally does NOT install its window/document/canvas @@ -123,13 +124,13 @@ test("supportsReasoning is set on gpt-5.3-thinking-free", () => { // ── Executor ────────────────────────────────────────────────────────────────── -test("getExecutor returns TinyCmsExecutor for 'tinycms-web'", () => { - const e = getExecutor("tinycms-web"); +test("getExecutor returns TinyCmsExecutor for 'tinycms-web'", async () => { + const e = await getExecutor("tinycms-web"); assert.ok(e instanceof TinyCmsExecutor, "executor must be TinyCmsExecutor"); }); -test("getExecutor returns TinyCmsExecutor for 'tcw' alias", () => { - const e = getExecutor("tcw"); +test("getExecutor returns TinyCmsExecutor for 'tcw' alias", async () => { + const e = await getExecutor("tcw"); assert.ok(e instanceof TinyCmsExecutor, "alias 'tcw' must resolve to TinyCmsExecutor"); }); @@ -150,7 +151,7 @@ test("TinyCmsExecutor returns 401 when UUID is missing", async () => { credentials: {}, signal: AbortSignal.timeout(5000), }); - assert.ok(result.response, "response must be present"); + assert.ok("response" in result, "response must be present"); assert.equal(result.response.status, 401); const body = await result.response.json(); const errMsg = body?.error?.message || ""; @@ -168,7 +169,7 @@ test("TinyCmsExecutor returns 401 when UUID does not start with 'R'", async () = credentials: { apiKey: "abc123" }, // does not start with 'R' signal: AbortSignal.timeout(5000), }); - assert.ok(result.response, "response must be present"); + assert.ok("response" in result, "response must be present"); assert.equal(result.response.status, 401); const body = await result.response.json(); const errMsg = body?.error?.message || ""; @@ -256,7 +257,7 @@ test("TinyCmsExecutor sanitizes errors (no stack traces in error response)", asy signal: AbortSignal.timeout(5000), }); - assert.ok(result.response, "response must be present"); + assert.ok("response" in result, "response must be present"); const body = await result.response.json(); const errMsg = body?.error?.message || ""; assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID"); diff --git a/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts b/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts index 7f31ef4b83..26a8779ae9 100644 --- a/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts +++ b/tests/unit/repro-9550-amazon-q-alias-resolution.test.ts @@ -27,8 +27,8 @@ describe("Issue #9550 - amazon-q alias resolution", () => { assert.equal(parsed.model, "amazon-q"); }); - it('getExecutor("amazon-q") should exist and be a KiroExecutor', () => { - const executor = getExecutor("amazon-q"); + it('getExecutor("amazon-q") should exist and be a KiroExecutor', async () => { + const executor = await getExecutor("amazon-q"); assert.ok(executor, "getExecutor('amazon-q') should return an executor"); assert.equal( executor.constructor.name, diff --git a/tests/unit/search-providers-chat-guard.test.ts b/tests/unit/search-providers-chat-guard.test.ts index ac04fd5650..f0785355b9 100644 --- a/tests/unit/search-providers-chat-guard.test.ts +++ b/tests/unit/search-providers-chat-guard.test.ts @@ -29,7 +29,7 @@ test("#10274: no search provider has a specialized chat executor", () => { } }); -test("#10274: a chat-completion request routed to a search provider must not silently hit OpenAI's endpoint", () => { +test("#10274: a chat-completion request routed to a search provider must not silently hit OpenAI's endpoint", async () => { // Desired behavior: search providers (registered only in SEARCH_PROVIDERS, never in the // chat REGISTRY) must not silently resolve to OpenAI's chat/completions endpoint when // routed through the normal chat-completions executor path. getExecutor() must throw a @@ -40,9 +40,9 @@ test("#10274: a chat-completion request routed to a search provider must not sil // OpenAI's endpoint -- this assertion FAILS on unfixed release/v3.8.50 code because no // error is thrown at all. for (const id of SEARCH_PROVIDER_IDS) { - assert.throws( - () => getExecutor(id), - (err) => { + await assert.rejects( + getExecutor(id), + (err: Error & { status?: number }) => { assert.match(err.message, /search provider/i); assert.match(err.message, /does not support chat completions/i); assert.match(err.message, /\/v1\/search/i); diff --git a/tests/unit/tinycms-secure-nonce-randomness.test.ts b/tests/unit/tinycms-secure-nonce-randomness.test.ts index ebc30cb13a..b731a2e676 100644 --- a/tests/unit/tinycms-secure-nonce-randomness.test.ts +++ b/tests/unit/tinycms-secure-nonce-randomness.test.ts @@ -16,7 +16,7 @@ import test, { before, after } from "node:test"; import assert from "node:assert/strict"; -import { TinyCmsExecutor } from "../../open-sse/executors/index.ts"; +import { TinyCmsExecutor } from "../../open-sse/executors/tinycms.ts"; import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts"; let restoreDomMocks: DomMockRestore; diff --git a/tests/unit/windsurf-devin-executors.test.ts b/tests/unit/windsurf-devin-executors.test.ts index fd27062aa8..123d446112 100644 --- a/tests/unit/windsurf-devin-executors.test.ts +++ b/tests/unit/windsurf-devin-executors.test.ts @@ -16,7 +16,7 @@ function containsBytes(haystack: Uint8Array, needle: Uint8Array): boolean { } test("Devin Desktop sends the curated raw model id without alias rewriting", async () => { - const executor = getExecutor("devin-desktop"); + const executor = await getExecutor("devin-desktop"); const originalFetch = globalThis.fetch; let requestBody: Uint8Array | null = null; globalThis.fetch = async (url, init) => { @@ -37,7 +37,7 @@ test("Devin Desktop sends the curated raw model id without alias rewriting", asy credentials: { accessToken: "test-devin-desktop-token" }, }); - assert.equal(result.response.status, 418); + assert.equal((result instanceof Response ? result : result.response).status, 418); assert.ok(requestBody); assert.equal(containsBytes(requestBody, new TextEncoder().encode(model)), true); } finally { diff --git a/tests/unit/xai-oauth-provider.test.ts b/tests/unit/xai-oauth-provider.test.ts index 6d58db0bc6..7bec1fbbc9 100644 --- a/tests/unit/xai-oauth-provider.test.ts +++ b/tests/unit/xai-oauth-provider.test.ts @@ -81,14 +81,14 @@ test("xAI OAuth maps refreshable tokens and safe id_token display metadata", () assert.equal(mapped.name, "Grok User"); }); -test("xAI OAuth is a distinct OAuth registry entry backed by the xAI executor", () => { +test("xAI OAuth is a distinct OAuth registry entry backed by the xAI executor", async () => { assert.equal(xai_oauthProvider.authType, "oauth"); assert.equal(xai_oauthProvider.baseUrl, "https://api.x.ai/v1/chat/completions"); assert.ok(xai_oauthProvider.models?.some((model) => model.id === "grok-4.5")); assert.equal(hasSpecializedExecutor("xai-oauth"), true); - assert.ok(getExecutor("xai-oauth") instanceof XaiExecutor); + assert.ok(await getExecutor("xai-oauth") instanceof XaiExecutor); - const headers = getExecutor("xai-oauth").buildHeaders({ accessToken: "oauth-access" }, false); + const headers = (await getExecutor("xai-oauth")).buildHeaders({ accessToken: "oauth-access" }, false); assert.equal(headers.Authorization, "Bearer oauth-access"); }); diff --git a/tests/unit/zed-provider.test.ts b/tests/unit/zed-provider.test.ts index e9f0cfdfab..4f48edc198 100644 --- a/tests/unit/zed-provider.test.ts +++ b/tests/unit/zed-provider.test.ts @@ -45,9 +45,9 @@ describe("zed-hosted registry entry", () => { assert.equal(entry.oauth, undefined); }); - test("executor is wired in the executors map", () => { + test("executor is wired in the executors map", async () => { assert.ok(hasSpecializedExecutor("zed-hosted")); - assert.ok(getExecutor("zed-hosted") instanceof ZedHostedExecutor); + assert.ok(await getExecutor("zed-hosted") instanceof ZedHostedExecutor); }); }); diff --git a/tests/unit/zenmux-free-provider.test.ts b/tests/unit/zenmux-free-provider.test.ts index ff677b4e9d..2728aa8076 100644 --- a/tests/unit/zenmux-free-provider.test.ts +++ b/tests/unit/zenmux-free-provider.test.ts @@ -86,13 +86,13 @@ test("zenmux-free model names are human-readable strings", () => { // ── Executor ────────────────────────────────────────────────────────────────── -test("getExecutor returns ZenmuxFreeExecutor for 'zenmux-free'", () => { - const e = getExecutor("zenmux-free"); +test("getExecutor returns ZenmuxFreeExecutor for 'zenmux-free'", async () => { + const e = await getExecutor("zenmux-free"); assert.ok(e instanceof ZenmuxFreeExecutor, "executor must be ZenmuxFreeExecutor"); }); -test("getExecutor returns ZenmuxFreeExecutor for 'zmf' alias", () => { - const e = getExecutor("zmf"); +test("getExecutor returns ZenmuxFreeExecutor for 'zmf' alias", async () => { + const e = await getExecutor("zmf"); assert.ok(e instanceof ZenmuxFreeExecutor, "alias 'zmf' must resolve to ZenmuxFreeExecutor"); });