fix(vscode): only carry a local ClinePass model into the cloud with a current plan

The local ClinePass provider also serves free models without a subscription,
so its model now becomes the cloud default only once the plan check confirms
a current plan. Without one, the user falls through to Cline's default model.
This commit is contained in:
Saoud Rizwan
2026-10-24 05:23:11 +00:00
parent 3c2f2a90aa
commit 2da6673a2a
2 changed files with 36 additions and 12 deletions
@@ -768,15 +768,18 @@ describe("SdkCloudSessionCoordinator ownership", () => {
checkClinePassSubscription: vi.fn(() => plan.promise),
})
// A local ClinePass model carries over without waiting for the plan check.
expect(coordinator.nextCloudModelId()).toBe("cline-pass/local")
// A local ClinePass model only carries over once the plan check confirms a current plan.
expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID)
await vi.waitFor(() => expect(options.postStateToWebview).toHaveBeenCalledTimes(1))
apiConfig = { actModeApiProvider: "anthropic" }
expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID)
plan.resolve(true)
await vi.waitFor(() => expect(coordinator.clinePassSubscribed()).toBe(true))
expect(options.postStateToWebview).toHaveBeenCalledTimes(2)
expect(coordinator.nextCloudModelId()).toBe("cline-pass/local")
apiConfig = { actModeApiProvider: "cline-pass", actModeClinePassModelId: "cline-free/local" }
expect(coordinator.nextCloudModelId()).toBe("cline-pass/first")
apiConfig = { actModeApiProvider: "anthropic" }
expect(coordinator.nextCloudModelId()).toBe("cline-pass/first")
apiConfig = { actModeApiProvider: "cline", actModeClineModelId: "usage/model" }
expect(coordinator.nextCloudModelId()).toBe("usage/model")
@@ -789,6 +792,26 @@ describe("SdkCloudSessionCoordinator ownership", () => {
await coordinator.dispose()
})
it("starts a local ClinePass provider user without a current plan on Cline's default model", async () => {
let apiConfig: Record<string, unknown> = { actModeApiProvider: "cline-pass", actModeClinePassModelId: "cline-free/local" }
const { coordinator } = makeCoordinator({
stateManager: { ...makeStateManager(), getApiConfiguration: () => apiConfig } as never,
getAccountScope: () => "user-1:",
loadCloudModels: async () => [
{ id: "cline-pass/local", name: "Local", catalogId: "cline-pass" },
{ id: "cline-free/local", name: "Free", catalogId: "cline" },
],
checkClinePassSubscription: async () => false,
})
coordinator.nextCloudModelId()
await vi.waitFor(() => expect(coordinator.clinePassSubscribed()).toBe(false))
expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID)
apiConfig = { actModeApiProvider: "cline-pass", actModeClinePassModelId: "cline-pass/local" }
expect(coordinator.nextCloudModelId()).toBe(CLINE_DEFAULT_MODEL_ID)
await coordinator.dispose()
})
it("keeps a local ClinePass model out of the cloud when the catalog doesn't offer ClinePass", async () => {
const { coordinator } = makeCoordinator({
stateManager: {
@@ -902,12 +902,12 @@ export class SdkCloudSessionCoordinator {
/**
* The Cline model a new sandbox runs: the cloud model picked in Settings,
* else the user's Act-mode Cline or ClinePass model when the cloud catalog
* offers it, else the first ClinePass model for a subscriber, else Cline's
* default model. Until the catalog is known, a local Cline or ClinePass
* model is assumed to be offered. Synchronous, and the only source for both
* the composer label and the start path, so the sandbox runs the model the
* user saw when they submitted.
* else the user's Act-mode Cline model, or ClinePass model when the plan
* check confirms a current plan, if the cloud catalog offers it; else the
* first ClinePass model for a subscriber, else Cline's default model. Until
* the catalog is known, a local Cline or ClinePass model is assumed to be
* offered. Synchronous, and the only source for both the composer label and
* the start path, so the sandbox runs the model the user saw when they submitted.
*/
nextCloudModelId(): string {
const chosen = this.options.stateManager.getGlobalStateKey("cloudModelId")?.trim()
@@ -915,15 +915,16 @@ export class SdkCloudSessionCoordinator {
return chosen
}
const apiConfig = this.options.stateManager.getApiConfiguration()
const account = this.accountCloudModels()
// The local ClinePass provider also serves free models without a subscription,
// so only a confirmed plan makes its model a ClinePass model to carry over.
const local =
apiConfig.actModeApiProvider === "cline"
? apiConfig.actModeClineModelId?.trim()
: apiConfig.actModeApiProvider === "cline-pass"
: apiConfig.actModeApiProvider === "cline-pass" && account?.subscribed
? apiConfig.actModeClinePassModelId?.trim()
: undefined
const account = this.accountCloudModels()
const catalog = account?.models
// The local ClinePass provider also offers free models, which the cloud does not take.
const offered = catalog
? catalog.some((model) => model.id === local)
: apiConfig.actModeApiProvider === "cline" || !!local?.startsWith("cline-pass/")