Compare commits

..

4 Commits

Author SHA1 Message Date
adevwithpurpose
d917e93974 fix(cli): reuse shared normalizeBootError helper in instrumentation.ts
The outermost instrumentation-hook boot boundary (#10171) was inlining its
own err-instanceof-Error normalization instead of reusing the existing
normalizeBootError() helper already defined in instrumentation-node.ts for
the same purpose (#6560/#7773). Extract it into a dependency-free
src/lib/instrumentationBootError.ts so both instrumentation.ts (which also
loads under the Edge runtime) and instrumentation-node.ts can import it
statically without risking a second failing dynamic import of
instrumentation-node.ts from within the catch block.
2026-08-17 22:38:55 -03:00
Diego Rodrigues de Sa e Souza
a3236a013f Merge branch 'release/v3.8.50' into fix/10171-instrumentation-hook-windows-wsl 2026-08-17 11:54:02 -03:00
adevwithpurpose
ebce750d54 fix(cli): normalize instrumentation boot errors
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-17 10:36:40 -03:00
adevwithpurpose
3e0ef5b4bb fix(cli): guarantee non-empty [STARTUP] Fatal log on instrumentation-hook boot throw
Refs #10171: on native Windows / WSL2 boots, an instrumentation-hook throw
during module-load or registerNodejs() leaves the HTTP listener up while
every DB-touching route 500s, with app.log staying completely empty. The
#7773/#7828 guard in ensureDbReadyForBoot only logs one specific failure
class (DB driver init). register() in src/instrumentation.ts now wraps the
boot call in a try/catch at the outermost boundary and unconditionally logs
a "[STARTUP] Fatal: instrumentation hook failed during boot:" line before
rethrowing, so app.log/stdout is never silently empty on a failed boot
regardless of platform or which step threw.

This is a partial diagnostic hardening, not the full fix for #10171 — the
platform-specific root cause on native Windows/WSL2 still needs the
reporter's raw child stderr from a real host (tracked separately, see
_tasks/pipeline/bugs/2-implementing/10171-instrumentation-hook-500-on-windows-wsl.plan.md).
2026-08-15 02:50:17 -03:00
74 changed files with 349 additions and 2078 deletions

View File

@@ -1048,69 +1048,31 @@ same process on one port, so there is no separate CLI-only package today.
</div>
## 📹 Video Guides
<div align="center">
<sub>Dados de cobertura social em 2026-08-17 · YT: 741 | TT: 137 | IG: 124 · Frescor (dias): YT 0 · TT 14 · IG 15</sub>
<table>
<tr>
<td align="center" width="320">
<a href="https://www.instagram.com/reel/Da8ZthUPK98/">
<img src="https://placehold.co/320x180/111827/FFFFFF?text=Instagram+Reel+%7C+nick_saraev&font=montserrat&bold=true" alt="Instagram Reel" width="300"/>
</a><br/>
<b>🎬 #1 — Instagram</b><br/>
<sub>nick_saraev — 1,628,910 views</sub>
<td align="center" width="264">
<a href="https://www.youtube.com/watch?v=Rxdc36yUyOQ"><img src="https://img.youtube.com/vi/Rxdc36yUyOQ/maxresdefault.jpg" alt="Guia em Português" width="260"/></a><br/>
<b>🇧🇷 Português</b><br/><sub>Guia completo</sub>
</td>
<td align="center" width="320">
<a href="https://www.youtube.com/watch?v=QucgvbO5gsM">
<img src="https://img.youtube.com/vi/QucgvbO5gsM/maxresdefault.jpg" alt="YouTube — Vaibhav Sisinty" width="300"/>
</a><br/>
<b>🎬 #2 — YouTube</b><br/>
<sub>Vaibhav Sisinty — 373,084 views</sub>
<td align="center" width="264">
<a href="https://www.youtube.com/watch?v=CMzyOiUyEVc"><img src="https://img.youtube.com/vi/CMzyOiUyEVc/maxresdefault.jpg" alt="English Guide" width="260"/></a><br/>
<b>🇺🇸 English</b><br/><sub>Complete walkthrough</sub>
</td>
<td align="center" width="320">
<a href="https://www.youtube.com/shorts/fZIBK_4fKq8">
<img src="https://img.youtube.com/vi/fZIBK_4fKq8/maxresdefault.jpg" alt="YouTube Shorts" width="300"/>
</a><br/>
<b>🎬 #3 — YouTube Shorts</b><br/>
<sub>Nick Automates — 207,714 views</sub>
</td>
<td align="center" width="320">
<a href="https://www.tiktok.com/@milesreevesai/video/7667980059189366019">
<img src="https://placehold.co/320x180/111827/FFFFFF?text=TikTok+Top+1&font=montserrat&bold=true" alt="TikTok Thumbnail" width="300"/>
</a><br/>
<b>🎬 #4 — TikTok</b><br/>
<sub>milesreevesai — 620,400 views</sub>
</td>
<td align="center" width="320">
<a href="https://www.youtube.com/watch?v=LkP6ocAoQkk">
<img src="https://img.youtube.com/vi/LkP6ocAoQkk/maxresdefault.jpg" alt="Valency Labs" width="300"/>
</a><br/>
<b>🎬 #5 — YouTube</b><br/>
<sub>Valency Labs — 135,974 views</sub>
<td align="center" width="264">
<a href="https://www.youtube.com/watch?v=il_5Ii6v4-Y"><img src="https://img.youtube.com/vi/il_5Ii6v4-Y/maxresdefault.jpg" alt="Руководство" width="260"/></a><br/>
<b>🇷🇺 Русский</b><br/><sub>Полное руководство</sub>
</td>
</tr>
</table>
</div>
**Ranking completo (`v > 0`, maior alcance):**
| #1 | #2 | #3 | #4 | #5 |
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| [nick_saraev — Instagram](https://www.instagram.com/reel/Da8ZthUPK98/) — **1,628,910** | [milesreevesai — TikTok](https://www.tiktok.com/@milesreevesai/video/7667980059189366019) — **620,400** | [Vaibhav Sisinty — YouTube](https://www.youtube.com/watch?v=QucgvbO5gsM) — **373,084** | [Nick Automates — YouTube Shorts](https://www.youtube.com/shorts/fZIBK_4fKq8) — **207,714** | [midudev — TikTok](https://www.tiktok.com/@midudev/video/7664636453544152342) — **177,800** |
| #6 | #7 | #8 | #9 | #10 |
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [theopenstack — Instagram](https://www.instagram.com/reel/DaSs65mMrHk/) — **155,453** | [t.ghoush.ai — TikTok](https://www.tiktok.com/@t.ghoush.ai/video/7669497680527248656) — **152,800** | [Valency Labs — YouTube](https://www.youtube.com/watch?v=LkP6ocAoQkk) — **135,974** | [Asati — YouTube](https://www.youtube.com/watch?v=JjPtJcqwhqg) — **126,130** | [Vaibhav Sisinty — YouTube](https://www.youtube.com/watch?v=NuNDpeZYQ28) — **122,672** |
Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações conhecidas · 595 perfis/canais · 13+ idiomas · 13+ criadores.
<div align="center">
> 🎬 **Made a video about OmniRoute?** Open an [issue](https://github.com/diegosouzapw/OmniRoute/issues/new) or [discussion](https://github.com/diegosouzapw/OmniRoute/discussions) with the link — we'll feature it here.
<br/>
</div>
<div align="center">

View File

@@ -208,7 +208,7 @@ _omniroute() {
COMPREPLY=()
cur="\${COMP_WORDS[COMP_CWORD]}"
prev="\${COMP_WORDS[COMP_CWORD-1]}"
cmds="setup doctor status logs providers config test update serve stop restart keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills run"
cmds="setup doctor status logs providers config test update serve stop restart keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills"
case "\${prev}" in
combo) COMPREPLY=($(compgen -W "list switch create delete show suggest" -- "\${cur}")); return 0 ;;
@@ -242,7 +242,7 @@ function generateFishScript() {
return `# OmniRoute CLI fish completion (dynamic)
complete -c omniroute -f
set -l commands serve stop restart setup doctor status logs providers config keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills update test run
set -l commands serve stop restart setup doctor status logs providers config keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills update test
for cmd in $commands
complete -c omniroute -n '__fish_is_nth_token 1' -a $cmd

View File

@@ -170,8 +170,8 @@ export function buildCodexEnv(baseEnv, authToken) {
* @param {string} baseUrl OmniRoute root URL (no /v1)
* @returns {string[]}
*/
export function buildCodexProviderArgs(baseUrl, model) {
const args = [
export function buildCodexProviderArgs(baseUrl) {
return [
"-c",
tomlAssign("model_provider", "omniroute"),
"-c",
@@ -185,15 +185,6 @@ export function buildCodexProviderArgs(baseUrl, model) {
"-c",
tomlAssign("model_providers.omniroute.requires_openai_auth", false),
];
if (model) {
const normalized = String(model).trim();
if (normalized) {
args.push("-c", tomlAssign("model_providers.omniroute.model", normalized));
}
}
return args;
}
/**
@@ -216,7 +207,7 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
// Provider injected via -c (works without config.toml); then the profile (model),
// then the user's pass-through args.
const providerArgs = buildCodexProviderArgs(baseUrl, opts.model);
const providerArgs = buildCodexProviderArgs(baseUrl);
const profileArgs = opts.profile ? ["--profile", opts.profile] : [];
const extraArgs = [...providerArgs, ...profileArgs, ...codexArgs];
const env = buildCodexEnv(process.env, authToken);

View File

@@ -190,10 +190,7 @@ export async function runLaunchCommand(opts = {}, claudeArgs = []) {
const configDir = opts.profile
? join(opts.claudeHome || join(os.homedir(), ".claude"), "profiles", opts.profile)
: undefined;
const env = buildClaudeEnv(process.env, baseUrl, authToken, {
configDir,
model: opts.model,
});
const env = buildClaudeEnv(process.env, baseUrl, authToken, { configDir });
const { command, shell } = await resolveClaudeSpawn(process.platform);

View File

@@ -60,7 +60,6 @@ import { registerAutostart } from "./autostart.mjs";
import { registerRepl } from "./repl.mjs";
import { registerLaunch } from "./launch.mjs";
import { registerLaunchCodex } from "./launch-codex.mjs";
import { registerRun } from "./run.mjs";
import { registerSetupCodex } from "./setup-codex.mjs";
import { registerSetupClaude } from "./setup-claude.mjs";
import { registerSetupOpencode } from "./setup-opencode.mjs";
@@ -146,7 +145,6 @@ export function registerCommands(program) {
registerRepl(program);
registerLaunch(program);
registerLaunchCodex(program);
registerRun(program);
registerSetupCodex(program);
registerSetupClaude(program);
registerSetupOpencode(program);

View File

@@ -1,316 +0,0 @@
import {
runLaunchCommand as runLaunchClaudeCommand,
buildClaudeEnv,
resolveClaudeSpawn,
quoteClaudeArgs,
resolveLaunchTarget,
} from "./launch.mjs";
import {
buildCodexEnv,
buildCodexProviderArgs,
resolveCodexSpawn,
quoteCodexArgs,
resolveCodexTarget,
runLaunchCodexCommand as runLaunchCodexCommand,
} from "./launch-codex.mjs";
import { t } from "../i18n.mjs";
import os from "node:os";
import { join } from "node:path";
import { resolveActiveContext } from "../contexts.mjs";
const RUN_TARGETS = {
claude: {
aliases: ["claude", "claude-code", "cc"],
description: "Claude Code",
},
codex: {
aliases: ["codex", "openai-codex", "openai"],
description: "OpenAI Codex CLI",
},
};
/** @type {Record<string,string>} */
const RUN_TARGET_ALIAS_TO_CANONICAL = {
claude: "claude",
"claude-code": "claude",
cc: "claude",
codex: "codex",
"openai-codex": "codex",
openai: "codex",
};
function isBlank(value) {
return value === undefined || value === null || String(value).trim() === "";
}
function toAuthSource(targetOpts) {
const explicit =
!isBlank(targetOpts.token) || !isBlank(targetOpts.apiKey) || !isBlank(targetOpts["api-key"]);
if (explicit) return "option";
try {
const context = resolveActiveContext(targetOpts.context || process.env.OMNIROUTE_CONTEXT);
if (context && (context.accessToken || context.apiKey)) return "context";
} catch {
// no active context
}
if (!isBlank(process.env.OMNIROUTE_API_KEY)) return "env";
if (!isBlank(process.env.ANTHROPIC_AUTH_TOKEN)) return "env";
return "none";
}
/** Resolve supported target to canonical id. */
export function resolveRunTarget(target) {
const raw = String(target || "")
.trim()
.toLowerCase();
return RUN_TARGET_ALIAS_TO_CANONICAL[raw];
}
export function listRunTargets() {
return Object.keys(RUN_TARGETS);
}
/**
* Normalize `--provider` + `--model` into one model id.
*
* - when model contains a slash, keep it as-is
* - when provider exists and model does not, prefix provider/
*/
export function resolveModelFromTargetOptions(targetOpts = {}) {
const provider = String(targetOpts.provider || "").trim();
const model = String(targetOpts.model || "").trim();
if (!model) return "";
if (provider && !model.includes("/")) return `${provider}/${model}`;
return model;
}
function describeCommand(command, shellMode) {
return `${command}${shellMode ? " [shell]" : ""}`;
}
function envPreview(before = {}, after = {}) {
const beforeKeys = new Set(Object.keys(before));
const changedOrAdded = [];
const removed = [];
for (const key of Object.keys(after)) {
if (!beforeKeys.has(key) || String(before[key]) !== String(after[key])) {
changedOrAdded.push(key);
}
}
for (const key of Object.keys(before)) {
if (!(key in after)) removed.push(key);
}
return {
changedOrAdded,
removed,
};
}
async function buildClaudePlan(rawOpts, args = []) {
const model = resolveModelFromTargetOptions(rawOpts);
const merged = {
...rawOpts,
model,
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
token: rawOpts.token || rawOpts.apiKey || rawOpts["api-key"],
profile: rawOpts.profile ?? rawOpts.p,
};
const { baseUrl, authToken } = resolveLaunchTarget(merged);
const commandSpec = await resolveClaudeSpawn(process.platform);
const configDir = merged.profile
? join(merged.claudeHome || join(os.homedir(), ".claude"), "profiles", merged.profile)
: undefined;
const env = buildClaudeEnv(process.env, baseUrl, authToken, {
configDir,
model: merged.model || undefined,
});
const quotedArgs = quoteClaudeArgs(args, process.platform);
return {
target: "claude",
baseUrl,
command: commandSpec.command,
shell: commandSpec.shell,
args: quotedArgs,
model: merged.model || undefined,
envDiff: envPreview(process.env, env),
authSource: toAuthSource(merged),
commandDisplay: describeCommand(commandSpec.command, commandSpec.shell),
};
}
async function buildCodexPlan(rawOpts, args = []) {
const model = resolveModelFromTargetOptions(rawOpts);
const merged = {
...rawOpts,
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
model,
profile: rawOpts.profile ?? rawOpts.p,
};
const { baseUrl, authToken } = resolveCodexTarget(merged);
const commandSpec = await resolveCodexSpawn(process.platform);
const providerArgs = buildCodexProviderArgs(baseUrl, merged.model || undefined);
const profileArgs = merged.profile ? ["--profile", merged.profile] : [];
const env = buildCodexEnv(process.env, authToken);
const fullArgs = [...providerArgs, ...profileArgs, ...args];
const quotedArgs = quoteCodexArgs(fullArgs, process.platform);
return {
target: "codex",
baseUrl,
command: commandSpec.command,
shell: commandSpec.shell,
args: quotedArgs,
model: merged.model || undefined,
envDiff: envPreview(process.env, env),
authSource: toAuthSource(merged),
commandDisplay: describeCommand(commandSpec.command, commandSpec.shell),
providerArgs,
profileArgs,
};
}
/** Build a launch plan and redact any resolved secret values. */
export async function buildRunPlan(target, rawOpts = {}, args = []) {
const canonical = resolveRunTarget(target);
if (!canonical) {
throw new Error("unsupported target");
}
if (canonical === "claude") {
return buildClaudePlan(rawOpts, args);
}
return buildCodexPlan(rawOpts, args);
}
function writeDryRunOutput(plan, opts = {}) {
const output = {
target: plan.target,
baseUrl: plan.baseUrl,
command: plan.command,
args: plan.args,
auth: {
source: plan.authSource,
present: plan.authSource !== "none",
},
shell: !!plan.shell,
model: plan.model || null,
env: {
changedOrAdded: plan.envDiff.changedOrAdded,
removed: plan.envDiff.removed,
},
};
if (opts.json) {
console.error(`Running in dry-run mode for '${plan.target}'.`);
console.log(JSON.stringify(output, null, 2));
} else {
console.log(`target: ${output.target}`);
console.log(`baseUrl: ${output.baseUrl}`);
console.log(`command: ${output.command}`);
console.log(`shell: ${output.shell ? "yes" : "no"}`);
console.log(`args: ${JSON.stringify(output.args)}`);
console.log(`auth: ${JSON.stringify(output.auth)}`);
console.log(`model: ${output.model || "(not set)"}`);
if (output.env.changedOrAdded.length) {
console.log(`env added/changed: ${output.env.changedOrAdded.join(", ")}`);
}
if (output.env.removed.length) {
console.log(`env removed: ${output.env.removed.join(", ")}`);
}
}
}
function buildExecutionOptionsForClaude(rawOpts) {
return {
...rawOpts,
model: resolveModelFromTargetOptions(rawOpts),
token: rawOpts.token || rawOpts.apiKey || rawOpts["api-key"],
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
profile: rawOpts.profile || rawOpts.p,
};
}
function buildExecutionOptionsForCodex(rawOpts) {
return {
...rawOpts,
model: resolveModelFromTargetOptions(rawOpts),
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
profile: rawOpts.profile || rawOpts.p,
};
}
/**
* Execute or preview one target launch.
*
* Return code conventions:
* 0 success, 1 runtime launch failure, 2 invalid args.
*/
export async function runCliTarget(target, opts = {}, args = []) {
const canonical = resolveRunTarget(target);
if (!canonical) {
process.stderr.write(
`Unsupported target '${target}'. Supported targets: ${Object.keys(RUN_TARGETS).join(", ")}\n`
);
return 2;
}
const plan = await buildRunPlan(target, opts, args);
if (opts.dryRun) {
writeDryRunOutput(plan, opts);
return 0;
}
if (canonical === "claude") {
return await runLaunchClaudeCommand(buildExecutionOptionsForClaude(opts), args);
}
return await runLaunchCodexCommand(buildExecutionOptionsForCodex(opts), args);
}
export function registerRun(program) {
program
.command("run <target>")
.description(t("run.description") || "Run a supported CLI target through OmniRoute")
.option(
"--port <port>",
"Local OmniRoute port (ignored when --remote or --base-url is set)",
"20128"
)
.option(
"--remote <url>",
"Remote OmniRoute base URL (overrides --port, --base-url, and the active context)"
)
.option("--provider <id>", "Provider id for shorthand model composition")
.option("--model <id>", "Model id to inject in the launched target where supported")
.option("--profile <name>", "Profile/alias argument for target launchers that support it")
.option("-p, --p <name>", "Alias for --profile")
.option("--token <token>", "Authentication token for the launched target (same as --api-key)")
.option("--api-key <key>", "Authentication token for the launched target")
.option("--dry-run", "Show planned command and env keys without executing")
.option("--json", "Return dry-run output in machine-readable format")
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("[toolArgs...]")
.action(async (target, toolArgs = [], opts, cmd) => {
const globalOpts = cmd?.optsWithGlobals ? cmd.optsWithGlobals() : {};
const merged = { ...globalOpts, ...opts };
const code = await runCliTarget(target, merged, toolArgs);
// process.exit() here can interrupt cleanup when the child terminates;
// setting process.exitCode lets the event loop drain first.
process.exitCode = code;
});
}

View File

@@ -1287,9 +1287,6 @@
"notRunning": "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.",
"notFound": "The 'claude' CLI was not found in PATH."
},
"run": {
"description": "Launch a supported CLI target through OmniRoute"
},
"setupClaude": {
"description": "Generate ~/.claude/profiles Claude Code profiles from the OmniRoute model catalog"
},

View File

@@ -1284,9 +1284,6 @@
"notRunning": "OmniRoute não está acessível em {port}. Inicie com 'omniroute serve'.",
"notFound": "O CLI 'claude' não foi encontrado no PATH."
},
"run": {
"description": "Inicia um alvo de CLI compatível pelo OmniRoute"
},
"setupClaude": {
"description": "Gera profiles do Claude Code em ~/.claude/profiles a partir do catálogo de modelos do OmniRoute"
},

View File

@@ -0,0 +1 @@
- fix(cli): guarantee a non-empty `[STARTUP] Fatal:` log line for any instrumentation-hook boot throw, not just DB-driver init failures (#10171)

View File

@@ -368,7 +368,7 @@ const result = await executor.execute({
});
````
Resolution goes through the `ExecutorRegistry` (`executors/registry.ts`): every specialized executor is declared in the built-in table of `executors/index.ts` and registered via `registerExecutor(alias, instance)` at module load; `getExecutor()` consults the registry and falls back to a memoized `DefaultExecutor` for any provider without a specialized entry. The full alias → executor mapping is characterized by the golden test `tests/unit/executor-map-golden.test.ts`.
The factory is generated from `config/providerRegistry.ts` which lists all 338 providers and their executor class.
---

View File

@@ -35,23 +35,22 @@ Every command honours the **active context** (set with `omniroute connect`, see
with `--remote` (or an active remote context) it fetches the catalog from that
server and writes the config locally.
| Command | Tool | What it writes | Key flags | Local vs remote |
| -------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `omniroute setup-codex` | OpenAI Codex CLI | `~/.codex/<name>.config.toml` — one profile per compatible text model (`codex --profile <name>`) | `--remote` `--api-key` `--only` `--dry-run` `--port` `--codex-home` | Both |
| `omniroute setup-claude` | Claude Code | `~/.claude/profiles/<name>/settings.json` — one profile per matched model (`CLAUDE_CONFIG_DIR`) | `--remote` `--api-key` `--only` `--dry-run` `--port` `--claude-home` | Both |
| `omniroute setup-opencode` | OpenCode (openai-compatible) | `~/.config/opencode/opencode.json``omniroute` provider with every catalog model (`opencode -m omniroute/<model>`) | `--remote` `--api-key` `--only` `--model` `--dry-run` `--port` | Both |
| `omniroute setup-cline` | Cline | `~/.cline/data/{globalState,secrets}.json` (CLI mode) + prints VS Code extension settings | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--cline-dir` | Both |
| `omniroute setup-kilo` | Kilo Code | `~/.local/share/kilo/auth.json` (CLI) + merges `kilocode.*` into VS Code `settings.json` if present | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--auth-path` `--vscode-settings` | Both |
| `omniroute setup-continue` | Continue / `cn` CLI | `~/.continue/config.yaml``provider: openai` models, key via `${{ secrets.OMNIROUTE_API_KEY }}` | `--remote` `--api-key` `--only` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-cursor` | Cursor | Nothing — prints the in-app steps (Cursor config is opaque SQLite) | `--remote` `--api-key` `--only` `--port` | Both |
| `omniroute setup-roo` | Roo Code | `~/.omniroute/roo-settings.json` (import doc) + sets `roo-cline.autoImportSettingsPath` if a VS Code `settings.json` exists | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--import-path` `--vscode-settings` | Both |
| `omniroute setup-crush` | Crush | `~/.config/crush/crush.json``openai-compat` provider, key via `$OMNIROUTE_API_KEY` | `--remote` `--api-key` `--only` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-goose` | Goose | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER`/`OPENAI_HOST`/`GOOSE_MODEL`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-aider` | Aider | `~/.aider.conf.yml` (`openai-api-base` + `model: openai/<id>`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-qwen` | Qwen Code | `~/.qwen/settings.json` — V4 `modelProviders.openai` array + `OMNIROUTE_API_KEY` in `~/.qwen/.env` | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` `--env-path` | Both |
| `omniroute run <target>` | Runtime launch (generic) | Nothing — spawn `claude`/`codex` with the right env and args | `--remote` `--base-url` `--context` `--provider` `--model` `--api-key` `--dry-run` `--json` `--port` `--profile` `--token` | Both |
| `omniroute launch` | Claude Code | Nothing — spawns `claude` with `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` injected | `--remote` `--api-key` `--token` `--profile` `--port` | Both |
| `omniroute launch-codex` | OpenAI Codex CLI | Nothing — spawns `codex` with the `omniroute` provider injected via `-c` flags | `--remote` `--api-key` `--profile` (`-p`) `--port` | Both |
| Command | Tool | What it writes | Key flags | Local vs remote |
|---------|------|----------------|-----------|-----------------|
| `omniroute setup-codex` | OpenAI Codex CLI | `~/.codex/<name>.config.toml` — one profile per compatible text model (`codex --profile <name>`) | `--remote` `--api-key` `--only` `--dry-run` `--port` `--codex-home` | Both |
| `omniroute setup-claude` | Claude Code | `~/.claude/profiles/<name>/settings.json` — one profile per matched model (`CLAUDE_CONFIG_DIR`) | `--remote` `--api-key` `--only` `--dry-run` `--port` `--claude-home` | Both |
| `omniroute setup-opencode` | OpenCode (openai-compatible) | `~/.config/opencode/opencode.json``omniroute` provider with every catalog model (`opencode -m omniroute/<model>`) | `--remote` `--api-key` `--only` `--model` `--dry-run` `--port` | Both |
| `omniroute setup-cline` | Cline | `~/.cline/data/{globalState,secrets}.json` (CLI mode) + prints VS Code extension settings | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--cline-dir` | Both |
| `omniroute setup-kilo` | Kilo Code | `~/.local/share/kilo/auth.json` (CLI) + merges `kilocode.*` into VS Code `settings.json` if present | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--auth-path` `--vscode-settings` | Both |
| `omniroute setup-continue` | Continue / `cn` CLI | `~/.continue/config.yaml``provider: openai` models, key via `${{ secrets.OMNIROUTE_API_KEY }}` | `--remote` `--api-key` `--only` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-cursor` | Cursor | Nothing — prints the in-app steps (Cursor config is opaque SQLite) | `--remote` `--api-key` `--only` `--port` | Both |
| `omniroute setup-roo` | Roo Code | `~/.omniroute/roo-settings.json` (import doc) + sets `roo-cline.autoImportSettingsPath` if a VS Code `settings.json` exists | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--import-path` `--vscode-settings` | Both |
| `omniroute setup-crush` | Crush | `~/.config/crush/crush.json``openai-compat` provider, key via `$OMNIROUTE_API_KEY` | `--remote` `--api-key` `--only` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-goose` | Goose | `~/.config/goose/config.yaml` (`GOOSE_PROVIDER`/`OPENAI_HOST`/`GOOSE_MODEL`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-aider` | Aider | `~/.aider.conf.yml` (`openai-api-base` + `model: openai/<id>`) + prints env recipe | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` | Both |
| `omniroute setup-qwen` | Qwen Code | `~/.qwen/settings.json` — V4 `modelProviders.openai` array + `OMNIROUTE_API_KEY` in `~/.qwen/.env` | `--remote` `--api-key` `--model` `--yes` `--dry-run` `--port` `--config-path` `--env-path` | Both |
| `omniroute launch` | Claude Code | Nothing — spawns `claude` with `ANTHROPIC_BASE_URL`/`ANTHROPIC_AUTH_TOKEN` injected | `--remote` `--api-key` `--token` `--profile` `--port` | Both |
| `omniroute launch-codex` | OpenAI Codex CLI | Nothing — spawns `codex` with the `omniroute` provider injected via `-c` flags | `--remote` `--api-key` `--profile` (`-p`) `--port` | Both |
Notes on flags (verified in the command source):
@@ -114,11 +113,6 @@ Launch without writing any config at all (env-injection only):
omniroute launch # Claude Code → local OmniRoute
omniroute launch-codex # Codex CLI → local OmniRoute
omniroute launch-codex --profile glm52
omniroute run claude --model openai/gpt-5.4
omniroute run codex --model openai/gpt-5.4 --dry-run --json
# Explicit command path: pass through whatever comes after --
omniroute run claude -- --print-system-prompt "review this diff"
```
---
@@ -162,15 +156,15 @@ OmniRoute exposes the OpenAI surface at `/v1`, the Anthropic surface at the root
and a native Gemini surface at `/v1beta`. Each integration is wired to the form its
tool expects (verified in the command source):
| Integration | Base URL written | `/v1`? |
| -------------------------------------------------------------------------- | ---------------- | ------------------------------------------- |
| `setup-cline` (`openAiBaseUrl`) | root | No — Cline appends `/v1/chat/completions` |
| `setup-goose` (`OPENAI_HOST`) | root | No — Goose appends the path |
| `setup-aider` (`OPENAI_API_BASE`) | root | No — LiteLLM appends `/v1/chat/completions` |
| `setup-kilo`, `setup-roo`, `setup-continue`, `setup-crush`, `setup-cursor` | with `/v1` | Yes |
| `setup-claude` (`ANTHROPIC_BASE_URL`), `launch` | root | No — Claude Code appends `/v1/messages` |
| `setup-codex`, `launch-codex` (`model_providers.omniroute.base_url`) | with `/v1` | Yes |
| `setup-qwen` (`modelProviders.openai[].baseUrl`) | with `/v1` | Yes |
| Integration | Base URL written | `/v1`? |
|-------------|------------------|--------|
| `setup-cline` (`openAiBaseUrl`) | root | No — Cline appends `/v1/chat/completions` |
| `setup-goose` (`OPENAI_HOST`) | root | No — Goose appends the path |
| `setup-aider` (`OPENAI_API_BASE`) | root | No — LiteLLM appends `/v1/chat/completions` |
| `setup-kilo`, `setup-roo`, `setup-continue`, `setup-crush`, `setup-cursor` | with `/v1` | Yes |
| `setup-claude` (`ANTHROPIC_BASE_URL`), `launch` | root | No — Claude Code appends `/v1/messages` |
| `setup-codex`, `launch-codex` (`model_providers.omniroute.base_url`) | with `/v1` | Yes |
| `setup-qwen` (`modelProviders.openai[].baseUrl`) | with `/v1` | Yes |
---

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -18,7 +18,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
- **Runtime:** Node.js `>=22.0.0 <23 || >=24.0.0 <27`, ES Modules (`"type": "module"`)
- **Framework:** Next.js 16 (App Router) with TypeScript 6
- **Database:** SQLite via better-sqlite3 (local, zero-config, 150 migrations)
- **Database:** SQLite via better-sqlite3 (local, zero-config, 149 migrations)
- **State management:** Zustand (client), SQLite (server persistence)
- **UI:** React 19, Tailwind CSS 4, Recharts for analytics, @lobehub/icons for 130+ provider SVG icons
- **Auth:** OAuth 2.0 (PKCE) for providers, bcrypt for local user auth
@@ -438,7 +438,7 @@ diagnostics) plus **memory**, **skill**, **agentSkill**, **githubSkill**, **pool
4. **Environment variables:** All configuration is in `.env` (from `.env.example`). Key vars: `PORT`, `NEXT_PUBLIC_BASE_URL`, `API_KEY`, `ADMIN_PASSWORD`.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 150 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
5. **Database layer:** Operations go through `src/lib/db/` modules (117 domain-specific files, 149 migrations). `localDb.ts` is re-exports only — add new functions to the proper `db/*.ts` module.
6. **Tests** use Node.js built-in test runner + Vitest. Run `npm test`. Vitest for MCP/autoCombo (`npm run test:vitest`). Playwright for E2E (`npm run test:e2e`). Coverage gate: ratchet vs `quality-baseline.json`, absolute floor 60% statements/lines/functions/branches.

View File

@@ -1,9 +1,4 @@
import { SEARCH_PROVIDERS } from "../config/searchRegistry.ts";
import {
registerExecutor,
getRegisteredExecutor,
hasRegisteredExecutor,
} from "./registry.ts";
import { AntigravityExecutor } from "./antigravity.ts";
import { GithubExecutor } from "./github.ts";
import { GheCopilotExecutor } from "./ghe-copilot.ts";
@@ -83,12 +78,6 @@ 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(),
@@ -232,13 +221,6 @@ const executors = {
cnl: new 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)) {
registerExecutor(alias, executor);
}
const defaultCache = new Map();
// #6699 — providers that exist ONLY as Cloud Agent task-API entries
@@ -264,8 +246,7 @@ const CHAT_UNSUPPORTED_CLOUD_AGENT_PROVIDERS = new Set(["jules"]);
const CHAT_UNSUPPORTED_SEARCH_PROVIDERS = new Set(Object.keys(SEARCH_PROVIDERS));
export function getExecutor(provider) {
const registered = getRegisteredExecutor(provider);
if (registered) return registered;
if (executors[provider]) return executors[provider];
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,11 +266,9 @@ export function getExecutor(provider) {
}
export function hasSpecializedExecutor(provider) {
return hasRegisteredExecutor(provider);
return !!executors[provider];
}
export { registerExecutor, listExecutorAliases } from "./registry.ts";
export { BaseExecutor } from "./base.ts";
export { AntigravityExecutor } from "./antigravity.ts";
export { GithubExecutor } from "./github.ts";

View File

@@ -1,38 +0,0 @@
import type { BaseExecutor } from "./base.ts";
// R0.3 — ExecutorRegistry: runtime registry for provider executors, mirroring
// open-sse/translator/registry.ts. Built-ins register at module load from
// executors/index.ts; getExecutor() resolves through this map instead of a
// hard-coded object literal. This is the seam the v4 plan (M1.6
// host.registerProvider) extends — today the surface is internal-only.
//
// The alias → executor mapping is characterized by
// tests/unit/executor-map-golden.test.ts (tests/snapshots/executors/): any
// change to keys, classes or instance sharing shows up as a golden diff.
const registry = new Map<string, BaseExecutor>();
/**
* Register an executor under an alias. Aliases are unique: registering the
* same alias twice throws, preserving the guarantee the old object literal
* gave at compile time (duplicate keys were impossible).
*/
export function registerExecutor(alias: string, executor: BaseExecutor): void {
if (registry.has(alias)) {
throw new Error(`executor alias already registered: "${alias}"`);
}
registry.set(alias, executor);
}
export function getRegisteredExecutor(alias: string): BaseExecutor | undefined {
return registry.get(alias);
}
export function hasRegisteredExecutor(alias: string): boolean {
return registry.has(alias);
}
/** All registered aliases, in registration order. */
export function listExecutorAliases(): string[] {
return [...registry.keys()];
}

View File

@@ -7,14 +7,8 @@ import { resolveChatCoreRequestSetup } from "./chatCore/requestSetup.ts";
import { normalizeOpenAICompatibleTools } from "./chatCore/openAICompatibleTools.ts";
import { buildFailureUsageRecord } from "./chatCore/failureUsage.ts";
import { estimateFinalInputTokens } from "./chatCore/contextEstimation.ts";
import {
extractSystemRoleMessages,
relocateDirectiveOnlyMessages,
} from "./chatCore/claudeSystemRole.ts";
export {
extractSystemRoleMessages,
relocateDirectiveOnlyMessages,
} from "./chatCore/claudeSystemRole.ts";
import { extractSystemRoleMessages, relocateDirectiveOnlyMessages } from "./chatCore/claudeSystemRole.ts";
export { extractSystemRoleMessages, relocateDirectiveOnlyMessages } from "./chatCore/claudeSystemRole.ts";
import { checkIdempotencyCache } from "./chatCore/idempotency.ts";
import { checkSemanticCache } from "./chatCore/semanticCache.ts";
import { checkLifecycle, resolveLifecycle } from "./chatCore/modelLifecyclePolicy.ts";
@@ -1160,11 +1154,6 @@ export async function handleChatCore({
let cavemanOutputModeIntensity: string | null = null;
let preCompressionBody: typeof body | null = null;
let compressionResponseMeta: string | null = null;
// OmniGlyph 1.3.x has native OpenAI Chat/Responses transformers. When the
// inbound protocol differs from the provider wire, defer only that engine to
// the post-translation body; the text engines still run in their legacy lane.
let runPostTranslationCompression:
((input: Record<string, unknown>) => Promise<CompressionResult>) | null = null;
// Delegated Context Editing (Claude only): captured at the canonical compression
// settings read below, then threaded to executor.execute() further down. Lives at
// function scope because the read happens inside the per-message compression block.
@@ -1552,15 +1541,9 @@ export async function handleChatCore({
// o engine omniglyph exige 'direct' — agregadores redimensionam imagens
// (medido 2026-07-06). OAuth 'claude' é rota direta oficial (#7863).
providerTransport:
provider === "anthropic" ||
provider === "claude" ||
provider === "openai" ||
provider === "xai"
provider === "anthropic" || provider === "claude"
? ("direct" as const)
: ("aggregator" as const),
sourceFormat,
targetFormat,
compressionStage: "pre-translation" as const,
config: compressionConfig,
cachingContext: cacheCtx,
principalId: compressionPrincipalId,
@@ -1590,26 +1573,6 @@ export async function handleChatCore({
};
const runCompression = (input: Record<string, unknown>) =>
applyCompressionAsync(input, mode, compressionOptions);
const omniglyphSelected =
mode === "omniglyph" ||
(mode === "stacked" &&
Array.isArray(compressionConfig.stackedPipeline) &&
compressionConfig.stackedPipeline.some((step) =>
typeof step === "string" ? step === "omniglyph" : step.engine === "omniglyph"
));
if (
omniglyphSelected &&
(targetFormat === FORMATS.CLAUDE ||
targetFormat === FORMATS.OPENAI ||
targetFormat === FORMATS.OPENAI_RESPONSES) &&
!(sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE)
) {
runPostTranslationCompression = (input) =>
applyCompressionAsync(input, mode, {
...compressionOptions,
compressionStage: "post-translation" as const,
});
}
let result: CompressionResult;
if (compressionConfig.liveZone?.enabled === true) {
const { applyLiveZoneCompression } = await import("../services/compression/liveZone.ts");
@@ -2351,76 +2314,6 @@ export async function handleChatCore({
return createErrorResult(statusCode, message);
}
// The latest OmniGlyph release has protocol-native OpenAI transforms. Run
// the deferred stage only after translation so Chat/Responses receives the
// exact provider wire shape (and so a source→target conversion never embeds
// Anthropic image blocks into an OpenAI request, or vice versa).
if (runPostTranslationCompression && translatedBody && typeof translatedBody === "object") {
const transientFields = new Map<string, unknown>();
const postInput = { ...(translatedBody as Record<string, unknown>) };
for (const [key, value] of Object.entries(postInput)) {
// Translators keep response-side aliases in Maps under private keys. They
// are not JSON request fields and would otherwise be stringified to `{}`
// by the OmniGlyph library wrapper; restore them after the wire transform.
if (key.startsWith("_") && value instanceof Map) {
transientFields.set(key, value);
delete postInput[key];
}
}
try {
const [{ formatCompressionAnnotation }, { trackCompressionStats }] = await Promise.all([
import("../services/compression/strategySelector.ts"),
import("../services/compression/stats.ts"),
]);
const postResult = await runPostTranslationCompression(postInput);
if (postResult.compressed) {
translatedBody = {
...(postResult.body as typeof translatedBody),
...Object.fromEntries(transientFields),
};
tokensCompressed += Math.max(
0,
(postResult.stats?.originalTokens ?? 0) - (postResult.stats?.compressedTokens ?? 0)
);
if (postResult.stats) {
const annotation = formatCompressionAnnotation(postResult.stats);
if (annotation) {
compressionResponseMeta = compressionResponseMeta
? `${compressionResponseMeta}; ${annotation}`
: annotation;
}
trackCompressionStats(postResult.stats);
compressionAnalyticsWritePromise = writeCompressionAnalytics({
stats: postResult.stats,
provider,
effectiveModel,
effectiveServiceTier,
comboName,
mode: postResult.stats.mode,
compressionComboId: postResult.stats.compressionComboId ?? null,
skillRequestId,
cavemanOutputModeApplied: false,
cavemanOutputModeIntensity: null,
log,
});
await compressionAnalyticsWritePromise;
}
log?.info?.(
"COMPRESSION",
`Post-translation OmniGlyph applied (${sourceFormat}${targetFormat})`
);
}
} catch (error) {
// Compression is deliberately fail-open. A provider-shaped transform
// must never turn an otherwise valid translated request into a 500.
log?.warn?.(
"COMPRESSION",
"Post-translation OmniGlyph skipped: " +
(error instanceof Error ? error.message : String(error))
);
}
}
trace("post_translation");
// Keep the request translator's namespace identities separate from toolNameMap:

View File

@@ -1,15 +1,15 @@
/**
* OmniGlyph — compressão contexto-como-imagem para os wires Anthropic e OpenAI.
* A versão 1.3.x do pacote também traz transformadores nativos para Chat
* Completions e Responses; o adaptador escolhe o transformador pelo formato do
* provider, nunca pelo formato que o cliente usou na entrada.
* OmniGlyph — compressão contexto-como-imagem (Anthropic/Fable 5 apenas).
* Renderiza system prompt, tool docs, histórico antigo e tool_results grandes
* como páginas PNG densas; o modelo lê as páginas no lugar do texto por ~10×
* menos tokens no bloco convertido (59-70% ponta a ponta, medido).
*
* GATES (todos fail-closed; cada skip vira técnica `skip:<motivo>` nos stats):
* - supportsVision !== true → skip:no_vision
* - modelo fora da allowlist medida → skip:model_not_approved
* - providerTransport !== 'direct' → skip:transport_not_direct
* (agregadores redimensionam imagens e destroem a legibilidade — medido)
* - wire não é Claude/OpenAI suportado → skip:target_format_not_supported
* - corpo não é formato Claude nativo → skip:source_format_not_claude
* - gate de rentabilidade interno do omniglyph decide o resto (patches 28px
* exatos; texto esparso/pequeno passa direto) → skip:not_profitable
*
@@ -19,14 +19,7 @@
import type { CompressionEngine, CompressionEngineApplyOptions } from "./types.ts";
import type { CompressionResult } from "../types.ts";
import { createCompressionStats } from "../stats.ts";
import {
isOmniGlyphSupportedGptModel,
isOmniGlyphSupportedModel,
transformAnthropicMessages,
transformOpenAIChatCompletions,
transformOpenAIResponses,
} from "omniglyph";
import { isModelImageable } from "omniglyph/applicability";
import { transformAnthropicMessages, isOmniGlyphSupportedModel } from "omniglyph";
function skip(body: Record<string, unknown>, reason: string): CompressionResult {
try {
@@ -43,8 +36,6 @@ function skip(body: Record<string, unknown>, reason: string): CompressionResult
}
}
type OmniGlyphWireFormat = "claude" | "openai" | "openai-responses";
/** Formato Claude nativo: system no topo, nunca role:"system" dentro de messages. */
function isClaudeFormat(body: Record<string, unknown>): boolean {
const messages = body.messages;
@@ -52,79 +43,22 @@ function isClaudeFormat(body: Record<string, unknown>): boolean {
return !messages.some((m) => (m as { role?: string } | null)?.role === "system");
}
function inferWireFormat(body: Record<string, unknown>): OmniGlyphWireFormat {
if (Array.isArray(body.input) || typeof body.instructions === "string") {
return "openai-responses";
}
if (!isClaudeFormat(body)) return "openai";
return "claude";
}
function resolveWireFormat(
body: Record<string, unknown>,
options?: CompressionEngineApplyOptions
): OmniGlyphWireFormat | null {
const stage = options?.compressionStage ?? "pre-translation";
const requested = stage === "post-translation" ? options?.targetFormat : options?.sourceFormat;
if (requested === "claude" || requested === "openai" || requested === "openai-responses") {
return requested;
}
if (requested) return null;
return inferWireFormat(body);
}
async function applyOmniglyph(
body: Record<string, unknown>,
options?: CompressionEngineApplyOptions
): Promise<CompressionResult> {
const model = options?.model ?? (body as { model?: string }).model ?? "";
if (options?.supportsVision !== true) return skip(body, "no_vision");
if (!isOmniGlyphSupportedModel(model)) return skip(body, "model_not_approved");
if (options?.providerTransport !== "direct") return skip(body, "transport_not_direct");
const stage = options?.compressionStage ?? "pre-translation";
const wireFormat = resolveWireFormat(body, options);
// The pre-translation lane is retained for the existing native Claude
// passthrough. OpenAI requests must wait until translation has produced the
// exact provider wire, otherwise Responses input[] would be flattened by the
// generic compression adapter and lose native tool/reasoning items.
if (stage === "pre-translation" && wireFormat !== "claude") {
return skip(body, "requires_post_translation");
}
if (!wireFormat) return skip(body, "target_format_not_supported");
if (wireFormat === "claude" && !isClaudeFormat(body)) {
return skip(body, "source_format_not_claude");
}
if (wireFormat === "openai" && !Array.isArray(body.messages)) {
return skip(body, "source_format_not_openai");
}
if (
wireFormat === "openai-responses" &&
!Array.isArray(body.input) &&
typeof body.input !== "string"
) {
return skip(body, "source_format_not_openai_responses");
}
const supportedModel =
wireFormat === "claude"
? isOmniGlyphSupportedModel(model)
: isOmniGlyphSupportedGptModel(model);
if (!supportedModel) return skip(body, "model_not_approved");
// OmniGlyph 1.3.x deliberately keeps unverified families (currently Grok)
// text-only until the operator acknowledges them via its own env gate.
if (!isModelImageable(model)) return skip(body, "model_not_imageable");
if (!isClaudeFormat(body)) return skip(body, "source_format_not_claude");
const started = Date.now();
let outBody: Record<string, unknown>;
try {
const encoded = new TextEncoder().encode(JSON.stringify(body));
const result =
wireFormat === "claude"
? await transformAnthropicMessages({ body: encoded, model })
: wireFormat === "openai"
? await transformOpenAIChatCompletions(encoded)
: await transformOpenAIResponses(encoded);
const applied = wireFormat === "claude" ? result.applied : result.info.compressed;
if (!applied) return skip(body, result.info?.reason ?? "not_profitable");
const result = await transformAnthropicMessages({ body: encoded, model });
if (!result.applied) return skip(body, result.reason ?? "not_profitable");
outBody = JSON.parse(new TextDecoder().decode(result.body)) as Record<string, unknown>;
} catch {
// Fail-open: qualquer erro no encode/transform/decode (ex.: corpo não serializável,
@@ -159,13 +93,11 @@ export const omniglyphEngine: CompressionEngine = {
metadata: {
id: "omniglyph",
name: "OmniGlyph",
description:
"Contexto-como-imagem para Claude Fable 5 e GPT 5.6 via wires nativos Anthropic/OpenAI em rota direta.",
description: "Contexto-como-imagem para Claude Fable 5 via rota direta Anthropic.",
inputScope: "mixed",
targetLatencyMs: 250, // render+encode PNG de páginas grandes
supportsPreview: true,
stable: false, // P1: preview — promover após o e2e P3 (30/30 via OmniRoute)
executionStages: ["pre-translation", "post-translation"],
},
// Contrato da interface: engines async-only mantêm apply síncrono como pass-through seguro.
apply(body) {

View File

@@ -2,11 +2,6 @@ import type { CompressionConfig, CompressionResult } from "../types.ts";
export type CompressionEngineTarget = "messages" | "tool_results" | "code_blocks";
/** Protocol shape and pipeline stage used by format-sensitive engines. */
export type CompressionWireFormat = "claude" | "openai" | "openai-responses" | string;
export type CompressionStage = "pre-translation" | "post-translation";
export interface EngineConfigField {
key: string;
type: "boolean" | "number" | "string" | "select" | "multiselect";
@@ -32,8 +27,6 @@ export interface CompressionEngineMetadata {
targetLatencyMs: number;
supportsPreview: boolean;
stable: boolean;
/** Stages at which this engine can receive a request body. Omitted means pre-translation. */
executionStages?: CompressionStage[];
}
export interface CompressionEngineApplyOptions {
@@ -44,12 +37,6 @@ export interface CompressionEngineApplyOptions {
* exige 'direct' — medição 2026-07-06: agregadores redimensionam as páginas
* e destroem a legibilidade. undefined = desconhecido = skip (fail-closed). */
providerTransport?: "direct" | "aggregator";
/** Protocol shape before the current compression stage. */
sourceFormat?: CompressionWireFormat;
/** Protocol shape expected by the upstream provider. */
targetFormat?: CompressionWireFormat;
/** Whether the body is still client-shaped or already provider-shaped. */
compressionStage?: CompressionStage;
config?: CompressionConfig;
compressionComboId?: string | null;
stepConfig?: Record<string, unknown>;

View File

@@ -12,11 +12,7 @@ import {
isCodexTokenizerContext,
tokenizerContextFromBody,
} from "../../../src/shared/utils/tiktokenCounter.ts";
import {
anthropicImageTokens,
ANTHROPIC_IMAGE_BLOCK_OVERHEAD_TOKENS,
openAIVisionTokens,
} from "omniglyph";
import { anthropicImageTokens, ANTHROPIC_IMAGE_BLOCK_OVERHEAD_TOKENS } from "omniglyph";
const CHARS_PER_TOKEN = 4;
@@ -31,17 +27,6 @@ interface AnthropicImageBlock {
source: { type: "base64"; media_type: string; data: string };
}
interface OpenAIChatImagePart {
type: "image_url";
image_url: { url: string; detail?: string };
}
interface OpenAIResponsesImagePart {
type: "input_image";
image_url: string;
detail?: string;
}
function isAnthropicPngImageBlock(value: unknown): value is AnthropicImageBlock {
if (!value || typeof value !== "object") return false;
const block = value as Record<string, unknown>;
@@ -53,36 +38,6 @@ function isAnthropicPngImageBlock(value: unknown): value is AnthropicImageBlock
);
}
function isOpenAIChatPngImagePart(value: unknown): value is OpenAIChatImagePart {
if (!value || typeof value !== "object") return false;
const part = value as Record<string, unknown>;
const image = part.image_url as Record<string, unknown> | undefined;
return (
part.type === "image_url" &&
!!image &&
typeof image === "object" &&
typeof image.url === "string" &&
image.url.startsWith("data:image/png;base64,")
);
}
function isOpenAIResponsesPngImagePart(value: unknown): value is OpenAIResponsesImagePart {
const part = value as Record<string, unknown> | null;
return (
!!part &&
part.type === "input_image" &&
typeof part.image_url === "string" &&
part.image_url.startsWith("data:image/png;base64,")
);
}
function pngDimensionsFromDataUrl(value: string): { width: number; height: number } | null {
const marker = ",base64,";
const markerIndex = value.indexOf(marker);
if (markerIndex < 0) return null;
return decodePngDimensions(value.slice(markerIndex + marker.length));
}
/**
* Decode PNG width/height from the IHDR chunk without decoding the whole image.
* PNG layout: 8-byte signature, then IHDR chunk `length(4) + "IHDR"(4) + width(4) +
@@ -134,32 +89,17 @@ function blankImageBlocksAndSumImageTokens(body: Record<string, unknown>): {
imageTokens: number;
} {
let imageTokens = 0;
const model = typeof body.model === "string" ? body.model : "";
const clone: Record<string, unknown> = { ...body };
const processContentArray = (content: unknown): unknown => {
if (!Array.isArray(content)) return content;
return content.map((block) => {
if (isAnthropicPngImageBlock(block)) {
const dims = decodePngDimensions(block.source.data);
if (!dims) return block; // fall back to char-counting this block as-is
imageTokens += anthropicImageTokens(dims.width, dims.height, "standard");
imageTokens += ANTHROPIC_IMAGE_BLOCK_OVERHEAD_TOKENS;
return { ...block, source: { ...block.source, data: "" } };
}
if (isOpenAIChatPngImagePart(block)) {
const dims = pngDimensionsFromDataUrl(block.image_url.url);
if (!dims) return block;
imageTokens += openAIVisionTokens(model, dims.width, dims.height);
return { ...block, image_url: { ...block.image_url, url: "" } };
}
if (isOpenAIResponsesPngImagePart(block)) {
const dims = pngDimensionsFromDataUrl(block.image_url);
if (!dims) return block;
imageTokens += openAIVisionTokens(model, dims.width, dims.height);
return { ...block, image_url: "" };
}
return block;
if (!isAnthropicPngImageBlock(block)) return block;
const dims = decodePngDimensions(block.source.data);
if (!dims) return block; // fall back to char-counting this block as-is
imageTokens += anthropicImageTokens(dims.width, dims.height, "standard");
imageTokens += ANTHROPIC_IMAGE_BLOCK_OVERHEAD_TOKENS;
return { ...block, source: { ...block.source, data: "" } };
});
};
@@ -176,16 +116,6 @@ function blankImageBlocksAndSumImageTokens(body: Record<string, unknown>): {
clone.system = processContentArray(clone.system);
}
if (Array.isArray(clone.input)) {
clone.input = clone.input.map((item) => {
if (!item || typeof item !== "object") return item;
const record = item as Record<string, unknown>;
return Array.isArray(record.content)
? { ...record, content: processContentArray(record.content) }
: record;
});
}
return { clone, imageTokens };
}

View File

@@ -7,11 +7,7 @@ import type {
import { applyHardBudget } from "./hardBudget.ts";
import { type FidelityGateConfig } from "./fidelityGate.ts";
import { gateAdvance } from "./fidelityGateStep.ts";
import type {
CompressionEngineApplyOptions,
CompressionStage,
CompressionWireFormat,
} from "./engines/types.ts";
import type { CompressionEngineApplyOptions } from "./engines/types.ts";
import { applyLiteCompression } from "./lite.ts";
import { cavemanCompress } from "./caveman.ts";
import { compressAggressive } from "./aggressive.ts";
@@ -266,9 +262,6 @@ export function applyCompression(
options?: {
model?: string;
supportsVision?: boolean | null;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
config?: CompressionConfig;
principalId?: string;
/**
@@ -292,9 +285,6 @@ function runCompression(
options?: {
model?: string;
supportsVision?: boolean | null;
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
config?: CompressionConfig;
principalId?: string;
bailout?: BailoutConfig;
@@ -479,9 +469,6 @@ export async function applyCompressionAsync(
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
config?: CompressionConfig;
principalId?: string;
onEngineStep?: (step: StackedCompressionStep) => void;
@@ -501,9 +488,6 @@ async function runCompressionAsync(
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
config?: CompressionConfig;
principalId?: string;
onEngineStep?: (step: StackedCompressionStep) => void;
@@ -539,15 +523,10 @@ async function runCompressionAsync(
// Single-mode omniglyph (async-only) — resolution lives in engines/omniglyphSingleMode.ts.
if (mode === "omniglyph") return applyOmniglyphSingleMode(body, options);
if (mode === "stacked") {
// Post-translation format-sensitive engines (currently OmniGlyph) must see
// the native provider wire shape. The generic adapter would turn Responses
// `input[]` into Chat `messages[]` before the engine gets a chance to use its
// native Responses transformer. Pre-translation callers retain the legacy
// adapter path for the text engines.
const adapter =
options?.compressionStage === "post-translation"
? { body, adapted: false, restore: (next: Record<string, unknown>) => next }
: adaptBodyForCompression(body, options?.config?.codexResponsesConfig?.preserveToolNames);
const adapter = adaptBodyForCompression(
body,
options?.config?.codexResponsesConfig?.preserveToolNames
);
const result = await applyStackedCompressionAsync(
adapter.body,
options?.config?.stackedPipeline,
@@ -693,9 +672,6 @@ interface StackOptions {
supportsVision?: boolean | null;
/** Direct-to-provider vs. aggregator transport (gates transport-sensitive engines like omniglyph). */
providerTransport?: "direct" | "aggregator";
sourceFormat?: CompressionWireFormat;
targetFormat?: CompressionWireFormat;
compressionStage?: CompressionStage;
config?: CompressionConfig;
compressionComboId?: string | null;
/** TV1 bail-out discipline (opt-in, default disabled). */
@@ -790,20 +766,6 @@ function buildStepOptions(
};
}
/**
* Engines that were not authored for the provider-shaped post-translation body
* stay in the legacy pre-translation lane. Format-sensitive engines opt into
* both lanes explicitly and perform their own wire-format gate.
*/
function canRunAtCompressionStage(
engine: NonNullable<ReturnType<typeof getCompressionEngine>>,
stage: CompressionStage | undefined
): boolean {
const effectiveStage = stage ?? "pre-translation";
const stages = engine.metadata.executionStages;
return stages ? stages.includes(effectiveStage) : effectiveStage === "pre-translation";
}
function finalizeStackedResult(
originalBody: Record<string, unknown>,
currentBody: Record<string, unknown>,
@@ -932,12 +894,6 @@ function runStackedCompression(
acc.validationErrors.add(`Unknown compression engine: "${step.engine}"`);
continue;
}
if (!canRunAtCompressionStage(engine, options?.compressionStage)) {
acc.validationWarnings.add(
`${step.engine}: skipped (stage ${options?.compressionStage ?? "pre-translation"})`
);
continue;
}
// Respect the registry enabled flag: a step naming a disabled engine is skipped, so an
// operator can turn an engine off (setEngineEnabled) without editing every pipeline.
if (getEngineEntry(step.engine)?.enabled === false) {
@@ -1044,12 +1000,6 @@ async function runStackedCompressionAsync(
acc.validationErrors.add(`Unknown compression engine: "${step.engine}"`);
continue;
}
if (!canRunAtCompressionStage(engine, options?.compressionStage)) {
acc.validationWarnings.add(
`${step.engine}: skipped (stage ${options?.compressionStage ?? "pre-translation"})`
);
continue;
}
// Respect the registry enabled flag (same as the sync loop) — keep both in lockstep.
if (getEngineEntry(step.engine)?.enabled === false) {
acc.validationWarnings.add(`${step.engine}: skipped (engine disabled in registry)`);

View File

@@ -7,6 +7,7 @@
*/
import { markServerReady, markServerStarting } from "@/lib/serverLifecycle";
import { normalizeBootError } from "@/lib/instrumentationBootError";
function getRandomBytes(byteLength: number): Uint8Array {
const bytes = new Uint8Array(byteLength);
@@ -37,23 +38,13 @@ export function renameProcessTitle(currentTitle: string): string {
return `omniroute${currentTitle.slice("next-server".length)}`;
}
/**
* Normalize any thrown/rejected value into a real `Error` instance.
*
* Next.js's own `registerInstrumentation()` wrapper (see
* `node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js`)
* unconditionally does `err.message = \`...${err.message}\`` on whatever our
* `register()` export rejects with, assuming it is always an `Error`. If a raw
* non-Error primitive bubbles up instead (e.g. sql.js's WASM adapter throws the
* bare string `"Database closed"` — see `./lib/db/adapters/sqljsAdapter.ts`),
* that assignment throws `TypeError: Cannot create property 'message' on
* string '...'` in strict mode, masking the original error and crashing the
* whole server on every boot (#6560). Normalizing before it leaves our code
* guarantees Next always receives something `.message`-assignable.
*/
export function normalizeBootError(err: unknown): Error {
return err instanceof Error ? err : new Error(String(err));
}
// `normalizeBootError` now lives in `@/lib/instrumentationBootError` (imported
// above) — shared, dependency-free, and reused by `src/instrumentation.ts`'s
// outermost boot boundary (#10171) so both boot-failure logging sites agree
// on the same normalization instead of maintaining two copies of the same
// one-liner. Re-exported here so existing callers/tests importing it from
// this module keep working unchanged.
export { normalizeBootError };
// Matches sql.js's raw `throw "Database closed"` (and similarly-worded
// variants) thrown when a query runs against an already-closed WASM handle —

View File

@@ -8,12 +8,43 @@
* @see https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
*/
export async function register() {
import { normalizeBootError } from "@/lib/instrumentationBootError";
/**
* `registerNodejsFn` is only for tests to inject a fake without module-mocking
* (`node:test` does not support `mock.module` reliably here) — mirrors the
* same injection pattern as `ensureDbReadyForBoot` in `./instrumentation-node`.
*/
export async function register(registerNodejsFn?: () => Promise<void>) {
if (process.env.NEXT_RUNTIME === "nodejs") {
// Literal path so Webpack emits the chunk (computed string breaks dev:
// MODULE_NOT_FOUND for ./instrumentation-node at runtime).
// Turbopack may still avoid tracing this into Edge when guarded by NEXT_RUNTIME.
const { registerNodejs } = await import("./instrumentation-node");
await registerNodejs();
try {
// Literal path so Webpack emits the chunk (computed string breaks dev:
// MODULE_NOT_FOUND for ./instrumentation-node at runtime).
// Turbopack may still avoid tracing this into Edge when guarded by NEXT_RUNTIME.
const registerNodejs =
registerNodejsFn ?? (await import("./instrumentation-node")).registerNodejs;
await registerNodejs();
} catch (err: unknown) {
// Outermost boot boundary (#10171): any throw during module-load of
// ./instrumentation-node OR anywhere inside registerNodejs() runs
// BEFORE initConsoleInterceptor() is wired up. ensureDbReadyForBoot's
// own #7773 guard already logs one specific failure class (DB driver
// init), but it does not cover a throw that happens before it is even
// reached (e.g. a module-load-time error importing ./instrumentation-node
// itself, as reported on native Windows/WSL2 boots). Without an
// unconditional log line here, the process can keep its HTTP listener
// up while every DB-touching route 500s forever with a permanently
// empty app.log. This guarantees stdout/app.log is never silently
// empty on a failed boot, regardless of platform or which step threw.
// Reuses the same normalizeBootError() helper as ensureDbReadyForBoot
// (./instrumentation-node) — imported from a dependency-free module so
// this file, which also loads under the Edge runtime, never has to pull
// in Node-only code (or risk a second failing dynamic import of
// ./instrumentation-node itself) just to normalize the caught value.
const normalizedError = normalizeBootError(err);
const message = normalizedError.message;
console.error("[STARTUP] Fatal: instrumentation hook failed during boot:", message);
throw normalizedError;
}
}
}

View File

@@ -0,0 +1,26 @@
/**
* Normalize any thrown/rejected value into a real `Error` instance.
*
* Next.js's own `registerInstrumentation()` wrapper (see
* `node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js`)
* unconditionally does `err.message = \`...${err.message}\`` on whatever our
* `register()` export (`src/instrumentation.ts`) rejects with, assuming it is
* always an `Error`. If a raw non-Error primitive bubbles up instead (e.g.
* sql.js's WASM adapter throws the bare string `"Database closed"` — see
* `./db/adapters/sqljsAdapter.ts`), that assignment throws `TypeError: Cannot
* create property 'message' on string '...'` in strict mode, masking the
* original error and crashing the whole server on every boot (#6560).
* Normalizing before it leaves our code guarantees Next always receives
* something `.message`-assignable.
*
* Deliberately dependency-free (no imports) so both `src/instrumentation.ts`
* (the shared Edge+Node instrumentation entry point) and
* `src/instrumentation-node.ts` (the Node-only boot sequence) can import it
* statically without pulling Node-specific modules (fs/path/os/better-sqlite3
* etc.) into the Edge runtime bundle, and without relying on a dynamic
* `import("./instrumentation-node")` that could itself be the thing that
* failed to load (#10171).
*/
export function normalizeBootError(err: unknown): Error {
return err instanceof Error ? err : new Error(String(err));
}

View File

@@ -466,7 +466,6 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
supportsThinking: true,
supportsTools: true,
supportsVision: true,
aliases: ["qwen3.8-max"],
},
"qwen3.6-plus": {
maxOutputTokens: 65536,

View File

@@ -1,86 +0,0 @@
{
"cloudAgentGuard": {
"jules": {
"message": "Provider \"jules\" is a cloud-agent provider and does not support direct chat completions; use the Cloud Agents task API instead.",
"status": 400,
"throws": true
}
},
"fallback": {
"className": "DefaultExecutor",
"configSource": "openai",
"provider": "golden-test-unknown-provider"
},
"searchGuard": {
"brave-search": {
"message": "Provider \"brave-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"duckduckgo-free": {
"message": "Provider \"duckduckgo-free\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"exa-search": {
"message": "Provider \"exa-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"firecrawl": {
"message": "Provider \"firecrawl\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"google-pse-search": {
"message": "Provider \"google-pse-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"linkup-search": {
"message": "Provider \"linkup-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"ollama-search": {
"message": "Provider \"ollama-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"perplexity-search": {
"message": "Provider \"perplexity-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"searchapi-search": {
"message": "Provider \"searchapi-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"searxng-search": {
"message": "Provider \"searxng-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"serper-search": {
"message": "Provider \"serper-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"tavily-search": {
"message": "Provider \"tavily-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"youcom-search": {
"message": "Provider \"youcom-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
},
"zai-search": {
"message": "Provider \"zai-search\" is a search provider and does not support chat completions; use the /v1/search endpoint instead.",
"status": 400,
"throws": true
}
}
}

View File

@@ -1,691 +0,0 @@
{
"entries": {
"9router": {
"className": "NineRouterExecutor",
"configSource": "<custom-config>",
"provider": "9router"
},
"adapta-web": {
"className": "AdaptaWebExecutor",
"configSource": "<custom-config>",
"provider": "adapta-web"
},
"adobe-firefly": {
"className": "AdobeFireflyExecutor",
"configSource": "<custom-config>",
"provider": "adobe-firefly"
},
"adp-web": {
"className": "AdaptaWebExecutor",
"configSource": "<custom-config>",
"provider": "adapta-web"
},
"agy": {
"className": "AntigravityExecutor",
"configSource": "antigravity",
"provider": "antigravity"
},
"amazon-q": {
"className": "KiroExecutor",
"configSource": "kiro",
"provider": "amazon-q"
},
"antigravity": {
"className": "AntigravityExecutor",
"configSource": "antigravity",
"provider": "antigravity"
},
"auggie": {
"className": "AuggieExecutor",
"configSource": "<custom-config>",
"provider": "auggie"
},
"azure-ai": {
"className": "AzureAiExecutor",
"configSource": "openai",
"provider": "azure-ai"
},
"azure-openai": {
"className": "AzureOpenAIExecutor",
"configSource": "openai",
"provider": "azure-openai"
},
"bb-web": {
"className": "BlackboxWebExecutor",
"configSource": "<custom-config>",
"provider": "blackbox-web"
},
"bedrock": {
"className": "BedrockExecutor",
"configSource": "bedrock",
"provider": "bedrock"
},
"blackbox-web": {
"className": "BlackboxWebExecutor",
"configSource": "<custom-config>",
"provider": "blackbox-web"
},
"cbcn": {
"className": "CodeBuddyCnExecutor",
"configSource": "codebuddy-cn",
"provider": "codebuddy-cn"
},
"cf": {
"className": "CloudflareAIExecutor",
"configSource": "cloudflare-ai",
"provider": "cloudflare-ai"
},
"cgpt-codex": {
"className": "ChatGptWebCodexExecutor",
"configSource": "<custom-config>",
"provider": "chatgpt-web-codex"
},
"cgpt-web": {
"className": "ChatGptWebExecutor",
"configSource": "<custom-config>",
"provider": "chatgpt-web"
},
"chatgpt-web": {
"className": "ChatGptWebExecutor",
"configSource": "<custom-config>",
"provider": "chatgpt-web"
},
"chatgpt-web-codex": {
"className": "ChatGptWebCodexExecutor",
"configSource": "<custom-config>",
"provider": "chatgpt-web-codex"
},
"cheaperinference": {
"className": "CheaperInferenceExecutor",
"configSource": "cheaperinference",
"provider": "cheaperinference"
},
"chipotle": {
"className": "ChipotleExecutor",
"configSource": "<custom-config>",
"provider": "chipotle"
},
"cinf": {
"className": "CheaperInferenceExecutor",
"configSource": "cheaperinference",
"provider": "cheaperinference"
},
"claude-web": {
"className": "ClaudeWebExecutor",
"configSource": "<custom-config>",
"provider": "claude-web"
},
"cliproxyapi": {
"className": "CliproxyapiExecutor",
"configSource": "<custom-config>",
"provider": "cliproxyapi"
},
"cloudflare-ai": {
"className": "CloudflareAIExecutor",
"configSource": "cloudflare-ai",
"provider": "cloudflare-ai"
},
"cmd": {
"className": "CommandCodeExecutor",
"configSource": "<custom-config>",
"provider": "command-code"
},
"cnl": {
"className": "ConolWebExecutor",
"configSource": "<custom-config>",
"provider": "conol-web"
},
"codebuddy-cn": {
"className": "CodeBuddyCnExecutor",
"configSource": "codebuddy-cn",
"provider": "codebuddy-cn"
},
"codex": {
"className": "CodexExecutor",
"configSource": "codex",
"provider": "codex"
},
"command-code": {
"className": "CommandCodeExecutor",
"configSource": "<custom-config>",
"provider": "command-code"
},
"conol-web": {
"className": "ConolWebExecutor",
"configSource": "<custom-config>",
"provider": "conol-web"
},
"copilot": {
"className": "CopilotWebExecutor",
"configSource": "<custom-config>",
"provider": "copilot-web"
},
"copilot-m365-web": {
"className": "CopilotM365WebExecutor",
"configSource": "<custom-config>",
"provider": "copilot-m365-web"
},
"copilot-web": {
"className": "CopilotWebExecutor",
"configSource": "<custom-config>",
"provider": "copilot-web"
},
"cpa": {
"className": "CliproxyapiExecutor",
"configSource": "<custom-config>",
"provider": "cliproxyapi"
},
"cu": {
"className": "CursorExecutor",
"configSource": "cursor",
"provider": "cursor"
},
"cursor": {
"className": "CursorExecutor",
"configSource": "cursor",
"provider": "cursor"
},
"cw-web": {
"className": "ClaudeWebExecutor",
"configSource": "<custom-config>",
"provider": "claude-web"
},
"dario": {
"className": "DarioExecutor",
"configSource": "<custom-config>",
"provider": "dario"
},
"db": {
"className": "DoubaoWebExecutor",
"configSource": "<custom-config>",
"provider": "doubao-web"
},
"ddgw": {
"className": "DuckDuckGoWebExecutor",
"configSource": "<custom-config>",
"provider": "duckduckgo-web"
},
"deepseek-web": {
"className": "DeepSeekWebWithAutoRefreshExecutor",
"configSource": "<custom-config>",
"provider": "deepseek-web"
},
"devin": {
"className": "DevinCliExecutor",
"configSource": "<custom-config>",
"provider": "devin-cli"
},
"devin-cli": {
"className": "DevinCliExecutor",
"configSource": "<custom-config>",
"provider": "devin-cli"
},
"devin-cli-agentic": {
"className": "DevinCliAgenticExecutor",
"configSource": "<custom-config>",
"provider": "devin-cli-agentic"
},
"devin-desktop": {
"className": "DevinDesktopExecutor",
"configSource": "devin-desktop",
"provider": "devin-desktop"
},
"doubao-web": {
"className": "DoubaoWebExecutor",
"configSource": "<custom-config>",
"provider": "doubao-web"
},
"dr": {
"className": "DarioExecutor",
"configSource": "<custom-config>",
"provider": "dario"
},
"ds-web": {
"className": "DeepSeekWebWithAutoRefreshExecutor",
"configSource": "<custom-config>",
"provider": "deepseek-web"
},
"duckduckgo-web": {
"className": "DuckDuckGoWebExecutor",
"configSource": "<custom-config>",
"provider": "duckduckgo-web"
},
"felo": {
"className": "FeloWebExecutor",
"configSource": "<custom-config>",
"provider": "felo-web"
},
"felo-web": {
"className": "FeloWebExecutor",
"configSource": "<custom-config>",
"provider": "felo-web"
},
"firefly": {
"className": "AdobeFireflyExecutor",
"configSource": "<custom-config>",
"provider": "adobe-firefly"
},
"gc": {
"className": "GrokCliExecutor",
"configSource": "grok-cli",
"provider": "grok-cli"
},
"gembiz": {
"className": "GeminiBusinessExecutor",
"configSource": "<custom-config>",
"provider": "gemini-business"
},
"gemini-business": {
"className": "GeminiBusinessExecutor",
"configSource": "<custom-config>",
"provider": "gemini-business"
},
"gemini-web": {
"className": "GeminiWebExecutor",
"configSource": "<custom-config>",
"provider": "gemini-web"
},
"ghe-copilot": {
"className": "GheCopilotExecutor",
"configSource": "<custom-config>",
"provider": "ghe-copilot"
},
"github": {
"className": "GithubExecutor",
"configSource": "github",
"provider": "github"
},
"gitlab": {
"className": "GitlabExecutor",
"configSource": "<custom-config>",
"provider": "gitlab"
},
"gitlab-duo": {
"className": "GitlabExecutor",
"configSource": "<custom-config>",
"provider": "gitlab-duo"
},
"glm": {
"className": "GlmExecutor",
"configSource": "glm",
"provider": "glm"
},
"glm-cn": {
"className": "GlmExecutor",
"configSource": "glm-cn",
"provider": "glm-cn"
},
"glmt": {
"className": "GlmExecutor",
"configSource": "glmt",
"provider": "glmt"
},
"grok-cli": {
"className": "GrokCliExecutor",
"configSource": "grok-cli",
"provider": "grok-cli"
},
"grok-web": {
"className": "GrokWebExecutor",
"configSource": "<custom-config>",
"provider": "grok-web"
},
"gweb": {
"className": "GeminiWebExecutor",
"configSource": "<custom-config>",
"provider": "gemini-web"
},
"ha": {
"className": "HyperAgentExecutor",
"configSource": "<custom-config>",
"provider": "hyperagent"
},
"hailuo-web": {
"className": "HailuoWebExecutor",
"configSource": "<custom-config>",
"provider": "hailuo-web"
},
"hc": {
"className": "HuggingChatExecutor",
"configSource": "<custom-config>",
"provider": "huggingchat"
},
"huggingchat": {
"className": "HuggingChatExecutor",
"configSource": "<custom-config>",
"provider": "huggingchat"
},
"hyperagent": {
"className": "HyperAgentExecutor",
"configSource": "<custom-config>",
"provider": "hyperagent"
},
"in-ai": {
"className": "InnerAiExecutor",
"configSource": "<custom-config>",
"provider": "inner-ai"
},
"inner-ai": {
"className": "InnerAiExecutor",
"configSource": "<custom-config>",
"provider": "inner-ai"
},
"kimi": {
"className": "MoonshotExecutor",
"configSource": "kimi",
"provider": "kimi"
},
"kimi-coding": {
"className": "KimiExecutor",
"configSource": "kimi-coding",
"provider": "kimi-coding"
},
"kimi-coding-apikey": {
"className": "KimiExecutor",
"configSource": "kimi-coding-apikey",
"provider": "kimi-coding-apikey"
},
"kimi-web": {
"className": "KimiWebExecutor",
"configSource": "<custom-config>",
"provider": "kimi-web"
},
"kiro": {
"className": "KiroExecutor",
"configSource": "kiro",
"provider": "kiro"
},
"lma": {
"className": "LMArenaExecutor",
"configSource": "<custom-config>",
"provider": "lmarena"
},
"lmarena": {
"className": "LMArenaExecutor",
"configSource": "<custom-config>",
"provider": "lmarena"
},
"mcode": {
"className": "MimocodeExecutor",
"configSource": "<custom-config>",
"provider": "mimocode"
},
"microsoft-designer-web": {
"className": "MicrosoftDesignerWebExecutor",
"configSource": "<custom-config>",
"provider": "microsoft-designer-web"
},
"mimocode": {
"className": "MimocodeExecutor",
"configSource": "<custom-config>",
"provider": "mimocode"
},
"moonshot": {
"className": "MoonshotExecutor",
"configSource": "moonshot",
"provider": "moonshot"
},
"ms-web": {
"className": "MuseSparkWebExecutor",
"configSource": "<custom-config>",
"provider": "muse-spark-web"
},
"msdesigner": {
"className": "MicrosoftDesignerWebExecutor",
"configSource": "<custom-config>",
"provider": "microsoft-designer-web"
},
"muse-spark-web": {
"className": "MuseSparkWebExecutor",
"configSource": "<custom-config>",
"provider": "muse-spark-web"
},
"nlpcloud": {
"className": "NlpCloudExecutor",
"configSource": "nlpcloud",
"provider": "nlpcloud"
},
"notion-web": {
"className": "NotionWebExecutor",
"configSource": "<custom-config>",
"provider": "notion-web"
},
"nr": {
"className": "NineRouterExecutor",
"configSource": "<custom-config>",
"provider": "9router"
},
"nw": {
"className": "NotionWebExecutor",
"configSource": "<custom-config>",
"provider": "notion-web"
},
"opencode": {
"className": "OpencodeExecutor",
"configSource": "opencode-zen",
"provider": "opencode-zen"
},
"opencode-go": {
"className": "OpencodeExecutor",
"configSource": "opencode-go",
"provider": "opencode-go"
},
"opencode-zen": {
"className": "OpencodeExecutor",
"configSource": "opencode-zen",
"provider": "opencode-zen"
},
"pepper": {
"className": "ChipotleExecutor",
"configSource": "<custom-config>",
"provider": "chipotle"
},
"perplexity-web": {
"className": "PerplexityWebExecutor",
"configSource": "<custom-config>",
"provider": "perplexity-web"
},
"poe-web": {
"className": "PoeWebExecutor",
"configSource": "<custom-config>",
"provider": "poe-web"
},
"pol": {
"className": "PollinationsExecutor",
"configSource": "pollinations",
"provider": "pollinations"
},
"pollinations": {
"className": "PollinationsExecutor",
"configSource": "pollinations",
"provider": "pollinations"
},
"pplx-web": {
"className": "PerplexityWebExecutor",
"configSource": "<custom-config>",
"provider": "perplexity-web"
},
"pql": {
"className": "PromptQlExecutor",
"configSource": "<custom-config>",
"provider": "promptql"
},
"promptql": {
"className": "PromptQlExecutor",
"configSource": "<custom-config>",
"provider": "promptql"
},
"qoder": {
"className": "QoderExecutor",
"configSource": "qoder",
"provider": "qoder"
},
"qw": {
"className": "QwenWebExecutor",
"configSource": "<custom-config>",
"provider": "qwen-web"
},
"qwen-web": {
"className": "QwenWebExecutor",
"configSource": "<custom-config>",
"provider": "qwen-web"
},
"raycast": {
"className": "RaycastExecutor",
"configSource": "raycast",
"provider": "raycast"
},
"rc": {
"className": "RaycastExecutor",
"configSource": "raycast",
"provider": "raycast"
},
"t3-web": {
"className": "T3ChatWebExecutor",
"configSource": "<custom-config>",
"provider": "t3-web"
},
"t3chat": {
"className": "T3ChatWebExecutor",
"configSource": "<custom-config>",
"provider": "t3-web"
},
"tasw": {
"className": "TencentAIStudioWebExecutor",
"configSource": "<custom-config>",
"provider": "tencent-aistudio-web"
},
"tcw": {
"className": "TinyCmsExecutor",
"configSource": "<custom-config>",
"provider": "tinycms-web"
},
"tencent-aistudio-web": {
"className": "TencentAIStudioWebExecutor",
"configSource": "<custom-config>",
"provider": "tencent-aistudio-web"
},
"theoldllm": {
"className": "TheOldLlmExecutor",
"configSource": "<custom-config>",
"provider": "theoldllm"
},
"tinycms-web": {
"className": "TinyCmsExecutor",
"configSource": "<custom-config>",
"provider": "tinycms-web"
},
"tllm": {
"className": "TheOldLlmExecutor",
"configSource": "<custom-config>",
"provider": "theoldllm"
},
"trae": {
"className": "TraeExecutor",
"configSource": "trae",
"provider": "trae"
},
"v0": {
"className": "V0VercelWebExecutor",
"configSource": "<custom-config>",
"provider": "v0-vercel-web"
},
"v0-vercel-web": {
"className": "V0VercelWebExecutor",
"configSource": "<custom-config>",
"provider": "v0-vercel-web"
},
"ven": {
"className": "VeniceWebExecutor",
"configSource": "<custom-config>",
"provider": "venice-web"
},
"venice-web": {
"className": "VeniceWebExecutor",
"configSource": "<custom-config>",
"provider": "venice-web"
},
"veo-free": {
"className": "VeoAIFreeWebExecutor",
"configSource": "<custom-config>",
"provider": "veoaifree-web"
},
"veoaifree-web": {
"className": "VeoAIFreeWebExecutor",
"configSource": "<custom-config>",
"provider": "veoaifree-web"
},
"vertex": {
"className": "VertexExecutor",
"configSource": "vertex",
"provider": "vertex"
},
"vertex-partner": {
"className": "VertexExecutor",
"configSource": "vertex",
"provider": "vertex"
},
"xai": {
"className": "XaiExecutor",
"configSource": "xai",
"provider": "xai"
},
"xai-oauth": {
"className": "XaiExecutor",
"configSource": "xai-oauth",
"provider": "xai-oauth"
},
"xao": {
"className": "XaiExecutor",
"configSource": "xai-oauth",
"provider": "xai-oauth"
},
"ybw": {
"className": "YuanbaoWebExecutor",
"configSource": "<custom-config>",
"provider": "yuanbao-web"
},
"yuanbao-web": {
"className": "YuanbaoWebExecutor",
"configSource": "<custom-config>",
"provider": "yuanbao-web"
},
"zai-web": {
"className": "ZaiWebExecutor",
"configSource": "<custom-config>",
"provider": "zai-web"
},
"zc": {
"className": "ZcodeExecutor",
"configSource": "<custom-config>",
"provider": "zcode"
},
"zcode": {
"className": "ZcodeExecutor",
"configSource": "<custom-config>",
"provider": "zcode"
},
"zed-hosted": {
"className": "ZedHostedExecutor",
"configSource": "zed-hosted",
"provider": "zed-hosted"
},
"zenmux-free": {
"className": "ZenmuxFreeExecutor",
"configSource": "<custom-config>",
"provider": "zenmux-free"
},
"zmf": {
"className": "ZenmuxFreeExecutor",
"configSource": "<custom-config>",
"provider": "zenmux-free"
},
"zw": {
"className": "ZaiWebExecutor",
"configSource": "<custom-config>",
"provider": "zai-web"
}
},
"keyCount": 137,
"sharedInstances": []
}

View File

@@ -48,14 +48,6 @@ test("buildCodexProviderArgs defines the omniroute provider inline (works withou
assert.equal(args.filter((a) => a === "-c").length, 6);
});
test("buildCodexProviderArgs accepts a model id and serializes it into provider args", () => {
const args = buildCodexProviderArgs("http://vps:20128", "glm/glm-4.5");
assert.equal(args.includes("-c"), true);
assert.ok(args.some((arg) => arg === 'model_providers.omniroute.model="glm/glm-4.5"'));
// model is optional => previous 6 assignments + one extra
assert.equal(args.filter((a) => a === "-c").length, 7);
});
test("resolveCodexTarget: --remote wins and /v1 is stripped from the root", () => {
const { baseUrl } = resolveCodexTarget({ remote: "http://vps:20128/v1" });
assert.equal(baseUrl, "http://vps:20128");

View File

@@ -1,101 +0,0 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import {
buildRunPlan,
resolveRunTarget,
resolveModelFromTargetOptions,
runCliTarget,
} from "../../../bin/cli/commands/run.mjs";
test("resolveRunTarget resolves aliases", () => {
assert.equal(resolveRunTarget("claude"), "claude");
assert.equal(resolveRunTarget("CLAUDE-CODE"), "claude");
assert.equal(resolveRunTarget("cc"), "claude");
assert.equal(resolveRunTarget("codex"), "codex");
assert.equal(resolveRunTarget("openai-codex"), "codex");
assert.equal(resolveRunTarget("openai"), "codex");
assert.equal(resolveRunTarget("unknown"), undefined);
});
test("resolveModelFromTargetOptions prefixes provider", () => {
assert.equal(resolveModelFromTargetOptions({ provider: "glm", model: "glm-4.5" }), "glm/glm-4.5");
assert.equal(
resolveModelFromTargetOptions({ provider: "glm", model: "glm/glm-4.5" }),
"glm/glm-4.5"
);
assert.equal(resolveModelFromTargetOptions({ model: "glm/glm-4.5" }), "glm/glm-4.5");
assert.equal(resolveModelFromTargetOptions({ provider: "glm" }), "");
});
test("buildRunPlan for claude includes env diff and model injection", async () => {
const plan = await buildRunPlan(
"claude",
{ port: "20128", apiKey: "sk_test_x", model: "gpt-5" },
["--help"]
);
assert.equal(plan.target, "claude");
assert.equal(plan.baseUrl, "http://localhost:20128");
assert.equal(plan.model, "gpt-5");
assert.equal(plan.args.includes("--help"), true);
assert.equal(plan.envDiff.changedOrAdded.includes("ANTHROPIC_AUTH_TOKEN"), true);
assert.equal(plan.authSource, "option");
assert.equal(plan.command.includes("claude"), true);
});
test("buildRunPlan for codex injects model into provider args", async () => {
const plan = await buildRunPlan(
"codex",
{ baseUrl: "http://localhost:20128", apiKey: "sk_test_x", model: "glm/glm-4.5" },
["--help"]
);
assert.equal(plan.target, "codex");
assert.equal(plan.baseUrl, "http://localhost:20128");
assert.equal(plan.model, "glm/glm-4.5");
assert.equal(plan.args.includes("--help"), true);
assert.equal(
plan.args.some((a) => String(a).includes("model_providers.omniroute.model")),
true
);
assert.equal(plan.authSource, "option");
});
test("runCliTarget returns usage error code for unsupported targets", async () => {
const seen = [];
const originalWrite = process.stderr.write;
// @ts-ignore
process.stderr.write = (chunk) => {
seen.push(String(chunk));
return true;
};
const code = await runCliTarget("nope", {}, ["--help"]);
// @ts-ignore
process.stderr.write = originalWrite;
assert.equal(code, 2);
assert.equal(seen.length > 0, true);
assert.match(seen.join(""), /Unsupported target/);
});
test("dry-run --json does not print resolved auth token", async () => {
const chunks = [];
const originalStdout = process.stdout.write;
// @ts-ignore
process.stdout.write = (chunk) => {
chunks.push(String(chunk));
return true;
};
const code = await runCliTarget(
"claude",
{ dryRun: true, json: true, apiKey: "sk_live_very_private_token", model: "gpt-5" },
[]
);
// @ts-ignore
process.stdout.write = originalStdout;
assert.equal(code, 0);
const raw = chunks.join("");
assert.equal(raw.includes("sk_live_very_private_token"), false);
});

View File

@@ -19,50 +19,6 @@ function claudeBody(): Record<string, unknown> {
};
}
const OK = { model: "claude-fable-5", supportsVision: true, providerTransport: "direct" as const };
const GPT_OK = {
model: "gpt-5.6",
supportsVision: true,
providerTransport: "direct" as const,
sourceFormat: "openai" as const,
targetFormat: "openai" as const,
compressionStage: "post-translation" as const,
};
function openaiChatBody(): Record<string, unknown> {
return {
model: "gpt-5.6",
messages: [
{ role: "system", content: DENSE },
{ role: "user", content: "oi" },
],
tools: [
{
type: "function",
function: {
name: "inspect_repository",
description: DENSE,
parameters: { type: "object", properties: { path: { type: "string" } } },
},
},
],
};
}
function openaiResponsesBody(): Record<string, unknown> {
return {
model: "gpt-5.6",
instructions: DENSE,
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "oi" }] }],
tools: [
{
type: "function",
name: "inspect_repository",
description: DENSE,
parameters: { type: "object", properties: { path: { type: "string" } } },
},
],
};
}
test("happy path: comprime corpo claude denso em blocos de imagem", async () => {
const r = await omniglyphEngine.applyAsync!(claudeBody(), OK);
@@ -124,26 +80,3 @@ test("fail-open: erro no transform vira skip, nunca propaga", async () => {
assert.equal(r.compressed, false);
assert.deepEqual(r.body, body); // corpo original devolvido intacto
});
test("versão atual: transforma Chat Completions OpenAI no wire pós-tradução", async () => {
const r = await omniglyphEngine.applyAsync!(openaiChatBody(), GPT_OK);
assert.equal(r.compressed, true);
assert.ok(JSON.stringify(r.body).includes('"type":"image_url"'));
});
test("versão atual: transforma OpenAI Responses preservando input[]", async () => {
const options = { ...GPT_OK, targetFormat: "openai-responses" as const };
const r = await omniglyphEngine.applyAsync!(openaiResponsesBody(), options);
assert.equal(r.compressed, true);
assert.ok(JSON.stringify(r.body).includes('"type":"input_image"'));
assert.ok(Array.isArray((r.body as { input?: unknown }).input));
});
test("OpenAI não roda no estágio pré-tradução", async () => {
const r = await omniglyphEngine.applyAsync!(openaiChatBody(), {
...GPT_OK,
compressionStage: "pre-translation",
});
assert.equal(r.compressed, false);
assert.ok(r.stats?.techniquesUsed.includes("skip:requires_post_translation"));
});

View File

@@ -4,13 +4,7 @@ import assert from "node:assert";
test("pacote omniglyph exporta a API que o adapter consome", async () => {
const mod = await import("omniglyph");
assert.equal(typeof mod.transformAnthropicMessages, "function");
assert.equal(typeof mod.transformOpenAIChatCompletions, "function");
assert.equal(typeof mod.transformOpenAIResponses, "function");
assert.equal(typeof mod.isOmniGlyphSupportedModel, "function");
assert.equal(typeof mod.isOmniGlyphSupportedGptModel, "function");
assert.equal(mod.isOmniGlyphSupportedModel("claude-fable-5"), true);
assert.equal(mod.isOmniGlyphSupportedModel("gpt-5.5"), false);
const applicability = await import("omniglyph/applicability");
assert.equal(typeof applicability.isModelImageable, "function");
});

View File

@@ -1,134 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// R0.3 GOLDEN LOCK (characterization BEFORE the ExecutorRegistry refactor):
// freeze the full provider-id → executor mapping of open-sse/executors/index.ts —
// every specialized key with its executor class, effective provider identity and
// which PROVIDERS config entry backs it — plus the getExecutor() dispatch rules
// (specialized hit, DefaultExecutor fallback + cache, cloud-agent guard #6699,
// search-provider guard #10274). The registry refactor must keep this snapshot
// byte-identical: any drift in keys, classes, provider identity or guard behavior
// is a golden diff, not a silent routing change.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-executor-golden-"));
process.env.DATA_DIR = TEST_DATA_DIR;
// Dynamic imports AFTER DATA_DIR is set so db/core.ts picks up the temp path.
const { getExecutor, hasSpecializedExecutor, DefaultExecutor } = await import(
"../../open-sse/executors/index.ts"
);
const { PROVIDERS } = await import("../../open-sse/config/constants.ts");
const { SEARCH_PROVIDERS } = await import("../../open-sse/config/searchRegistry.ts");
const { goldenSnapshot } = await import("../helpers/goldenSnapshot.ts");
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// The specialized keys are not exported; enumerate them through the public
// surface by probing every plausible id source AND the literal keys read from
// the module source. Reading the source keeps the golden honest: a key added
// to (or removed from) the hard-coded map cannot hide from the snapshot.
function readSpecializedKeys(): string[] {
const src = fs.readFileSync(
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 keys: string[] = [];
for (const line of mapMatch[1].split("\n")) {
const m = line.match(/^\s*(?:"([^"]+)"|([A-Za-z0-9_$-]+)):\s*new /);
if (m) keys.push(m[1] ?? m[2]);
}
return keys;
}
// Map a ProviderConfig object back to its PROVIDERS key by identity, so the
// snapshot records WHICH config backs each executor without freezing the whole
// (huge, frequently-edited) config content.
const providerConfigKeyByRef = new Map<object, string>();
for (const [key, cfg] of Object.entries(PROVIDERS)) {
if (cfg && typeof cfg === "object" && !providerConfigKeyByRef.has(cfg)) {
providerConfigKeyByRef.set(cfg, key);
}
}
function describeExecutor(instance: unknown): {
className: string;
provider: string | null;
configSource: string | null;
} {
const inst = instance as { constructor: { name: string }; provider?: string; config?: object };
const cfg = inst.config;
return {
className: inst.constructor.name,
provider: typeof inst.provider === "string" ? inst.provider : null,
configSource:
cfg == null ? null : (providerConfigKeyByRef.get(cfg) ?? "<custom-config>"),
};
}
const specializedKeys = readSpecializedKeys();
test("golden: specialized executor map — key → class + provider identity + config source", () => {
assert.ok(specializedKeys.length >= 100, `suspiciously few keys: ${specializedKeys.length}`);
const entries: Record<
string,
{ className: string; provider: string | null; configSource: string | null }
> = {};
const byInstance = new Map<unknown, string[]>();
for (const key of [...specializedKeys].sort()) {
assert.equal(hasSpecializedExecutor(key), true, `hasSpecializedExecutor(${key})`);
const instance = getExecutor(key);
entries[key] = describeExecutor(instance);
const group = byInstance.get(instance) ?? [];
group.push(key);
byInstance.set(instance, group);
}
// Keys sharing the SAME instance share per-instance state (session pools,
// rotation cooldowns); today every map entry is its own `new X()`. Freeze that.
const sharedInstances = [...byInstance.values()]
.filter((keys) => keys.length > 1)
.map((keys) => keys.sort())
.sort((a, b) => a[0].localeCompare(b[0]));
goldenSnapshot("executors/executor-map", {
keyCount: specializedKeys.length,
entries,
sharedInstances,
});
});
test("golden: getExecutor dispatch rules — fallback, cache and 400-guards", () => {
// 1. Unknown provider → DefaultExecutor for that provider, memoized.
const unknown = "golden-test-unknown-provider";
assert.equal(hasSpecializedExecutor(unknown), false);
const fallback = getExecutor(unknown);
assert.ok(fallback instanceof DefaultExecutor, "fallback must be DefaultExecutor");
assert.equal(getExecutor(unknown), fallback, "DefaultExecutor fallback must be cached");
// 2. Cloud-agent guard (#6699) and search guard (#10274) → status-400 throw.
const guardOutcome = (provider: string) => {
try {
getExecutor(provider);
return { throws: false as const };
} catch (err) {
const e = err as Error & { status?: number };
return { throws: true as const, status: e.status ?? null, message: e.message };
}
};
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)])),
});
});

View File

@@ -1,57 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
// R0.3 — unit tests for the ExecutorRegistry seam itself (registration
// semantics + wiring of the built-ins). Behavior parity of the full map is
// covered separately by tests/unit/executor-map-golden.test.ts.
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-executor-registry-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const { registerExecutor, getRegisteredExecutor, hasRegisteredExecutor, listExecutorAliases } =
await import("../../open-sse/executors/registry.ts");
const { getExecutor, hasSpecializedExecutor, BaseExecutor, DefaultExecutor } = await import(
"../../open-sse/executors/index.ts"
);
test.after(() => {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
test("built-ins are registered at module load and resolve through the registry", () => {
const aliases = listExecutorAliases();
assert.ok(aliases.length >= 100, `expected the built-in table, got ${aliases.length} aliases`);
for (const alias of ["antigravity", "kiro", "glm", "9router", "conol-web"]) {
assert.ok(hasRegisteredExecutor(alias), `missing built-in: ${alias}`);
assert.equal(getExecutor(alias), getRegisteredExecutor(alias));
assert.ok(getExecutor(alias) instanceof BaseExecutor);
}
});
test("registerExecutor throws on duplicate alias", () => {
assert.throws(() => registerExecutor("kiro", getRegisteredExecutor("kiro")!), {
message: /already registered: "kiro"/,
});
});
test("registering a new alias makes it resolvable via getExecutor and hasSpecializedExecutor", () => {
const alias = "registry-test-provider";
assert.equal(hasSpecializedExecutor(alias), false);
const instance = new DefaultExecutor(alias);
registerExecutor(alias, instance);
assert.equal(hasSpecializedExecutor(alias), true);
assert.equal(getExecutor(alias), instance);
});
test("registry lookup is exact — Object.prototype names are not executors", () => {
// The old object-literal lookup (`executors[provider]`) leaked prototype
// members: getExecutor("constructor") returned Object's constructor. The Map
// registry must treat these as unknown providers (DefaultExecutor fallback).
for (const name of ["constructor", "toString", "hasOwnProperty", "__proto__"]) {
assert.equal(hasSpecializedExecutor(name), false, name);
assert.ok(getExecutor(name) instanceof DefaultExecutor, name);
}
});

View File

@@ -0,0 +1,122 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { register } from "../../src/instrumentation";
// Regression guard for #10171: on native Windows/WSL2 boots, the reported
// symptom is a bare HTTP 500 on every DB-touching route with `app.log`
// staying completely empty, even though the CLI prints "OmniRoute is
// running!". ensureDbReadyForBoot() (#7773/#7828) already guarantees a
// non-empty [STARTUP] Fatal: log line for ONE specific failure class
// (database driver init), but nothing previously guaranteed a fatal throw
// ANYWHERE during instrumentation-hook boot (including a throw before
// ensureDbReadyForBoot is even reached) produces a diagnostic line. This
// test proves register() now catches any such throw at the outermost
// boundary and unconditionally logs a `[STARTUP] Fatal:` line to stdout
// before rethrowing, so app.log is never silently empty on a failed boot.
function captureConsoleError(): { captured: string[]; restore: () => void } {
const originalError = console.error;
const captured: string[] = [];
console.error = (...args: unknown[]) => {
captured.push(args.map((arg) => String(arg)).join(" "));
};
return {
captured,
restore: () => {
console.error = originalError;
},
};
}
test("#10171: any instrumentation-hook boot throw is logged with a non-empty [STARTUP] Fatal: line before rethrow", async () => {
const { captured, restore } = captureConsoleError();
const previousRuntime = process.env.NEXT_RUNTIME;
process.env.NEXT_RUNTIME = "nodejs";
// Simulates a module-load-time failure reaching the instrumentation hook
// BEFORE ensureDbReadyForBoot's own #7773 guard is reached — the class of
// failure the reporter's empty app.log suggests on native Windows/WSL2.
const bootFailureMessage = "Cannot find native binding for platform=win32-x64";
const fakeRegisterNodejs = async () => {
throw new Error(bootFailureMessage);
};
try {
await assert.rejects(
() => register(fakeRegisterNodejs),
(err: Error) => err.message === bootFailureMessage
);
const fatalLine = captured.find(
(line) => line.includes("[STARTUP] Fatal:") && line.includes(bootFailureMessage)
);
assert.ok(
fatalLine,
"Expected a non-empty '[STARTUP] Fatal:' console.error line containing the boot " +
"failure before it propagates, so app.log is never silently empty on a failed " +
"boot (#10171). None was logged."
);
} finally {
if (previousRuntime === undefined) {
delete process.env.NEXT_RUNTIME;
} else {
process.env.NEXT_RUNTIME = previousRuntime;
}
restore();
}
});
test("#10171: a non-Error throw during instrumentation-hook boot is still logged with a non-empty [STARTUP] Fatal: line", async () => {
const { captured, restore } = captureConsoleError();
const previousRuntime = process.env.NEXT_RUNTIME;
process.env.NEXT_RUNTIME = "nodejs";
// Mirrors real-world non-Error throws (e.g. sql.js's bare `throw "Database closed"`,
// #6560) reaching this outermost boundary before normalization.
const fakeRegisterNodejs = async () => {
throw "raw string boot failure";
};
try {
await assert.rejects(
() => register(fakeRegisterNodejs),
(err: unknown) => {
assert.ok(err instanceof Error, "register() must rethrow a real Error instance");
err.message += " (augmented by Next)";
assert.equal(err.message, "raw string boot failure (augmented by Next)");
return true;
}
);
const fatalLine = captured.find(
(line) => line.includes("[STARTUP] Fatal:") && line.includes("raw string boot failure")
);
assert.ok(fatalLine, "Expected a non-empty '[STARTUP] Fatal:' line for a non-Error throw too.");
} finally {
if (previousRuntime === undefined) {
delete process.env.NEXT_RUNTIME;
} else {
process.env.NEXT_RUNTIME = previousRuntime;
}
restore();
}
});
test("register() does not log anything on a clean successful boot", async () => {
const { captured, restore } = captureConsoleError();
const previousRuntime = process.env.NEXT_RUNTIME;
process.env.NEXT_RUNTIME = "nodejs";
const fakeRegisterNodejs = async () => {};
try {
await assert.doesNotReject(register(fakeRegisterNodejs));
assert.equal(captured.length, 0, "a successful boot must not emit any fatal startup log lines");
} finally {
if (previousRuntime === undefined) {
delete process.env.NEXT_RUNTIME;
} else {
process.env.NEXT_RUNTIME = previousRuntime;
}
restore();
}
});

View File

@@ -107,8 +107,6 @@ test("opencode-go family: context/output caps match upstream provider docs", ()
assert.equal(getModelSpec("qwen3.8-max-preview").supportsThinking, true);
assert.equal(getModelSpec("qwen3.8-max-preview").supportsTools, true);
assert.equal(getModelSpec("qwen3.8-max-preview").supportsVision, true);
assert.equal(getModelSpec("qwen3.8-max").contextWindow, 1000000);
assert.equal(getModelSpec("qwen3.8-max").maxOutputTokens, 65536);
assert.equal(getModelSpec("qwen3.7-max").contextWindow, 1000000);
assert.equal(getModelSpec("qwen3-max-2026-01-23").contextWindow, 1000000);
assert.equal(getModelSpec("qwen3.6-plus").contextWindow, 1000000);