fix(cursor): preserved max-mode flag
Parsed Cursor GetUsableModels max_mode metadata into catalog models and sent it on Cursor run requests. Added focused regression coverage for discovery and request payload propagation. Fixes #4797
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Cursor `max_mode` requests to send discovered max-mode metadata on both model payload fields. ([#4797](https://github.com/can1357/oh-my-pi/issues/4797))
|
||||
|
||||
## [16.3.11] - 2026-07-06
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
RequestContextResultSchema,
|
||||
RequestContextSchema,
|
||||
RequestContextSuccessSchema,
|
||||
RequestedModelSchema,
|
||||
ResumeActionSchema,
|
||||
SelectedContextSchema,
|
||||
SelectedImageSchema,
|
||||
@@ -2790,16 +2791,24 @@ function buildGrpcRequest(
|
||||
turns,
|
||||
});
|
||||
|
||||
const wireModelId = model.requestModelId ?? model.id;
|
||||
const cursorMaxMode = model.cursorMaxMode === true;
|
||||
const modelDetails = create(ModelDetailsSchema, {
|
||||
modelId: model.id,
|
||||
modelId: wireModelId,
|
||||
displayModelId: model.id,
|
||||
displayName: model.name,
|
||||
...(cursorMaxMode ? { maxMode: true } : undefined),
|
||||
});
|
||||
const requestedModel = create(RequestedModelSchema, {
|
||||
modelId: wireModelId,
|
||||
maxMode: cursorMaxMode,
|
||||
});
|
||||
|
||||
const runRequest = create(AgentRunRequestSchema, {
|
||||
conversationState,
|
||||
action,
|
||||
modelDetails,
|
||||
requestedModel,
|
||||
conversationId: state.conversationId,
|
||||
});
|
||||
|
||||
|
||||
@@ -23,9 +23,23 @@ const cursorModel: Model<"cursor-agent"> = buildModel({
|
||||
maxTokens: 1,
|
||||
});
|
||||
|
||||
function captureCursorPayload(context: Context): Promise<AgentRunRequest> {
|
||||
const cursorMaxModeModel: Model<"cursor-agent"> = buildModel({
|
||||
id: "cursor-composer-2.5-max",
|
||||
name: "Cursor Composer 2.5 Max",
|
||||
api: "cursor-agent",
|
||||
provider: "cursor",
|
||||
baseUrl: "",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 1,
|
||||
maxTokens: 1,
|
||||
cursorMaxMode: true,
|
||||
});
|
||||
|
||||
function captureCursorPayload(context: Context, model: Model<"cursor-agent"> = cursorModel): Promise<AgentRunRequest> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<AgentRunRequest>();
|
||||
streamCursor(cursorModel, context, {
|
||||
streamCursor(model, context, {
|
||||
apiKey: "test-token",
|
||||
onPayload: payload => {
|
||||
if (isAgentRunRequest(payload)) {
|
||||
@@ -165,6 +179,19 @@ describe("Cursor request action encoding", () => {
|
||||
expect(payload.action?.action.case).toBe("userMessageAction");
|
||||
});
|
||||
|
||||
it("sends Cursor max-mode metadata on model details and requested model", async () => {
|
||||
const payload = await captureCursorPayload(
|
||||
{
|
||||
messages: [{ role: "user", content: "continue", timestamp: 0 }],
|
||||
},
|
||||
cursorMaxModeModel,
|
||||
);
|
||||
|
||||
expect(payload.modelDetails?.maxMode).toBe(true);
|
||||
expect(payload.requestedModel?.modelId).toBe("cursor-composer-2.5-max");
|
||||
expect(payload.requestedModel?.maxMode).toBe(true);
|
||||
});
|
||||
|
||||
it("uses a resume action when a tool result is the final context message", async () => {
|
||||
const payload = await captureCursorPayload(toolResultContext());
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Cursor discovery to preserve `GetUsableModels` max-mode metadata for premium models. ([#4797](https://github.com/can1357/oh-my-pi/issues/4797))
|
||||
|
||||
## [16.3.11] - 2026-07-06
|
||||
|
||||
### Added
|
||||
|
||||
@@ -28,6 +28,7 @@ const CursorModelDetailsSchema = type({
|
||||
displayModelId: OptionalDisplayNameSchema.default(undefined),
|
||||
aliases: CursorAliasesSchema.default(() => []),
|
||||
"thinkingDetails?": "unknown",
|
||||
maxMode: "boolean = false",
|
||||
});
|
||||
|
||||
const CursorModelsInnerSchema = type("unknown[]");
|
||||
@@ -283,6 +284,7 @@ function normalizeCursorModel(
|
||||
name,
|
||||
baseUrl: baseUrlOverride ?? reference.baseUrl,
|
||||
reasoning,
|
||||
cursorMaxMode: details.maxMode,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -296,6 +298,7 @@ function normalizeCursorModel(
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: DEFAULT_CONTEXT_WINDOW,
|
||||
maxTokens: DEFAULT_MAX_TOKENS,
|
||||
cursorMaxMode: details.maxMode,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -711,6 +711,8 @@ export interface Model<TApi extends Api = Api> {
|
||||
supportsTools?: boolean;
|
||||
/** GitLab Duo Workflow root namespace selected during catalog discovery. */
|
||||
gitlabDuoWorkflowRootNamespaceId?: string;
|
||||
/** Cursor `max_mode` request flag returned by `GetUsableModels` for premium models that require max mode. */
|
||||
cursorMaxMode?: boolean;
|
||||
cost: {
|
||||
input: number; // $/million tokens
|
||||
output: number; // $/million tokens
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import * as http2 from "node:http2";
|
||||
import type * as net from "node:net";
|
||||
import { create, toBinary } from "@bufbuild/protobuf";
|
||||
import { fetchCursorUsableModels } from "@oh-my-pi/pi-catalog/discovery/cursor";
|
||||
import { GetUsableModelsResponseSchema, ModelDetailsSchema } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
||||
|
||||
const servers = new Set<http2.Http2Server>();
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
[...servers].map(server => {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
server.close(error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
return promise;
|
||||
}),
|
||||
);
|
||||
servers.clear();
|
||||
});
|
||||
|
||||
function requireTcpAddress(address: string | net.AddressInfo | null): net.AddressInfo {
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("HTTP/2 test server did not bind to a TCP address");
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
function startCursorDiscoveryServer(body: Uint8Array): Promise<string> {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<string>();
|
||||
const server = http2.createServer();
|
||||
servers.add(server);
|
||||
server.once("error", reject);
|
||||
server.on("stream", (stream: http2.ServerHttp2Stream) => {
|
||||
stream.respond({ ":status": 200, "content-type": "application/proto" });
|
||||
stream.end(Buffer.from(body));
|
||||
});
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
resolve(`http://127.0.0.1:${requireTcpAddress(server.address()).port}`);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
describe("fetchCursorUsableModels", () => {
|
||||
it("preserves Cursor max-mode metadata from GetUsableModels", async () => {
|
||||
const response = create(GetUsableModelsResponseSchema, {
|
||||
models: [
|
||||
create(ModelDetailsSchema, {
|
||||
modelId: "cursor-composer-max",
|
||||
displayName: "Cursor Composer Max",
|
||||
maxMode: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const baseUrl = await startCursorDiscoveryServer(toBinary(GetUsableModelsResponseSchema, response));
|
||||
|
||||
const models = await fetchCursorUsableModels({ apiKey: "test-token", baseUrl, timeoutMs: 1_000 });
|
||||
|
||||
expect(models).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "cursor-composer-max",
|
||||
name: "Cursor Composer Max",
|
||||
api: "cursor-agent",
|
||||
provider: "cursor",
|
||||
cursorMaxMode: true,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user