Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/server/models-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import type { CursorEffortTable } from "../integrations/cursor-effort-table";
* Completions, Responses and Anthropic Messages, streams, and accepts tool calls, so those are
* constants; context length and vision come from catalog data when known and are omitted
* otherwise, matching Cursor's optional-field schema.
*
* Top-level capacity metrics (`context_window`, `context_length`, `max_output_tokens`) are
* mirrored directly on each model row for external client discovery (e.g. pi-ai, DSH,
* LibreChat) that inspects flat properties rather than Cursor's nested `capabilities.*` shape.
Comment on lines +14 to +16

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the missing co-author trailer

The commit explicitly says this continues #4802 by @Yum-wu and claims the Co-authored-by trailer is present, but the raw commit contains no such trailer; the prose mention does not give the contributor attribution. Add the required trailer to the PR description or a branch commit so it survives the squash.

AGENTS.md reference: AGENTS.md:L288-L292

Useful? React with 👍 / 👎.

* A row that gains a nested capacity value must gain the top-level mirror in the same change.
Comment on lines +14 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the new model-list fields publicly

This changes the user-facing plain GET /v1/models response specifically for external clients, but docs-site/src/content/docs/reference/proxy-formats.md still describes the plain OpenAI list only in terms of its envelope and model IDs. Document the three flat capacity fields, their omission rules, and the long-tier behavior in the public reference, keeping the translated references synchronized.

AGENTS.md reference: AGENTS.md:L380-L381

Useful? React with 👍 / 👎.

*/

