Compare commits

...

7 Commits

Author SHA1 Message Date
Bee 09d89cca5d Update src/api/providers/cline.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-10 14:55:35 -07:00
abeatrix 577f578ffa Merge branch 'main' into bee/credit-fix 2025-07-10 14:54:03 -07:00
abeatrix 6f6fd3f6b3 changeset 2025-07-10 14:46:32 -07:00
abeatrix 622d54f80b Request validation and remove credit balance for cline team
Add request validation for Cline API requests and fix the user interface for displaying credit-related information in UI.

The changes include:

- **Credit Balance Validation:** Implemented `validateRequest` method in `ClineAccountService` to check user's credit balance before making API requests.  Requests from active organizations are skipped. An error is thrown if the balance is insufficient.
- **Error Handling:** Improved error handling in `ClineHandler` to provide more informative error messages to the user.
- **UI Enhancements:**
    - Updated `CreditLimitError` component to display the current balance that matches the account view balance format (4 decimal places).
    - Modified `ChatRow` to parse error messages and display the `CreditLimitError` component when applicable.
    - Updated `AccountView` to only display credit balance for user accounts, not organization accounts.
    - Removed unused props from `CreditLimitError` component. Context: https://cline-space.slack.com/archives/C08KYBFL9DJ/p1752182278852399?thread_ts=1752164726.247429&cid=C08KYBFL9DJ
- **Dependencies:** Updated dependencies in `webview-ui` to include `tailwindcss` and configured `tailwind.config.js` to support VSCode theme variables.
2025-07-10 14:43:26 -07:00
abeatrix 61dcec30c7 move this._authService.getAuthToken() to ensureClient 2025-07-10 12:29:22 -07:00
abeatrix 7c771df448 changeset 2025-07-10 12:21:44 -07:00
abeatrix 2ab4ff703f Fix: Ensure Cline client is initialized with the latest auth token
The Cline client was not being re-initialized with the latest authentication token after the user signs in. This resulted in the client using an outdated or non-existent token, leading to authentication errors when making API requests.

