feat(pi): add Cursor models through the OMP gateway

This commit is contained in:
Rasyidan Akbar F. 2026-10-13 16:02:22 +07:00
commit 73e9b17305
4 changed files with 573 additions and 0 deletions

View file

@ -0,0 +1,138 @@
import type { Model, ModelThinkingLevel, ThinkingLevelMap } from "@earendil-works/pi-ai";
export const PROVIDER_ID = "cursor";
export const THINKING_LEVELS: ModelThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function positiveInteger(value: unknown): value is number {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
}
function rate(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
function modelId(value: unknown): value is string {
return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
}
function modalities(value: unknown): ("text" | "image")[] {
if (!Array.isArray(value)) return [];
const result: ("text" | "image")[] = [];
for (const item of value) {
if ((item === "text" || item === "image") && !result.includes(item)) result.push(item);
}
return result;
}
function thinkingLevel(value: unknown): ModelThinkingLevel | undefined {
switch (value) {
case "off": case "minimal": case "low": case "medium": case "high": case "xhigh": case "max": return value;
default: return undefined;
}
}
type GatewayCursorRow = {
id: string;
name: string | undefined;
input: Model<"openai-completions">["input"];
contextWindow: number | undefined;
maxTokens: number | undefined;
};
/** Reject accidental connections to a gateway backed by a shared credential vault. */
export function assertCursorOnlyCatalog(payload: unknown): void {
if (!record(payload) || !Array.isArray(payload.data)) throw new Error("OMP gateway returned an invalid model catalog.");
if (payload.data.some(row => !record(row) || row.owned_by !== PROVIDER_ID ||
typeof row.id !== "string" || !row.id.startsWith(`${PROVIDER_ID}/`))) {
throw new Error("OMP gateway exposes non-Cursor models. Use the dedicated .omp-cursor configuration.");
}
}
export function gatewayCursorRows(payload: unknown): GatewayCursorRow[] {
if (!record(payload) || !Array.isArray(payload.data)) throw new Error("OMP gateway returned an invalid model catalog.");
const rows: GatewayCursorRow[] = [];
for (const row of payload.data) {
if (!record(row) || row.owned_by !== PROVIDER_ID || typeof row.id !== "string" ||
!row.id.startsWith(`${PROVIDER_ID}/`) || (row.kind !== undefined && row.kind !== "chat") ||
row.supports_tools === false) continue;
const id = row.id.slice(PROVIDER_ID.length + 1);
if (!modelId(id)) continue;
rows.push({
id,
name: typeof row.display_name === "string" ? row.display_name : undefined,
input: modalities(row.input_modalities),
contextWindow: positiveInteger(row.context_length) ? row.context_length : undefined,
maxTokens: positiveInteger(row.max_output_tokens) ? row.max_output_tokens : undefined,
});
}
return rows;
}
/** Join the gateway's routable models with OMP's pricing and thinking metadata. */
export function discoverCursorModels(gateway: unknown, local: unknown, baseUrl: string): Model<"openai-completions">[] {
const rows = gatewayCursorRows(gateway);
if (!record(local) || !Array.isArray(local.models)) throw new Error("OMP CLI returned an invalid model catalog.");
const metadata = new Map<string, Record<string, unknown>>();
for (const row of local.models) {
if (record(row) && row.provider === PROVIDER_ID && modelId(row.id) && (row.kind === undefined || row.kind === "chat")) {
metadata.set(row.id, row);
}
}
const models = new Map<string, Model<"openai-completions">>();
for (const row of rows) {
const id = row.id;
const meta = metadata.get(id);
if (!meta) continue;
const cost = meta.cost;
if (!record(cost) || !rate(cost.input) || !rate(cost.output) || !rate(cost.cacheRead) || !rate(cost.cacheWrite)) {
throw new Error(`OMP model ${id} has invalid pricing metadata.`);
}
if (!positiveInteger(meta.contextWindow) || !positiveInteger(meta.maxTokens) || typeof meta.reasoning !== "boolean") {
throw new Error(`OMP model ${id} has invalid capability metadata.`);
}
const localInput = modalities(meta.input);
const input = row.input.filter(item => localInput.includes(item));
if (!input.includes("text")) throw new Error(`OMP model ${id} does not declare text input.`);
const thinkingLevelMap: ThinkingLevelMap = {};
const supported = new Set<ModelThinkingLevel>();
if (Array.isArray(meta.thinking)) {
for (const item of meta.thinking) {
const level = thinkingLevel(item);
if (level) supported.add(level);
}
}
if (meta.reasoning && supported.size === 0) throw new Error(`OMP model ${id} has no thinking-level metadata.`);
for (const level of THINKING_LEVELS) thinkingLevelMap[level] = supported.has(level) ? level : null;
const name = row.name ?? (typeof meta.name === "string" ? meta.name : id);
models.set(id, {
id, provider: PROVIDER_ID, name: name.replace(/[\u0000-\u001f\u007f-\u009f]/g, ""),
api: "openai-completions", baseUrl, input,
reasoning: meta.reasoning, thinkingLevelMap,
contextWindow: row.contextWindow === undefined ? meta.contextWindow : Math.min(row.contextWindow, meta.contextWindow),
maxTokens: row.maxTokens === undefined ? meta.maxTokens : Math.min(row.maxTokens, meta.maxTokens),
cost: { input: cost.input, output: cost.output, cacheRead: cost.cacheRead, cacheWrite: cost.cacheWrite },
compat: { supportsReasoningEffort: true, supportsDeveloperRole: true, supportsStore: false, maxTokensField: "max_tokens" },
});
}
if (rows.length > 0 && models.size === 0) throw new Error("OMP gateway models do not match the local OMP catalog.");
return [...models.values()];
}
/** Preserve Pi's public model id while routing unambiguously through Cursor. */
export function gatewayPayload(payload: unknown, id: string, sessionId?: string): Record<string, unknown> {
if (!record(payload) || !modelId(id)) throw new Error("Invalid OMP gateway request payload.");
return { ...payload, model: `${PROVIDER_ID}/${id}`, ...(sessionId ? { prompt_cache_key: sessionId } : {}) };
}
export function loopbackBaseUrl(value: string): string {
const url = new URL(value);
if (!['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname) || !['http:', 'https:'].includes(url.protocol) ||
url.username || url.password || url.search || url.hash || url.pathname.replace(/\/$/, "") !== "/v1") {
throw new Error("OMP_GATEWAY_URL must be an authenticated loopback URL ending in /v1.");
}
return url.toString().replace(/\/$/, "");
}

View file

@ -0,0 +1,146 @@
import { execFile } from "node:child_process";
import { readFile, stat } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { createProvider, type ProviderStreams, type StreamOptions } from "@earendil-works/pi-ai";
import { openAICompletionsApi } from "@earendil-works/pi-ai/compat";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import { assertCursorOnlyCatalog, discoverCursorModels, gatewayCursorRows, gatewayPayload, loopbackBaseUrl, PROVIDER_ID } from "./catalog.ts";
const execute = promisify(execFile);
const TIMEOUT_MS = 10_000;
function timeoutSignal(signal?: AbortSignal): AbortSignal {
const timeout = AbortSignal.timeout(TIMEOUT_MS);
return signal ? AbortSignal.any([signal, timeout]) : timeout;
}
export default function ompGateway(pi: ExtensionAPI): void {
const baseUrl = loopbackBaseUrl(process.env.OMP_GATEWAY_URL ?? "http://127.0.0.1:4000/v1");
const tokenFile = process.env.OMP_GATEWAY_TOKEN_FILE ?? join(homedir(), ".omp-cursor", "auth-gateway.token");
const omp = process.env.OMP_GATEWAY_COMMAND ?? "omp";
// Pi exports its own PI_CODING_AGENT_DIR. Never let OMP create its vault there.
const metadataEnv: NodeJS.ProcessEnv = {
...process.env,
PI_CONFIG_DIR: ".omp-cursor",
PI_CODING_AGENT_DIR: process.env.OMP_GATEWAY_AGENT_DIR ?? join(homedir(), ".omp-cursor", "agent"),
OMP_PROFILE: "",
PI_PROFILE: "",
PI_CONFIG_FILES: "",
};
delete metadataEnv.OMP_AUTH_BROKER_URL;
delete metadataEnv.OMP_AUTH_BROKER_TOKEN;
async function token(): Promise<string | undefined> {
try {
const info = await stat(tokenFile);
if (!info.isFile() || (info.mode & 0o077) !== 0) throw new Error("OMP gateway token file must have owner-only permissions.");
const key = (await readFile(tokenFile, "utf8")).trim();
if (!key || /\s/.test(key)) throw new Error("OMP gateway token file is empty or invalid.");
return key;
} catch (error) {
if (error instanceof Error && "code" in error && error.code === "ENOENT") return undefined;
throw error;
}
}
async function catalog(signal?: AbortSignal): Promise<unknown> {
const key = await token();
if (!key) throw new Error("OMP gateway token is missing. Start the managed OMP services first.");
let response: Response;
try {
response = await fetch(`${baseUrl}/models`, {
headers: { Authorization: `Bearer ${key}` }, signal: timeoutSignal(signal), redirect: "error",
});
} catch {
signal?.throwIfAborted();
throw new Error("OMP gateway is unavailable. Check the local omp-auth-broker and omp-auth-gateway services.");
}
if (!response.ok) throw new Error(`OMP gateway model discovery failed with HTTP ${response.status}.`);
let payload: unknown;
try { payload = await response.json(); }
catch { throw new Error("OMP gateway returned invalid model JSON."); }
assertCursorOnlyCatalog(payload);
return payload;
}
// Keep Pi's streaming, tools, abort handling, and usage accounting. Only qualify
// the wire model id and carry the Pi session id into OMP's conversation cache.
const supportedApi = openAICompletionsApi();
function optionsForGateway(options?: StreamOptions): StreamOptions {
return {
...options,
onPayload: async (payload, model) => {
const routed = gatewayPayload(payload, model.id, options?.sessionId);
const replacement = await options?.onPayload?.(routed, model);
return gatewayPayload(replacement ?? routed, model.id, options?.sessionId);
},
};
}
const api: ProviderStreams = {
stream: (model, context, options) => supportedApi.stream(model, context, optionsForGateway(options)),
streamSimple: (model, context, options) => supportedApi.streamSimple(model, context, optionsForGateway(options)),
};
const provider = createProvider({
id: PROVIDER_ID,
name: "Cursor through OMP",
baseUrl,
models: [],
api,
auth: {
apiKey: {
name: "OMP gateway token file",
check: async () => (await token()) ? { type: "api_key", source: "OMP gateway token file" } : undefined,
resolve: async ({ signal }) => {
signal.throwIfAborted();
const key = await token();
return key ? { auth: { apiKey: key, baseUrl }, source: "OMP gateway token file" } : undefined;
},
},
},
fetchModels: async context => {
const signal = timeoutSignal(context.signal);
const gateway = await catalog(signal);
let stdout: string;
try {
({ stdout } = await execute(omp, ["models", "cursor", "--json", "--no-extensions"], {
signal, timeout: TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, env: metadataEnv,
}));
} catch {
context.signal.throwIfAborted();
// execFile errors include child stderr. Never forward those bytes to Pi.
throw new Error("Cannot read OMP model metadata. Check OMP_GATEWAY_COMMAND and the dedicated Cursor configuration.");
}
let metadata: unknown;
try { metadata = JSON.parse(stdout); }
catch { throw new Error("OMP CLI returned invalid model JSON."); }
return discoverCursorModels(gateway, metadata, baseUrl);
},
});
pi.registerProvider(provider);
pi.registerCommand("omp-gateway", {
description: "Check the OMP gateway or refresh Cursor models: /omp-gateway status|refresh",
handler: async (args, ctx) => {
try {
const action = args.trim() || "status";
if (action === "refresh") {
const result = await ctx.modelRegistry.refresh({ providers: [PROVIDER_ID], allowNetwork: true, force: true });
const error = result.errors.get(PROVIDER_ID);
if (error) throw error;
if (result.aborted) throw new Error("OMP gateway model refresh was cancelled.");
const count = ctx.modelRegistry.getAll().filter(model => model.provider === PROVIDER_ID).length;
ctx.ui.notify(`OMP gateway refreshed ${count} Cursor models. Select one with /model.`, "info");
} else if (action === "status") {
const count = gatewayCursorRows(await catalog()).length;
ctx.ui.notify(`OMP gateway connected at ${baseUrl}. ${count} Cursor chat models are routable.`, "info");
} else {
ctx.ui.notify("Usage: /omp-gateway status|refresh", "warning");
}
} catch (error) {
ctx.ui.notify(error instanceof Error ? error.message : "OMP gateway check failed.", "error");
}
},
});
}

View file

@ -0,0 +1,77 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { assertCursorOnlyCatalog, discoverCursorModels, gatewayCursorRows, gatewayPayload, loopbackBaseUrl } from "../catalog.ts";
function catalogs() {
return {
gateway: { data: [{ id: "cursor/grok-4.7", owned_by: "cursor", display_name: "Grok 4.7", context_length: 256000,
max_output_tokens: 64000, input_modalities: ["text", "image"] }] },
local: { models: [{ provider: "cursor", kind: "chat", id: "grok-4.7", name: "Grok 4.7", input: ["text", "image"],
reasoning: true, thinking: ["low", "medium", "high", "xhigh"], contextWindow: 500000, maxTokens: 64000,
cost: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 } }] },
};
}
test("joins routable models with OMP pricing, capabilities, and thinking levels", () => {
const { gateway, local } = catalogs();
const models = discoverCursorModels(gateway, local, "http://127.0.0.1:4000/v1");
assert.equal(models.length, 1);
const [model] = models;
assert.equal(model.provider, "cursor");
assert.equal(model.id, "grok-4.7");
assert.equal(model.contextWindow, 256000, "Use the conservative intersection, not a guessed 500k limit");
assert.deepEqual(model.cost, { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 });
assert.equal(model.thinkingLevelMap?.max, null);
assert.equal(model.thinkingLevelMap?.xhigh, "xhigh");
assert.deepEqual(model.input, ["text", "image"]);
});
test("excludes other providers, unsupported tools, non-chat models, and unsafe ids", () => {
const { gateway } = catalogs();
const valid = gateway.data[0];
const payload = { data: [valid, { ...valid, owned_by: "xai" }, { ...valid, supports_tools: false },
{ ...valid, kind: "image" }, { ...valid, id: "cursor/../../key" }, null] };
assert.equal(gatewayCursorRows(payload).length, 1);
assert.throws(() => gatewayCursorRows({ error: "not a catalog" }), /invalid model catalog/);
});
test("rejects a shared-provider gateway before model discovery", () => {
assertCursorOnlyCatalog(catalogs().gateway);
assertCursorOnlyCatalog({ data: [] });
for (const row of [{ id: "xai/grok-4.7", owned_by: "xai" },
{ id: "xai/grok-4.7", owned_by: "cursor" }, null]) {
assert.throws(() => assertCursorOnlyCatalog({ data: [row] }), /non-Cursor/);
}
});
test("never substitutes zero prices or guessed capabilities for malformed metadata", () => {
const { gateway, local } = catalogs();
local.models[0].cost.input = Number.NaN;
assert.throws(() => discoverCursorModels(gateway, local, "http://localhost:4000/v1"), /invalid pricing/);
const fresh = catalogs();
fresh.local.models[0].thinking = [];
assert.throws(() => discoverCursorModels(fresh.gateway, fresh.local, "http://localhost:4000/v1"), /thinking-level/);
assert.throws(() => discoverCursorModels(fresh.gateway, { models: [] }, "http://localhost:4000/v1"), /do not match/);
});
test("accepts an authoritative empty catalog and deduplicates model ids", () => {
const { gateway, local } = catalogs();
gateway.data.push(gateway.data[0]);
assert.equal(discoverCursorModels(gateway, local, "http://localhost:4000/v1").length, 1);
assert.deepEqual(discoverCursorModels({ data: [] }, local, "http://localhost:4000/v1"), []);
});
test("qualifies wire ids and keeps session identity and request fields", () => {
assert.deepEqual(gatewayPayload({ model: "grok-4.7", messages: [], temperature: 0.5 }, "grok-4.7", "session-one"),
{ model: "cursor/grok-4.7", messages: [], temperature: 0.5, prompt_cache_key: "session-one" });
assert.throws(() => gatewayPayload(null, "grok-4.7"), /Invalid/);
assert.throws(() => gatewayPayload({}, "../bad"), /Invalid/);
});
test("rejects remote endpoints, embedded credentials, and query strings", () => {
assert.equal(loopbackBaseUrl("http://127.0.0.1:4000/v1/"), "http://127.0.0.1:4000/v1");
assert.equal(loopbackBaseUrl("http://[::1]:4000/v1"), "http://[::1]:4000/v1");
for (const url of ["https://remote.example/v1", "http://token@localhost:4000/v1", "http://localhost/v1?token=x", "http://localhost/"]) {
assert.throws(() => loopbackBaseUrl(url), /loopback/);
}
});

View file

@ -0,0 +1,212 @@
const assert = require('node:assert/strict');
const { spawn } = require('node:child_process');
const fs = require('node:fs');
const http = require('node:http');
const os = require('node:os');
const path = require('node:path');
const { test } = require('node:test');
// Runs the installed Pi provider runtime against a real local HTTP server.
// Only OMP's external CLI and upstream inference are fixtures; no paid requests.
test('native provider discovery, streaming, tools, usage, token rotation, and cache restoration', { timeout: 60000 }, async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-omp-gateway-'));
const tokenFile = path.join(dir, 'gateway.token');
const metadataFile = path.join(dir, 'catalog.json');
const fixture = path.join(dir, 'fixture.txt');
const omp = path.join(dir, 'omp');
const helper = path.join(dir, 'helper.ts');
const defaultModel = { id: 'idle', name: 'Idle', reasoning: false, input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: 32000, maxTokens: 1024 };
fs.writeFileSync(tokenFile, 'test-token', { mode: 0o600 });
fs.writeFileSync(fixture, 'LOCAL_FIXTURE_OK');
const meta = { provider: 'cursor', kind: 'chat', id: 'grok-4.7', name: 'Grok 4.7', input: ['text', 'image'],
reasoning: true, thinking: ['low', 'medium', 'high', 'xhigh'], contextWindow: 256000, maxTokens: 64000,
cost: { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 } };
fs.writeFileSync(metadataFile, JSON.stringify({ models: [meta] }));
const mockCli = path.join(dir, 'omp.cjs');
fs.writeFileSync(mockCli, `const fs=require('node:fs');
if (process.argv.slice(2).join(' ') !== 'models cursor --json --no-extensions') process.exit(2);
if (process.env.PI_CONFIG_DIR !== '.omp-cursor' || process.env.PI_CODING_AGENT_DIR !== ${JSON.stringify(path.join(dir, 'cursor-agent'))} ||
process.env.OMP_PROFILE !== '' || process.env.PI_PROFILE !== '' || process.env.PI_CONFIG_FILES !== '' ||
process.env.OMP_AUTH_BROKER_URL || process.env.OMP_AUTH_BROKER_TOKEN) process.exit(3);
process.stdout.write(fs.readFileSync(${JSON.stringify(metadataFile)},'utf8'));`);
fs.writeFileSync(omp, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(mockCli)} "$@"\n`, { mode: 0o700 });
let expectedToken = 'test-token';
let catalogStatus = 200;
let rows = [{ id: 'cursor/grok-4.7', owned_by: 'cursor', display_name: 'Grok 4.7', input_modalities: ['text', 'image'],
context_length: 256000, max_output_tokens: 64000 }];
const requests = [];
const server = http.createServer(async (req, res) => {
if (req.headers.authorization !== `Bearer ${expectedToken}`) {
res.writeHead(401); res.end('DO_NOT_LOG_THIS_SECRET'); return;
}
if (req.url === '/v1/models') {
res.writeHead(catalogStatus, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(catalogStatus === 200 ? { object: 'list', data: rows } : { error: 'DO_NOT_LOG_THIS_SECRET' }));
return;
}
if (req.url !== '/v1/chat/completions') { res.writeHead(404); res.end(); return; }
let raw = '';
for await (const chunk of req) raw += chunk;
const body = JSON.parse(raw);
requests.push({ body, authorization: req.headers.authorization });
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
const send = (delta, finish = null) => res.write(`data: ${JSON.stringify({ id: 'mock', object: 'chat.completion.chunk',
model: body.model, choices: [{ index: 0, delta, finish_reason: finish }] })}\n\n`);
send({ role: 'assistant' });
const toolResult = body.messages.findLast(message => message.role === 'tool');
const user = body.messages.findLast(message => message.role === 'user');
const prompt = typeof user?.content === 'string' ? user.content :
Array.isArray(user?.content) ? user.content.filter(block => block.type === 'text').map(block => block.text).join('') : '';
if (prompt === 'USE_TOOL' && !toolResult) {
send({ tool_calls: [{ index: 0, id: 'call_read', type: 'function', function: {
name: 'read', arguments: JSON.stringify({ path: fixture }) } }] });
send({}, 'tool_calls');
} else {
if (prompt !== 'EMPTY') send({ content: toolResult ? 'TOOL_OK' : 'Gateway ✓ Unicode 你好' });
send({}, 'stop');
}
res.write(`data: ${JSON.stringify({ id: 'mock', choices: [], usage: {
prompt_tokens: 100, completion_tokens: 4, total_tokens: 104, prompt_tokens_details: { cached_tokens: 10 } } })}\n\n`);
res.end('data: [DONE]\n\n');
});
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
const baseUrl = `http://127.0.0.1:${server.address().port}/v1`;
fs.writeFileSync(helper, `export default function(pi) { pi.registerProvider('gateway-test', {
api: 'openai-completions', apiKey: 'test-token', baseUrl: ${JSON.stringify(baseUrl)}, models: [${JSON.stringify(defaultModel)}]
}); }`);
const children = [];
const allStderr = [];
function start() {
const child = spawn('pi', ['--offline', '--no-extensions', '--no-mcp', '--no-skills', '--no-prompt-templates',
'--no-context-files', '--no-session', '-e', path.resolve(__dirname, '../index.ts'), '-e', helper,
'--model', 'gateway-test/idle', '--mode', 'rpc', '--tools', 'read'], {
env: { ...process.env, PI_CODING_AGENT_DIR: dir, OMP_GATEWAY_URL: baseUrl,
OMP_GATEWAY_TOKEN_FILE: tokenFile, OMP_GATEWAY_COMMAND: omp, OMP_GATEWAY_AGENT_DIR: path.join(dir, "cursor-agent"),
OMP_PROFILE: "do-not-use", PI_PROFILE: "do-not-use", PI_CONFIG_FILES: "/do-not-use.yml",
OMP_AUTH_BROKER_URL: "http://do-not-use", OMP_AUTH_BROKER_TOKEN: "do-not-use" },
});
children.push(child);
let buffer = '', stderr = '', seq = 0;
const events = [], waiters = new Set();
child.stderr.on('data', chunk => { stderr += chunk; allStderr.push(String(chunk)); });
child.stdout.on('data', chunk => {
buffer += chunk;
let newline;
while ((newline = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, newline); buffer = buffer.slice(newline + 1);
let record;
try { record = JSON.parse(line); } catch { continue; }
events.push(record);
for (const waiter of [...waiters]) if (waiter.predicate(record)) {
waiters.delete(waiter); clearTimeout(waiter.timer); waiter.resolve(record);
}
}
});
function wait(predicate) {
return new Promise((resolve, reject) => {
const waiter = { predicate, resolve, timer: setTimeout(() => {
waiters.delete(waiter); reject(new Error(`RPC timeout: ${stderr}`));
}, 15000) };
waiters.add(waiter);
});
}
async function command(type, fields = {}) {
const id = String(++seq);
const response = wait(record => record.type === 'response' && record.id === id);
child.stdin.write(JSON.stringify({ id, type, ...fields }) + '\n');
const record = await response;
assert.equal(record.success, true, JSON.stringify(record));
return record;
}
async function prompt(message) {
const from = events.length;
const done = wait(record => record.type === 'agent_settled');
await command('prompt', { message }); await done;
return events.slice(from).filter(record => record.type === 'message_end' && record.message?.role === 'assistant').at(-1).message;
}
async function close() {
for (const waiter of waiters) clearTimeout(waiter.timer);
if (child.exitCode !== null) return;
const done = new Promise(resolve => child.once('close', resolve));
child.stdin.end();
const timer = setTimeout(() => child.kill('SIGKILL'), 3000);
await done; clearTimeout(timer);
}
return { child, command, prompt, close, events };
}
try {
const runtime = start();
await runtime.command('set_auto_retry', { enabled: false });
const before = await runtime.command('get_available_models');
assert.equal(before.data.models.filter(model => model.provider === 'cursor').length, 0, 'No network or CLI startup work in the factory');
await runtime.prompt('BOOT');
requests.length = 0;
await runtime.command('prompt', { message: '/omp-gateway refresh' });
const available = await runtime.command('get_available_models');
const model = available.data.models.find(model => model.provider === 'cursor' && model.id === 'grok-4.7');
assert.ok(model, JSON.stringify(runtime.events));
assert.equal(model.thinkingLevelMap.max, null);
await runtime.command('set_model', { provider: 'cursor', modelId: 'grok-4.7' });
await runtime.command('set_thinking_level', { level: 'high' });
const selected = await runtime.command('get_state');
assert.equal(selected.data.model.provider, 'cursor', JSON.stringify(selected));
const text = await runtime.prompt('TEXT');
assert.equal(text.stopReason, 'stop', JSON.stringify(text));
assert.equal(text.content.find(block => block.type === 'text').text, 'Gateway ✓ Unicode 你好');
assert.equal(text.provider, 'cursor');
assert.equal(text.model, 'grok-4.7');
assert.equal(text.usage.input, 90);
assert.equal(text.usage.cacheRead, 10);
assert.equal(text.usage.output, 4);
assert.ok(text.usage.cost.total > 0);
assert.equal(requests[0].body.model, 'cursor/grok-4.7');
assert.equal(requests[0].body.reasoning_effort, 'high');
assert.ok(requests[0].body.prompt_cache_key);
await runtime.command('new_session');
await runtime.prompt('BOOT');
await runtime.command('set_model', { provider: 'cursor', modelId: 'grok-4.7' });
await runtime.command('set_thinking_level', { level: 'high' });
const tool = await runtime.prompt('USE_TOOL');
assert.equal(tool.stopReason, 'stop', JSON.stringify(tool));
assert.equal(tool.content.find(block => block.type === 'text').text, 'TOOL_OK');
assert.ok(requests.at(-1).body.messages.some(message => message.role === 'tool' && JSON.stringify(message.content).includes('LOCAL_FIXTURE_OK')));
assert.equal(requests.at(-1).body.prompt_cache_key, requests.at(-2).body.prompt_cache_key, 'Keep the tool handoff in the same gateway conversation');
assert.notEqual(requests[0].body.prompt_cache_key, requests.at(-1).body.prompt_cache_key, 'New sessions must not share Cursor conversations');
expectedToken = 'rotated-token';
fs.writeFileSync(tokenFile, expectedToken);
await runtime.prompt('AFTER_ROTATION');
assert.equal(requests.at(-1).authorization, 'Bearer rotated-token');
const empty = await runtime.prompt('EMPTY');
assert.equal(empty.stopReason, 'stop');
assert.deepEqual(empty.content, []);
catalogStatus = 401;
await runtime.command('prompt', { message: '/omp-gateway refresh' });
assert.ok(runtime.events.some(record => record.type === 'extension_ui_request' && /HTTP 401/.test(record.message || '')));
const retained = await runtime.command('get_available_models');
assert.ok(retained.data.models.some(model => model.provider === 'cursor'), 'Failed refresh must preserve the last catalog');
assert.ok(!JSON.stringify(runtime.events).includes('DO_NOT_LOG_THIS_SECRET'));
catalogStatus = 200;
await runtime.close();
const restored = start();
const cached = await restored.command('get_available_models');
assert.ok(cached.data.models.some(model => model.provider === 'cursor'), 'Pi restores persisted catalog offline');
rows = [{ id: "xai/grok-4.7", owned_by: "xai" }];
await restored.command("prompt", { message: "/omp-gateway refresh" });
assert.ok(restored.events.some(record => record.type === "extension_ui_request" && /non-Cursor/.test(record.message || "")));
const scoped = await restored.command("get_available_models");
assert.ok(scoped.data.models.some(model => model.provider === "cursor"), "Reject mixed catalogs without losing cached Cursor models");
assert.ok(!scoped.data.models.some(model => model.provider === "xai"));
rows = [];
await restored.command('prompt', { message: '/omp-gateway refresh' });
const removed = await restored.command('get_available_models');
assert.equal(removed.data.models.filter(model => model.provider === 'cursor').length, 0, 'Authoritative empty catalogs remove models');
await restored.close();
assert.ok(!allStderr.join('').includes('DO_NOT_LOG_THIS_SECRET'));
} finally {
for (const child of children) if (child.exitCode === null) child.kill('SIGKILL');
server.closeAllConnections();
await new Promise(resolve => server.close(resolve));
fs.rmSync(dir, { recursive: true, force: true });
}
});