Compare commits

..

4 Commits

Author SHA1 Message Date
adevwithpurpose
ca4ba987b5 fix(cliproxy): thread runtime platform as a parameter instead of re-reading os.platform()
extractZip(), installVersion(), and rollbackVersion() each independently called
os.platform() inline in their own module scope even after #10244 switched the
detection helpers to os.platform()/os.arch(). Each independent call site is its
own opportunity for a bundler to constant-fold that particular occurrence away.

Detect the runtime platform once per orchestrating call (installVersion,
downloadRelease, rollbackVersion) and thread the already-detected value down as
an explicit parameter into extractZip and the symlink/copy decisions, instead of
re-reading the global in every helper.
2026-08-17 22:40:54 -03:00
Diego Rodrigues de Sa e Souza
060a906dee Merge branch 'release/v3.8.50' into fix/10244-cliproxy-win-platform-detection 2026-08-17 11:56:53 -03:00
adevwithpurpose
6b5d82133a fix(cliproxy): use runtime platform for binary install paths
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-17 10:51:31 -03:00
adevwithpurpose
6bc9dad0ce fix(cliproxy): read os.platform()/os.arch() at runtime in binaryManager platform detection (#10244)
detectPlatform()/detectArch() read the module's process.platform/process.arch,
which Turbopack `next build` (run only on Linux) constant-folds, pruning every
Windows/arm64 branch from the published npm artifact — so the embedded CLIProxyAPI
installer downloads the Linux ELF binary on Windows. Switch to runtime os.platform()/
os.arch() calls (the repo's established anti-fold pattern) so the Windows/ARM branches
survive any build machine. Add a regression guard mocking os.platform()/os.arch() to
win32/arm64 asserting the Windows/ARM path is reachable — RED before, GREEN after.
2026-08-15 11:34:33 -03:00
65 changed files with 351 additions and 1036 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(cliproxy):** read platform/arch at runtime via `os.platform()`/`os.arch()` in `binaryManager` so the embedded installer selects the Windows/ARM assets even when the release bundle is built on a Linux runner (fixes #10244)

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

@@ -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

@@ -16,7 +16,7 @@ type Platform = "linux" | "darwin" | "windows" | "freebsd";
type Arch = "amd64" | "arm64";
function detectPlatform(): Platform {
const p = process.platform;
const p = os.platform();
if (p === "linux") return "linux";
if (p === "darwin") return "darwin";
if (p === "win32") return "windows";
@@ -24,7 +24,7 @@ function detectPlatform(): Platform {
}
function detectArch(): Arch {
const a = process.arch;
const a = os.arch();
if (a === "x64") return "amd64";
if (a === "arm64") return "arm64";
return "amd64";
@@ -81,8 +81,21 @@ export function buildExtractZipCommand(
return { command: "unzip", args: ["-o", archivePath, "-d", destDir] };
}
async function extractZip(archivePath: string, destDir: string): Promise<void> {
const { command, args } = buildExtractZipCommand(process.platform, archivePath, destDir);
/**
* #10244/#10293: `platform` MUST be an explicit parameter threaded down from the
* caller's single runtime detection (see `installVersion`/`downloadRelease`), not
* an independent `os.platform()` read inside this function. Multiple, independently
* evaluated `os.platform()` call sites scattered across the module are each an
* opportunity for a bundler to constant-fold that particular occurrence away — a
* single detected value threaded as data through the call chain has no per-call-site
* literal for the bundler to fold.
*/
async function extractZip(
archivePath: string,
destDir: string,
platform: NodeJS.Platform
): Promise<void> {
const { command, args } = buildExtractZipCommand(platform, archivePath, destDir);
await execFileAsync(command, args);
}
@@ -110,12 +123,16 @@ function findBinaryInDir(dir: string): string | null {
export async function downloadRelease(
version: string,
targetDir: string,
signal?: AbortSignal
signal?: AbortSignal,
// Optional pre-detected target: lets a top-level orchestrator (installVersion)
// read the runtime platform/arch exactly once and pass the value down instead of
// this function independently re-reading os.platform()/os.arch() (#10244/#10293).
target?: { platform: Platform; arch: Arch }
): Promise<string> {
const release = await getReleaseByVersion(version);
if (!release) throw new Error(`Version ${version} not found`);
const { platform, arch } = getTargetPlatform();
const { platform, arch } = target || getTargetPlatform();
const ext = platform === "windows" ? ".zip" : ".tar.gz";
const assetName = `CLIProxyAPI_${release.version}_${platform}_${arch}${ext}`;
const asset = release.assets.find((a) => a.name === assetName);
@@ -140,7 +157,10 @@ export async function downloadRelease(
}
if (platform === "windows") {
await extractZip(archivePath, versionDir);
// Already inside the `platform === "windows"` branch of the single value
// detected above (or threaded in via `target`) — pass the corresponding
// NodeJS.Platform literal directly rather than calling os.platform() again.
await extractZip(archivePath, versionDir, "win32");
} else {
await extractTarGz(archivePath, versionDir);
}
@@ -159,13 +179,18 @@ export async function installVersion(version: string, dataDir?: string): Promise
const binDir = path.join(dir, "bin");
await fs.mkdir(binDir, { recursive: true });
const binary = await downloadRelease(version, binDir);
// Single runtime detection for this whole orchestration: read once here and
// thread the value into downloadRelease() and the symlink/copy decision below,
// instead of each step re-reading os.platform()/os.arch() independently
// (#10244/#10293 — redundant reads are each an independent build-folding risk).
const target = getTargetPlatform();
const binary = await downloadRelease(version, binDir, undefined, target);
const symlinkPath = path.join(binDir, "cliproxyapi");
try {
await fs.unlink(symlinkPath);
} catch {}
if (process.platform === "win32") {
if (target.platform === "windows") {
await fs.copyFile(binary, symlinkPath);
} else {
await fs.symlink(binary, symlinkPath);
@@ -219,7 +244,11 @@ export async function rollbackVersion(dataDir?: string): Promise<string | null>
try {
await fs.unlink(symlinkPath);
} catch {}
if (process.platform === "win32") {
// Single runtime detection for this orchestration, via the module's one
// canonical read point (getTargetPlatform -> detectPlatform -> os.platform()),
// rather than a separate ad hoc os.platform() call (#10244/#10293).
const { platform } = getTargetPlatform();
if (platform === "windows") {
await fs.copyFile(oldBinary, symlinkPath);
} else {
await fs.symlink(oldBinary, symlinkPath);

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,4 +1,4 @@
import { describe, it, afterEach, after } from "node:test";
import { describe, it, afterEach, after, mock } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";
import fs from "node:fs";
@@ -26,6 +26,7 @@ describe("binaryManager", () => {
mod = await import("../../src/lib/versionManager/binaryManager.ts");
assert.ok(mod.getAssetName);
assert.ok(mod.getTargetPlatform);
assert.ok(mod.downloadRelease);
assert.ok(mod.installVersion);
assert.ok(mod.getCurrentBinaryPath);
assert.ok(mod.getInstalledVersions);
@@ -63,6 +64,24 @@ describe("binaryManager", () => {
assert.ok(["linux", "darwin", "windows"].includes(platform));
assert.ok(["amd64", "arm64"].includes(arch));
});
it("should read platform/arch at runtime from os (anti build-folding guard) (#10244)", () => {
// Regression guard for #10244/#10293: detectPlatform/detectArch must read
// os.platform()/os.arch() at call time, NOT the build-machine foldable
// process.platform/process.arch constants. Turbopack `next build` running
// on Linux constant-folds `process.platform` and prunes every Windows/arm64
// branch from the published npm artifact. Simulate a Windows arm64 host via
// the runtime os.* functions; the Windows/arm64 branch must be reachable.
const platformMock = mock.method(os, "platform", () => "win32");
const archMock = mock.method(os, "arch", () => "arm64");
try {
assert.deepEqual(mod.getTargetPlatform(), { platform: "windows", arch: "arm64" });
assert.equal(mod.getAssetName(), "CLIProxyAPI_{version}_windows_arm64.zip");
} finally {
platformMock.mock.restore();
archMock.mock.restore();
}
});
});
describe("getCurrentBinaryPath", () => {
@@ -139,6 +158,145 @@ describe("binaryManager", () => {
assert.ok(real.includes("1.0.0"));
}
});
it("should use the runtime Windows path for extraction, install, and rollback", async () => {
const binDir = path.join(tmpDir, "bin");
const fakePowerShellDir = path.join(tmpDir, "fake-powershell");
const extractedDir = path.join(binDir, "cliproxyapi-1.0.0");
const commandLog = path.join(tmpDir, "powershell-command.txt");
const originalPath = process.env.PATH;
const originalFetch = globalThis.fetch;
fs.mkdirSync(fakePowerShellDir, { recursive: true });
fs.writeFileSync(
path.join(fakePowerShellDir, "powershell"),
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$OMNI_TEST_COMMAND_LOG\"\n"
+ "mkdir -p \"$OMNI_TEST_EXTRACT_DIR\"\nprintf 'installed-binary' > \"$OMNI_TEST_EXTRACT_DIR/cli-proxy-api\"\n"
);
fs.chmodSync(path.join(fakePowerShellDir, "powershell"), 0o755);
process.env.PATH = `${fakePowerShellDir}:${originalPath || ""}`;
process.env.OMNI_TEST_COMMAND_LOG = commandLog;
process.env.OMNI_TEST_EXTRACT_DIR = extractedDir;
globalThis.fetch = async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("/releases/tags/")) {
return new Response(
JSON.stringify({
tag_name: "v1.0.0",
published_at: "2026-01-01T00:00:00Z",
assets: [
{
name: "CLIProxyAPI_1.0.0_windows_amd64.zip",
browser_download_url: "https://example.test/cliproxy.zip",
size: 3,
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
if (url.endsWith("checksums.txt")) return new Response("", { status: 404 });
return new Response("zip", { status: 200 });
};
const platformMock = mock.method(os, "platform", () => "win32");
const archMock = mock.method(os, "arch", () => "x64");
try {
const installedPath = await mod.installVersion("1.0.0", tmpDir);
assert.equal(fs.readFileSync(installedPath, "utf8"), "installed-binary");
assert.equal(fs.lstatSync(installedPath).isSymbolicLink(), false);
const command = fs.readFileSync(commandLog, "utf8");
assert.match(command, /Expand-Archive -LiteralPath/);
assert.doesNotMatch(command, /unzip/);
const previousDir = path.join(binDir, "cliproxyapi-0.9.0");
fs.mkdirSync(previousDir, { recursive: true });
fs.writeFileSync(path.join(previousDir, "cli-proxy-api"), "rollback-binary");
assert.equal(await mod.rollbackVersion(tmpDir), "0.9.0");
assert.equal(fs.readFileSync(installedPath, "utf8"), "rollback-binary");
assert.equal(fs.lstatSync(installedPath).isSymbolicLink(), false);
} finally {
platformMock.mock.restore();
archMock.mock.restore();
globalThis.fetch = originalFetch;
process.env.PATH = originalPath;
delete process.env.OMNI_TEST_COMMAND_LOG;
delete process.env.OMNI_TEST_EXTRACT_DIR;
}
});
});
describe("downloadRelease platform parameter threading (#10244/#10293)", () => {
it("uses an explicitly-passed Windows target without reading os.platform() at all", async () => {
// Closing-fix regression guard: unlike the os.platform()/os.arch() mock-based
// tests above (which prove the single top-level detection reaches the right
// place, but would still pass even if extractZip re-read os.platform() itself
// since the mock is global), this test proves the actual PARAMETER THREADING:
// downloadRelease() is called with an explicit `target` and os.platform()/
// os.arch() are NOT mocked at all — the real test host is Linux/darwin/etc.
// If downloadRelease or extractZip ever regressed to independently re-reading
// os.platform() instead of using the threaded `platform` value, this would
// resolve to the host's real (non-Windows) platform, `unzip` would run against
// a fake zip body, and the test would fail.
const binDir = path.join(tmpDir, "bin-param-thread");
const extractedDir = path.join(binDir, "cliproxyapi-1.0.0");
const fakePowerShellDir = path.join(tmpDir, "fake-powershell-param-thread");
const commandLog = path.join(tmpDir, "powershell-command-param-thread.txt");
const originalPath = process.env.PATH;
const originalFetch = globalThis.fetch;
fs.mkdirSync(fakePowerShellDir, { recursive: true });
fs.writeFileSync(
path.join(fakePowerShellDir, "powershell"),
"#!/bin/sh\nprintf '%s\\n' \"$@\" > \"$OMNI_TEST_COMMAND_LOG_PT\"\n"
+ "mkdir -p \"$OMNI_TEST_EXTRACT_DIR_PT\"\nprintf 'installed-binary' > \"$OMNI_TEST_EXTRACT_DIR_PT/cli-proxy-api\"\n"
);
fs.chmodSync(path.join(fakePowerShellDir, "powershell"), 0o755);
process.env.PATH = `${fakePowerShellDir}:${originalPath || ""}`;
process.env.OMNI_TEST_COMMAND_LOG_PT = commandLog;
process.env.OMNI_TEST_EXTRACT_DIR_PT = extractedDir;
globalThis.fetch = async (input: string | URL | Request) => {
const url = String(input);
if (url.includes("/releases/tags/")) {
return new Response(
JSON.stringify({
tag_name: "v1.0.0",
published_at: "2026-01-01T00:00:00Z",
assets: [
{
name: "CLIProxyAPI_1.0.0_windows_amd64.zip",
browser_download_url: "https://example.test/cliproxy.zip",
size: 3,
},
],
}),
{ status: 200, headers: { "content-type": "application/json" } }
);
}
if (url.endsWith("checksums.txt")) return new Response("", { status: 404 });
return new Response("zip", { status: 200 });
};
try {
const binary = await mod.downloadRelease("1.0.0", binDir, undefined, {
platform: "windows",
arch: "amd64",
});
assert.equal(fs.readFileSync(binary, "utf8"), "installed-binary");
const command = fs.readFileSync(commandLog, "utf8");
assert.match(command, /Expand-Archive -LiteralPath/);
assert.doesNotMatch(command, /unzip/);
} finally {
globalThis.fetch = originalFetch;
process.env.PATH = originalPath;
delete process.env.OMNI_TEST_COMMAND_LOG_PT;
delete process.env.OMNI_TEST_EXTRACT_DIR_PT;
}
});
});
describe("removeVersion", () => {

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

@@ -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);