mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(cursor): discover models via official CLI command (#7692)
Co-authored-by: Makcim Ivanov <makcimbx@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
This commit is contained in:
@@ -107,11 +107,36 @@ export function humanizeCursorModelId(id: string): string {
|
||||
}
|
||||
|
||||
export function parseCursorAgentModels(text: string): string[] {
|
||||
const match = text.match(/Available models:\s*([^\n]+)/);
|
||||
if (!match) return [];
|
||||
// Older Cursor Agent releases only exposed the catalog as part of the
|
||||
// invalid-model error produced by `--model --help`.
|
||||
const legacyMatch = text.match(/Available models:\s*([^\n]+)/);
|
||||
if (legacyMatch) {
|
||||
return deduplicateCursorModelIds(legacyMatch[1].split(","));
|
||||
}
|
||||
|
||||
// Current releases expose an official `models` command whose output is:
|
||||
//
|
||||
// Available models
|
||||
//
|
||||
// auto - Auto (default)
|
||||
// gpt-5.3-codex - Codex 5.3
|
||||
const headerMatch = /(?:^|\n)Available models\s*(?:\n|$)/.exec(text);
|
||||
if (!headerMatch) return [];
|
||||
const lines = text.slice(headerMatch.index + headerMatch[0].length).split("\n");
|
||||
const ids: string[] = [];
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith("Tip:")) break;
|
||||
const separator = trimmed.indexOf(" - ");
|
||||
if (separator > 0) ids.push(trimmed.slice(0, separator));
|
||||
}
|
||||
return deduplicateCursorModelIds(ids);
|
||||
}
|
||||
|
||||
function deduplicateCursorModelIds(ids: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const out: string[] = [];
|
||||
for (const raw of match[1].split(",")) {
|
||||
for (const raw of ids) {
|
||||
const id = raw.trim();
|
||||
if (!id || seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
@@ -138,12 +163,13 @@ export async function fetchCursorAgentModels(
|
||||
);
|
||||
}
|
||||
|
||||
// cursor-agent prints "Available models: ..." to stderr and exits non-zero
|
||||
// when given an unknown model id, so we intentionally pass `--help` as the
|
||||
// model value to coerce it into listing.
|
||||
const startedAt = Date.now();
|
||||
let result: { stdout: string; stderr: string };
|
||||
try {
|
||||
result = await runCursorAgent(binary, ["--model", "--help"], timeoutMs);
|
||||
// Modern Cursor Agent releases provide a dedicated catalog flag. Prefer the
|
||||
// flag over the equivalent `models` subcommand because older releases can
|
||||
// interpret an unknown positional subcommand as an agent prompt.
|
||||
result = await runCursorAgent(binary, ["--list-models"], timeoutMs);
|
||||
} catch (err: unknown) {
|
||||
const e = err as NodeJS.ErrnoException;
|
||||
if (e?.code === "ENOENT") {
|
||||
@@ -151,11 +177,24 @@ export async function fetchCursorAgentModels(
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const combined = `${result.stdout}\n${result.stderr}`;
|
||||
let combined = `${result.stdout}\n${result.stderr}`;
|
||||
let ids = parseCursorAgentModels(combined);
|
||||
|
||||
// Backward compatibility for releases from before the dedicated catalog interface.
|
||||
if (ids.length === 0 && !/Authentication required|Not logged in/i.test(combined)) {
|
||||
const remainingTimeoutMs = timeoutMs - (Date.now() - startedAt);
|
||||
if (remainingTimeoutMs > 0) {
|
||||
result = await runCursorAgent(binary, ["--model", "--help"], remainingTimeoutMs);
|
||||
combined = `${result.stdout}\n${result.stderr}`;
|
||||
ids = parseCursorAgentModels(combined);
|
||||
}
|
||||
}
|
||||
|
||||
const ids = parseCursorAgentModels(combined);
|
||||
if (ids.length === 0) {
|
||||
throw new Error("cursor-agent did not return an 'Available models:' line");
|
||||
if (/Authentication required|Not logged in/i.test(combined)) {
|
||||
throw new Error("cursor-agent is not authenticated; run 'agent login' on the OmniRoute host");
|
||||
}
|
||||
throw new Error("cursor-agent did not return a model catalog from 'agent --list-models'");
|
||||
}
|
||||
|
||||
return ids.map((id) => ({
|
||||
|
||||
@@ -22,6 +22,21 @@ test("parseCursorAgentModels deduplicates and trims", () => {
|
||||
assert.deepEqual(parseCursorAgentModels("Available models: a, a , b"), ["a", "b"]);
|
||||
});
|
||||
|
||||
test("parseCursorAgentModels parses the multiline output from the models command", () => {
|
||||
const text = `Available models
|
||||
|
||||
auto - Auto (default)
|
||||
gpt-5.3-codex - Codex 5.3
|
||||
claude-opus-4-8-thinking-high-fast - Opus 4.8 1M Thinking Fast
|
||||
|
||||
Tip: use --model <id> to switch.`;
|
||||
assert.deepEqual(parseCursorAgentModels(text), [
|
||||
"auto",
|
||||
"gpt-5.3-codex",
|
||||
"claude-opus-4-8-thinking-high-fast",
|
||||
]);
|
||||
});
|
||||
|
||||
test("parseCursorAgentModels returns [] when the marker is missing", () => {
|
||||
assert.deepEqual(parseCursorAgentModels("nothing here"), []);
|
||||
});
|
||||
@@ -40,7 +55,10 @@ test("humanizeCursorModelId pretty-prints common patterns", () => {
|
||||
humanizeCursorModelId("claude-opus-4-8-thinking-high-fast"),
|
||||
"Claude Opus 4.8 Thinking High Fast"
|
||||
);
|
||||
assert.equal(humanizeCursorModelId("claude-fable-5-thinking-xhigh"), "Claude Fable 5 Thinking XHigh");
|
||||
assert.equal(
|
||||
humanizeCursorModelId("claude-fable-5-thinking-xhigh"),
|
||||
"Claude Fable 5 Thinking XHigh"
|
||||
);
|
||||
assert.equal(humanizeCursorModelId("claude-sonnet-5-max"), "Claude Sonnet 5 Max");
|
||||
assert.equal(humanizeCursorModelId("kimi-k2.5"), "Kimi K2.5");
|
||||
assert.equal(humanizeCursorModelId("gemini-3.1-pro"), "Gemini 3.1 Pro");
|
||||
|
||||
Reference in New Issue
Block a user