Files
OmniRoute/open-sse/executors/cursor/agentEndpoint.ts
Praveen K Palaniswamy 65e81158ab fix(ollama): route models by advertised capability (#11088)
Landed with the design call resolved per the owner's pick — **option 1**: the synced store is now endpoint-agnostic (persistDiscoveredModels and managedModelImport no longer drop non-chat models at write time), and chat selectability moved to read time (auto-pool expansion in autoStrategy applies filterChatSelectableModels; the models-route projection already had its chatOnly filter). Your discovery test now passes end-to-end (3/3): /api/show capabilities persist per connection and image/embedding requests route through the advertising host.

Reconciliation notes: conflicted areas merged onto the current tip (adobe discovery import, requestedModel preflight signature, resolvedProvider fast-path coexists with the synced-route override — explicit resolution wins); carried base-red drains (#10055 memoization, #11071 test variants) dropped as already-landed; the managed-model-import exclusion test was propagated to the new contract (image/video models persist; the read filter still hides them from chat pickers — pinned by a new assertion). Full battery: 205/206 focused (the one red is a confirmed periodic-timer timing flake on the loaded devbox — 20/20 isolated), autoCombo vitest 30/30, combo suites 46/46, gates + typecheck clean.

Thank you @yourspraveen — the capability probe + routing design was right; it just needed the store contract opened up. Fixes #11087.
2026-08-23 11:45:01 -03:00

122 lines
4.6 KiB
TypeScript

import { createHmac } from "node:crypto";
import { mergeAbortSignals, type ProviderCredentials } from "../base.ts";
import { stripCursorOAuthTokenPrefix } from "../../services/cursorApiKeyAuth.ts";
import {
formatCursorAgentClientVersion,
getCursorAgentCliVersion,
} from "../../utils/cursorAgentCliVersion.ts";
import { decodeFields } from "../../utils/cursorAgentProtobuf/wire.ts";
const CURSOR_API_URL = "https://api2.cursor.sh";
const CURSOR_SERVER_CONFIG_PATH = "/aiserver.v1.ServerConfigService/GetServerConfig";
const CURSOR_AGENT_PATH = "/agent.v1.AgentService/Run";
const CURSOR_SERVER_CONFIG_TIMEOUT_MS = 10_000;
const CURSOR_AGENT_URL_CACHE_TTL_MS = 60 * 60 * 1000;
const CURSOR_AGENT_URL_CACHE_LIMIT = 1_000;
type CursorAgentUrls = { agentUrl: string; agentnUrl: string };
type CursorAgentUrlCacheEntry = CursorAgentUrls & { expiresAt: number };
const cursorAgentUrlCache = new Map<string, CursorAgentUrlCacheEntry>();
/** Reports an HTTP error from Cursor server-config discovery. */
export class CursorServerConfigError extends Error {
constructor(
message: string,
readonly status: number
) {
super(message);
}
}
function validateCursorAgentUrl(value: string): string {
const url = new URL(value);
const isCursorAgentHost =
url.hostname === "api5.cursor.sh" || url.hostname.endsWith(".api5.cursor.sh");
if (
url.protocol !== "https:" ||
!isCursorAgentHost ||
url.username ||
url.password ||
url.search ||
url.hash
) {
throw new Error("Cursor server config included an invalid Agent URL");
}
return url.origin;
}
function parseCursorAgentUrls(payload: Buffer): CursorAgentUrls {
const agentUrlConfig = decodeFields(payload).find(
(field) => field.fieldNumber === 27 && field.wireType === 2
);
if (!agentUrlConfig || agentUrlConfig.wireType !== 2) {
throw new Error("Cursor server config did not include Agent URLs");
}
const fields = decodeFields(agentUrlConfig.bytes);
const agentUrl = fields.find((field) => field.fieldNumber === 1 && field.wireType === 2);
const agentnUrl = fields.find((field) => field.fieldNumber === 2 && field.wireType === 2);
if (!agentUrl || agentUrl.wireType !== 2 || !agentnUrl || agentnUrl.wireType !== 2) {
throw new Error("Cursor server config included incomplete Agent URLs");
}
return {
agentUrl: validateCursorAgentUrl(agentUrl.bytes.toString("utf8")),
agentnUrl: validateCursorAgentUrl(agentnUrl.bytes.toString("utf8")),
};
}
async function fetchCursorAgentUrls(
accessToken: string,
signal?: AbortSignal | null
): Promise<CursorAgentUrls> {
const timeoutSignal = AbortSignal.timeout(CURSOR_SERVER_CONFIG_TIMEOUT_MS);
const response = await fetch(`${CURSOR_API_URL}${CURSOR_SERVER_CONFIG_PATH}`, {
method: "POST",
headers: {
authorization: `Bearer ${accessToken}`,
"connect-protocol-version": "1",
"content-type": "application/proto",
"user-agent": "connect-es/1.6.1",
"x-cursor-client-type": "cli",
"x-cursor-client-version": formatCursorAgentClientVersion(getCursorAgentCliVersion()),
},
body: Buffer.alloc(0),
signal: signal ? mergeAbortSignals(signal, timeoutSignal) : timeoutSignal,
});
if (!response.ok) {
throw new CursorServerConfigError(
`Cursor server config request failed with status ${response.status}`,
response.status
);
}
return parseCursorAgentUrls(Buffer.from(await response.arrayBuffer()));
}
/** Resolve the Agent RPC URL that Cursor assigned to this connection. */
export async function resolveCursorAgentUrl(
credentials: ProviderCredentials,
signal?: AbortSignal | null
): Promise<string> {
const accessToken = stripCursorOAuthTokenPrefix(credentials.accessToken || "");
if (!accessToken) throw new Error("Cursor access token is required");
const cacheKey =
`${credentials.connectionId || "anonymous"}:` +
createHmac("sha256", "omniroute-cursor-agent-url-cache-v1").update(accessToken).digest("hex");
const now = Date.now();
let urls = cursorAgentUrlCache.get(cacheKey);
if (!urls || urls.expiresAt <= now) {
const fetched = await fetchCursorAgentUrls(accessToken, signal);
urls = { ...fetched, expiresAt: now + CURSOR_AGENT_URL_CACHE_TTL_MS };
if (
!cursorAgentUrlCache.has(cacheKey) &&
cursorAgentUrlCache.size >= CURSOR_AGENT_URL_CACHE_LIMIT
) {
const oldestKey = cursorAgentUrlCache.keys().next().value as string | undefined;
if (oldestKey !== undefined) cursorAgentUrlCache.delete(oldestKey);
}
cursorAgentUrlCache.set(cacheKey, urls);
}
const ghostMode = credentials.providerSpecificData?.ghostMode !== false;
return `${ghostMode ? urls.agentUrl : urls.agentnUrl}${CURSOR_AGENT_PATH}`;
}