This commit ensures that the Cline client is initialized with the most recent authentication token by setting the `apiKey` property of the `OpenAI` client instance to the current auth token retrieved from `AuthService` before every request. This guarantees that the client always uses the valid and up-to-date token for authentication.
2025-07-10 12:19:48 -07:00
6 changed files with 121 additions and 117 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Better error handling for credits-related issues.
+13 -12
View File
@@ -64,19 +64,19 @@ export class ClineHandler implements ApiHandler {
@withRetry()
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const client = await this.ensureClient()
this.lastGenerationId = undefined
const me = await this.clineAccountService.fetchMe()
console.log(
"SwitchAuthToken: Active Organization",
me?.organizations.filter((org) => org.active)[0]?.name || "No active organization",
)
let didOutputUsage: boolean = false
try {
// Only continue the request if the user:
// 1. Has signed in to Cline with a token
// 2. Has more than 0 credits
// Or an error is thrown.
await this.clineAccountService.validateRequest()
const client = await this.ensureClient()
this.lastGenerationId = undefined
let didOutputUsage: boolean = false
const stream = await createOpenRouterStream(
client,
systemPrompt,
@@ -183,6 +183,7 @@ export class ClineHandler implements ApiHandler {
throw new Error("Unauthorized: Please sign in to Cline before trying again.")
}
console.error("Cline API Error:", error)
throw error instanceof Error ? error : new Error(String(error))
}
}
@@ -82,6 +82,42 @@ export class ClineAccountService {
}
}
/**
* Validates if the user has sufficient credits to make API requests.
* This checks the user's balance and throws an error if the balance is insufficient or if the request fails.
* @throws Error if the user has insufficient credits or if the request fails
* @returns {Promise<void>} A promise that resolves if the user has sufficient credits.
*/
async validateRequest(): Promise<void> {
try {
const { organizations, id } = await this.authenticatedRequest<UserResponse>(`/api/v1/users/me`)
const activeOrganization = organizations.find((org) => org.active)
console.log("SwitchAuthToken: Active Organization", activeOrganization?.name || "No active organization")
// Skip balance check for active organizations
if (activeOrganization) {
return
}
const balance = await this.authenticatedRequest<BalanceResponse>(`/api/v1/users/${id}/balance`)
const currentBalance = Number(balance?.balance) || 0
// Throw error if insufficient credits (balance <= 0)
if (currentBalance <= 0) {
throw new Error(
JSON.stringify({
code: "insufficient_credits",
current_balance: currentBalance,
message: "Not enough credits available",
}),
)
}
} catch (error) {
console.error("Invalid Cline API request:", error)
throw error instanceof Error ? error : new Error(`Invalid Request: ${error}`)
}
}
/**
* RPC variant that fetches the user's current credit balance without posting to webview
* @returns Balance data or undefined if failed
@@ -1,5 +1,5 @@
import { VSCodeButton, VSCodeDivider, VSCodeLink, VSCodeDropdown, VSCodeOption } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { memo, useCallback, useEffect, useState } from "react"
import { BadgeCent } from "lucide-react"
import { useClineAuth } from "@/context/ClineAuthContext"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
@@ -10,7 +10,7 @@ import { UsageTransaction, PaymentTransaction } from "@shared/ClineAccount"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AccountServiceClient } from "@/services/grpc-client"
import { EmptyRequest } from "@shared/proto/common"
import { GetOrganizationCreditsRequest, UserOrganization, UserOrganizationUpdateRequest } from "@shared/proto/account"
import { UserOrganization, UserOrganizationUpdateRequest } from "@shared/proto/account"
type VSCodeDropdownChangeEvent = Event & {
target: {
@@ -72,31 +72,6 @@ export const ClineAccountView = () => {
}
}
async function getOrganizationCredits() {
setIsLoading(true)
if (!activeOrganization) {
await getUserCredits()
return
}
try {
const response = await AccountServiceClient.getOrganizationCredits(
GetOrganizationCreditsRequest.create({
organizationId: activeOrganization.organizationId,
}),
)
setBalance(response.balance?.currentBalance || 0)
setUsageData(response.usageTransactions)
setPaymentsData(response.paymentTransactions)
} catch (error) {
console.error("Failed to fetch organization credits data:", error)
setBalance(0)
setUsageData([])
setPaymentsData([])
} finally {
setIsLoading(false)
}
}
async function getUserOrganizations() {
setIsLoading(true)
try {
@@ -118,8 +93,7 @@ export const ClineAccountView = () => {
const fetchUserData = async () => {
try {
await getUserCredits()
await getUserOrganizations()
Promise.all([getUserCredits(), getUserOrganizations()])
} catch (error) {
console.error("Failed to fetch user data:", error)
}
@@ -128,20 +102,6 @@ export const ClineAccountView = () => {
fetchUserData()
}, [user])
useEffect(() => {
if (!activeOrganization) return
const fetchOrgCredits = async () => {
try {
await getOrganizationCredits()
} catch (error) {
console.error("Failed to fetch organization credits:", error)
}
}
fetchOrgCredits()
}, [activeOrganization])
const handleLogin = () => {
handleSignIn()
}
@@ -150,18 +110,23 @@ export const ClineAccountView = () => {
handleSignOut()
}
const handleOrganizationChange = async (event: any) => {
const newOrgId = (event.target as VSCodeDropdownChangeEvent["target"]).value
const handleOrganizationChange = useCallback(
async (event: any) => {
const newOrgId = (event.target as VSCodeDropdownChangeEvent["target"]).value
if (!activeOrganization || activeOrganization.organizationId !== newOrgId) {
try {
await AccountServiceClient.setUserOrganization(UserOrganizationUpdateRequest.create({ organizationId: newOrgId }))
await getUserOrganizations()
} catch (error) {
console.error("Failed to update organization:", error)
if (activeOrganization?.organizationId !== newOrgId) {
try {
await AccountServiceClient.setUserOrganization(
UserOrganizationUpdateRequest.create({ organizationId: newOrgId }),
)
await getUserOrganizations()
} catch (error) {
console.error("Failed to update organization:", error)
}
}
}
}
},
[activeOrganization],
)
return (
<div className="h-full flex flex-col">
@@ -217,34 +182,37 @@ export const ClineAccountView = () => {
</VSCodeButton>
</div>
<VSCodeDivider className="w-full my-6" />
{/* Credit balance is not available for organization account */}
{activeOrganization === null && <VSCodeDivider className="w-full my-6" />}
<div className="w-full flex flex-col items-center">
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3">CURRENT BALANCE</div>
{activeOrganization === null && (
<div className="w-full flex flex-col items-center">
<div className="text-sm text-[var(--vscode-descriptionForeground)] mb-3">CURRENT BALANCE</div>
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6 flex items-center gap-2">
{isLoading ? (
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
) : (
<>
<BadgeCent className="size-6 text-[var(--vscode-foreground)]" />
{/* TODO: Do this in a more correct way. We have to divide by 10000
* because the balance is stored in microcredits in the backend.
*/}
<CountUp end={balance / 10000} duration={0.66} decimals={4} />
<VSCodeButton appearance="icon" className="mt-1" onClick={getUserCredits}>
<span className="codicon codicon-refresh"></span>
</VSCodeButton>
</>
)}
<div className="text-4xl font-bold text-[var(--vscode-foreground)] mb-6 flex items-center gap-2">
{isLoading ? (
<div className="text-[var(--vscode-descriptionForeground)]">Loading...</div>
) : (
<>
<BadgeCent className="size-6 text-[var(--vscode-foreground)]" />
{/* TODO: Do this in a more correct way. We have to divide by 10000
* because the balance is stored in microcredits in the backend.
*/}
<CountUp end={balance / 10000} duration={0.66} decimals={4} />
<VSCodeButton appearance="icon" className="mt-1" onClick={getUserCredits}>
<span className="codicon codicon-refresh"></span>
</VSCodeButton>
</>
)}
</div>
<div className="w-full">
<VSCodeButtonLink href={dashboardAddCreditsURL} className="w-full">
Add Credits
</VSCodeButtonLink>
</div>
</div>
<div className="w-full">
<VSCodeButtonLink href={dashboardAddCreditsURL} className="w-full">
Add Credits
</VSCodeButtonLink>
</div>
</div>
)}
<VSCodeDivider className="mt-6 mb-3 w-full" />
+6 -10
View File
@@ -1,8 +1,8 @@
import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import deepEqual from "fast-deep-equal"
import React, { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"
import styled from "styled-components"
import { useEvent, useSize } from "react-use"
import { useSize } from "react-use"
import CreditLimitError from "@/components/chat/CreditLimitError"
import { OptionsButtons } from "@/components/chat/OptionsButtons"
@@ -12,14 +12,12 @@ import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
import MarkdownBlock from "@/components/common/MarkdownBlock"
import SuccessButton from "@/components/common/SuccessButton"
import { WithCopyButton } from "@/components/common/CopyButton"
import Thumbnails from "@/components/common/Thumbnails"
import McpResponseDisplay from "@/components/mcp/chat-display/McpResponseDisplay"
import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server-row/McpResourceRow"
import McpToolRow from "@/components/mcp/configuration/tabs/installed/server-row/McpToolRow"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
import { vscode } from "@/utils/vscode"
import {
ClineApiReqInfo,
ClineAskQuestion,
@@ -28,7 +26,6 @@ import {
ClinePlanModeResponse,
ClineSayTool,
COMPLETION_RESULT_CHANGES_FLAG,
ExtensionMessage,
} from "@shared/ExtensionMessage"
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
import { Int64Request, StringRequest } from "@shared/proto/common"
@@ -945,14 +942,13 @@ export const ChatRowContent = memo(
<>
{(() => {
// Try to parse the error message as JSON for credit limit error
const errorData = parseErrorText(apiRequestFailedMessage)
const errorData = parseErrorText(
apiRequestFailedMessage || apiReqStreamingFailedMessage,
)
if (errorData) {
if (
errorData.code === "insufficient_credits" &&
typeof errorData.current_balance === "number" &&
typeof errorData.total_spent === "number" &&
typeof errorData.total_promotions === "number" &&
typeof errorData.message === "string"
typeof errorData.current_balance === "number"
) {
return (
<CreditLimitError
@@ -6,27 +6,25 @@ import React from "react"
interface CreditLimitErrorProps {
currentBalance: number
totalSpent: number
totalPromotions: number
totalSpent?: number
totalPromotions?: number
message: string
}
const CreditLimitError: React.FC<CreditLimitErrorProps> = ({ currentBalance, totalSpent, totalPromotions, message }) => {
const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
currentBalance = 0,
totalSpent = 0,
totalPromotions = 0,
message = "You have run out of credit.",
}) => {
// We have to divide because the balance is stored in microcredits
return (
<div
style={{
backgroundColor: "var(--vscode-textBlockQuote-background)",
padding: "12px",
borderRadius: "4px",
marginBottom: "12px",
}}>
<div style={{ color: "var(--vscode-errorForeground)", marginBottom: "8px" }}>{message}</div>
<div style={{ marginBottom: "12px" }}>
<div style={{ color: "var(--vscode-foreground)" }}>
Current Balance: <span style={{ fontWeight: "bold" }}>${currentBalance.toFixed(2)}</span>
<div className="p-2 border-none rounded-md mb-2 bg-[var(--vscode-textBlockQuote-background)]">
<div className="mb-2">{message}</div>
<div className="mb-3">
<div className="text-[var(--vscode-foreground)]">
Current Balance: <span className="font-bold">${(currentBalance / 1000000).toFixed(4)}</span>
</div>
<div style={{ color: "var(--vscode-foreground)" }}>Total Spent: ${totalSpent.toFixed(2)}</div>
<div style={{ color: "var(--vscode-foreground)" }}>Total Promotions: ${totalPromotions.toFixed(2)}</div>
</div>
<VSCodeButtonLink
@@ -35,7 +33,7 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({ currentBalance, tot
width: "100%",
marginBottom: "8px",
}}>
<span className="codicon codicon-credit-card" style={{ fontSize: "14px", marginRight: "6px" }} />
<span className="codicon codicon-credit-card mr-0.5 text-sm" />
Buy Credits
</VSCodeButtonLink>