mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 05:02:15 +03:00
docs: update omni route video guides ranking layout
This commit is contained in:
60
README.md
60
README.md
@@ -1048,31 +1048,69 @@ 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="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 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>
|
||||
<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 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>
|
||||
<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 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>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
**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.
|
||||
|
||||
> 🎬 **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">
|
||||
|
||||
|
||||
@@ -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"
|
||||
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"
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
for cmd in $commands
|
||||
complete -c omniroute -n '__fish_is_nth_token 1' -a $cmd
|
||||
|
||||
@@ -170,8 +170,8 @@ export function buildCodexEnv(baseEnv, authToken) {
|
||||
* @param {string} baseUrl OmniRoute root URL (no /v1)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function buildCodexProviderArgs(baseUrl) {
|
||||
return [
|
||||
export function buildCodexProviderArgs(baseUrl, model) {
|
||||
const args = [
|
||||
"-c",
|
||||
tomlAssign("model_provider", "omniroute"),
|
||||
"-c",
|
||||
@@ -185,6 +185,15 @@ export function buildCodexProviderArgs(baseUrl) {
|
||||
"-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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +216,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);
|
||||
const providerArgs = buildCodexProviderArgs(baseUrl, opts.model);
|
||||
const profileArgs = opts.profile ? ["--profile", opts.profile] : [];
|
||||
const extraArgs = [...providerArgs, ...profileArgs, ...codexArgs];
|
||||
const env = buildCodexEnv(process.env, authToken);
|
||||
|
||||
@@ -190,7 +190,10 @@ 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 });
|
||||
const env = buildClaudeEnv(process.env, baseUrl, authToken, {
|
||||
configDir,
|
||||
model: opts.model,
|
||||
});
|
||||
|
||||
const { command, shell } = await resolveClaudeSpawn(process.platform);
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ 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";
|
||||
@@ -145,6 +146,7 @@ export function registerCommands(program) {
|
||||
registerRepl(program);
|
||||
registerLaunch(program);
|
||||
registerLaunchCodex(program);
|
||||
registerRun(program);
|
||||
registerSetupCodex(program);
|
||||
registerSetupClaude(program);
|
||||
registerSetupOpencode(program);
|
||||
|
||||
316
bin/cli/commands/run.mjs
Normal file
316
bin/cli/commands/run.mjs
Normal file
@@ -0,0 +1,316 @@
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -1287,6 +1287,9 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -1284,6 +1284,9 @@
|
||||
"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"
|
||||
},
|
||||
|
||||
@@ -35,22 +35,23 @@ 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 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 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 |
|
||||
|
||||
Notes on flags (verified in the command source):
|
||||
|
||||
@@ -113,6 +114,11 @@ 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"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -156,15 +162,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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -7,8 +7,14 @@ 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";
|
||||
@@ -1154,6 +1160,11 @@ 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.
|
||||
@@ -1541,9 +1552,15 @@ 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 === "anthropic" ||
|
||||
provider === "claude" ||
|
||||
provider === "openai" ||
|
||||
provider === "xai"
|
||||
? ("direct" as const)
|
||||
: ("aggregator" as const),
|
||||
sourceFormat,
|
||||
targetFormat,
|
||||
compressionStage: "pre-translation" as const,
|
||||
config: compressionConfig,
|
||||
cachingContext: cacheCtx,
|
||||
principalId: compressionPrincipalId,
|
||||
@@ -1573,6 +1590,26 @@ 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");
|
||||
@@ -2314,6 +2351,76 @@ 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:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
/**
|
||||
* 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).
|
||||
* 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.
|
||||
*
|
||||
* 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)
|
||||
* - corpo não é formato Claude nativo → skip:source_format_not_claude
|
||||
* - wire não é Claude/OpenAI suportado → skip:target_format_not_supported
|
||||
* - gate de rentabilidade interno do omniglyph decide o resto (patches 28px
|
||||
* exatos; texto esparso/pequeno passa direto) → skip:not_profitable
|
||||
*
|
||||
@@ -19,7 +19,14 @@
|
||||
import type { CompressionEngine, CompressionEngineApplyOptions } from "./types.ts";
|
||||
import type { CompressionResult } from "../types.ts";
|
||||
import { createCompressionStats } from "../stats.ts";
|
||||
import { transformAnthropicMessages, isOmniGlyphSupportedModel } from "omniglyph";
|
||||
import {
|
||||
isOmniGlyphSupportedGptModel,
|
||||
isOmniGlyphSupportedModel,
|
||||
transformAnthropicMessages,
|
||||
transformOpenAIChatCompletions,
|
||||
transformOpenAIResponses,
|
||||
} from "omniglyph";
|
||||
import { isModelImageable } from "omniglyph/applicability";
|
||||
|
||||
function skip(body: Record<string, unknown>, reason: string): CompressionResult {
|
||||
try {
|
||||
@@ -36,6 +43,8 @@ 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;
|
||||
@@ -43,22 +52,79 @@ 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");
|
||||
if (!isClaudeFormat(body)) return skip(body, "source_format_not_claude");
|
||||
|
||||
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");
|
||||
|
||||
const started = Date.now();
|
||||
let outBody: Record<string, unknown>;
|
||||
try {
|
||||
const encoded = new TextEncoder().encode(JSON.stringify(body));
|
||||
const result = await transformAnthropicMessages({ body: encoded, model });
|
||||
if (!result.applied) return skip(body, result.reason ?? "not_profitable");
|
||||
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");
|
||||
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,
|
||||
@@ -93,11 +159,13 @@ export const omniglyphEngine: CompressionEngine = {
|
||||
metadata: {
|
||||
id: "omniglyph",
|
||||
name: "OmniGlyph",
|
||||
description: "Contexto-como-imagem para Claude Fable 5 via rota direta Anthropic.",
|
||||
description:
|
||||
"Contexto-como-imagem para Claude Fable 5 e GPT 5.6 via wires nativos Anthropic/OpenAI em rota direta.",
|
||||
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) {
|
||||
|
||||
@@ -2,6 +2,11 @@ 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";
|
||||
@@ -27,6 +32,8 @@ 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 {
|
||||
@@ -37,6 +44,12 @@ 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>;
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
isCodexTokenizerContext,
|
||||
tokenizerContextFromBody,
|
||||
} from "../../../src/shared/utils/tiktokenCounter.ts";
|
||||
import { anthropicImageTokens, ANTHROPIC_IMAGE_BLOCK_OVERHEAD_TOKENS } from "omniglyph";
|
||||
import {
|
||||
anthropicImageTokens,
|
||||
ANTHROPIC_IMAGE_BLOCK_OVERHEAD_TOKENS,
|
||||
openAIVisionTokens,
|
||||
} from "omniglyph";
|
||||
|
||||
const CHARS_PER_TOKEN = 4;
|
||||
|
||||
@@ -27,6 +31,17 @@ 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>;
|
||||
@@ -38,6 +53,36 @@ 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) +
|
||||
@@ -89,17 +134,32 @@ 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)) 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: "" } };
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -116,6 +176,16 @@ 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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ import type {
|
||||
import { applyHardBudget } from "./hardBudget.ts";
|
||||
import { type FidelityGateConfig } from "./fidelityGate.ts";
|
||||
import { gateAdvance } from "./fidelityGateStep.ts";
|
||||
import type { CompressionEngineApplyOptions } from "./engines/types.ts";
|
||||
import type {
|
||||
CompressionEngineApplyOptions,
|
||||
CompressionStage,
|
||||
CompressionWireFormat,
|
||||
} from "./engines/types.ts";
|
||||
import { applyLiteCompression } from "./lite.ts";
|
||||
import { cavemanCompress } from "./caveman.ts";
|
||||
import { compressAggressive } from "./aggressive.ts";
|
||||
@@ -262,6 +266,9 @@ export function applyCompression(
|
||||
options?: {
|
||||
model?: string;
|
||||
supportsVision?: boolean | null;
|
||||
sourceFormat?: CompressionWireFormat;
|
||||
targetFormat?: CompressionWireFormat;
|
||||
compressionStage?: CompressionStage;
|
||||
config?: CompressionConfig;
|
||||
principalId?: string;
|
||||
/**
|
||||
@@ -285,6 +292,9 @@ function runCompression(
|
||||
options?: {
|
||||
model?: string;
|
||||
supportsVision?: boolean | null;
|
||||
sourceFormat?: CompressionWireFormat;
|
||||
targetFormat?: CompressionWireFormat;
|
||||
compressionStage?: CompressionStage;
|
||||
config?: CompressionConfig;
|
||||
principalId?: string;
|
||||
bailout?: BailoutConfig;
|
||||
@@ -469,6 +479,9 @@ 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;
|
||||
@@ -488,6 +501,9 @@ 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;
|
||||
@@ -523,10 +539,15 @@ async function runCompressionAsync(
|
||||
// Single-mode omniglyph (async-only) — resolution lives in engines/omniglyphSingleMode.ts.
|
||||
if (mode === "omniglyph") return applyOmniglyphSingleMode(body, options);
|
||||
if (mode === "stacked") {
|
||||
const adapter = adaptBodyForCompression(
|
||||
body,
|
||||
options?.config?.codexResponsesConfig?.preserveToolNames
|
||||
);
|
||||
// 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 result = await applyStackedCompressionAsync(
|
||||
adapter.body,
|
||||
options?.config?.stackedPipeline,
|
||||
@@ -672,6 +693,9 @@ 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). */
|
||||
@@ -766,6 +790,20 @@ 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>,
|
||||
@@ -894,6 +932,12 @@ 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) {
|
||||
@@ -1000,6 +1044,12 @@ 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)`);
|
||||
|
||||
@@ -466,6 +466,7 @@ export const MODEL_SPECS: Record<string, ModelSpec> = {
|
||||
supportsThinking: true,
|
||||
supportsTools: true,
|
||||
supportsVision: true,
|
||||
aliases: ["qwen3.8-max"],
|
||||
},
|
||||
"qwen3.6-plus": {
|
||||
maxOutputTokens: 65536,
|
||||
|
||||
@@ -48,6 +48,14 @@ 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");
|
||||
|
||||
101
tests/unit/cli/run-command.test.ts
Normal file
101
tests/unit/cli/run-command.test.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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);
|
||||
});
|
||||
@@ -19,6 +19,50 @@ 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);
|
||||
@@ -80,3 +124,26 @@ 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"));
|
||||
});
|
||||
|
||||
@@ -4,7 +4,13 @@ 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");
|
||||
});
|
||||
|
||||
@@ -107,6 +107,8 @@ 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);
|
||||
|
||||
Reference in New Issue
Block a user