History

Probe Cursor models via list_available_models (#2428)

Julius Marminge committed 5 months agov0.0.25-nightly.20260603.451

Commit d78e02c1 parent f0116e46 files · +121 −360Browse files

Unified · Split

6 files
  1. apps/server/scripts/acp-mock-agent.ts +35 −0
  2. apps/server/src/provider/Drivers/CursorDriver.ts +6 −10
  3. apps/server/src/provider/Layers/CursorProvider.test.ts +1 −133
  4. apps/server/src/provider/Layers/CursorProvider.ts +41 −217
  5. apps/server/src/provider/acp/CursorAcpExtension.test.ts +27 −0
  6. apps/server/src/provider/acp/CursorAcpExtension.ts +11 −0
apps/server/scripts/acp-mock-agent.ts+35−0
@@ -183,6 +183,35 @@ function configOptions(): ReadonlyArray<AcpSchema.SessionConfigOption> { · 182 unmodified lines
183183 ];
184184 }
185185
186+function modelConfigOptionsFor(modelId: string): ReadonlyArray<AcpSchema.SessionConfigOption> {
187+ const previousModelId = currentModelId;
188+ try {
189+ currentModelId = modelId;
190+ return configOptions().filter(
191+ (option) => option.category !== "mode" && option.category !== "model",
192+ );
193+ } finally {
194+ currentModelId = previousModelId;
195+ }
196+}
197+
198+function availableModels(): ReadonlyArray<{
199+ readonly value: string;
200+ readonly name: string;
201+ readonly configOptions: ReadonlyArray<AcpSchema.SessionConfigOption>;
202+}> {
203+ return [
204+ { value: "default", name: "Auto" },
205+ { value: "composer-2", name: "Composer 2" },
206+ { value: "gpt-5.4", name: "GPT-5.4" },
207+ { value: "claude-opus-4-6", name: "Opus 4.6" },
208+ ].map((model) => ({
209+ value: model.value,
210+ name: model.name,
211+ configOptions: modelConfigOptionsFor(model.value),
212+ }));
213+}
214+
186215 const availableModes: ReadonlyArray<AcpSchema.SessionMode> = [
187216 {
188217 id: "ask",
@@ -517,6 +546,12 @@ const program = Effect.gen(function* () { · 328 unmodified lines
517546 );
518547
519548 yield* agent.handleUnknownExtRequest((method, params) => {
549+ if (method === "cursor/list_available_models") {
550+ return Effect.succeed({
551+ models: availableModels(),
552+ });
553+ }
554+
520555 if (method !== "session/mode/set") {
521556 return Effect.fail(AcpError.AcpRequestError.methodNotFound(method));
522557 }
apps/server/src/provider/Drivers/CursorDriver.ts+6−10
@@ -1,10 +1,9 @@
11 /**
22 * CursorDriver — `ProviderDriver` for the Cursor Agent (`agent`) runtime.
33 *
4− * Cursor exposes an ACP-based CLI. The driver is still a plain value, but
5− * its snapshot uses `makeManagedServerProvider`'s optional `enrichSnapshot`
6− * hook to run the slow ACP model-capability probe in the background without
7− * blocking the initial `ready`-state publish.
4+ * Cursor exposes an ACP-based CLI. Model catalog and capability refreshes
5+ * happen during the managed provider status check via Cursor's
6+ * `list_available_models` extension method.
87 *
98 * Text generation is supported via the ACP runtime — `makeCursorTextGeneration`
109 * drives `runtime.prompt` with a structured-output schema and collects the
@@ -139,20 +138,17 @@ export const CursorDriver: ProviderDriver<CursorSettings, CursorDriverEnv> = { · 128 unmodified lines
139138 initialSnapshot: (settings) =>
140139 buildInitialCursorProviderSnapshot(settings).pipe(Effect.map(stampIdentity)),
141140 checkProvider,
142− // Preserve the background ACP model-capability probe that used to
143− // live on `CursorProviderLive`. Only fires when the snapshot reports
144− // an authenticated, enabled provider with at least one non-custom
145− // model whose capabilities haven't been captured yet.
141+ // Model catalog and capabilities come exclusively from Cursor's
142+ // list_available_models extension method during provider checks.
146143 enrichSnapshot: ({ settings, snapshot: currentSnapshot, publishSnapshot }) =>
147144 enrichCursorSnapshot({
148145 settings,
149− environment: processEnv,
150146 snapshot: currentSnapshot,
151147 maintenanceCapabilities,
152148 publishSnapshot,
153149 stampIdentity,
154150 httpClient,
155− }).pipe(Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner)),
151+ }),
156152 refreshInterval: SNAPSHOT_REFRESH_INTERVAL,
157153 }).pipe(
158154 Effect.mapError(
apps/server/src/provider/Layers/CursorProvider.test.ts+1−133
@@ -6,15 +6,13 @@ import * as FileSystem from "effect/FileSystem"; · 5 unmodified lines
66 import * as Path from "effect/Path";
77 import { describe, expect, it } from "vitest";
88 import type * as EffectAcpSchema from "effect-acp/schema";
9−import type { CursorSettings, ServerProviderModel } from "@t3tools/contracts";
9+import type { CursorSettings } from "@t3tools/contracts";
1010 import { createModelCapabilities } from "@t3tools/shared/model";
1111
1212 import {
1313 buildCursorProviderSnapshot,
1414 buildCursorCapabilitiesFromConfigOptions,
15− buildCursorDiscoveredModelsFromConfigOptions,
1615 checkCursorProviderStatus,
17− discoverCursorModelCapabilitiesViaAcp,
1816 discoverCursorModelsViaAcp,
1917 getCursorFallbackModels,
2018 getCursorParameterizedModelPickerUnsupportedMessage,
@@ -295,48 +293,6 @@ const parameterizedClaudeModelOptionConfigOptions = [ · 274 unmodified lines
295293 },
296294 ] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>;
297295
298−const sessionNewCursorConfigOptions = [
299− {
300− type: "select",
301− currentValue: "agent",
302− options: [
303− { name: "Agent", value: "agent", description: "Full agent capabilities with tool access" },
304− ],
305− category: "mode",
306− id: "mode",
307− name: "Mode",
308− description: "Controls how the agent executes tasks",
309− },
310− {
311− type: "select",
312− currentValue: "composer-2",
313− options: [
314− { name: "Auto", value: "default" },
315− { name: "Composer 2", value: "composer-2" },
316− { name: "GPT-5.4", value: "gpt-5.4" },
317− { name: "Sonnet 4.6", value: "claude-sonnet-4-6" },
318− { name: "Opus 4.6", value: "claude-opus-4-6" },
319− { name: "Codex 5.3 Spark", value: "gpt-5.3-codex-spark" },
320− ],
321− category: "model",
322− id: "model",
323− name: "Model",
324− description: "Controls which model is used for responses",
325− },
326− {
327− type: "select",
328− currentValue: "true",
329− options: [
330− { name: "Off", value: "false" },
331− { name: "Fast", value: "true" },
332− ],
333− category: "model_config",
334− id: "fast",
335− name: "Fast",
336− description: "Faster speeds.",
337− },
338−] satisfies ReadonlyArray<EffectAcpSchema.SessionConfigOption>;
339−
340296 const baseCursorSettings: CursorSettings = {
341297 enabled: true,
342298 binaryPath: "agent",
@@ -344,8 +300,6 @@ const baseCursorSettings: CursorSettings = { · 1 unmodified line
344300 customModels: [],
345301 };
346302
347−const emptyCapabilities = createModelCapabilities({ optionDescriptors: [] });
348−
349303 describe("getCursorFallbackModels", () => {
350304 it("does not publish any built-in cursor models before ACP discovery", () => {
351305 expect(
@@ -462,51 +416,6 @@ describe("buildCursorCapabilitiesFromConfigOptions", () => { · 110 unmodified lines
462416 });
463417 });
464418
465−describe("buildCursorDiscoveredModelsFromConfigOptions", () => {
466− it("publishes ACP model choices immediately from session/new config options", () => {
467− expect(buildCursorDiscoveredModelsFromConfigOptions(sessionNewCursorConfigOptions)).toEqual([
468− {
469− slug: "default",
470− name: "Auto",
471− isCustom: false,
472− capabilities: emptyCapabilities,
473− },
474− {
475− slug: "composer-2",
476− name: "Composer 2",
477− isCustom: false,
478− capabilities: createModelCapabilities({
479− optionDescriptors: [booleanDescriptor("fastMode", "Fast", true)],
480− }),
481− },
482− {
483− slug: "gpt-5.4",
484− name: "GPT-5.4",
485− isCustom: false,
486− capabilities: emptyCapabilities,
487− },
488− {
489− slug: "claude-sonnet-4-6",
490− name: "Sonnet 4.6",
491− isCustom: false,
492− capabilities: emptyCapabilities,
493− },
494− {
495− slug: "claude-opus-4-6",
496− name: "Opus 4.6",
497− isCustom: false,
498− capabilities: emptyCapabilities,
499− },
500− {
501− slug: "gpt-5.3-codex-spark",
502− name: "Codex 5.3 Spark",
503− isCustom: false,
504− capabilities: emptyCapabilities,
505− },
506− ]);
507− });
508−});
509−
510419 describe("checkCursorProviderStatus", () => {
511420 it("passes the injected environment to ACP model discovery", async () => {
512421 const { requestLogPath, wrapperPath } = await runNode(makeProviderStatusEnvFixture());
@@ -576,47 +485,6 @@ describe("discoverCursorModelsViaAcp", () => { · 63 unmodified lines
576485 });
577486 });
578487
579−describe("discoverCursorModelCapabilitiesViaAcp", () => {
580− it("closes all ACP probe runtimes after capability enrichment completes", async () => {
581− const { exitLogPath, wrapperPath } = await runNode(
582− makeExitLogFixture("cursor-capabilities-exit-log-"),
583− );
584− const existingModels: ReadonlyArray<ServerProviderModel> = [
585− { slug: "default", name: "Auto", isCustom: false, capabilities: emptyCapabilities },
586− { slug: "composer-2", name: "Composer 2", isCustom: false, capabilities: emptyCapabilities },
587− { slug: "gpt-5.4", name: "GPT-5.4", isCustom: false, capabilities: emptyCapabilities },
588− {
589− slug: "claude-opus-4-6",
590− name: "Opus 4.6",
591− isCustom: false,
592− capabilities: emptyCapabilities,
593− },
594− ];
595−
596− const models = await Effect.runPromise(
597− discoverCursorModelCapabilitiesViaAcp(
598− {
599− enabled: true,
600− binaryPath: wrapperPath,
601− apiEndpoint: "",
602− customModels: [],
603− },
604− existingModels,
605− ).pipe(Effect.provide(NodeServices.layer)),
606− );
607−
608− expect(models.map((model) => model.slug)).toEqual([
609− "default",
610− "composer-2",
611− "gpt-5.4",
612− "claude-opus-4-6",
613− ]);
614−
615− const exitLog = await runNode(waitForFileContent(exitLogPath));
616− expect(exitLog.match(/SIGTERM/g)?.length ?? 0).toBe(4);
617− });
618−});
619−
620488 describe("parseCursorAboutOutput", () => {
621489 it("parses json about output and forwards subscription metadata", () => {
622490 expect(
apps/server/src/provider/Layers/CursorProvider.ts+41−217
@@ -19,6 +19,7 @@ import * as Layer from "effect/Layer"; · 18 unmodified lines
1919 import * as Option from "effect/Option";
2020 import * as Path from "effect/Path";
2121 import * as Result from "effect/Result";
22+import * as Schema from "effect/Schema";
2223 import { HttpClient } from "effect/unstable/http";
2324 import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
2425 import {
@@ -42,6 +43,7 @@ import { · 17 unmodified lines
4243 type ProviderMaintenanceCapabilities,
4344 } from "../providerMaintenance.ts";
4445 import { AcpSessionRuntime } from "../acp/AcpSessionRuntime.ts";
46+import { CursorListAvailableModelsResponse } from "../acp/CursorAcpExtension.ts";
4547
4648 const PROVIDER = ProviderDriverKind.make("cursor");
4749 const CURSOR_PRESENTATION = {
@@ -54,8 +56,6 @@ const EMPTY_CAPABILITIES: ModelCapabilities = createModelCapabilities({ · 6 unmodified lines
5456 });
5557
5658 const CURSOR_ACP_MODEL_DISCOVERY_TIMEOUT_MS = 15_000;
57−const CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT = "4 seconds";
58−const CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY = 4;
5959 const CURSOR_PARAMETERIZED_MODEL_PICKER_MIN_VERSION_DATE = 2026_04_08;
6060 export const CURSOR_PARAMETERIZED_MODEL_PICKER_CAPABILITIES = {
6161 _meta: {
@@ -154,12 +154,6 @@ function normalizeCursorReasoningValue(value: string | null | undefined): string · 92 unmodified lines
154154 }
155155 }
156156
157−function findCursorModelConfigOption(
158− configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption>,
159−): EffectAcpSchema.SessionConfigOption | undefined {
160− return configOptions.find((option) => option.category === "model");
161−}
162−
163157 function getCursorConfigOptionCategory(option: EffectAcpSchema.SessionConfigOption): string {
164158 return option.category?.trim().toLowerCase() ?? "";
165159 }
@@ -373,36 +367,25 @@ function buildCursorDiscoveredModels( · 207 unmodified lines
373367 });
374368 }
375369
376−function hasCursorModelCapabilities(model: Pick<ServerProviderModel, "capabilities">): boolean {
377− return (model.capabilities?.optionDescriptors?.length ?? 0) > 0;
378−}
379−
380−export function buildCursorDiscoveredModelsFromConfigOptions(
381− configOptions: ReadonlyArray<EffectAcpSchema.SessionConfigOption> | null | undefined,
370+function buildCursorDiscoveredModelsFromAvailableModelsResponse(
371+ response: typeof CursorListAvailableModelsResponse.Type,
382372 ): ReadonlyArray<ServerProviderModel> {
383− if (!configOptions || configOptions.length === 0) {
384− return [];
385− }
386−
387− const modelOption = findCursorModelConfigOption(configOptions);
388− const modelChoices = flattenSessionConfigSelectOptions(modelOption);
389− if (!modelOption || modelChoices.length === 0) {
390− return [];
391− }
392−
393− const currentModelValue =
394− modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined;
395− const currentModelCapabilities = buildCursorCapabilitiesFromConfigOptions(configOptions);
396−
397373 return buildCursorDiscoveredModels(
398− modelChoices.map((modelChoice) => ({
399− slug: modelChoice.value.trim(),
400− name: modelChoice.name.trim(),
401− capabilities:
402− currentModelValue === modelChoice.value.trim()
403− ? currentModelCapabilities
404− : EMPTY_CAPABILITIES,
405− })),
374+ response.models.flatMap((model) => {
375+ const slug = model.value.trim();
376+ const name = model.name.trim();
377+ if (!slug || !name) {
378+ return [];
379+ }
380+
381+ return [
382+ {
383+ slug,
384+ name,
385+ capabilities: buildCursorCapabilitiesFromConfigOptions(model.configOptions),
386+ },
387+ ];
388+ }),
406389 );
407390 }
408391
@@ -554,140 +537,28 @@ export function resolveCursorAcpConfigUpdates( · 145 unmodified lines
554537 return updates;
555538 }
556539
557−export const discoverCursorModelsViaAcp = (
540+const discoverCursorModelsViaListAvailableModels = (
558541 cursorSettings: CursorSettings,
559542 environment: NodeJS.ProcessEnv = process.env,
560543 ) =>
561544 withCursorAcpProbeRuntime(
562545 cursorSettings,
563546 (acp) =>
564− Effect.map(acp.start(), (started) =>
565− buildCursorDiscoveredModelsFromConfigOptions(
566− started.sessionSetupResult.configOptions ?? [],
567− ),
568− ),
547+ Effect.gen(function* () {
548+ yield* acp.start();
549+ const response = yield* acp.request("cursor/list_available_models", {});
550+ const decoded = yield* Schema.decodeUnknownEffect(CursorListAvailableModelsResponse)(
551+ response,
552+ );
553+ return buildCursorDiscoveredModelsFromAvailableModelsResponse(decoded);
554+ }),
569555 environment,
570556 );
571557
572−export const discoverCursorModelCapabilitiesViaAcp = (
558+export const discoverCursorModelsViaAcp = (
573559 cursorSettings: CursorSettings,
574− existingModels: ReadonlyArray<ServerProviderModel>,
575560 environment: NodeJS.ProcessEnv = process.env,
576−) =>
577− withCursorAcpProbeRuntime(
578− cursorSettings,
579− (acp) =>
580− Effect.gen(function* () {
581− const started = yield* acp.start();
582− const initialConfigOptions = started.sessionSetupResult.configOptions ?? [];
583− const modelOption = findCursorModelConfigOption(initialConfigOptions);
584− const modelChoices = flattenSessionConfigSelectOptions(modelOption);
585− if (!modelOption || modelChoices.length === 0) {
586− return [];
587− }
588−
589− const currentModelValue =
590− modelOption.type === "select" ? modelOption.currentValue?.trim() || undefined : undefined;
591− const capabilitiesBySlug = new Map<string, ModelCapabilities>();
592− if (currentModelValue) {
593− capabilitiesBySlug.set(
594− currentModelValue,
595− buildCursorCapabilitiesFromConfigOptions(initialConfigOptions),
596− );
597− }
598−
599− const targetModelSlugs = new Set<string>();
600− for (const model of existingModels) {
601− if (!model.isCustom && !hasCursorModelCapabilities(model)) {
602− targetModelSlugs.add(model.slug);
603− }
604− }
605− if (targetModelSlugs.size === 0) {
606− return buildCursorDiscoveredModels(
607− modelChoices.map((modelChoice) => ({
608− slug: modelChoice.value.trim(),
609− name: modelChoice.name.trim(),
610− capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES,
611− })),
612− );
613− }
614−
615− const probedCapabilities = yield* Effect.forEach(
616− modelChoices,
617− (modelChoice) => {
618− const modelSlug = modelChoice.value.trim();
619− if (
620− !modelSlug ||
621− !targetModelSlugs.has(modelSlug) ||
622− capabilitiesBySlug.has(modelSlug)
623− ) {
624− return Effect.void.pipe(
625− Effect.as<readonly [string, ModelCapabilities] | undefined>(undefined),
626− );
627− }
628−
629− return withCursorAcpProbeRuntime(
630− cursorSettings,
631− (probeAcp) =>
632− Effect.gen(function* () {
633− const probeStarted = yield* probeAcp.start();
634− const probeConfigOptions = probeStarted.sessionSetupResult.configOptions ?? [];
635− const probeModelOption = findCursorModelConfigOption(probeConfigOptions);
636− const probeCurrentModelValue =
637− probeModelOption?.type === "select"
638− ? probeModelOption.currentValue?.trim() || undefined
639− : undefined;
640− yield* Effect.annotateCurrentSpan({
641− "cursor.acp.model.value": modelSlug,
642− "cursor.acp.model.currentValue": probeCurrentModelValue,
643− "cursor.acp.config_option_id": probeModelOption?.id ?? modelOption.id,
644− });
645− const nextConfigOptions =
646− probeCurrentModelValue === modelSlug
647− ? probeConfigOptions
648− : yield* probeAcp
649− .setConfigOption(probeModelOption?.id ?? modelOption.id, modelSlug)
650− .pipe(
651− Effect.map((response) => response.configOptions ?? probeConfigOptions),
652− );
653− return [
654− modelSlug,
655− buildCursorCapabilitiesFromConfigOptions(nextConfigOptions),
656− ] as const;
657− }),
658− environment,
659− ).pipe(
660− Effect.timeout(CURSOR_ACP_MODEL_CAPABILITY_TIMEOUT),
661− Effect.retry({ times: 3 }),
662− Effect.withSpan("cursor-acp-model-capability-probe"),
663− Effect.catchCause((cause) =>
664− Effect.logWarning("Cursor ACP capability probe failed", {
665− modelSlug,
666− cause: Cause.pretty(cause),
667− }),
668− ),
669− );
670− },
671− { concurrency: CURSOR_ACP_MODEL_DISCOVERY_CONCURRENCY },
672− );
673−
674− for (const entry of probedCapabilities) {
675− if (!entry) {
676− continue;
677− }
678− capabilitiesBySlug.set(entry[0], entry[1]);
679− }
680−
681− return buildCursorDiscoveredModels(
682− modelChoices.map((modelChoice) => ({
683− slug: modelChoice.value.trim(),
684− name: modelChoice.name.trim(),
685− capabilities: capabilitiesBySlug.get(modelChoice.value.trim()) ?? EMPTY_CAPABILITIES,
686− })),
687− );
688− }).pipe(Effect.withSpan("cursor-acp-model-capability-discovery", {})),
689− environment,
690− );
561+) => discoverCursorModelsViaListAvailableModels(cursorSettings, environment);
691562
692563 export function getCursorFallbackModels(
693564 cursorSettings: Pick<CursorSettings, "customModels">,
@@ -1219,36 +1090,30 @@ export const checkCursorProviderStatus = Effect.fn("checkCursorProviderStatus")( · 525 unmodified lines
12191090 });
12201091 });
12211092
1222−export function hasUncapturedCursorModels(snapshot: Pick<ServerProvider, "models">): boolean {
1223− return snapshot.models.some((model) => !model.isCustom && !hasCursorModelCapabilities(model));
1224−}
1225−
12261093 /**
1227− * Background capability enrichment for a Cursor snapshot.
1094+ * Background maintenance enrichment for a Cursor snapshot.
12281095 *
12291096 * Used by `CursorDriver` as the `makeManagedServerProvider.enrichSnapshot`
1230− * hook: runs the slow ACP per-model capability probe, and republishes the
1231− * snapshot through `publishSnapshot` when new capabilities arrive. Skips
1232− * the probe when the provider is disabled, unauthenticated, or has no
1233− * uncaptured models. Keeps `EMPTY_CAPABILITIES` and the `PROVIDER` literal
1234− * private to this module.
1097+ * hook: republishes update/version advisory metadata without performing any
1098+ * model or capability discovery. Cursor model data comes exclusively from
1099+ * `cursor/list_available_models` during provider status checks.
12351100 */
12361101 export const enrichCursorSnapshot = (input: {
12371102 readonly settings: CursorSettings;
1238− readonly environment?: NodeJS.ProcessEnv;
12391103 readonly snapshot: ServerProvider;
12401104 readonly maintenanceCapabilities: ProviderMaintenanceCapabilities;
12411105 readonly publishSnapshot: (snapshot: ServerProvider) => Effect.Effect<void>;
12421106 readonly stampIdentity?: (snapshot: ServerProvider) => ServerProvider;
12431107 readonly httpClient: HttpClient.HttpClient;
1244−}): Effect.Effect<void, never, ChildProcessSpawner.ChildProcessSpawner> => {
1108+}): Effect.Effect<void> => {
12451109 const { settings, snapshot, publishSnapshot } = input;
12461110 const stampIdentity = input.stampIdentity ?? ((value) => value);
12471111
1248− const enrichVersionAdvisory = enrichProviderSnapshotWithVersionAdvisory(
1249− snapshot,
1250− input.maintenanceCapabilities,
1251− ).pipe(
1112+ if (!settings.enabled || snapshot.auth.status === "unauthenticated") {
1113+ return Effect.void;
1114+ }
1115+
1116+ return enrichProviderSnapshotWithVersionAdvisory(snapshot, input.maintenanceCapabilities).pipe(
12521117 Effect.provideService(HttpClient.HttpClient, input.httpClient),
12531118 Effect.flatMap((enrichedSnapshot) =>
12541119 publishSnapshot(stampIdentity(enrichedSnapshot)).pipe(Effect.as(enrichedSnapshot)),
@@ -1256,48 +1121,7 @@ export const enrichCursorSnapshot = (input: { · 1 unmodified line
12561121 Effect.catchCause((cause) =>
12571122 Effect.logWarning("Cursor version advisory enrichment failed", {
12581123 cause: Cause.pretty(cause),
1259− }).pipe(Effect.as(snapshot)),
1124+ }).pipe(Effect.asVoid),
12601125 ),
12611126 );
1262−
1263− return enrichVersionAdvisory.pipe(
1264− Effect.flatMap((baseSnapshot) => {
1265− if (
1266− !settings.enabled ||
1267− baseSnapshot.auth.status === "unauthenticated" ||
1268− !hasUncapturedCursorModels(baseSnapshot)
1269− ) {
1270− return Effect.void;
1271− }
1272−
1273− return discoverCursorModelCapabilitiesViaAcp(
1274− settings,
1275− baseSnapshot.models,
1276− input.environment,
1277− ).pipe(
1278− Effect.flatMap((discoveredModels) => {
1279− if (discoveredModels.length === 0) {
1280− return Effect.void;
1281− }
1282− return publishSnapshot(
1283− stampIdentity({
1284− ...baseSnapshot,
1285− models: providerModelsFromSettings(
1286− discoveredModels,
1287− PROVIDER,
1288− settings.customModels,
1289− EMPTY_CAPABILITIES,
1290− ),
1291− }),
1292− );
1293− }),
1294− Effect.catchCause((cause) =>
1295− Effect.logWarning("Cursor ACP background capability enrichment failed", {
1296− models: baseSnapshot.models.map((model) => model.slug),
1297− cause: Cause.pretty(cause),
1298− }).pipe(Effect.asVoid),
1299− ),
1300− );
1301− }),
1302− );
13031127 };
apps/server/src/provider/acp/CursorAcpExtension.test.ts+27−0
@@ -1,6 +1,7 @@
11 import { describe, expect, it } from "vitest";
22
33 import {
4+ CursorListAvailableModelsResponse,
45 extractAskQuestions,
56 extractPlanMarkdown,
67 extractTodosAsPlan,
@@ -105,4 +106,30 @@ describe("CursorAcpExtension", () => { · 98 unmodified lines
105106 ],
106107 });
107108 });
109+
110+ it("decodes Cursor list_available_models responses with per-model config options", () => {
111+ const decoded = CursorListAvailableModelsResponse.make({
112+ models: [
113+ {
114+ value: "gpt-5.4",
115+ name: "GPT-5.4",
116+ configOptions: [
117+ {
118+ id: "reasoning",
119+ name: "Reasoning",
120+ category: "thought_level",
121+ type: "select",
122+ currentValue: "medium",
123+ options: [
124+ { value: "low", name: "Low" },
125+ { value: "medium", name: "Medium" },
126+ ],
127+ },
128+ ],
129+ },
130+ ],
131+ });
132+
133+ expect(decoded.models[0]?.configOptions?.[0]?.id).toBe("reasoning");
134+ });
108135 });
apps/server/src/provider/acp/CursorAcpExtension.ts+11−0
@@ -3,6 +3,7 @@ · 2 unmodified lines
33 * Additional reference provided by the Cursor team: https://anysphere.enterprise.slack.com/files/U068SSJE141/F0APT1HSZRP/cursor-acp-extension-method-schemas.md
44 */
55 import type { UserInputQuestion } from "@t3tools/contracts";
6+import * as AcpSchema from "effect-acp/schema";
67 import * as Schema from "effect/Schema";
78
89 const CursorAskQuestionOption = Schema.Struct({
@@ -53,6 +54,16 @@ export const CursorUpdateTodosRequest = Schema.Struct({ · 44 unmodified lines
5354 merge: Schema.Boolean,
5455 });
5556
57+const CursorAvailableModel = Schema.Struct({
58+ value: Schema.String,
59+ name: Schema.String,
60+ configOptions: Schema.optional(Schema.Array(AcpSchema.SessionConfigOption)),
61+});
62+
63+export const CursorListAvailableModelsResponse = Schema.Struct({
64+ models: Schema.Array(CursorAvailableModel),
65+});
66+
5667 export function extractAskQuestions(
5768 params: typeof CursorAskQuestionRequest.Type,
5869 ): ReadonlyArray<UserInputQuestion> {

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t