Skip to content
25 changes: 16 additions & 9 deletions core/config/usesFreeTrialApiKey.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { decodeSecretLocation, SecretType } from "@continuedev/config-yaml";
import { BrowserSerializedContinueConfig } from "..";
import { BrowserSerializedContinueConfig, ModelDescription } from "..";

/**
* Helper function to determine if the config uses a free trial API key
* Helper function to determine if the config uses an API key that relies on Continue credits (free trial or models add-on)
* @param config The serialized config object
* @returns true if the config is using any free trial models
*/
export function usesFreeTrialApiKey(
export function usesCreditsBasedApiKey(
config: BrowserSerializedContinueConfig | null,
): boolean {
if (!config) {
Expand All @@ -19,12 +19,7 @@ export function usesFreeTrialApiKey(

// Check if any of the chat models use free-trial provider
try {
const hasFreeTrial = allModels?.some(
(model) =>
model.apiKeyLocation &&
decodeSecretLocation(model.apiKeyLocation).secretType ===
SecretType.FreeTrial,
);
const hasFreeTrial = allModels?.some(modelUsesCreditsBasedApiKey);

return hasFreeTrial;
} catch (e) {
Expand All @@ -33,3 +28,15 @@ export function usesFreeTrialApiKey(

return false;
}

const modelUsesCreditsBasedApiKey = (model: ModelDescription) => {
if (!model.apiKeyLocation) {
return false;
}

const secretType = decodeSecretLocation(model.apiKeyLocation).secretType;

return (
secretType === SecretType.FreeTrial || secretType === SecretType.ModelsAddOn
);
};
4 changes: 2 additions & 2 deletions core/config/usesFreeTrialApiKey.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ vi.mock("@continuedev/config-yaml", () => ({
}));

describe("usesFreeTrialApiKey", () => {
let usesFreeTrialApiKey: typeof import("./usesFreeTrialApiKey").usesFreeTrialApiKey;
let usesFreeTrialApiKey: typeof import("./usesFreeTrialApiKey").usesCreditsBasedApiKey;
let SecretType: typeof import("@continuedev/config-yaml").SecretType;

beforeEach(async () => {
mockDecodeSecretLocation.mockReset();
usesFreeTrialApiKey = (await import("./usesFreeTrialApiKey"))
.usesFreeTrialApiKey;
.usesCreditsBasedApiKey;
SecretType = (await import("@continuedev/config-yaml")).SecretType;
});

Expand Down
19 changes: 9 additions & 10 deletions core/control-plane/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import fetch, { RequestInit, Response } from "node-fetch";

import { OrganizationDescription } from "../config/ProfileLifecycleManager.js";
import {
BaseSessionMetadata,
IDE,
ModelDescription,
Session,
BaseSessionMetadata,
} from "../index.js";
import { Logger } from "../util/Logger.js";

Expand All @@ -39,12 +39,11 @@ export interface ControlPlaneWorkspace {

export interface ControlPlaneModelDescription extends ModelDescription {}

export interface FreeTrialStatus {
export interface CreditStatus {
optedInToFreeTrial: boolean;
chatCount?: number;
autocompleteCount?: number;
chatLimit: number;
autocompleteLimit: number;
hasCredits: boolean;
creditBalance: number;
hasPurchasedCredits: boolean;
}

export const TRIAL_PROXY_URL =
Expand Down Expand Up @@ -260,20 +259,20 @@ export class ControlPlaneClient {
}
}

public async getFreeTrialStatus(): Promise<FreeTrialStatus | null> {
public async getCreditStatus(): Promise<CreditStatus | null> {
if (!(await this.isSignedIn())) {
return null;
}

try {
const resp = await this.requestAndHandleError("ide/free-trial-status", {
const resp = await this.requestAndHandleError("ide/credits", {
method: "GET",
});
return (await resp.json()) as FreeTrialStatus;
return (await resp.json()) as CreditStatus;
} catch (e) {
// Capture control plane API failures to Sentry
Logger.error(e, {
context: "control_plane_free_trial_status",
context: "control_plane_credit_status",
});
return null;
}
Expand Down
10 changes: 2 additions & 8 deletions core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,14 +483,8 @@ export class Core {
return await getControlPlaneEnv(this.ide.getIdeSettings());
});

on("controlPlane/getFreeTrialStatus", async (msg) => {
return this.configHandler.controlPlaneClient.getFreeTrialStatus();
});

on("controlPlane/getModelsAddOnUpgradeUrl", async (msg) => {
return this.configHandler.controlPlaneClient.getModelsAddOnCheckoutUrl(
msg.data.vsCodeUriScheme,
);
on("controlPlane/getCreditStatus", async (msg) => {
return this.configHandler.controlPlaneClient.getCreditStatus();
});

on("mcp/reloadServer", async (msg) => {
Expand Down
26 changes: 11 additions & 15 deletions core/llm/streamChat.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { fetchwithRequestOptions } from "@continuedev/fetch";
import { ChatMessage, IDE, PromptLog } from "..";
import { ConfigHandler } from "../config/ConfigHandler";
import { usesFreeTrialApiKey } from "../config/usesFreeTrialApiKey";
import { usesCreditsBasedApiKey } from "../config/usesFreeTrialApiKey";
import { FromCoreProtocol, ToCoreProtocol } from "../protocol";
import { IMessenger, Message } from "../protocol/messenger";
import { Telemetry } from "../util/posthog";
import { TTS } from "../util/tts";
import { isOutOfStarterCredits } from "./utils/starterCredits";

export async function* llmStreamChat(
configHandler: ConfigHandler,
Expand Down Expand Up @@ -151,7 +152,7 @@ export async function* llmStreamChat(
true,
);

void checkForFreeTrialExceeded(configHandler, messenger);
void checkForOutOfStarterCredits(configHandler, messenger);

if (!next.done) {
throw new Error("Will never happen");
Expand Down Expand Up @@ -182,24 +183,19 @@ export async function* llmStreamChat(
}
}

async function checkForFreeTrialExceeded(
async function checkForOutOfStarterCredits(
configHandler: ConfigHandler,
messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
) {
const { config } = await configHandler.getSerializedConfig();

// Only check if the user is using the free trial
if (config && !usesFreeTrialApiKey(config)) {
return;
}

try {
const freeTrialStatus =
await configHandler.controlPlaneClient.getFreeTrialStatus();
const { config } = await configHandler.getSerializedConfig();
const creditStatus =
await configHandler.controlPlaneClient.getCreditStatus();

if (
freeTrialStatus &&
freeTrialStatus.chatCount &&
freeTrialStatus.chatCount > freeTrialStatus.chatLimit
config &&
creditStatus &&
isOutOfStarterCredits(usesCreditsBasedApiKey(config), creditStatus)
) {
void messenger.request("freeTrialExceeded", undefined);
}
Expand Down
12 changes: 12 additions & 0 deletions core/llm/utils/starterCredits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CreditStatus } from "../../control-plane/client";

export function isOutOfStarterCredits(
usingModelsAddOnApiKey: boolean,
creditStatus: CreditStatus,
): boolean {
return (
usingModelsAddOnApiKey &&
!creditStatus.hasCredits &&
!creditStatus.hasPurchasedCredits
);
}
11 changes: 2 additions & 9 deletions core/protocol/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ import {
ControlPlaneEnv,
ControlPlaneSessionInfo,
} from "../control-plane/AuthTypes";
import {
FreeTrialStatus,
RemoteSessionMetadata,
} from "../control-plane/client";
import { CreditStatus, RemoteSessionMetadata } from "../control-plane/client";
import { ProcessedItem } from "../nextEdit/NextEditPrefetchQueue";
import { NextEditOutcome } from "../nextEdit/types";
import { ContinueErrorReason } from "../util/errors";
Expand Down Expand Up @@ -327,11 +324,7 @@ export type ToCoreFromIdeOrWebviewProtocol = {
"clipboardCache/add": [{ content: string }, void];
"controlPlane/openUrl": [{ path: string; orgSlug?: string }, void];
"controlPlane/getEnvironment": [undefined, ControlPlaneEnv];
"controlPlane/getFreeTrialStatus": [undefined, FreeTrialStatus | null];
"controlPlane/getModelsAddOnUpgradeUrl": [
{ vsCodeUriScheme?: string },
{ url: string } | null,
];
"controlPlane/getCreditStatus": [undefined, CreditStatus | null];
isItemTooBig: [{ item: ContextItemWithId }, boolean];
didChangeControlPlaneSessionInfo: [
{ sessionInfo: ControlPlaneSessionInfo | undefined },
Expand Down
3 changes: 1 addition & 2 deletions core/protocol/passThrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
"tools/evaluatePolicy",
"tools/preprocessArgs",
"controlPlane/getEnvironment",
"controlPlane/getFreeTrialStatus",
"controlPlane/getModelsAddOnUpgradeUrl",
"controlPlane/getCreditStatus",
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Sep 25, 2025

Choose a reason for hiding this comment

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

New message type requires corresponding IntelliJ ContinueBrowser update; missing mapping may break webview→core pass-through in IntelliJ.

Prompt for AI agents
Address the following comment on core/protocol/passThrough.ts at line 86:

<comment>New message type requires corresponding IntelliJ ContinueBrowser update; missing mapping may break webview→core pass-through in IntelliJ.</comment>

<file context>
@@ -83,8 +83,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
     &quot;controlPlane/getEnvironment&quot;,
-    &quot;controlPlane/getFreeTrialStatus&quot;,
-    &quot;controlPlane/getModelsAddOnUpgradeUrl&quot;,
+    &quot;controlPlane/getCreditStatus&quot;,
     &quot;controlPlane/openUrl&quot;,
     &quot;isItemTooBig&quot;,
</file context>
Fix with Cubic

"controlPlane/openUrl",
"isItemTooBig",
"process/markAsBackgrounded",
Expand Down
2 changes: 1 addition & 1 deletion extensions/cli/src/ui/FreeTrialTransitionUI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const FreeTrialTransitionUI: React.FC<FreeTrialTransitionUIProps> = ({
if (selectedOption === 1) {
// Option 1: Open models setup page
setCurrentStep("processing");
const modelsUrl = new URL("setup-models", env.appUrl).toString();
const modelsUrl = new URL("settings/billing", env.appUrl).toString();
setWasModelsSetup(true); // Track that user went through models setup

try {
Expand Down
Loading
Loading