/**
Expand Down Expand Up @@ -132,6 +137,19 @@ export interface ModelCapabilityFields {
supports_vision?: boolean;
reasoning_effort?: string[];
};
/**
* Mirrored top-level context window for external/legacy client discovery (e.g. pi-ai, DSH)
* that reads top-level context_window / context_length instead of nested capabilities.
*/
context_window?: number;
/**
* Top-level context length alias matching capabilities.context_length for clients expecting context_length.
*/
context_length?: number;
/**
* Mirrored top-level max output token limit for external/legacy client discovery.
*/
max_output_tokens?: number;
/**
* Cursor reads the long-context threshold from `pricing.overrides[].min_prompt_tokens`. That
* key sits outside its validated capability schema, so it is the one place a threshold can
Expand All @@ -153,16 +171,15 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil
const longContextLength = positiveInt(input.longContextWindow);
const maxOutputTokens = positiveInt(input.maxOutputTokens);
const hasLongTier = contextLength !== undefined && longContextLength !== undefined && longContextLength > contextLength;
const effectiveContextLength = hasLongTier ? longContextLength : contextLength;
const modalities = Array.isArray(input.inputModalities)
? input.inputModalities.filter(modality => typeof modality === "string" && modality.length > 0)
: undefined;
const supportsVision = modalities !== undefined ? modalities.includes("image") : undefined;
return {
api_types: [...OPENCODEX_MODEL_API_TYPES],
capabilities: {
...(hasLongTier
? { context_length: longContextLength }
: contextLength !== undefined ? { context_length: contextLength } : {}),
...(effectiveContextLength !== undefined ? { context_length: effectiveContextLength } : {}),
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
// Once a gateway advertises api_types, Cursor keeps only rows whose output_modalities
// include "text"; omitting the key drops the row from the extended catalog.
Expand All @@ -174,6 +191,10 @@ export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabil
...(supportsVision !== undefined ? { supports_vision: supportsVision } : {}),
...(efforts.length > 0 ? { reasoning_effort: [...efforts] } : {}),
},
...(effectiveContextLength !== undefined
? { context_window: effectiveContextLength, context_length: effectiveContextLength }
: {}),
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
...(hasLongTier ? { pricing: { overrides: [{ min_prompt_tokens: contextLength }] } } : {}),
};
}
4 changes: 4 additions & 0 deletions structure/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ then trusted catalog metadata such as a configured qualified provider/model alia
This overlay never changes route identity or the upstream wire model, and its catalog fingerprint makes
a label edit refresh Codex output.

Raw `/v1/models` rows advertise positive safe capacity values in both Cursor's nested
`capabilities` object and top-level discovery fields used by other clients. A model with a larger
opt-in context tier uses that effective long window in both shapes; invalid values are omitted.
Comment on lines +183 to +185

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update every document mapped to src/server

Only structure/catalog.md is updated, although structure/INDEX.md maps src/server/ to thirteen additional architecture documents such as runtime.md, subagents.md, and the transport/data-plane documents. Update every mapped document with the new /v1/models capacity-field contract or an appropriate boundary/link; changing only one mapped owner violates the repository's source-to-doc synchronization rule.

AGENTS.md reference: structure/AGENTS.md:L44-L50

Useful? React with 👍 / 👎.


Supported bare native GPT rows also consume `providers.openai.modelDisplayNames`. Retained sync
and convergence pass the same map to the observed-state merge. After native normalization and
ordering, the merge applies the exact nonblank trimmed label and saves
Expand Down
35 changes: 35 additions & 0 deletions tests/providers/cursor/cursor-local-models-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,35 @@ describe("modelCapabilityFields", () => {
expect(modelCapabilityFields({ maxOutputTokens: 1.9 }).capabilities.supports_reasoning)
.toBe(false);
});

test("mirrors top-level context_window and max_output_tokens for external and legacy client discovery", () => {
const fields = modelCapabilityFields({ contextWindow: 200000, maxOutputTokens: 64000 });
expect(fields.context_window).toBe(200000);
expect(fields.context_length).toBe(200000);
expect(fields.max_output_tokens).toBe(64000);
expect(fields.capabilities.context_length).toBe(200000);
expect(fields.capabilities.max_output_tokens).toBe(64000);

// Empty or non-positive / unsafe inputs: keys omitted entirely
const empty = modelCapabilityFields({});
expect("context_window" in empty).toBe(false);
expect("context_length" in empty).toBe(false);
expect("max_output_tokens" in empty).toBe(false);

for (const value of [0, -50, Number.NaN, Number.MAX_SAFE_INTEGER + 2]) {
const fields = modelCapabilityFields({ contextWindow: value, maxOutputTokens: value });
expect("context_window" in fields).toBe(false);
expect("context_length" in fields).toBe(false);
expect("max_output_tokens" in fields).toBe(false);
}
});

test("mirrors the effective long context window at the top level", () => {
const fields = modelCapabilityFields({ contextWindow: 272000, longContextWindow: 922000 });
expect(fields.capabilities.context_length).toBe(922000);
expect(fields.context_window).toBe(922000);
expect(fields.context_length).toBe(922000);
});
});

describe("nativeOpenAiContextTier", () => {
Expand Down Expand Up @@ -165,6 +194,9 @@ describe("raw /v1/models list advertises Cursor local-agent capabilities", () =>
supports_vision: true,
reasoning_effort: ["low", "high", "max"],
});
expect(k3!.context_window).toBe(200000);
expect(k3!.context_length).toBe(200000);
expect(k3!.max_output_tokens).toBe(64_000);
// Grok Build's discovery fields stay untouched next to the new keys.
expect(k3!.supports_reasoning_effort).toBe(true);
expect(k3!.reasoning_effort).toBe("high");
Expand All @@ -186,6 +218,9 @@ describe("raw /v1/models list advertises Cursor local-agent capabilities", () =>
// Native GPT-5.6: 272k default window, 922k opt-in ceiling → Cursor Context selector.
expect(solCaps.context_length).toBe(922000);
expect(solCaps.max_output_tokens).toBe(128_000);
expect(sol!.context_window).toBe(922000);
expect(sol!.context_length).toBe(922000);
expect(sol!.max_output_tokens).toBe(128_000);
expect(sol!.pricing).toEqual({ overrides: [{ min_prompt_tokens: 272000 }] });
expect("long_context_threshold_tokens" in sol!).toBe(false);
expect(solCaps.supports_vision).toBe(true);
Expand Down
Loading