diff --git a/src/auth/index.js b/src/auth/index.js index 4f951cf..29ec6a9 100644 --- a/src/auth/index.js +++ b/src/auth/index.js @@ -35,10 +35,4 @@ export { export { importOrchidsToken, handleOrchidsOAuth -} from './orchids-oauth.js'; - -// Letta OAuth -export { - handleLettaOAuth, - refreshLettaToken -} from './letta-oauth.js'; \ No newline at end of file +} from './orchids-oauth.js'; \ No newline at end of file diff --git a/src/auth/letta-oauth.js b/src/auth/letta-oauth.js deleted file mode 100644 index c9b37dd..0000000 --- a/src/auth/letta-oauth.js +++ /dev/null @@ -1,215 +0,0 @@ -import axios from 'axios'; -import fs from 'fs'; -import path from 'path'; -import crypto from 'crypto'; -import { broadcastEvent } from '../services/ui-manager.js'; -import { autoLinkProviderConfigs } from '../services/service-manager.js'; -import { CONFIG } from '../core/config-manager.js'; -import { getProxyConfigForProvider } from '../utils/proxy-utils.js'; - -/** - * Letta OAuth Configuration - */ -const LETTA_OAUTH_CONFIG = { - authServiceEndpoint: 'https://api.letta.com', // Base URL for Letta API - callbackPort: 19876, - authTimeout: 5 * 60 * 1000, - credentialsDir: 'letta', - logPrefix: '[Letta Auth]' -}; - -/** - * 活动的 Letta 回调服务器管理 - */ -let activeLettaServer = null; - -/** - * Helper for fetch with proxy - */ -async function fetchWithProxy(url, options = {}, providerType = 'openai-letta') { - const proxyConfig = getProxyConfigForProvider(CONFIG, providerType); - const axiosConfig = { - url, - method: options.method || 'GET', - headers: options.headers || {}, - data: options.body, - timeout: 30000, - }; - - if (proxyConfig) { - axiosConfig.httpAgent = proxyConfig.httpAgent; - axiosConfig.httpsAgent = proxyConfig.httpsAgent; - axiosConfig.proxy = false; - } - - try { - const response = await axios(axiosConfig); - return { - ok: response.status >= 200 && response.status < 300, - status: response.status, - json: async () => response.data, - text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data), - }; - } catch (error) { - if (error.response) { - return { - ok: false, - status: error.response.status, - json: async () => error.response.data, - text: async () => JSON.stringify(error.response.data), - }; - } - throw error; - } -} - -/** - * Handle Letta OAuth flow - */ -export async function handleLettaOAuth(currentConfig, options = {}) { - const state = crypto.randomBytes(16).toString('hex'); - const port = LETTA_OAUTH_CONFIG.callbackPort; - - // In a real OAuth flow, we would redirect to a login page. - // Since the task implies implementing token logic similar to tt/oauth.ts, - // we'll simulate the URL generation. - const redirectUri = `http://127.0.0.1:${port}/callback`; - - // For Letta, if it's a standard OAuth2 flow: - const authUrl = `${LETTA_OAUTH_CONFIG.authServiceEndpoint}/auth/authorize?` + - `response_type=code&` + - `client_id=${process.env.LETTA_CLIENT_ID || 'letta-code'}&` + - `redirect_uri=${encodeURIComponent(redirectUri)}&` + - `state=${state}&` + - `scope=read:agents write:messages`; - - // Start local server to wait for callback - await startLettaCallbackServer(state, options); - - return { - authUrl, - authInfo: { - provider: 'openai-letta', - port: port, - state: state, - ...options - } - }; -} - -async function startLettaCallbackServer(expectedState, options = {}) { - if (activeLettaServer) { - activeLettaServer.close(); - } - - const server = (await import('http')).createServer(async (req, res) => { - const url = new URL(req.url, `http://127.0.0.1:${LETTA_OAUTH_CONFIG.callbackPort}`); - - if (url.pathname === '/callback') { - const code = url.searchParams.get('code'); - const state = url.searchParams.get('state'); - - if (state !== expectedState) { - res.writeHead(400); - res.end('Invalid state'); - return; - } - - try { - // Exchange code for token - const tokenResponse = await fetchWithProxy(`${LETTA_OAUTH_CONFIG.authServiceEndpoint}/auth/token`, { - method: 'POST', - body: { - grant_type: 'authorization_code', - code, - client_id: process.env.LETTA_CLIENT_ID || 'letta-code', - redirect_uri: `http://127.0.0.1:${LETTA_OAUTH_CONFIG.callbackPort}/callback` - } - }); - - if (!tokenResponse.ok) throw new Error('Token exchange failed'); - - const tokenData = await tokenResponse.json(); - await saveLettaToken(tokenData, options); - - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end('

Authorization Successful!

You can close this window.

'); - - server.close(); - activeLettaServer = null; - } catch (err) { - res.writeHead(500); - res.end(`Error: ${err.message}`); - } - } - }); - - server.listen(LETTA_OAUTH_CONFIG.callbackPort); - activeLettaServer = server; - - setTimeout(() => { - if (activeLettaServer === server) { - server.close(); - activeLettaServer = null; - } - }, LETTA_OAUTH_CONFIG.authTimeout); -} - -async function saveLettaToken(tokenData, options) { - const timestamp = Date.now(); - const folderName = `${timestamp}_letta-token`; - const targetDir = path.join(process.cwd(), 'configs', 'letta', folderName); - await fs.promises.mkdir(targetDir, { recursive: true }); - - const credPath = path.join(targetDir, `token.json`); - - // 使用用户提供的接口返回结构: - // env.LETTA_API_KEY, refreshToken, tokenExpiresAt, lastAgent - const saveData = { - LETTA_API_KEY: tokenData.env?.LETTA_API_KEY || tokenData.access_token, - refreshToken: tokenData.refreshToken || tokenData.refresh_token, - expiresAt: tokenData.tokenExpiresAt ? new Date(tokenData.tokenExpiresAt).toISOString() : new Date(Date.now() + (tokenData.expires_in || 3600) * 1000).toISOString(), - LETTA_BASE_URL: LETTA_OAUTH_CONFIG.authServiceEndpoint, - LETTA_AGENT_ID: tokenData.lastAgent || tokenData.agentId || options.agentId - }; - - await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2)); - - broadcastEvent('oauth_success', { - provider: 'openai-letta', - credPath, - relativePath: path.relative(process.cwd(), credPath), - timestamp: new Date().toISOString() - }); - - await autoLinkProviderConfigs(CONFIG); -} - -/** - * Refresh Letta Token - */ -export async function refreshLettaToken(refreshToken) { - console.log(`${LETTA_OAUTH_CONFIG.logPrefix} Refreshing token...`); - const response = await fetchWithProxy(`${LETTA_OAUTH_CONFIG.authServiceEndpoint}/auth/token`, { - method: 'POST', - body: { - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: process.env.LETTA_CLIENT_ID || 'letta-code' - } - }); - - if (!response.ok) { - throw new Error(`Failed to refresh Letta token: ${response.status}`); - } - - const data = await response.json(); - - // 映射返回字段 - return { - accessToken: data.env?.LETTA_API_KEY || data.access_token, - refreshToken: data.refreshToken || data.refresh_token || refreshToken, - expiresAt: data.tokenExpiresAt ? new Date(data.tokenExpiresAt).toISOString() : new Date(Date.now() + (data.expires_in || 3600) * 1000).toISOString(), - agentId: data.lastAgent || data.agentId - }; -} diff --git a/src/auth/oauth-handlers.js b/src/auth/oauth-handlers.js index 39ed193..712afeb 100644 --- a/src/auth/oauth-handlers.js +++ b/src/auth/oauth-handlers.js @@ -23,7 +23,5 @@ export { refreshIFlowTokens, // Orchids OAuth importOrchidsToken, - handleOrchidsOAuth, - handleLettaOAuth, - refreshLettaToken + handleOrchidsOAuth } from './index.js'; \ No newline at end of file diff --git a/src/providers/adapter.js b/src/providers/adapter.js index 3ee7186..649fd04 100644 --- a/src/providers/adapter.js +++ b/src/providers/adapter.js @@ -8,7 +8,6 @@ import { OrchidsApiService } from './claude/claude-orchids.js'; // 导入Orchids import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService import { CodexApiService } from './openai/codex-core.js'; // 导入CodexApiService -import { LettaApiService } from './openai/letta-core.js'; // 导入LettaApiService import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER // 定义AI服务适配器接口 @@ -629,70 +628,6 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter { } } -// Letta API 服务适配器 -export class LettaApiServiceAdapter extends ApiServiceAdapter { - constructor(config) { - super(); - this.lettaApiService = new LettaApiService(config); - } - - async initialize() { - if (!this.lettaApiService.isInitialized) { - await this.lettaApiService.initialize(); - } - } - - async generateContent(model, requestBody) { - if (!this.lettaApiService.isInitialized) { - await this.lettaApiService.initialize(); - } - return this.lettaApiService.generateContent(model, requestBody); - } - - async *generateContentStream(model, requestBody) { - if (!this.lettaApiService.isInitialized) { - await this.lettaApiService.initialize(); - } - yield* this.lettaApiService.generateContentStream(model, requestBody); - } - - async listModels() { - if (!this.lettaApiService.isInitialized) { - await this.lettaApiService.initialize(); - } - return this.lettaApiService.listModels(); - } - - async refreshToken() { - if (!this.lettaApiService.isInitialized) { - await this.lettaApiService.initialize(); - } - if (this.isExpiryDateNear()) { - await this.lettaApiService.refreshAuthToken(); - } - return Promise.resolve(); - } - - async forceRefreshToken() { - if (!this.lettaApiService.isInitialized) { - await this.lettaApiService.initialize(); - } - return this.lettaApiService.refreshAuthToken(); - } - - isExpiryDateNear() { - return this.lettaApiService.isExpiryDateNear(); - } - - /** - * 获取用量限制信息 - * Letta 暂不支持用量查询 - */ - async getUsageLimits() { - throw new Error('Letta does not support usage query.'); - } -} - // 用于存储服务适配器单例的映射 export const serviceInstances = {}; @@ -734,9 +669,6 @@ export function getServiceAdapter(config) { case MODEL_PROVIDER.CODEX_API: serviceInstances[providerKey] = new CodexApiServiceAdapter(config); break; - case MODEL_PROVIDER.LETTA_API: - serviceInstances[providerKey] = new LettaApiServiceAdapter(config); - break; default: throw new Error(`Unsupported model provider: ${provider}`); } diff --git a/src/providers/openai/letta-core.js b/src/providers/openai/letta-core.js deleted file mode 100644 index 4cac8b9..0000000 --- a/src/providers/openai/letta-core.js +++ /dev/null @@ -1,408 +0,0 @@ -import axios from 'axios'; -import * as http from 'http'; -import * as https from 'https'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { configureAxiosProxy } from '../../utils/proxy-utils.js'; -import { isRetryableNetworkError } from '../../utils/common.js'; - -/** - * Letta API Service - * Letta uses a streaming protocol that needs to be mapped to OpenAI compatible format - * inside the core logic as per requirements. - */ -export class LettaApiService { - constructor(config) { - // if (!config.LETTA_API_KEY) { - // throw new Error("Letta API Key is required for LettaApiService."); - // } - this.config = config; - this.apiKey = config.LETTA_API_KEY; - this.baseUrl = config.LETTA_BASE_URL || 'https://api.letta.com'; - this.useSystemProxy = config?.USE_SYSTEM_PROXY_LETTA ?? false; - - // Letta specific config - this.agentId = config.LETTA_AGENT_ID; - this.refreshToken = config.refreshToken; - this.expiresAt = config.expiresAt; - - console.log(`[Letta] Initialized with baseUrl: ${this.baseUrl}, agentId: ${this.agentId || 'default'}`); - - const httpAgent = new http.Agent({ - keepAlive: true, - maxSockets: 100, - maxFreeSockets: 5, - timeout: 120000, - }); - const httpsAgent = new https.Agent({ - keepAlive: true, - maxSockets: 100, - maxFreeSockets: 5, - timeout: 120000, - }); - - const axiosConfig = { - baseURL: this.baseUrl, - httpAgent, - httpsAgent, - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${this.apiKey}`, - 'X-Letta-Source': 'letta-code' - }, - }; - - if (!this.useSystemProxy) { - axiosConfig.proxy = false; - } - - configureAxiosProxy(axiosConfig, config, 'openai-letta'); - - this.axiosInstance = axios.create(axiosConfig); - } - - async initialize() { - if (this.isInitialized) return; - - // 兼容不同的配置键名,确保能取到凭据文件路径 - let tokenFilePath = this.config.LETTA_TOKEN_FILE_PATH || this.config.letta_token_file_path; - console.log(`[Letta] Configured token file path: ${tokenFilePath}`); - // 如果没有配置路径,尝试默认路径 ~/.letta/settings.json - if (!tokenFilePath) { - tokenFilePath = path.join(os.homedir(), '.letta', 'settings.json'); - console.log(`[Letta] No token file path configured, trying default path: ${tokenFilePath}`); - } - - if (tokenFilePath && fs.existsSync(tokenFilePath)) { - try { - const fileContent = JSON.parse(fs.readFileSync(tokenFilePath, 'utf8')); - console.log(`[Letta] Loaded credentials from file: ${tokenFilePath}`); - - // 根据提供的 JSON 数据结构解析 - // 优先级:文件中的 env.LETTA_API_KEY > 文件顶层的 LETTA_API_KEY > 现有 this.apiKey - this.apiKey = fileContent.env?.LETTA_API_KEY || fileContent.LETTA_API_KEY || this.apiKey; - this.refreshToken = fileContent.refreshToken || this.refreshToken; - this.expiresAt = fileContent.tokenExpiresAt || fileContent.expiresAt || this.expiresAt; - - // 代理 Agent ID: 优先级 lastAgent > LETTA_AGENT_ID > 现有 this.agentId - this.agentId = fileContent.lastAgent || fileContent.LETTA_AGENT_ID || this.agentId; - - // 更新 axios 实例 - this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`; - } catch (error) { - console.error(`[Letta] Failed to load credentials from file: ${error.message}`); - } - } else { - console.warn(`[Letta] No valid token file path found or file does not exist: ${tokenFilePath}`); - } - - this.isInitialized = true; - console.log(`[Letta] Service initialized. AgentId: ${this.agentId || 'default'}`); - } - - /** - * 刷新 Token 逻辑,参考 Kiro 实现 - */ - async refreshAuthToken() { - if (!this.refreshToken) { - throw new Error('No refresh token available for Letta.'); - } - - try { - const { refreshLettaToken } = await import('../../auth/index.js'); - const newData = await refreshLettaToken(this.refreshToken); - - this.apiKey = newData.accessToken; - this.refreshToken = newData.refreshToken; - this.expiresAt = newData.expiresAt; - if (newData.agentId) { - this.agentId = newData.agentId; - } - - // 更新当前实例的 axios header - this.axiosInstance.defaults.headers['Authorization'] = `Bearer ${this.apiKey}`; - - // 获取令牌文件路径 - let tokenFilePath = this.config.LETTA_TOKEN_FILE_PATH || this.config.letta_token_file_path; - if (!tokenFilePath) { - tokenFilePath = path.join(os.homedir(), '.letta', 'settings.json'); - } - - // 持久化到本地 - await this.saveCredentialsToFile(tokenFilePath, { - LETTA_API_KEY: this.apiKey, - refreshToken: this.refreshToken, - expiresAt: this.expiresAt, - LETTA_AGENT_ID: this.agentId - }); - - console.info('[Letta] Token refreshed and saved successfully'); - - return newData; - } catch (error) { - console.error('[Letta] Token refresh failed:', error.message); - throw error; - } - } - - /** - * 判断日期是否接近过期 - * @returns {boolean} - */ - isExpiryDateNear() { - if (this.expiresAt) { - const expiry = new Date(this.expiresAt).getTime(); - // 24小时内过期视为接近过期 - return (expiry - Date.now()) < 24 * 60 * 60 * 1000; - } - return false; - } - - /** - * 保存凭据到文件,参考 Kiro 实现 - * 保持对原始结构的兼容性,但也更新 env 字段以匹配 Letta 默认格式 - */ - async saveCredentialsToFile(filePath, newData) { - try { - let existingData = {}; - if (fs.existsSync(filePath)) { - const fileContent = fs.readFileSync(filePath, 'utf8'); - try { - existingData = JSON.parse(fileContent); - } catch (e) { - existingData = {}; - } - } - - // 构造符合 Letta 结构的更新对象 - const updateObject = { - ...newData, - lastAgent: newData.LETTA_AGENT_ID || existingData.lastAgent, - tokenExpiresAt: newData.expiresAt || existingData.tokenExpiresAt, - env: { - ...(existingData.env || {}), - LETTA_API_KEY: newData.LETTA_API_KEY || (existingData.env && existingData.env.LETTA_API_KEY) - } - }; - - const mergedData = { ...existingData, ...updateObject }; - - // 确保目录存在 - const dir = path.dirname(filePath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(filePath, JSON.stringify(mergedData, null, 2), 'utf8'); - console.info(`[Letta] Updated token file: ${filePath}`); - } catch (error) { - console.error(`[Letta] Failed to save credentials: ${error.message}`); - } - } - - async callApi(endpoint, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; - const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; - - try { - const response = await this.axiosInstance.post(endpoint, body); - return response.data; - } catch (error) { - const status = error.response?.status; - const data = error.response?.data; - const errorCode = error.code; - - if (status === 401 || status === 403) { - console.error(`[Letta API] Received ${status}. API Key might be invalid or expired.`); - throw error; - } - - if ((status === 429 || (status >= 500 && status < 600) || isRetryableNetworkError(error)) && retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); - console.log(`[Letta API] Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`); - await new Promise(resolve => setTimeout(resolve, delay)); - return this.callApi(endpoint, body, isRetry, retryCount + 1); - } - - throw error; - } - } - - async *streamApi(endpoint, body, isRetry = false, retryCount = 0) { - const maxRetries = this.config.REQUEST_MAX_RETRIES || 3; - const baseDelay = this.config.REQUEST_BASE_DELAY || 1000; - - try { - const response = await this.axiosInstance.post(endpoint, { ...body, streaming: true }, { - responseType: 'stream' - }); - - const stream = response.data; - let buffer = ''; - - for await (const chunk of stream) { - buffer += chunk.toString(); - let newlineIndex; - while ((newlineIndex = buffer.indexOf('\n')) !== -1) { - const line = buffer.substring(0, newlineIndex).trim(); - buffer = buffer.substring(newlineIndex + 1); - - if (line.startsWith('data: ')) { - const jsonData = line.substring(6).trim(); - try { - const parsedChunk = JSON.parse(jsonData); - // Transform Letta chunk to OpenAI compatible format - const transformedChunk = this.transformLettaToOpenAI(parsedChunk, body.model); - if (transformedChunk) { - yield transformedChunk; - } - } catch (e) { - console.warn("[LettaApiService] Failed to parse stream chunk:", e.message); - } - } - } - } - } catch (error) { - const status = error.response?.status; - if ((status === 429 || (status >= 500 && status < 600) || isRetryableNetworkError(error)) && retryCount < maxRetries) { - const delay = baseDelay * Math.pow(2, retryCount); - await new Promise(resolve => setTimeout(resolve, delay)); - yield* this.streamApi(endpoint, body, isRetry, retryCount + 1); - return; - } - throw error; - } - } - - /** - * Transforms Letta streaming response chunks to OpenAI chat completion chunks - * Based on reference code in tt/accumulator.ts - */ - transformLettaToOpenAI(lettaChunk, model) { - const timestamp = Math.floor(Date.now() / 1000); - const baseResponse = { - id: lettaChunk.id || `letta-${Date.now()}`, - object: 'chat.completion.chunk', - created: timestamp, - model: model, - choices: [{ - index: 0, - delta: {}, - finish_reason: null - }] - }; - - switch (lettaChunk.message_type) { - case 'reasoning_message': - // Map reasoning to content or a specialized reasoning field if supported - baseResponse.choices[0].delta = { content: lettaChunk.reasoning || '' }; - return baseResponse; - case 'assistant_message': - let content = ''; - if (typeof lettaChunk.content === 'string') { - content = lettaChunk.content; - } else if (Array.isArray(lettaChunk.content)) { - content = lettaChunk.content.map(p => p.text || p.delta || '').join(''); - } - baseResponse.choices[0].delta = { content }; - return baseResponse; - case 'tool_call_message': - const toolCall = lettaChunk.tool_call || (Array.isArray(lettaChunk.tool_calls) ? lettaChunk.tool_calls[0] : null); - if (toolCall) { - baseResponse.choices[0].delta = { - tool_calls: [{ - index: 0, - id: toolCall.tool_call_id, - type: 'function', - function: { - name: toolCall.name, - arguments: toolCall.arguments - } - }] - }; - return baseResponse; - } - break; - case 'usage_statistics': - // OpenAI stream usage is usually in the last chunk - return { - ...baseResponse, - choices: [], - usage: { - prompt_tokens: lettaChunk.prompt_tokens || 0, - completion_tokens: lettaChunk.completion_tokens || 0, - total_tokens: lettaChunk.total_tokens || 0 - } - }; - case 'stop_reason': - baseResponse.choices[0].delta = {}; - baseResponse.choices[0].finish_reason = lettaChunk.stop_reason === 'end_turn' ? 'stop' : lettaChunk.stop_reason; - return baseResponse; - } - return null; - } - - async generateContent(model, requestBody) { - const agentId = this.agentId || 'default'; - const endpoint = `/v1/agents/${agentId}/messages`; - - // Convert OpenAI body to Letta body if needed - const lettaBody = { - messages: requestBody.messages, - stream_tokens: false, - // Add other Letta specific params if needed - }; - - const response = await this.callApi(endpoint, lettaBody); - - // Transform Letta response to OpenAI compatible format - return this.transformFullResponse(response, model); - } - - async *generateContentStream(model, requestBody) { - const agentId = this.agentId || 'default'; - const endpoint = `/v1/agents/${agentId}/messages/stream`; - - const lettaBody = { - messages: requestBody.messages, - stream_tokens: true, - }; - - yield* this.streamApi(endpoint, lettaBody); - } - - transformFullResponse(lettaResponse, model) { - // Implementation for unary response transformation if needed - // For now, minimal implementation - const timestamp = Math.floor(Date.now() / 1000); - return { - id: `letta-${Date.now()}`, - object: 'chat.completion', - created: timestamp, - model: model, - choices: [{ - index: 0, - message: { - role: 'assistant', - content: lettaResponse.messages?.filter(m => m.message_type === 'assistant_message').map(m => m.content).join('\n') || '' - }, - finish_reason: 'stop' - }], - usage: { - prompt_tokens: 0, - completion_tokens: 0, - total_tokens: 0 - } - }; - } - - async listModels() { - return { - object: 'list', - data: [ - { id: 'letta-agent', object: 'model', created: Date.now(), owned_by: 'letta' } - ] - }; - } -} diff --git a/src/providers/provider-models.js b/src/providers/provider-models.js index 272c8e6..2b6ac3d 100644 --- a/src/providers/provider-models.js +++ b/src/providers/provider-models.js @@ -79,9 +79,6 @@ export const PROVIDER_MODELS = { 'gpt-5.1-codex-max', 'gpt-5.2', 'gpt-5.2-codex' - ], - 'openai-letta': [ - 'letta-agent' ] }; diff --git a/src/providers/provider-pool-manager.js b/src/providers/provider-pool-manager.js index f5b22c2..8d4e290 100644 --- a/src/providers/provider-pool-manager.js +++ b/src/providers/provider-pool-manager.js @@ -21,8 +21,7 @@ export class ProviderPoolManager { 'openai-qwen-oauth': 'qwen3-coder-flash', 'openai-iflow': 'qwen3-coder-plus', 'openai-codex-oauth': 'gpt-5-codex-mini', - 'openaiResponses-custom': 'gpt-4o-mini', - 'openai-letta': 'letta-agent' + 'openaiResponses-custom': 'gpt-4o-mini' }; constructor(providerPools, options = {}) { @@ -101,8 +100,6 @@ export class ProviderPoolManager { configPath = config.CODEX_OAUTH_CREDS_FILE_PATH; } else if (providerType.startsWith('claude-orchids')) { configPath = config.ORCHIDS_CREDS_FILE_PATH; - } else if (providerType.startsWith('openai-letta')) { - configPath = config.LETTA_TOKEN_FILE_PATH; } // console.log(`Checking node ${providerStatus.uuid} (${providerType}) expiry date... configPath: ${configPath}`); @@ -111,7 +108,32 @@ export class ProviderPoolManager { if (configPath && fs.existsSync(configPath)) { try { - if (true) { + const fileContent = fs.readFileSync(configPath, 'utf8'); + const data = JSON.parse(fileContent); + + // 获取对应的适配器 + const tempConfig = { + ...config, + MODEL_PROVIDER: providerType + }; + const serviceAdapter = getServiceAdapter(tempConfig); + + // 调用提供商适配器内的 isExpiryDateNear 方法 + let needsRefresh = false; + if (typeof serviceAdapter.isExpiryDateNear === 'function') { + // 适配器内部自行判断,不传参 + needsRefresh = serviceAdapter.isExpiryDateNear(); + this._log('info', `Node ${providerStatus.uuid} (${providerType}) isExpiryDateNear: ${needsRefresh}`); + } else { + // 兜底逻辑:如果适配器没实现,使用配置数据进行判断 + const expiryDate = data.expiry_date || data.expires_at || data.expiry; + if (expiryDate) { + const expiry = new Date(expiryDate).getTime(); + needsRefresh = (expiry - Date.now()) < 24 * 60 * 60 * 1000; + } + } + + if (needsRefresh) { this._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`); this._enqueueRefresh(providerType, providerStatus); } @@ -1267,15 +1289,6 @@ export class ProviderPoolManager { }); return requests; } - - // Letta 使用 OpenAI 协议(内部转换) - if (providerType === MODEL_PROVIDER.LETTA_API) { - requests.push({ - messages: [baseMessage], - model: modelName - }); - return requests; - } // 其他提供商(OpenAI、Claude、Qwen)使用标准 messages 格式 requests.push({ diff --git a/src/services/api-server.js b/src/services/api-server.js index ebdbb85..d357987 100644 --- a/src/services/api-server.js +++ b/src/services/api-server.js @@ -242,7 +242,7 @@ async function startServer() { } // Initialize API services - const services = await initApiService(CONFIG, true); + const services = await initApiService(CONFIG); // Initialize UI management features initializeUIManagement(CONFIG); diff --git a/src/services/service-manager.js b/src/services/service-manager.js index 242c192..ce3b154 100644 --- a/src/services/service-manager.js +++ b/src/services/service-manager.js @@ -165,7 +165,7 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options * @param {Object} config - The server configuration * @returns {Promise} The initialized services */ -export async function initApiService(config, isReady = false) { +export async function initApiService(config) { if (config.providerPools && Object.keys(config.providerPools).length > 0) { providerPoolManager = new ProviderPoolManager(config.providerPools, { @@ -175,18 +175,16 @@ export async function initApiService(config, isReady = false) { }); console.log('[Initialization] ProviderPoolManager initialized with configured pools.'); - if(isReady){ - // --- V2: 触发系统预热 --- - // 预热逻辑是异步的,不会阻塞服务器启动 - providerPoolManager.warmupNodes().catch(err => { - console.error(`[Initialization] Warmup failed: ${err.message}`); - }); + // --- V2: 触发系统预热 --- + // 预热逻辑是异步的,不会阻塞服务器启动 + providerPoolManager.warmupNodes().catch(err => { + console.error(`[Initialization] Warmup failed: ${err.message}`); + }); - // 检查并刷新即将过期的节点(异步调用,不阻塞启动) - providerPoolManager.checkAndRefreshExpiringNodes().catch(err => { - console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`); - }); - } + // 检查并刷新即将过期的节点(异步调用,不阻塞启动) + providerPoolManager.checkAndRefreshExpiringNodes().catch(err => { + console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`); + }); // 健康检查将在服务器完全启动后执行 } else { @@ -395,8 +393,7 @@ export async function getProviderStatus(config, options = {}) { 'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH', 'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH', 'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', - 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH', - 'openai-letta': 'LETTA_TOKEN_FILE_PATH' + 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH' }; let providerPoolsSlim = []; let unhealthyProvideIdentifyList = []; diff --git a/src/ui-modules/oauth-api.js b/src/ui-modules/oauth-api.js index 7ee3d85..458e625 100644 --- a/src/ui-modules/oauth-api.js +++ b/src/ui-modules/oauth-api.js @@ -7,7 +7,6 @@ import { handleIFlowOAuth, handleOrchidsOAuth, handleCodexOAuth, - handleLettaOAuth, batchImportKiroRefreshTokensStream, importAwsCredentials, importOrchidsToken @@ -63,11 +62,6 @@ export async function handleGenerateAuthUrl(req, res, currentConfig, providerTyp const result = await handleCodexOAuth(currentConfig, options); authUrl = result.authUrl; authInfo = result.authInfo; - } else if (providerType === 'openai-letta') { - // Letta OAuth - const result = await handleLettaOAuth(currentConfig, options); - authUrl = result.authUrl; - authInfo = result.authInfo; } else { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js index ef67408..965bd9b 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -826,7 +826,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { - message: 'Unable to identify provider type for config file, please ensure file is in configs/kiro/, configs/gemini/, configs/qwen/, configs/antigravity/, configs/iflow/ or configs/letta/ directory' + message: 'Unable to identify provider type for config file, please ensure file is in configs/kiro/, configs/gemini/, configs/qwen/ or configs/antigravity/ directory' } })); return true; diff --git a/src/ui-modules/usage-api.js b/src/ui-modules/usage-api.js index 7cb4852..de7d25e 100644 --- a/src/ui-modules/usage-api.js +++ b/src/ui-modules/usage-api.js @@ -190,8 +190,7 @@ function getProviderDisplayName(provider, providerType) { 'gemini-cli-oauth': 'GEMINI_OAUTH_CREDS_FILE_PATH', 'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH', - 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH', - 'openai-letta': 'LETTA_TOKEN_FILE_PATH' + 'openai-iflow': 'IFLOW_TOKEN_FILE_PATH' }[providerType]; if (credPathKey && provider[credPathKey]) { diff --git a/src/utils/common.js b/src/utils/common.js index d8ae4ff..886b9a2 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -70,7 +70,6 @@ export const MODEL_PROVIDER = { QWEN_API: 'openai-qwen-oauth', IFLOW_API: 'openai-iflow', CODEX_API: 'openai-codex-oauth', - LETTA_API: 'openai-letta', } /** diff --git a/src/utils/provider-utils.js b/src/utils/provider-utils.js index b3152f3..f3e4630 100644 --- a/src/utils/provider-utils.js +++ b/src/utils/provider-utils.js @@ -87,17 +87,6 @@ export const PROVIDER_MAPPINGS = [ displayName: 'OpenAI Codex OAuth', needsProjectId: false, urlKeys: ['CODEX_BASE_URL'] - }, - { - // Letta 配置 - dirName: 'letta', - patterns: ['configs/letta/', '/letta/'], - providerType: 'openai-letta', - credPathKey: 'LETTA_TOKEN_FILE_PATH', - defaultCheckModel: 'letta-agent', - displayName: 'Letta API', - needsProjectId: false, - urlKeys: ['LETTA_BASE_URL', 'LETTA_AGENT_ID'] } ]; diff --git a/static/app/file-upload.js b/static/app/file-upload.js index 0a82805..a6dd610 100644 --- a/static/app/file-upload.js +++ b/static/app/file-upload.js @@ -58,8 +58,7 @@ class FileUploadHandler { 'gemini-antigravity': 'antigravity', 'claude-kiro-oauth': 'kiro', 'openai-qwen-oauth': 'qwen', - 'openai-iflow': 'iflow', - 'openai-letta': 'letta' + 'openai-iflow': 'iflow' }; return providerMap[provider] || 'gemini'; } diff --git a/static/app/i18n.js b/static/app/i18n.js index 1be2458..7bfc1ae 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -91,7 +91,6 @@ const translations = { 'dashboard.routing.nodeName.iflow': 'iFlow OAuth', 'dashboard.routing.nodeName.orchids': 'Orchids OAuth', 'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth', - 'dashboard.routing.nodeName.letta': 'Letta API', 'dashboard.contact.title': '联系与赞助', 'dashboard.contact.wechat': '扫码进群,注明来意', 'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流', @@ -339,7 +338,6 @@ const translations = { 'upload.providerFilter.orchids': 'Orchids OAuth', 'upload.providerFilter.codex': 'Codex OAuth', 'upload.providerFilter.iflow': 'iFlow OAuth', - 'upload.providerFilter.letta': 'Letta API', 'upload.providerFilter.other': '其他/未识别', 'upload.statusFilter': '关联状态', 'upload.statusFilter.all': '全部状态', @@ -576,7 +574,6 @@ const translations = { 'guide.providers.claude.desc': '使用 Claude 官方 API 或第三方代理访问 Claude 系列模型', 'guide.providers.openai.desc': '使用 OpenAI 官方 API 或第三方代理访问 GPT 系列模型', 'guide.providers.iflow.desc': '通过 iFlow OAuth 认证访问 Qwen、Kimi、DeepSeek、GLM 等模型', - 'guide.providers.letta.desc': '通过 Letta 平台使用自主 Agent 驱动的大模型服务', 'guide.providers.orchids.desc': '通过 Orchids 平台免费使用 Claude Sonnet 4.5 等模型', 'guide.client.title': '客户端配置指南', 'guide.client.desc': '以下是常见 AI 客户端的配置方法,将 API 端点设置为本服务地址即可使用:', @@ -807,7 +804,6 @@ const translations = { 'dashboard.routing.nodeName.iflow': 'iFlow OAuth', 'dashboard.routing.nodeName.orchids': 'Orchids OAuth', 'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth', - 'dashboard.routing.nodeName.letta': 'Letta API', 'dashboard.contact.title': 'Contact & Support', 'dashboard.contact.wechat': 'Scan to Join Group', 'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication', @@ -1055,7 +1051,6 @@ const translations = { 'upload.providerFilter.orchids': 'Orchids OAuth', 'upload.providerFilter.codex': 'Codex OAuth', 'upload.providerFilter.iflow': 'iFlow OAuth', - 'upload.providerFilter.letta': 'Letta API', 'upload.providerFilter.other': 'Other/Unknown', 'upload.statusFilter': 'Association Status', 'upload.statusFilter.all': 'All Status', @@ -1292,7 +1287,6 @@ const translations = { 'guide.providers.claude.desc': 'Access Claude models via official API or third-party proxy', 'guide.providers.openai.desc': 'Access GPT models via official API or third-party proxy', 'guide.providers.iflow.desc': 'Access Qwen, Kimi, DeepSeek, GLM via iFlow OAuth', - 'guide.providers.letta.desc': 'Access autonomous Agent driven models via Letta platform', 'guide.providers.orchids.desc': 'Free access to Claude Sonnet 4.5 via Orchids platform', 'guide.client.title': 'Client Configuration Guide', 'guide.client.desc': 'Here are configuration methods for common AI clients. Set the API endpoint to this service address:', diff --git a/static/app/modal.js b/static/app/modal.js index 33bbc60..bc839cc 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -520,7 +520,7 @@ function renderProviderConfig(provider) { const field1Label = getFieldLabel(field1Key); const field1Value = provider[field1Key]; const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password'); - const field1IsOAuthFilePath = field1Key.includes('OAUTH_CREDS_FILE_PATH') || field1Key.includes('LETTA_TOKEN_FILE_PATH'); + const field1IsOAuthFilePath = field1Key.includes('OAUTH_CREDS_FILE_PATH'); const field1DisplayValue = field1IsPassword && field1Value ? '••••••••' : (field1Value || ''); const field1Def = fieldConfigs.find(f => f.id === field1Key) || fieldConfigs.find(f => f.id.toUpperCase() === field1Key.toUpperCase()) || {}; @@ -582,7 +582,7 @@ function renderProviderConfig(provider) { const field2Label = getFieldLabel(field2Key); const field2Value = provider[field2Key]; const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password'); - const field2IsOAuthFilePath = field2Key.includes('OAUTH_CREDS_FILE_PATH') || field2Key.includes('LETTA_TOKEN_FILE_PATH'); + const field2IsOAuthFilePath = field2Key.includes('OAUTH_CREDS_FILE_PATH'); const field2DisplayValue = field2IsPassword && field2Value ? '••••••••' : (field2Value || ''); const field2Def = fieldConfigs.find(f => f.id === field2Key) || fieldConfigs.find(f => f.id.toUpperCase() === field2Key.toUpperCase()) || {}; @@ -1138,7 +1138,7 @@ function addDynamicConfigFields(form, providerType) { // 检查是否为密码类型字段 const isPassword1 = field1.type === 'password'; // 检查是否为OAuth凭据文件路径字段(兼容两种命名方式) - const isOAuthFilePath1 = field1.id.includes('OAUTH_CREDS_FILE_PATH') || field1.id.includes('OauthCredsFilePath') || field1.id.includes('LETTA_TOKEN_FILE_PATH'); + const isOAuthFilePath1 = field1.id.includes('OAUTH_CREDS_FILE_PATH') || field1.id.includes('OauthCredsFilePath'); if (isPassword1) { fields += ` @@ -1181,7 +1181,7 @@ function addDynamicConfigFields(form, providerType) { // 检查是否为密码类型字段 const isPassword2 = field2.type === 'password'; // 检查是否为OAuth凭据文件路径字段(兼容两种命名方式) - const isOAuthFilePath2 = field2.id.includes('OAUTH_CREDS_FILE_PATH') || field2.id.includes('OauthCredsFilePath') || field2.id.includes('LETTA_TOKEN_FILE_PATH'); + const isOAuthFilePath2 = field2.id.includes('OAUTH_CREDS_FILE_PATH') || field2.id.includes('OauthCredsFilePath'); if (isPassword2) { fields += ` diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index e4a26f0..98641c1 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -216,8 +216,7 @@ function renderProviders(providers) { 'openai-qwen-oauth', 'openaiResponses-custom', 'openai-iflow', - 'openai-codex-oauth', - 'openai-letta' + 'openai-codex-oauth' ]; // 获取所有提供商类型并按指定顺序排序 @@ -1639,8 +1638,7 @@ function getAuthFilePath(provider) { 'gemini-antigravity': '~/.antigravity/oauth_creds.json', 'openai-qwen-oauth': '~/.qwen/oauth_creds.json', 'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json', - 'openai-iflow': '~/.iflow/oauth_creds.json', - 'openai-letta': 'configs/letta/token.json' + 'openai-iflow': '~/.iflow/oauth_creds.json' }; return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径'); } diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js index a70e0d6..7399928 100644 --- a/static/app/upload-config-manager.js +++ b/static/app/upload-config-manager.js @@ -861,12 +861,6 @@ function detectProviderFromPath(filePath) { providerType: 'openai-iflow-oauth', displayName: 'OpenAI iFlow OAuth', shortName: 'iflow-oauth' - }, - { - patterns: ['configs/letta/', '/letta/'], - providerType: 'openai-letta', - displayName: 'Letta API', - shortName: 'letta' } ]; diff --git a/static/app/utils.js b/static/app/utils.js index 1fbea72..6236e2f 100644 --- a/static/app/utils.js +++ b/static/app/utils.js @@ -83,7 +83,6 @@ function getFieldLabel(key) { 'QWEN_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', 'IFLOW_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', - 'LETTA_TOKEN_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', 'GEMINI_BASE_URL': 'Gemini Base URL', 'KIRO_BASE_URL': 'Base URL', 'KIRO_REFRESH_URL': 'Refresh URL', @@ -92,9 +91,7 @@ function getFieldLabel(key) { 'QWEN_OAUTH_BASE_URL': 'OAuth Base URL', 'ANTIGRAVITY_BASE_URL_DAILY': 'Daily Base URL', 'ANTIGRAVITY_BASE_URL_AUTOPUSH': 'Autopush Base URL', - 'IFLOW_BASE_URL': 'iFlow Base URL', - 'LETTA_BASE_URL': 'Letta Base URL', - 'LETTA_AGENT_ID': 'Agent ID' + 'IFLOW_BASE_URL': 'iFlow Base URL' }; return labelMap[key] || key; @@ -275,26 +272,6 @@ function getProviderTypeFields(providerType) { type: 'text', placeholder: 'https://api.openai.com/v1/codex' } - ], - 'openai-letta': [ - { - id: 'LETTA_TOKEN_FILE_PATH', - label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径', - type: 'text', - placeholder: isEn ? 'e.g.: configs/letta/token.json' : '例如: configs/letta/token.json' - }, - { - id: 'LETTA_BASE_URL', - label: `Letta Base URL ${t('config.optional')}`, - type: 'text', - placeholder: 'https://api.letta.com' - }, - { - id: 'LETTA_AGENT_ID', - label: `Agent ID ${t('config.optional')}`, - type: 'text', - placeholder: '例如: agent-...' - } ] }; diff --git a/static/components/section-config.html b/static/components/section-config.html index 263fa1d..4553f30 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -66,10 +66,6 @@ OpenAI Codex OAuth - 点击选择启动时初始化的模型提供商 (必须至少选择一个) @@ -129,10 +125,6 @@ OpenAI Codex OAuth - 点击选择需要通过代理访问的提供商,未选中的提供商将直接连接 diff --git a/static/components/section-upload-config.html b/static/components/section-upload-config.html index b5db808..e2bbd88 100644 --- a/static/components/section-upload-config.html +++ b/static/components/section-upload-config.html @@ -26,7 +26,6 @@ -