Skip to content
Draft
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
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 @@ -480,14 +480,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
);
}
13 changes: 3 additions & 10 deletions core/protocol/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { SharedConfigSchema } from "../config/sharedConfig";
import { GlobalContextModelSelections } from "../util/GlobalContext";

import {
BaseSessionMetadata,
BrowserSerializedContinueConfig,
ChatMessage,
CompiledMessagesResult,
Expand All @@ -35,7 +36,6 @@ import {
RangeInFileWithNextEditInfo,
SerializedContinueConfig,
Session,
BaseSessionMetadata,
SiteIndexingConfig,
SlashCommandDescWithSource,
StreamDiffLinesPayload,
Expand All @@ -49,10 +49,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";

Expand Down Expand Up @@ -311,11 +308,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 @@ -83,8 +83,7 @@ export const WEBVIEW_TO_CORE_PASS_THROUGH: (keyof ToCoreFromWebviewProtocol)[] =
"tools/call",
"tools/evaluatePolicy",
"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();
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.

Updated URL to "settings/billing" conflicts with tests expecting "setup-models"; update tests or ensure route mapping to avoid failures.

Prompt for AI agents
Address the following comment on extensions/cli/src/ui/FreeTrialTransitionUI.tsx at line 179:

<comment>Updated URL to &quot;settings/billing&quot; conflicts with tests expecting &quot;setup-models&quot;; update tests or ensure route mapping to avoid failures.</comment>

<file context>
@@ -176,7 +176,7 @@ const FreeTrialTransitionUI: React.FC&lt;FreeTrialTransitionUIProps&gt; = ({
       // Option 1: Open models setup page
       setCurrentStep(&quot;processing&quot;);
-      const modelsUrl = new URL(&quot;setup-models&quot;, env.appUrl).toString();
+      const modelsUrl = new URL(&quot;settings/billing&quot;, env.appUrl).toString();
       setWasModelsSetup(true); // Track that user went through models setup
 
</file context>
Fix with Cubic

setWasModelsSetup(true); // Track that user went through models setup

try {
Expand Down
Loading
Loading