mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
feat(codex): generate fallback profiles for compatible models (#5701)
setup-codex now generates Codex profiles for compatible text models from the live /v1/models catalog when the model id doesn't match a hand-tuned pattern, skipping media/embedding models. Integrated into release/v3.8.43.
This commit is contained in:
@@ -80,6 +80,88 @@ export function categoriseModel(modelId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function firstPositiveNumber(...values) {
|
||||
for (const value of values) {
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function shortHash(value) {
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
hash = ((hash << 5) + hash) ^ value.charCodeAt(i);
|
||||
}
|
||||
return (hash >>> 0).toString(36);
|
||||
}
|
||||
|
||||
function profileNameFromModelId(modelId) {
|
||||
const normalized = String(modelId)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
const base = normalized || "model";
|
||||
if (base.length <= 96) return base;
|
||||
return `${base.slice(0, 84).replace(/-+$/g, "")}-${shortHash(base)}`;
|
||||
}
|
||||
|
||||
function hasAnyValue(values, patterns) {
|
||||
return values.some((value) => patterns.some((pattern) => pattern.test(value)));
|
||||
}
|
||||
|
||||
export function isCodexCompatibleTextModel(model) {
|
||||
if (typeof model === "string") return true;
|
||||
|
||||
const id = String(model?.id ?? "").toLowerCase();
|
||||
const type = String(model?.type ?? "").toLowerCase();
|
||||
const outputModalities = Array.isArray(model?.output_modalities)
|
||||
? model.output_modalities.map((value) => String(value).toLowerCase())
|
||||
: [];
|
||||
|
||||
if (type && !["chat", "text", "language", "llm", "model"].includes(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsupportedPatterns = [
|
||||
/(^|[/_-])(image|img|video|veo|seedance|audio|speech|voice|tts|stt|whisper)([/_-]|$)/,
|
||||
/(^|[/_-])(embedding|embeddings|embed|rerank|moderation|transcription)([/_-]|$)/,
|
||||
];
|
||||
if (hasAnyValue([id, type], unsupportedPatterns)) return false;
|
||||
|
||||
const nonTextModalities = [/^(image|video|audio)$/];
|
||||
if (hasAnyValue(outputModalities, nonTextModalities)) return false;
|
||||
|
||||
if (outputModalities.length > 0 && !outputModalities.includes("text")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function fallbackCodexProfile(modelId, model) {
|
||||
if (!isCodexCompatibleTextModel(model)) return null;
|
||||
|
||||
const ctx =
|
||||
typeof model === "string"
|
||||
? 128000
|
||||
: firstPositiveNumber(model.context_length, model.max_context_window_tokens, model.max_input_tokens) ?? 128000;
|
||||
const maxOutput =
|
||||
typeof model === "string"
|
||||
? null
|
||||
: firstPositiveNumber(model.max_output_tokens, model.output_token_limit);
|
||||
const toolLimit = Math.min(Math.max(maxOutput ?? 16384, 8192), 32768);
|
||||
|
||||
return {
|
||||
name: profileNameFromModelId(modelId),
|
||||
ctx,
|
||||
compact: Math.floor(ctx * 0.85),
|
||||
summary: false,
|
||||
toolLimit,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build the TOML content for a single profile. */
|
||||
function buildProfileToml(modelId, cfg) {
|
||||
const lines = [
|
||||
@@ -160,7 +242,7 @@ export async function runSetupCodexCommand(opts = {}) {
|
||||
if (!id) continue;
|
||||
if (onlyFilter && !onlyFilter.some((f) => id.includes(f))) continue;
|
||||
|
||||
const cfg = categoriseModel(id);
|
||||
const cfg = categoriseModel(id) ?? fallbackCodexProfile(id, m);
|
||||
if (!cfg) continue;
|
||||
|
||||
const filePath = join(codexHome, `${cfg.name}.config.toml`);
|
||||
|
||||
@@ -37,7 +37,7 @@ 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 matched model (`codex --profile <name>`) | `--remote` `--api-key` `--only` `--dry-run` `--port` `--codex-home` | Both |
|
||||
| `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 |
|
||||
|
||||
@@ -241,7 +241,7 @@ omniroute setup-codex --only glm,kimi
|
||||
omniroute setup-codex --codex-home /path/to/.codex
|
||||
```
|
||||
|
||||
The command fetches `/v1/models`, categorises each model (thinking / good / simple / fast) and writes `~/.codex/<name>.config.toml` for each. Idempotent — safe to re-run.
|
||||
The command fetches `/v1/models`, uses tuned profiles for known models, falls back to catalog metadata for other compatible text models, and writes `~/.codex/<name>.config.toml` for each. Idempotent — safe to re-run.
|
||||
|
||||
---
|
||||
|
||||
|
||||
42
tests/unit/cli/setup-codex.test.ts
Normal file
42
tests/unit/cli/setup-codex.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
fallbackCodexProfile,
|
||||
isCodexCompatibleTextModel,
|
||||
} from "../../../bin/cli/commands/setup-codex.mjs";
|
||||
|
||||
test("fallbackCodexProfile creates profiles for compatible live catalog models", () => {
|
||||
const cfg = fallbackCodexProfile("new-provider/future-chat-1", {
|
||||
id: "new-provider/future-chat-1",
|
||||
context_length: 250000,
|
||||
max_output_tokens: 65536,
|
||||
output_modalities: ["text"],
|
||||
});
|
||||
|
||||
assert.deepEqual(cfg, {
|
||||
name: "new-provider-future-chat-1",
|
||||
ctx: 250000,
|
||||
compact: 212500,
|
||||
summary: false,
|
||||
toolLimit: 32768,
|
||||
});
|
||||
});
|
||||
|
||||
test("fallbackCodexProfile skips media and non-text models", () => {
|
||||
assert.equal(
|
||||
isCodexCompatibleTextModel({
|
||||
id: "antigravity/gemini-3.1-flash-image",
|
||||
type: "image",
|
||||
output_modalities: ["image"],
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
fallbackCodexProfile("veo-free/seedance", {
|
||||
id: "veo-free/seedance",
|
||||
name: "Seedance",
|
||||
context_length: 128000,
|
||||
}),
|
||||
null
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user