Make provider model lists dynamic based on auth context#1556
Make provider model lists dynamic based on auth context#1556juliusmarminge merged 15 commits intomainfrom
Conversation
- Probe Claude account type when auth status lacks it - Prefer 1M context for premium tiers and keep 200k default otherwise
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Normalization strips underscores but set contains underscore entry
- Changed the set entry from "max_plan" to "maxplan" to match the post-normalization form produced by the underscore-stripping regex.
Or push these changes by commenting:
@cursor push fb69915178
Preview (fb69915178)
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts
--- a/apps/server/src/provider/Layers/ClaudeProvider.ts
+++ b/apps/server/src/provider/Layers/ClaudeProvider.ts
@@ -269,7 +269,7 @@
/** Subscription types where the 1M context window is included in the plan. */
const PREMIUM_SUBSCRIPTION_TYPES = new Set([
"max",
- "max_plan",
+ "maxplan",
"max5",
"max20",
"enterprise",The normalization regex strips underscores (/[\s_-]+/g), so 'max_plan' normalizes to 'maxplan' which never matched the set entry 'max_plan'. Changed the set entry to 'maxplan' to match the post-normalization form.
- Keep context window options intact when adjusting subscription models - Mark the 1m window as default without rebuilding option objects
- Replace flat `authStatus` with structured `auth` across server and web - Surface Claude subscription type in provider summaries - Update provider status tests and shared contracts
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Timeout cannot abort the spawned Claude child process
- Moved AbortController creation outside the Effect.tryPromise async function and added Effect.ensuring to guarantee abort() is called when the Effect is interrupted by timeoutOption, preventing leaked child processes.
Or push these changes by commenting:
@cursor push e409cf506b
Preview (e409cf506b)
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts
--- a/apps/server/src/provider/Layers/ClaudeProvider.ts
+++ b/apps/server/src/provider/Layers/ClaudeProvider.ts
@@ -324,27 +324,28 @@
* This is used as a fallback when `claude auth status` does not include
* subscription type information.
*/
-const probeClaudeCapabilities = (binaryPath: string) =>
- Effect.tryPromise(async () => {
- const abort = new AbortController();
- try {
- const q = claudeQuery({
- prompt: ".",
- options: {
- pathToClaudeCodeExecutable: binaryPath,
- abortController: abort,
- maxTurns: 0,
- settingSources: [],
- allowedTools: [],
- stderr: () => {},
- },
- });
- const init = await q.initializationResult();
- return { subscriptionType: init.account?.subscriptionType };
- } finally {
- if (!abort.signal.aborted) abort.abort();
- }
+const probeClaudeCapabilities = (binaryPath: string) => {
+ const abort = new AbortController();
+ return Effect.tryPromise(async () => {
+ const q = claudeQuery({
+ prompt: ".",
+ options: {
+ pathToClaudeCodeExecutable: binaryPath,
+ abortController: abort,
+ maxTurns: 0,
+ settingSources: [],
+ allowedTools: [],
+ stderr: () => {},
+ },
+ });
+ const init = await q.initializationResult();
+ return { subscriptionType: init.account?.subscriptionType };
}).pipe(
+ Effect.ensuring(
+ Effect.sync(() => {
+ if (!abort.signal.aborted) abort.abort();
+ }),
+ ),
Effect.timeoutOption(CAPABILITIES_PROBE_TIMEOUT_MS),
Effect.result,
Effect.map((result) => {
@@ -352,6 +353,7 @@
return Option.isSome(result.success) ? result.success.value : undefined;
}),
);
+};
const runClaudeCommand = (args: ReadonlyArray<string>) =>
Effect.gen(function* () {…tController outside Promise Move the AbortController creation outside the Effect.tryPromise async function and use Effect.ensuring to guarantee abort() is called when the Effect completes or is interrupted (e.g. by timeoutOption). This prevents leaked child processes when initializationResult() hangs indefinitely. Applied via @cursor push command
The normalization regex strips underscores (/[\s_-]+/g), so 'max_plan' normalizes to 'maxplan' which never matched the set entry 'max_plan'. Changed the set entry to 'maxplan' to match the post-normalization form. Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Whitespace-only subscription type causes schema validation failure
- Updated
asNonEmptyStringto trim the string before checking length, so whitespace-only strings are correctly rejected asOption.none()and never reach theTrimmedNonEmptyStringschema.
- Updated
- ✅ Fixed: Exported function only used within same file
- Removed the
exportkeyword fromadjustModelsForSubscriptionsince it is only called within the same file.
- Removed the
Or push these changes by commenting:
@cursor push 0fe51dd194
Preview (0fe51dd194)
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts
--- a/apps/server/src/provider/Layers/ClaudeProvider.ts
+++ b/apps/server/src/provider/Layers/ClaudeProvider.ts
@@ -218,8 +218,11 @@
const SUBSCRIPTION_CONTAINER_KEYS = ["account", "subscription", "user", "billing"] as const;
/** Lift an unknown value into `Option<string>` if it is a non-empty string. */
-const asNonEmptyString = (v: unknown): Option.Option<string> =>
- typeof v === "string" && v.length > 0 ? Option.some(v) : Option.none();
+const asNonEmptyString = (v: unknown): Option.Option<string> => {
+ if (typeof v !== "string") return Option.none();
+ const trimmed = v.trim();
+ return trimmed.length > 0 ? Option.some(trimmed) : Option.none();
+};
/** Lift an unknown value into `Option<Record>` if it is a plain object. */
const asRecord = (v: unknown): Option.Option<Record<string, unknown>> =>
@@ -283,7 +286,7 @@
* - Other tiers (Pro, free, unknown): 200k context stays the default;
* 1M remains available as a manual option so users can still enable it.
*/
-export function adjustModelsForSubscription(
+function adjustModelsForSubscription(
baseModels: ReadonlyArray<ServerProviderModel>,
subscriptionType: string | undefined,
): ReadonlyArray<ServerProviderModel> {| ), | ||
| }, | ||
| }; | ||
| }); |
There was a problem hiding this comment.
- Probe Codex account data to gate Spark models and surface auth type/label - Add Claude subscription labels for provider status and settings UI - Update provider picker and contracts to carry auth labels
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 4 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Codex probe child process leaks on timeout
- Added AbortSignal support to probeCodexAccount and wrapped probeCodexCapabilities with Effect.ensuring to abort the signal (and thus kill the child process) on timeout or interruption, mirroring the existing probeClaudeCapabilities pattern.
- ✅ Fixed:
Object.assignsets explicitisDefault: undefinedproperty- Replaced Object.assign with explicit object construction that only includes the isDefault key when set to true, omitting it entirely for non-default options.
Or push these changes by commenting:
@cursor push c5f4489623
Preview (c5f4489623)
diff --git a/apps/server/src/provider/Layers/ClaudeProvider.ts b/apps/server/src/provider/Layers/ClaudeProvider.ts
--- a/apps/server/src/provider/Layers/ClaudeProvider.ts
+++ b/apps/server/src/provider/Layers/ClaudeProvider.ts
@@ -333,7 +333,9 @@
capabilities: {
...caps,
contextWindowOptions: caps.contextWindowOptions.map((opt) =>
- Object.assign({}, opt, { isDefault: opt.value === "1m" ? true : undefined }),
+ opt.value === "1m"
+ ? { value: opt.value, label: opt.label, isDefault: true as const }
+ : { value: opt.value, label: opt.label },
),
},
};
diff --git a/apps/server/src/provider/Layers/CodexProvider.ts b/apps/server/src/provider/Layers/CodexProvider.ts
--- a/apps/server/src/provider/Layers/CodexProvider.ts
+++ b/apps/server/src/provider/Layers/CodexProvider.ts
@@ -313,8 +313,14 @@
const probeCodexCapabilities = (input: {
readonly binaryPath: string;
readonly homePath?: string;
-}) =>
- Effect.tryPromise(() => probeCodexAccount(input)).pipe(
+}) => {
+ const abort = new AbortController();
+ return Effect.tryPromise(() => probeCodexAccount({ ...input, signal: abort.signal })).pipe(
+ Effect.ensuring(
+ Effect.sync(() => {
+ if (!abort.signal.aborted) abort.abort();
+ }),
+ ),
Effect.timeoutOption(CAPABILITIES_PROBE_TIMEOUT_MS),
Effect.result,
Effect.map((result) => {
@@ -322,6 +328,7 @@
return Option.isSome(result.success) ? result.success.value : undefined;
}),
);
+};
const runCodexCommand = (args: ReadonlyArray<string>) =>
Effect.gen(function* () {
diff --git a/apps/server/src/provider/codexAppServer.ts b/apps/server/src/provider/codexAppServer.ts
--- a/apps/server/src/provider/codexAppServer.ts
+++ b/apps/server/src/provider/codexAppServer.ts
@@ -43,6 +43,7 @@
export async function probeCodexAccount(input: {
readonly binaryPath: string;
readonly homePath?: string;
+ readonly signal?: AbortSignal;
}): Promise<CodexAccountSnapshot> {
return await new Promise((resolve, reject) => {
const child = spawn(input.binaryPath, ["app-server"], {
@@ -82,6 +83,12 @@
),
);
+ if (input.signal?.aborted) {
+ fail(new Error("Codex account probe aborted."));
+ return;
+ }
+ input.signal?.addEventListener("abort", () => fail(new Error("Codex account probe aborted.")));
+
const writeMessage = (message: unknown) => {
if (!child.stdin.writable) {
fail(new Error("Cannot write to codex app-server stdin."));…ault: undefined - Add AbortSignal support to probeCodexAccount and use Effect.ensuring in probeCodexCapabilities to kill the child process when the timeout fires, mirroring the existing pattern in probeClaudeCapabilities. - Replace Object.assign with explicit object construction in adjustModelsForSubscription to avoid setting an explicit isDefault: undefined property on non-default context window options.
- Disable Spark for API key and unknown Codex accounts - Add Codex and Claude auth labels for API key and subscription states - Update provider tests for dynamic model gating
…ault: undefined - Add AbortSignal support to probeCodexAccount and use Effect.ensuring in probeCodexCapabilities to kill the child process when the timeout fires, mirroring the existing pattern in probeClaudeCapabilities. - Replace Object.assign with explicit object construction in adjustModelsForSubscription to avoid setting an explicit isDefault: undefined property on non-default context window options. Applied via @cursor push command
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Session default sparkEnabled contradicts new account logic
- Changed the default sparkEnabled from true to false in the session context initializer at codexAppServerManager.ts:483, so that when account/read fails the fallback correctly denies spark model access, matching the new whitelist-only-for-pro policy in readCodexAccountSnapshot.
Or push these changes by commenting:
@cursor push f032a4203d
Preview (f032a4203d)
diff --git a/apps/server/src/codexAppServerManager.ts b/apps/server/src/codexAppServerManager.ts
--- a/apps/server/src/codexAppServerManager.ts
+++ b/apps/server/src/codexAppServerManager.ts
@@ -480,7 +480,7 @@
account: {
type: "unknown",
planType: null,
- sparkEnabled: true,
+ sparkEnabled: false,
},
child,
output,| }, | ||
| } as const; | ||
| } | ||
|
|
There was a problem hiding this comment.
Session default sparkEnabled contradicts new account logic
Medium Severity
The default account in the session context initializes sparkEnabled: true for the "unknown" type, but readCodexAccountSnapshot now returns sparkEnabled: false for unknown accounts. When account/read fails during session startup, the fallback default keeps spark enabled, contradicting the new whitelist-only-for-pro policy. This lets users without a confirmed pro plan use the spark model if the account probe request fails.
Additional Locations (1)
| * - Other tiers (Pro, free, unknown): 200k context stays the default; | ||
| * 1M remains available as a manual option so users can still enable it. | ||
| */ | ||
| export function adjustModelsForSubscription( |
There was a problem hiding this comment.
🟢 Low Layers/ClaudeProvider.ts:375
When contextWindowOptions lacks a "1m" entry, adjustModelsForSubscription maps every option to an object without isDefault, leaving the model with no default context window. The original model may have had isDefault: true on another option, but this is stripped away. Consider returning the model unchanged when "1m" isn't present, or preserve the existing default otherwise.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/server/src/provider/Layers/ClaudeProvider.ts around line 375:
When `contextWindowOptions` lacks a `"1m"` entry, `adjustModelsForSubscription` maps every option to an object without `isDefault`, leaving the model with no default context window. The original model may have had `isDefault: true` on another option, but this is stripped away. Consider returning the model unchanged when `"1m"` isn't present, or preserve the existing default otherwise.
Evidence trail:
apps/server/src/provider/Layers/ClaudeProvider.ts lines 385-397 (REVIEWED_COMMIT): The map callback creates objects without preserving `isDefault` for non-"1m" options.
packages/shared/src/model.ts lines 62-63: `getDefaultContextWindow` relies on finding an option with `isDefault` property.
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>



Summary
claude auth statusoutput, with an SDK-based fallback when the CLI output is missing the plan field.1Mcontext as the default while keeping200kavailable.Testing
bun fmtbun lintbun typecheckbun run testNote
High Risk
Introduces a breaking contract change (
ServerProvider.authreplacingauthStatus) and alters provider health-check behavior to dynamically filter/adjust models based on detected account/subscription, which can affect model availability and UI display if detection is wrong.Overview
Providers now surface richer auth context and use it to shape available models. The
ServerProvidercontract replacesauthStatuswith a structuredauthobject (status, optionaltype/label), and the web settings UI now displays “Authenticated · ” when available.Codex: adds an
app-serverJSON-RPC probe (probeCodexAccount) cached for 5 minutes, derivesCodexAccountSnapshot, and hidesgpt-5.3-codex-sparkunless the account is ChatGPT Pro (Spark now disabled for API keys/unknown plans and requests for Spark fall back togpt-5.3-codex).Claude: detects subscription type from
claude auth statusJSON with an SDK-based fallback (cached 5 minutes), annotatesauth.type/auth.label, and for premium tiers (Max/Enterprise/Team) flips the default context window to1Mwhile keeping200kavailable. Tests/fixtures are updated accordingly, including model picker behavior depending on server-reported model lists.Written by Cursor Bugbot for commit 4c5408d. This will update automatically on new commits. Configure here.
Note
Make Codex and Claude provider model lists dynamic based on account auth context
authStatusstring onServerProviderwith a structuredauth: { status, type, label }object across the contracts, server, and web layers.gpt-5.3-codex-spark) is now filtered from the model list unless the account has a 'pro' ChatGPT plan; API key and unknown plan accounts get spark disabled.ServerProvider.authStatusis removed and replaced byServerProvider.auth—any consumers reading the old field will receiveundefined.Macroscope summarized 4c5408d.