fix: pro enterprise plan messaging (#355)
This commit is contained in:
1
bun.lock
1
bun.lock
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@letta-ai/letta-code",
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
} from "../../auth/anthropic-oauth";
|
||||
import {
|
||||
ANTHROPIC_PROVIDER_NAME,
|
||||
checkAnthropicOAuthEligibility,
|
||||
createOrUpdateAnthropicProvider,
|
||||
removeAnthropicProvider,
|
||||
} from "../../providers/anthropic-provider";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
import { getErrorMessage } from "../../utils/error";
|
||||
import type { Buffers, Line } from "../helpers/accumulator";
|
||||
|
||||
// tiny helper for unique ids
|
||||
@@ -141,15 +143,46 @@ export async function handleConnect(
|
||||
// Start the OAuth flow (step 1)
|
||||
ctx.setCommandRunning(true);
|
||||
|
||||
// Show initial status
|
||||
const cmdId = addCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
msg,
|
||||
"Checking account eligibility...",
|
||||
true,
|
||||
"running",
|
||||
);
|
||||
|
||||
try {
|
||||
// 1. Start OAuth flow - generate PKCE and authorization URL
|
||||
// 1. Check eligibility before starting OAuth flow
|
||||
const eligibility = await checkAnthropicOAuthEligibility();
|
||||
if (!eligibility.eligible) {
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Claude OAuth requires a Pro or Enterprise plan\n\n` +
|
||||
`This feature is only available for Letta Pro or Enterprise customers.\n` +
|
||||
`Current plan: ${eligibility.billing_tier}\n\n` +
|
||||
`To upgrade your plan, visit:\n\n` +
|
||||
` https://app.letta.com/settings/organization/usage\n\n` +
|
||||
`If you have an Anthropic API key, you can use it directly by setting:\n` +
|
||||
` export ANTHROPIC_API_KEY=your-key`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Start OAuth flow - generate PKCE and authorization URL
|
||||
const { authorizationUrl, state, codeVerifier } =
|
||||
await startAnthropicOAuth();
|
||||
|
||||
// 2. Store state for validation when user returns with code
|
||||
// 3. Store state for validation when user returns with code
|
||||
settingsManager.storeOAuthState(state, codeVerifier, "anthropic");
|
||||
|
||||
// 3. Try to open browser
|
||||
// 4. Try to open browser
|
||||
let browserOpened = false;
|
||||
try {
|
||||
const { default: open } = await import("open");
|
||||
@@ -163,14 +196,15 @@ export async function handleConnect(
|
||||
// If auto-open fails, user can still manually visit the URL
|
||||
}
|
||||
|
||||
// 4. Show instructions
|
||||
// 5. Show instructions
|
||||
const browserMsg = browserOpened
|
||||
? "Opening browser for authorization..."
|
||||
: "Please open the following URL in your browser:";
|
||||
|
||||
addCommandResult(
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`${browserMsg}\n\n${authorizationUrl}\n\n` +
|
||||
"After authorizing, you'll be redirected to a page showing: code#state\n" +
|
||||
@@ -178,17 +212,20 @@ export async function handleConnect(
|
||||
" /connect claude <code#state>\n\n" +
|
||||
"Example: /connect claude abc123...#def456...",
|
||||
true,
|
||||
"finished",
|
||||
);
|
||||
} catch (error) {
|
||||
// Clear any partial state
|
||||
settingsManager.clearOAuthState();
|
||||
|
||||
addCommandResult(
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Failed to start OAuth flow: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`✗ Failed to start OAuth flow: ${getErrorMessage(error)}`,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
} finally {
|
||||
ctx.setCommandRunning(false);
|
||||
@@ -261,7 +298,7 @@ async function completeOAuthFlow(
|
||||
stateToUse,
|
||||
);
|
||||
|
||||
// 4. Update status
|
||||
// 5. Update status
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
@@ -272,7 +309,7 @@ async function completeOAuthFlow(
|
||||
"running",
|
||||
);
|
||||
|
||||
// 5. Validate tokens work
|
||||
// 6. Validate tokens work
|
||||
const isValid = await validateAnthropicCredentials(tokens.access_token);
|
||||
if (!isValid) {
|
||||
throw new Error(
|
||||
@@ -280,10 +317,10 @@ async function completeOAuthFlow(
|
||||
);
|
||||
}
|
||||
|
||||
// 6. Store tokens locally
|
||||
// 7. Store tokens locally
|
||||
settingsManager.storeAnthropicTokens(tokens);
|
||||
|
||||
// 7. Update status for provider creation
|
||||
// 8. Update status for provider creation
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
@@ -294,13 +331,13 @@ async function completeOAuthFlow(
|
||||
"running",
|
||||
);
|
||||
|
||||
// 8. Create or update provider in Letta with the access token
|
||||
// 9. Create or update provider in Letta with the access token
|
||||
await createOrUpdateAnthropicProvider(tokens.access_token);
|
||||
|
||||
// 9. Clear OAuth state
|
||||
// 10. Clear OAuth state
|
||||
settingsManager.clearOAuthState();
|
||||
|
||||
// 10. Success!
|
||||
// 11. Success!
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
@@ -313,12 +350,28 @@ async function completeOAuthFlow(
|
||||
"finished",
|
||||
);
|
||||
} catch (error) {
|
||||
// Check if this is a plan upgrade requirement error from provider creation
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
let displayMessage: string;
|
||||
if (errorMessage === "PLAN_UPGRADE_REQUIRED") {
|
||||
displayMessage =
|
||||
`✗ Claude OAuth requires a Pro or Enterprise plan\n\n` +
|
||||
`This feature is only available for Letta Pro or Enterprise customers.\n` +
|
||||
`To upgrade your plan, visit:\n\n` +
|
||||
` https://app.letta.com/settings/organization/usage\n\n` +
|
||||
`If you have an Anthropic API key, you can use it directly by setting:\n` +
|
||||
` export ANTHROPIC_API_KEY=your-key`;
|
||||
} else {
|
||||
displayMessage = `✗ Failed to connect: ${errorMessage}`;
|
||||
}
|
||||
|
||||
updateCommandResult(
|
||||
ctx.buffersRef,
|
||||
ctx.refreshDerived,
|
||||
cmdId,
|
||||
msg,
|
||||
`✗ Failed to connect: ${error instanceof Error ? error.message : String(error)}`,
|
||||
displayMessage,
|
||||
false,
|
||||
"finished",
|
||||
);
|
||||
@@ -401,7 +454,7 @@ export async function handleDisconnect(
|
||||
cmdId,
|
||||
msg,
|
||||
`✓ Disconnected from Claude OAuth.\n\n` +
|
||||
`Warning: Failed to remove provider from Letta: ${error instanceof Error ? error.message : String(error)}\n` +
|
||||
`Warning: Failed to remove provider from Letta: ${getErrorMessage(error)}\n` +
|
||||
`Your local OAuth tokens have been removed.`,
|
||||
true,
|
||||
"finished",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../../auth/anthropic-oauth";
|
||||
import {
|
||||
ANTHROPIC_PROVIDER_NAME,
|
||||
checkAnthropicOAuthEligibility,
|
||||
createOrUpdateAnthropicProvider,
|
||||
} from "../../providers/anthropic-provider";
|
||||
import { settingsManager } from "../../settings-manager";
|
||||
@@ -21,6 +22,7 @@ type Props = {
|
||||
|
||||
type FlowState =
|
||||
| "initializing"
|
||||
| "checking_eligibility"
|
||||
| "waiting_for_code"
|
||||
| "exchanging"
|
||||
| "validating"
|
||||
@@ -58,6 +60,23 @@ export const OAuthCodeDialog = memo(
|
||||
return;
|
||||
}
|
||||
|
||||
// Check eligibility before starting OAuth flow
|
||||
setFlowState("checking_eligibility");
|
||||
const eligibility = await checkAnthropicOAuthEligibility();
|
||||
if (!eligibility.eligible) {
|
||||
onComplete(
|
||||
false,
|
||||
`✗ Claude OAuth requires a Pro or Enterprise plan\n\n` +
|
||||
`This feature is only available for Letta Pro or Enterprise customers.\n` +
|
||||
`Current plan: ${eligibility.billing_tier}\n\n` +
|
||||
`To upgrade your plan, visit:\n\n` +
|
||||
` https://app.letta.com/settings/organization/usage\n\n` +
|
||||
`If you have an Anthropic API key, you can use it directly by setting:\n` +
|
||||
` export ANTHROPIC_API_KEY=your-key`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start OAuth flow
|
||||
const {
|
||||
authorizationUrl,
|
||||
@@ -248,11 +267,13 @@ export const OAuthCodeDialog = memo(
|
||||
}
|
||||
};
|
||||
|
||||
if (flowState === "initializing") {
|
||||
if (flowState === "initializing" || flowState === "checking_eligibility") {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1}>
|
||||
<Text color={colors.status.processing}>
|
||||
Starting Claude OAuth flow...
|
||||
{flowState === "checking_eligibility"
|
||||
? "Checking account eligibility..."
|
||||
: "Starting Claude OAuth flow..."}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,19 @@ interface ProviderResponse {
|
||||
base_url?: string;
|
||||
}
|
||||
|
||||
interface BalanceResponse {
|
||||
total_balance: number;
|
||||
monthly_credit_balance: number;
|
||||
purchased_credit_balance: number;
|
||||
billing_tier: string;
|
||||
}
|
||||
|
||||
interface EligibilityCheckResult {
|
||||
eligible: boolean;
|
||||
billing_tier: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Letta API base URL and auth token
|
||||
*/
|
||||
@@ -53,6 +66,29 @@ async function providersRequest<T>(
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
|
||||
// Check if this is a pro/enterprise plan limitation error
|
||||
if (response.status === 403) {
|
||||
try {
|
||||
const errorData = JSON.parse(errorText);
|
||||
if (
|
||||
errorData.error &&
|
||||
typeof errorData.error === "string" &&
|
||||
errorData.error.includes("only available for pro or enterprise")
|
||||
) {
|
||||
throw new Error("PLAN_UPGRADE_REQUIRED");
|
||||
}
|
||||
} catch (parseError) {
|
||||
// If it's not valid JSON or doesn't match our pattern, fall through to generic error
|
||||
if (
|
||||
parseError instanceof Error &&
|
||||
parseError.message === "PLAN_UPGRADE_REQUIRED"
|
||||
) {
|
||||
throw parseError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Provider API error (${response.status}): ${errorText}`);
|
||||
}
|
||||
|
||||
@@ -187,3 +223,40 @@ export async function removeAnthropicProvider(): Promise<void> {
|
||||
await deleteAnthropicProvider(existing.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is eligible for Anthropic OAuth
|
||||
* Requires Pro or Enterprise billing tier
|
||||
*/
|
||||
export async function checkAnthropicOAuthEligibility(): Promise<EligibilityCheckResult> {
|
||||
try {
|
||||
const balance = await providersRequest<BalanceResponse>(
|
||||
"GET",
|
||||
"/v1/metadata/balance",
|
||||
);
|
||||
|
||||
const billingTier = balance.billing_tier.toLowerCase();
|
||||
|
||||
// OAuth is available for pro and enterprise tiers
|
||||
if (billingTier === "pro" || billingTier === "enterprise") {
|
||||
return {
|
||||
eligible: true,
|
||||
billing_tier: balance.billing_tier,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
eligible: false,
|
||||
billing_tier: balance.billing_tier,
|
||||
reason: `Claude OAuth requires a Pro or Enterprise plan. Current plan: ${balance.billing_tier}`,
|
||||
};
|
||||
} catch (error) {
|
||||
// If we can't check eligibility, allow the flow to continue
|
||||
// The provider creation will handle the error appropriately
|
||||
console.warn("Failed to check Anthropic OAuth eligibility:", error);
|
||||
return {
|
||||
eligible: true,
|
||||
billing_tier: "unknown",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user