mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 17:12:27 +03:00
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.
276 lines
18 KiB
TypeScript
276 lines
18 KiB
TypeScript
import { SEARCH_PROVIDERS } from "../config/searchRegistry.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 { DefaultExecutor } from "./default.ts";
|
||
|
||
// 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<string, () => Promise<BaseExecutor>> = {
|
||
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": () => 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: 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();
|
||
|
||
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
|
||
// (CLOUD_AGENT_PROVIDERS / staticModels "Available Models" catalog) and have no
|
||
// chat-completions REGISTRY entry anywhere in open-sse/. Without this guard,
|
||
// getExecutor() silently falls through to DefaultExecutor's
|
||
// `PROVIDERS[provider] || PROVIDERS.openai` fallback, sending the user's real
|
||
// provider key to OpenAI's endpoint (mislabeled as coming from the provider the
|
||
// user actually selected). Starting with just "jules" (the reported case);
|
||
// "devin" and "codex-cloud" share the same structural gap and are left for a
|
||
// follow-up once their own chat-routing behavior is confirmed.
|
||
const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
|
||
|
||
// #10274 — providers that exist ONLY as /v1/search endpoint entries
|
||
// (SEARCH_PROVIDERS in open-sse/config/searchRegistry.ts) and have no chat-completions
|
||
// REGISTRY entry anywhere in open-sse/. Without this guard, getExecutor() silently falls
|
||
// through to DefaultExecutor's `PROVIDERS[provider] || PROVIDERS.openai` fallback, sending
|
||
// the user's real search API key (e.g. a Tavily `tvly-...` key) to OpenAI's endpoint and
|
||
// surfacing OpenAI's own "Incorrect API key provided" error for a provider the user believes
|
||
// is the search provider. The set is DERIVED from SEARCH_PROVIDERS so adding a new search
|
||
// provider without updating this guard fails the regression test automatically. Search
|
||
// providers must be executed through /v1/search, never the chat-completions path.
|
||
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
|
||
|
||
export async function getExecutor(provider: string): Promise<BaseExecutor> {
|
||
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.`
|
||
);
|
||
(err as Error & { status?: number }).status = 400;
|
||
throw err;
|
||
}
|
||
if (CHAT_UNSUPPORTED_SEARCH_PROVIDERS.has(provider)) {
|
||
const err = new Error(
|
||
`Provider "${provider}" is a search provider and does not support chat completions; use the /v1/search endpoint instead.`
|
||
);
|
||
(err as Error & { status?: number }).status = 400;
|
||
throw err;
|
||
}
|
||
if (!defaultCache.has(provider)) defaultCache.set(provider, new DefaultExecutor(provider));
|
||
return defaultCache.get(provider)!;
|
||
}
|
||
|
||
export function hasSpecializedExecutor(provider: string): boolean {
|
||
return hasRegisteredExecutor(provider);
|
||
}
|
||
|
||
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 { DefaultExecutor } from "./default.ts";
|