Spaces:
Sleeping
Sleeping
| /** | |
| * REST/Connect-RPC client for Windsurf/Codeium cloud services. | |
| * | |
| * Unlike client.js (which talks to the local language server binary over gRPC), | |
| * this module hits public Connect-RPC endpoints that accept JSON, so we don't | |
| * need proto builders/parsers to fetch account metadata. | |
| * | |
| * POST https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus | |
| * Content-Type: application/json | |
| * Connect-Protocol-Version: 1 | |
| * | |
| * Currently exposes: | |
| * - getUserStatus(apiKey, proxy) β plan info, quotas, credit balance | |
| * - getCascadeModelConfigs(apiKey, proxy) β live model catalog (82+ models) | |
| * - checkMessageRateLimit(apiKey, proxy) β pre-flight rate limit check | |
| */ | |
| import http from 'http'; | |
| import https from 'https'; | |
| import { log } from './config.js'; | |
| const SERVER_HOSTS = [ | |
| 'server.codeium.com', | |
| 'server.self-serve.windsurf.com', | |
| ]; | |
| const USER_STATUS_PATH = '/exa.seat_management_pb.SeatManagementService/GetUserStatus'; | |
| const MODEL_CONFIGS_PATH = '/exa.api_server_pb.ApiServerService/GetCascadeModelConfigs'; | |
| const RATE_LIMIT_PATH = '/exa.api_server_pb.ApiServerService/CheckUserMessageRateLimit'; | |
| // Tunnel HTTPS through an HTTP CONNECT proxy. Mirrors dashboard/windsurf-login.js | |
| // so per-account outbound IPs stay consistent across login and credit fetch. | |
| function createProxyTunnel(proxy, targetHost, targetPort) { | |
| return new Promise((resolve, reject) => { | |
| const proxyHost = proxy.host.replace(/:\d+$/, ''); | |
| const proxyPort = proxy.port || 8080; | |
| const req = http.request({ | |
| host: proxyHost, | |
| port: proxyPort, | |
| method: 'CONNECT', | |
| path: `${targetHost}:${targetPort}`, | |
| headers: { | |
| Host: `${targetHost}:${targetPort}`, | |
| ...(proxy.username ? { | |
| 'Proxy-Authorization': `Basic ${Buffer.from(`${proxy.username}:${proxy.password || ''}`).toString('base64')}`, | |
| } : {}), | |
| }, | |
| }); | |
| req.on('connect', (res, socket) => { | |
| if (res.statusCode === 200) resolve(socket); | |
| else { socket.destroy(); reject(new Error(`Proxy CONNECT failed: ${res.statusCode}`)); } | |
| }); | |
| req.on('error', (err) => reject(new Error(`Proxy tunnel: ${err.message}`))); | |
| req.setTimeout(15000, () => { req.destroy(); reject(new Error('Proxy tunnel timeout')); }); | |
| req.end(); | |
| }); | |
| } | |
| /** Detect errors caused by the proxy itself (not the upstream API). */ | |
| function isProxyError(err) { | |
| const m = err?.message || ''; | |
| return /Proxy CONNECT failed|Proxy tunnel|Proxy connection/i.test(m); | |
| } | |
| function postJson(host, path, body, proxy) { | |
| return new Promise(async (resolve, reject) => { | |
| const postData = JSON.stringify(body); | |
| const opts = { | |
| hostname: host, | |
| port: 443, | |
| path, | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Content-Length': Buffer.byteLength(postData), | |
| 'Connect-Protocol-Version': '1', | |
| 'Accept': 'application/json', | |
| 'User-Agent': 'windsurf/1.108.2', | |
| }, | |
| }; | |
| const onRes = (res) => { | |
| const bufs = []; | |
| res.on('data', d => bufs.push(d)); | |
| res.on('end', () => { | |
| const raw = Buffer.concat(bufs).toString('utf8'); | |
| try { | |
| const parsed = raw ? JSON.parse(raw) : {}; | |
| resolve({ status: res.statusCode, data: parsed, raw }); | |
| } catch { | |
| reject(new Error(`Non-JSON response (${res.statusCode}): ${raw.slice(0, 200)}`)); | |
| } | |
| }); | |
| res.on('error', reject); | |
| }; | |
| try { | |
| let req; | |
| if (proxy && proxy.host) { | |
| const socket = await createProxyTunnel(proxy, host, 443); | |
| opts.socket = socket; | |
| opts.agent = false; | |
| req = https.request(opts, onRes); | |
| } else { | |
| req = https.request(opts, onRes); | |
| } | |
| req.on('error', (err) => reject(new Error(`Request: ${err.message}`))); | |
| req.setTimeout(20000, () => { req.destroy(); reject(new Error('Request timeout')); }); | |
| req.write(postData); | |
| req.end(); | |
| } catch (err) { reject(err); } | |
| }); | |
| } | |
| /** | |
| * Fetch account status: plan, quotas, credit balance, and model catalog. | |
| * Tries both known Connect-RPC hostnames before giving up. | |
| * | |
| * Returns a normalized shape that covers both the legacy credit contract | |
| * (availablePromptCredits / usedPromptCredits) and the newer quota contract | |
| * (dailyQuotaRemainingPercent / weeklyQuotaRemainingPercent). | |
| * | |
| * @param {string} apiKey | |
| * @param {object} [proxy] optional HTTP CONNECT proxy | |
| * @returns {Promise<{planName, dailyPercent, weeklyPercent, dailyResetAt, weeklyResetAt, prompt:{used,limit}, flex:{used,limit}, raw}>} | |
| */ | |
| export async function getUserStatus(apiKey, proxy = null) { | |
| const body = { | |
| metadata: { | |
| apiKey, | |
| ideName: 'windsurf', | |
| ideVersion: '1.108.2', | |
| extensionName: 'windsurf', | |
| extensionVersion: '1.108.2', | |
| locale: 'en', | |
| }, | |
| }; | |
| // Try with proxy first, then retry direct if proxy itself fails (407 etc.). | |
| const proxyModes = proxy ? [proxy, null] : [null]; | |
| let lastErr = null; | |
| for (const px of proxyModes) { | |
| for (const host of SERVER_HOSTS) { | |
| try { | |
| const res = await postJson(host, USER_STATUS_PATH, body, px); | |
| if (res.status >= 400) { | |
| lastErr = new Error(`GetUserStatus ${host} β ${res.status}: ${res.raw.slice(0, 160)}`); | |
| continue; | |
| } | |
| return normalizeUserStatus(res.data); | |
| } catch (e) { | |
| lastErr = e; | |
| log.debug(`getCreditUsage ${host} failed: ${e.message}`); | |
| if (px && isProxyError(e)) break; // skip second host, go straight to direct | |
| } | |
| } | |
| } | |
| throw lastErr || new Error('GetUserStatus: all hosts failed'); | |
| } | |
| function normalizeUserStatus(data) { | |
| const ps = data?.userStatus?.planStatus || {}; | |
| const plan = ps.planInfo || {}; | |
| // Legacy values come in hundredths; divide by 100 for display. | |
| const legacyDiv = (n) => (typeof n === 'number' ? n / 100 : null); | |
| // Unix timestamps may be numeric or string depending on server version. | |
| const asUnix = (v) => { | |
| if (v == null) return null; | |
| if (typeof v === 'number') return v; | |
| const n = parseInt(v, 10); | |
| return Number.isFinite(n) ? n : null; | |
| }; | |
| const out = { | |
| planName: plan.planName || 'Unknown', | |
| dailyPercent: typeof ps.dailyQuotaRemainingPercent === 'number' ? ps.dailyQuotaRemainingPercent : null, | |
| weeklyPercent: typeof ps.weeklyQuotaRemainingPercent === 'number' ? ps.weeklyQuotaRemainingPercent : null, | |
| dailyResetAt: asUnix(ps.dailyQuotaResetAtUnix), | |
| weeklyResetAt: asUnix(ps.weeklyQuotaResetAtUnix), | |
| overageBalance: typeof ps.overageBalanceMicros === 'number' ? ps.overageBalanceMicros / 1_000_000 : null, | |
| prompt: { | |
| limit: legacyDiv(plan.monthlyPromptCredits), | |
| used: legacyDiv(ps.usedPromptCredits), | |
| remaining: legacyDiv(ps.availablePromptCredits), | |
| }, | |
| flex: { | |
| limit: legacyDiv(plan.monthlyFlexCreditPurchaseAmount), | |
| used: legacyDiv(ps.usedFlexCredits), | |
| remaining: legacyDiv(ps.availableFlexCredits), | |
| }, | |
| planStart: ps.planStart || null, | |
| planEnd: ps.planEnd || null, | |
| // Preserve the untouched response so downstream caching (model catalog) | |
| // can inspect fields we haven't normalized yet. | |
| raw: data, | |
| fetchedAt: Date.now(), | |
| }; | |
| // Derive a single display-friendly percent: prefer daily remaining; otherwise | |
| // compute from prompt credits; otherwise null. | |
| if (out.dailyPercent != null) { | |
| out.percent = out.dailyPercent; | |
| } else if (out.prompt.limit && out.prompt.remaining != null) { | |
| out.percent = (out.prompt.remaining / out.prompt.limit) * 100; | |
| } else { | |
| out.percent = null; | |
| } | |
| return out; | |
| } | |
| // βββ Dynamic model catalog ββββββββββββββββββββββββββββββββ | |
| function buildMetadata(apiKey) { | |
| return { | |
| apiKey, | |
| ideName: 'windsurf', | |
| ideVersion: '1.108.2', | |
| extensionName: 'windsurf', | |
| extensionVersion: '1.108.2', | |
| locale: 'en', | |
| }; | |
| } | |
| /** | |
| * Fetch the live model catalog from Codeium's cloud. | |
| * Returns an array of ClientModelConfig objects with modelUid, label, | |
| * creditMultiplier, provider, maxTokens, supportsImages, etc. | |
| * | |
| * @param {string} apiKey | |
| * @param {object} [proxy] | |
| * @returns {Promise<{configs: object[], sorts: object[], defaultOverride: object|null}>} | |
| */ | |
| export async function getCascadeModelConfigs(apiKey, proxy = null) { | |
| const body = { metadata: buildMetadata(apiKey) }; | |
| const proxyModes = proxy ? [proxy, null] : [null]; | |
| let lastErr = null; | |
| for (const px of proxyModes) { | |
| for (const host of SERVER_HOSTS) { | |
| try { | |
| const res = await postJson(host, MODEL_CONFIGS_PATH, body, px); | |
| if (res.status >= 400) { | |
| lastErr = new Error(`GetCascadeModelConfigs ${host} β ${res.status}: ${res.raw.slice(0, 160)}`); | |
| continue; | |
| } | |
| return { | |
| configs: res.data.clientModelConfigs || [], | |
| sorts: res.data.clientModelSorts || [], | |
| defaultOverride: res.data.defaultOverrideModelConfig || null, | |
| }; | |
| } catch (e) { | |
| lastErr = e; | |
| log.debug(`GetCascadeModelConfigs host ${host} failed: ${e.message}`); | |
| if (px && isProxyError(e)) break; | |
| } | |
| } | |
| } | |
| throw lastErr || new Error('GetCascadeModelConfigs: all hosts failed'); | |
| } | |
| /** | |
| * Pre-flight check: does this account still have message capacity? | |
| * Returns { hasCapacity, messagesRemaining, maxMessages }. | |
| * -1 means unlimited. | |
| * | |
| * @param {string} apiKey | |
| * @param {object} [proxy] | |
| * @returns {Promise<{hasCapacity: boolean, messagesRemaining: number, maxMessages: number}>} | |
| */ | |
| export async function checkMessageRateLimit(apiKey, proxy = null) { | |
| const body = { metadata: buildMetadata(apiKey) }; | |
| const proxyModes = proxy ? [proxy, null] : [null]; | |
| let lastErr = null; | |
| for (const px of proxyModes) { | |
| for (const host of SERVER_HOSTS) { | |
| try { | |
| const res = await postJson(host, RATE_LIMIT_PATH, body, px); | |
| if (res.status >= 400) { | |
| lastErr = new Error(`CheckRateLimit ${host} β ${res.status}: ${res.raw.slice(0, 160)}`); | |
| continue; | |
| } | |
| return { | |
| hasCapacity: res.data.hasCapacity !== false, | |
| messagesRemaining: res.data.messagesRemaining ?? -1, | |
| maxMessages: res.data.maxMessages ?? -1, | |
| }; | |
| } catch (e) { | |
| lastErr = e; | |
| log.debug(`CheckRateLimit host ${host} failed: ${e.message}`); | |
| if (px && isProxyError(e)) break; | |
| } | |
| } | |
| } | |
| // On failure, assume capacity so we don't block requests. | |
| log.warn(`CheckRateLimit failed: ${lastErr?.message}`); | |
| return { hasCapacity: true, messagesRemaining: -1, maxMessages: -1 }; | |
| } | |