/** * Direct API calls to Letta for managing ChatGPT OAuth provider * Uses the chatgpt_oauth provider type - backend handles request transformation * (transforms OpenAI API format → ChatGPT backend API format) */ import { getLettaCodeHeaders } from "../agent/http-headers"; import { LETTA_CLOUD_API_URL } from "../auth/oauth"; import { settingsManager } from "../settings-manager"; // Provider name constant for letta-code's ChatGPT OAuth provider export const OPENAI_CODEX_PROVIDER_NAME = "chatgpt-plus-pro"; // Provider type for ChatGPT OAuth (backend handles transformation) export const CHATGPT_OAUTH_PROVIDER_TYPE = "chatgpt_oauth"; /** * ChatGPT OAuth configuration sent to Letta backend * Backend uses this to authenticate with ChatGPT and transform requests */ export interface ChatGPTOAuthConfig { access_token: string; id_token: string; refresh_token?: string; account_id: string; expires_at: number; // Unix timestamp in milliseconds } interface ProviderResponse { id: string; name: string; provider_type: string; api_key?: string; 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 */ async function getLettaConfig(): Promise<{ baseUrl: string; apiKey: string }> { const settings = await settingsManager.getSettingsWithSecureTokens(); const baseUrl = process.env.LETTA_BASE_URL || settings.env?.LETTA_BASE_URL || LETTA_CLOUD_API_URL; const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY || ""; return { baseUrl, apiKey }; } /** * Make a request to the Letta providers API */ async function providersRequest( method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: Record, ): Promise { const { baseUrl, apiKey } = await getLettaConfig(); const url = `${baseUrl}${path}`; const response = await fetch(url, { method, headers: getLettaCodeHeaders(apiKey), ...(body && { body: JSON.stringify(body) }), }); 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}`); } // Handle empty responses (e.g., DELETE) const text = await response.text(); if (!text) { return {} as T; } return JSON.parse(text) as T; } /** * List all providers to find if our provider exists */ export async function listProviders(): Promise { try { const response = await providersRequest( "GET", "/v1/providers", ); return response; } catch { return []; } } /** * Get the chatgpt-plus-pro provider if it exists */ export async function getOpenAICodexProvider(): Promise { const providers = await listProviders(); return providers.find((p) => p.name === OPENAI_CODEX_PROVIDER_NAME) || null; } /** * Create a new ChatGPT OAuth provider * OAuth config is JSON-encoded in api_key field to avoid backend schema changes * Backend parses api_key as JSON when provider_type is "chatgpt_oauth" */ export async function createOpenAICodexProvider( config: ChatGPTOAuthConfig, ): Promise { // Encode OAuth config as JSON in api_key field const apiKeyJson = JSON.stringify({ access_token: config.access_token, id_token: config.id_token, refresh_token: config.refresh_token, account_id: config.account_id, expires_at: config.expires_at, }); return providersRequest("POST", "/v1/providers", { name: OPENAI_CODEX_PROVIDER_NAME, provider_type: CHATGPT_OAUTH_PROVIDER_TYPE, api_key: apiKeyJson, }); } /** * Update an existing ChatGPT OAuth provider with new OAuth config * OAuth config is JSON-encoded in api_key field */ export async function updateOpenAICodexProvider( providerId: string, config: ChatGPTOAuthConfig, ): Promise { // Encode OAuth config as JSON in api_key field const apiKeyJson = JSON.stringify({ access_token: config.access_token, id_token: config.id_token, refresh_token: config.refresh_token, account_id: config.account_id, expires_at: config.expires_at, }); return providersRequest( "PATCH", `/v1/providers/${providerId}`, { api_key: apiKeyJson, }, ); } /** * Delete the ChatGPT OAuth provider */ export async function deleteOpenAICodexProvider( providerId: string, ): Promise { await providersRequest("DELETE", `/v1/providers/${providerId}`); } /** * Create or update the ChatGPT OAuth provider * This is the main function called after successful /connect codex * * The Letta backend will: * 1. Store the OAuth tokens securely * 2. Handle token refresh when needed * 3. Transform requests from OpenAI format to ChatGPT backend format * 4. Add required headers (Authorization, ChatGPT-Account-Id, etc.) * 5. Forward to chatgpt.com/backend-api/codex */ export async function createOrUpdateOpenAICodexProvider( config: ChatGPTOAuthConfig, ): Promise { const existing = await getOpenAICodexProvider(); if (existing) { // Update existing provider with new OAuth config return updateOpenAICodexProvider(existing.id, config); } else { // Create new provider return createOpenAICodexProvider(config); } } /** * Remove the ChatGPT OAuth provider (called on /disconnect) */ export async function removeOpenAICodexProvider(): Promise { const existing = await getOpenAICodexProvider(); if (existing) { await deleteOpenAICodexProvider(existing.id); } } /** * Check if user is eligible for ChatGPT OAuth * Requires Pro or Enterprise billing tier */ export async function checkOpenAICodexEligibility(): Promise { try { const balance = await providersRequest( "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: `ChatGPT 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 ChatGPT OAuth eligibility:", error); return { eligible: true, billing_tier: "unknown", }; } }