Revert "add letta"
This reverts commit 3faa599b95523160232df3d5483840b56d5a4702.
This commit is contained in:
parent
b7f2142411
commit
f8faee93fc
22 changed files with 51 additions and 809 deletions
|
|
@ -36,9 +36,3 @@ export {
|
||||||
importOrchidsToken,
|
importOrchidsToken,
|
||||||
handleOrchidsOAuth
|
handleOrchidsOAuth
|
||||||
} from './orchids-oauth.js';
|
} from './orchids-oauth.js';
|
||||||
|
|
||||||
// Letta OAuth
|
|
||||||
export {
|
|
||||||
handleLettaOAuth,
|
|
||||||
refreshLettaToken
|
|
||||||
} from './letta-oauth.js';
|
|
||||||
|
|
@ -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('<h1>Authorization Successful!</h1><p>You can close this window.</p>');
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,5 @@ export {
|
||||||
refreshIFlowTokens,
|
refreshIFlowTokens,
|
||||||
// Orchids OAuth
|
// Orchids OAuth
|
||||||
importOrchidsToken,
|
importOrchidsToken,
|
||||||
handleOrchidsOAuth,
|
handleOrchidsOAuth
|
||||||
handleLettaOAuth,
|
|
||||||
refreshLettaToken
|
|
||||||
} from './index.js';
|
} from './index.js';
|
||||||
|
|
@ -8,7 +8,6 @@ import { OrchidsApiService } from './claude/claude-orchids.js'; // 导入Orchids
|
||||||
import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService
|
import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService
|
||||||
import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService
|
import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService
|
||||||
import { CodexApiService } from './openai/codex-core.js'; // 导入CodexApiService
|
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
|
import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER
|
||||||
|
|
||||||
// 定义AI服务适配器接口
|
// 定义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 = {};
|
export const serviceInstances = {};
|
||||||
|
|
||||||
|
|
@ -734,9 +669,6 @@ export function getServiceAdapter(config) {
|
||||||
case MODEL_PROVIDER.CODEX_API:
|
case MODEL_PROVIDER.CODEX_API:
|
||||||
serviceInstances[providerKey] = new CodexApiServiceAdapter(config);
|
serviceInstances[providerKey] = new CodexApiServiceAdapter(config);
|
||||||
break;
|
break;
|
||||||
case MODEL_PROVIDER.LETTA_API:
|
|
||||||
serviceInstances[providerKey] = new LettaApiServiceAdapter(config);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported model provider: ${provider}`);
|
throw new Error(`Unsupported model provider: ${provider}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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' }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -79,9 +79,6 @@ export const PROVIDER_MODELS = {
|
||||||
'gpt-5.1-codex-max',
|
'gpt-5.1-codex-max',
|
||||||
'gpt-5.2',
|
'gpt-5.2',
|
||||||
'gpt-5.2-codex'
|
'gpt-5.2-codex'
|
||||||
],
|
|
||||||
'openai-letta': [
|
|
||||||
'letta-agent'
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,7 @@ export class ProviderPoolManager {
|
||||||
'openai-qwen-oauth': 'qwen3-coder-flash',
|
'openai-qwen-oauth': 'qwen3-coder-flash',
|
||||||
'openai-iflow': 'qwen3-coder-plus',
|
'openai-iflow': 'qwen3-coder-plus',
|
||||||
'openai-codex-oauth': 'gpt-5-codex-mini',
|
'openai-codex-oauth': 'gpt-5-codex-mini',
|
||||||
'openaiResponses-custom': 'gpt-4o-mini',
|
'openaiResponses-custom': 'gpt-4o-mini'
|
||||||
'openai-letta': 'letta-agent'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(providerPools, options = {}) {
|
constructor(providerPools, options = {}) {
|
||||||
|
|
@ -101,8 +100,6 @@ export class ProviderPoolManager {
|
||||||
configPath = config.CODEX_OAUTH_CREDS_FILE_PATH;
|
configPath = config.CODEX_OAUTH_CREDS_FILE_PATH;
|
||||||
} else if (providerType.startsWith('claude-orchids')) {
|
} else if (providerType.startsWith('claude-orchids')) {
|
||||||
configPath = config.ORCHIDS_CREDS_FILE_PATH;
|
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}`);
|
// console.log(`Checking node ${providerStatus.uuid} (${providerType}) expiry date... configPath: ${configPath}`);
|
||||||
|
|
@ -111,7 +108,32 @@ export class ProviderPoolManager {
|
||||||
|
|
||||||
if (configPath && fs.existsSync(configPath)) {
|
if (configPath && fs.existsSync(configPath)) {
|
||||||
try {
|
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._log('warn', `Node ${providerStatus.uuid} (${providerType}) is near expiration. Enqueuing refresh...`);
|
||||||
this._enqueueRefresh(providerType, providerStatus);
|
this._enqueueRefresh(providerType, providerStatus);
|
||||||
}
|
}
|
||||||
|
|
@ -1268,15 +1290,6 @@ export class ProviderPoolManager {
|
||||||
return requests;
|
return requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Letta 使用 OpenAI 协议(内部转换)
|
|
||||||
if (providerType === MODEL_PROVIDER.LETTA_API) {
|
|
||||||
requests.push({
|
|
||||||
messages: [baseMessage],
|
|
||||||
model: modelName
|
|
||||||
});
|
|
||||||
return requests;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他提供商(OpenAI、Claude、Qwen)使用标准 messages 格式
|
// 其他提供商(OpenAI、Claude、Qwen)使用标准 messages 格式
|
||||||
requests.push({
|
requests.push({
|
||||||
messages: [baseMessage],
|
messages: [baseMessage],
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ async function startServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize API services
|
// Initialize API services
|
||||||
const services = await initApiService(CONFIG, true);
|
const services = await initApiService(CONFIG);
|
||||||
|
|
||||||
// Initialize UI management features
|
// Initialize UI management features
|
||||||
initializeUIManagement(CONFIG);
|
initializeUIManagement(CONFIG);
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ async function scanProviderDirectory(dirPath, linkedPaths, newProviders, options
|
||||||
* @param {Object} config - The server configuration
|
* @param {Object} config - The server configuration
|
||||||
* @returns {Promise<Object>} The initialized services
|
* @returns {Promise<Object>} The initialized services
|
||||||
*/
|
*/
|
||||||
export async function initApiService(config, isReady = false) {
|
export async function initApiService(config) {
|
||||||
|
|
||||||
if (config.providerPools && Object.keys(config.providerPools).length > 0) {
|
if (config.providerPools && Object.keys(config.providerPools).length > 0) {
|
||||||
providerPoolManager = new ProviderPoolManager(config.providerPools, {
|
providerPoolManager = new ProviderPoolManager(config.providerPools, {
|
||||||
|
|
@ -175,18 +175,16 @@ export async function initApiService(config, isReady = false) {
|
||||||
});
|
});
|
||||||
console.log('[Initialization] ProviderPoolManager initialized with configured pools.');
|
console.log('[Initialization] ProviderPoolManager initialized with configured pools.');
|
||||||
|
|
||||||
if(isReady){
|
// --- V2: 触发系统预热 ---
|
||||||
// --- V2: 触发系统预热 ---
|
// 预热逻辑是异步的,不会阻塞服务器启动
|
||||||
// 预热逻辑是异步的,不会阻塞服务器启动
|
providerPoolManager.warmupNodes().catch(err => {
|
||||||
providerPoolManager.warmupNodes().catch(err => {
|
console.error(`[Initialization] Warmup failed: ${err.message}`);
|
||||||
console.error(`[Initialization] Warmup failed: ${err.message}`);
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// 检查并刷新即将过期的节点(异步调用,不阻塞启动)
|
// 检查并刷新即将过期的节点(异步调用,不阻塞启动)
|
||||||
providerPoolManager.checkAndRefreshExpiringNodes().catch(err => {
|
providerPoolManager.checkAndRefreshExpiringNodes().catch(err => {
|
||||||
console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`);
|
console.error(`[Initialization] Check and refresh expiring nodes failed: ${err.message}`);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// 健康检查将在服务器完全启动后执行
|
// 健康检查将在服务器完全启动后执行
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -395,8 +393,7 @@ export async function getProviderStatus(config, options = {}) {
|
||||||
'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH',
|
'claude-kiro-oauth': 'KIRO_OAUTH_CREDS_FILE_PATH',
|
||||||
'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH',
|
'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH',
|
||||||
'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
||||||
'openai-iflow': 'IFLOW_TOKEN_FILE_PATH',
|
'openai-iflow': 'IFLOW_TOKEN_FILE_PATH'
|
||||||
'openai-letta': 'LETTA_TOKEN_FILE_PATH'
|
|
||||||
};
|
};
|
||||||
let providerPoolsSlim = [];
|
let providerPoolsSlim = [];
|
||||||
let unhealthyProvideIdentifyList = [];
|
let unhealthyProvideIdentifyList = [];
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import {
|
||||||
handleIFlowOAuth,
|
handleIFlowOAuth,
|
||||||
handleOrchidsOAuth,
|
handleOrchidsOAuth,
|
||||||
handleCodexOAuth,
|
handleCodexOAuth,
|
||||||
handleLettaOAuth,
|
|
||||||
batchImportKiroRefreshTokensStream,
|
batchImportKiroRefreshTokensStream,
|
||||||
importAwsCredentials,
|
importAwsCredentials,
|
||||||
importOrchidsToken
|
importOrchidsToken
|
||||||
|
|
@ -63,11 +62,6 @@ export async function handleGenerateAuthUrl(req, res, currentConfig, providerTyp
|
||||||
const result = await handleCodexOAuth(currentConfig, options);
|
const result = await handleCodexOAuth(currentConfig, options);
|
||||||
authUrl = result.authUrl;
|
authUrl = result.authUrl;
|
||||||
authInfo = result.authInfo;
|
authInfo = result.authInfo;
|
||||||
} else if (providerType === 'openai-letta') {
|
|
||||||
// Letta OAuth
|
|
||||||
const result = await handleLettaOAuth(currentConfig, options);
|
|
||||||
authUrl = result.authUrl;
|
|
||||||
authInfo = result.authInfo;
|
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -826,7 +826,7 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
|
||||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
res.end(JSON.stringify({
|
res.end(JSON.stringify({
|
||||||
error: {
|
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;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,7 @@ function getProviderDisplayName(provider, providerType) {
|
||||||
'gemini-cli-oauth': 'GEMINI_OAUTH_CREDS_FILE_PATH',
|
'gemini-cli-oauth': 'GEMINI_OAUTH_CREDS_FILE_PATH',
|
||||||
'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
'gemini-antigravity': 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
||||||
'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH',
|
'openai-qwen-oauth': 'QWEN_OAUTH_CREDS_FILE_PATH',
|
||||||
'openai-iflow': 'IFLOW_TOKEN_FILE_PATH',
|
'openai-iflow': 'IFLOW_TOKEN_FILE_PATH'
|
||||||
'openai-letta': 'LETTA_TOKEN_FILE_PATH'
|
|
||||||
}[providerType];
|
}[providerType];
|
||||||
|
|
||||||
if (credPathKey && provider[credPathKey]) {
|
if (credPathKey && provider[credPathKey]) {
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ export const MODEL_PROVIDER = {
|
||||||
QWEN_API: 'openai-qwen-oauth',
|
QWEN_API: 'openai-qwen-oauth',
|
||||||
IFLOW_API: 'openai-iflow',
|
IFLOW_API: 'openai-iflow',
|
||||||
CODEX_API: 'openai-codex-oauth',
|
CODEX_API: 'openai-codex-oauth',
|
||||||
LETTA_API: 'openai-letta',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -87,17 +87,6 @@ export const PROVIDER_MAPPINGS = [
|
||||||
displayName: 'OpenAI Codex OAuth',
|
displayName: 'OpenAI Codex OAuth',
|
||||||
needsProjectId: false,
|
needsProjectId: false,
|
||||||
urlKeys: ['CODEX_BASE_URL']
|
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']
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,8 +58,7 @@ class FileUploadHandler {
|
||||||
'gemini-antigravity': 'antigravity',
|
'gemini-antigravity': 'antigravity',
|
||||||
'claude-kiro-oauth': 'kiro',
|
'claude-kiro-oauth': 'kiro',
|
||||||
'openai-qwen-oauth': 'qwen',
|
'openai-qwen-oauth': 'qwen',
|
||||||
'openai-iflow': 'iflow',
|
'openai-iflow': 'iflow'
|
||||||
'openai-letta': 'letta'
|
|
||||||
};
|
};
|
||||||
return providerMap[provider] || 'gemini';
|
return providerMap[provider] || 'gemini';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ const translations = {
|
||||||
'dashboard.routing.nodeName.iflow': 'iFlow OAuth',
|
'dashboard.routing.nodeName.iflow': 'iFlow OAuth',
|
||||||
'dashboard.routing.nodeName.orchids': 'Orchids OAuth',
|
'dashboard.routing.nodeName.orchids': 'Orchids OAuth',
|
||||||
'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth',
|
'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth',
|
||||||
'dashboard.routing.nodeName.letta': 'Letta API',
|
|
||||||
'dashboard.contact.title': '联系与赞助',
|
'dashboard.contact.title': '联系与赞助',
|
||||||
'dashboard.contact.wechat': '扫码进群,注明来意',
|
'dashboard.contact.wechat': '扫码进群,注明来意',
|
||||||
'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流',
|
'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流',
|
||||||
|
|
@ -339,7 +338,6 @@ const translations = {
|
||||||
'upload.providerFilter.orchids': 'Orchids OAuth',
|
'upload.providerFilter.orchids': 'Orchids OAuth',
|
||||||
'upload.providerFilter.codex': 'Codex OAuth',
|
'upload.providerFilter.codex': 'Codex OAuth',
|
||||||
'upload.providerFilter.iflow': 'iFlow OAuth',
|
'upload.providerFilter.iflow': 'iFlow OAuth',
|
||||||
'upload.providerFilter.letta': 'Letta API',
|
|
||||||
'upload.providerFilter.other': '其他/未识别',
|
'upload.providerFilter.other': '其他/未识别',
|
||||||
'upload.statusFilter': '关联状态',
|
'upload.statusFilter': '关联状态',
|
||||||
'upload.statusFilter.all': '全部状态',
|
'upload.statusFilter.all': '全部状态',
|
||||||
|
|
@ -576,7 +574,6 @@ const translations = {
|
||||||
'guide.providers.claude.desc': '使用 Claude 官方 API 或第三方代理访问 Claude 系列模型',
|
'guide.providers.claude.desc': '使用 Claude 官方 API 或第三方代理访问 Claude 系列模型',
|
||||||
'guide.providers.openai.desc': '使用 OpenAI 官方 API 或第三方代理访问 GPT 系列模型',
|
'guide.providers.openai.desc': '使用 OpenAI 官方 API 或第三方代理访问 GPT 系列模型',
|
||||||
'guide.providers.iflow.desc': '通过 iFlow OAuth 认证访问 Qwen、Kimi、DeepSeek、GLM 等模型',
|
'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.providers.orchids.desc': '通过 Orchids 平台免费使用 Claude Sonnet 4.5 等模型',
|
||||||
'guide.client.title': '客户端配置指南',
|
'guide.client.title': '客户端配置指南',
|
||||||
'guide.client.desc': '以下是常见 AI 客户端的配置方法,将 API 端点设置为本服务地址即可使用:',
|
'guide.client.desc': '以下是常见 AI 客户端的配置方法,将 API 端点设置为本服务地址即可使用:',
|
||||||
|
|
@ -807,7 +804,6 @@ const translations = {
|
||||||
'dashboard.routing.nodeName.iflow': 'iFlow OAuth',
|
'dashboard.routing.nodeName.iflow': 'iFlow OAuth',
|
||||||
'dashboard.routing.nodeName.orchids': 'Orchids OAuth',
|
'dashboard.routing.nodeName.orchids': 'Orchids OAuth',
|
||||||
'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth',
|
'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth',
|
||||||
'dashboard.routing.nodeName.letta': 'Letta API',
|
|
||||||
'dashboard.contact.title': 'Contact & Support',
|
'dashboard.contact.title': 'Contact & Support',
|
||||||
'dashboard.contact.wechat': 'Scan to Join Group',
|
'dashboard.contact.wechat': 'Scan to Join Group',
|
||||||
'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication',
|
'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication',
|
||||||
|
|
@ -1055,7 +1051,6 @@ const translations = {
|
||||||
'upload.providerFilter.orchids': 'Orchids OAuth',
|
'upload.providerFilter.orchids': 'Orchids OAuth',
|
||||||
'upload.providerFilter.codex': 'Codex OAuth',
|
'upload.providerFilter.codex': 'Codex OAuth',
|
||||||
'upload.providerFilter.iflow': 'iFlow OAuth',
|
'upload.providerFilter.iflow': 'iFlow OAuth',
|
||||||
'upload.providerFilter.letta': 'Letta API',
|
|
||||||
'upload.providerFilter.other': 'Other/Unknown',
|
'upload.providerFilter.other': 'Other/Unknown',
|
||||||
'upload.statusFilter': 'Association Status',
|
'upload.statusFilter': 'Association Status',
|
||||||
'upload.statusFilter.all': 'All 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.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.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.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.providers.orchids.desc': 'Free access to Claude Sonnet 4.5 via Orchids platform',
|
||||||
'guide.client.title': 'Client Configuration Guide',
|
'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:',
|
'guide.client.desc': 'Here are configuration methods for common AI clients. Set the API endpoint to this service address:',
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,7 @@ function renderProviderConfig(provider) {
|
||||||
const field1Label = getFieldLabel(field1Key);
|
const field1Label = getFieldLabel(field1Key);
|
||||||
const field1Value = provider[field1Key];
|
const field1Value = provider[field1Key];
|
||||||
const field1IsPassword = field1Key.toLowerCase().includes('key') || field1Key.toLowerCase().includes('password');
|
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 field1DisplayValue = field1IsPassword && field1Value ? '••••••••' : (field1Value || '');
|
||||||
const field1Def = fieldConfigs.find(f => f.id === field1Key) || fieldConfigs.find(f => f.id.toUpperCase() === field1Key.toUpperCase()) || {};
|
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 field2Label = getFieldLabel(field2Key);
|
||||||
const field2Value = provider[field2Key];
|
const field2Value = provider[field2Key];
|
||||||
const field2IsPassword = field2Key.toLowerCase().includes('key') || field2Key.toLowerCase().includes('password');
|
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 field2DisplayValue = field2IsPassword && field2Value ? '••••••••' : (field2Value || '');
|
||||||
const field2Def = fieldConfigs.find(f => f.id === field2Key) || fieldConfigs.find(f => f.id.toUpperCase() === field2Key.toUpperCase()) || {};
|
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';
|
const isPassword1 = field1.type === 'password';
|
||||||
// 检查是否为OAuth凭据文件路径字段(兼容两种命名方式)
|
// 检查是否为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) {
|
if (isPassword1) {
|
||||||
fields += `
|
fields += `
|
||||||
|
|
@ -1181,7 +1181,7 @@ function addDynamicConfigFields(form, providerType) {
|
||||||
// 检查是否为密码类型字段
|
// 检查是否为密码类型字段
|
||||||
const isPassword2 = field2.type === 'password';
|
const isPassword2 = field2.type === 'password';
|
||||||
// 检查是否为OAuth凭据文件路径字段(兼容两种命名方式)
|
// 检查是否为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) {
|
if (isPassword2) {
|
||||||
fields += `
|
fields += `
|
||||||
|
|
|
||||||
|
|
@ -216,8 +216,7 @@ function renderProviders(providers) {
|
||||||
'openai-qwen-oauth',
|
'openai-qwen-oauth',
|
||||||
'openaiResponses-custom',
|
'openaiResponses-custom',
|
||||||
'openai-iflow',
|
'openai-iflow',
|
||||||
'openai-codex-oauth',
|
'openai-codex-oauth'
|
||||||
'openai-letta'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 获取所有提供商类型并按指定顺序排序
|
// 获取所有提供商类型并按指定顺序排序
|
||||||
|
|
@ -1639,8 +1638,7 @@ function getAuthFilePath(provider) {
|
||||||
'gemini-antigravity': '~/.antigravity/oauth_creds.json',
|
'gemini-antigravity': '~/.antigravity/oauth_creds.json',
|
||||||
'openai-qwen-oauth': '~/.qwen/oauth_creds.json',
|
'openai-qwen-oauth': '~/.qwen/oauth_creds.json',
|
||||||
'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json',
|
'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json',
|
||||||
'openai-iflow': '~/.iflow/oauth_creds.json',
|
'openai-iflow': '~/.iflow/oauth_creds.json'
|
||||||
'openai-letta': 'configs/letta/token.json'
|
|
||||||
};
|
};
|
||||||
return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径');
|
return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -861,12 +861,6 @@ function detectProviderFromPath(filePath) {
|
||||||
providerType: 'openai-iflow-oauth',
|
providerType: 'openai-iflow-oauth',
|
||||||
displayName: 'OpenAI iFlow OAuth',
|
displayName: 'OpenAI iFlow OAuth',
|
||||||
shortName: 'iflow-oauth'
|
shortName: 'iflow-oauth'
|
||||||
},
|
|
||||||
{
|
|
||||||
patterns: ['configs/letta/', '/letta/'],
|
|
||||||
providerType: 'openai-letta',
|
|
||||||
displayName: 'Letta API',
|
|
||||||
shortName: 'letta'
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,6 @@ function getFieldLabel(key) {
|
||||||
'QWEN_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
'QWEN_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||||
'ANTIGRAVITY_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凭据文件路径',
|
'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',
|
'GEMINI_BASE_URL': 'Gemini Base URL',
|
||||||
'KIRO_BASE_URL': 'Base URL',
|
'KIRO_BASE_URL': 'Base URL',
|
||||||
'KIRO_REFRESH_URL': 'Refresh URL',
|
'KIRO_REFRESH_URL': 'Refresh URL',
|
||||||
|
|
@ -92,9 +91,7 @@ function getFieldLabel(key) {
|
||||||
'QWEN_OAUTH_BASE_URL': 'OAuth Base URL',
|
'QWEN_OAUTH_BASE_URL': 'OAuth Base URL',
|
||||||
'ANTIGRAVITY_BASE_URL_DAILY': 'Daily Base URL',
|
'ANTIGRAVITY_BASE_URL_DAILY': 'Daily Base URL',
|
||||||
'ANTIGRAVITY_BASE_URL_AUTOPUSH': 'Autopush Base URL',
|
'ANTIGRAVITY_BASE_URL_AUTOPUSH': 'Autopush Base URL',
|
||||||
'IFLOW_BASE_URL': 'iFlow Base URL',
|
'IFLOW_BASE_URL': 'iFlow Base URL'
|
||||||
'LETTA_BASE_URL': 'Letta Base URL',
|
|
||||||
'LETTA_AGENT_ID': 'Agent ID'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return labelMap[key] || key;
|
return labelMap[key] || key;
|
||||||
|
|
@ -275,26 +272,6 @@ function getProviderTypeFields(providerType) {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
placeholder: 'https://api.openai.com/v1/codex'
|
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 <span class="optional-tag">${t('config.optional')}</span>`,
|
|
||||||
type: 'text',
|
|
||||||
placeholder: 'https://api.letta.com'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'LETTA_AGENT_ID',
|
|
||||||
label: `Agent ID <span class="optional-tag">${t('config.optional')}</span>`,
|
|
||||||
type: 'text',
|
|
||||||
placeholder: '例如: agent-...'
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,10 +66,6 @@
|
||||||
<i class="fas fa-code"></i>
|
<i class="fas fa-code"></i>
|
||||||
<span>OpenAI Codex OAuth</span>
|
<span>OpenAI Codex OAuth</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="provider-tag" data-value="openai-letta">
|
|
||||||
<i class="fas fa-microchip"></i>
|
|
||||||
<span>Letta API</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选择一个)</small>
|
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选择一个)</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -129,10 +125,6 @@
|
||||||
<i class="fas fa-code"></i>
|
<i class="fas fa-code"></i>
|
||||||
<span>OpenAI Codex OAuth</span>
|
<span>OpenAI Codex OAuth</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="provider-tag" data-value="openai-letta">
|
|
||||||
<i class="fas fa-microchip"></i>
|
|
||||||
<span>Letta API</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
|
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@
|
||||||
<option value="claude-orchids-oauth" data-i18n="upload.providerFilter.orchids">Orchids OAuth</option>
|
<option value="claude-orchids-oauth" data-i18n="upload.providerFilter.orchids">Orchids OAuth</option>
|
||||||
<option value="openai-codex-oauth" data-i18n="upload.providerFilter.codex">Codex OAuth</option>
|
<option value="openai-codex-oauth" data-i18n="upload.providerFilter.codex">Codex OAuth</option>
|
||||||
<option value="openai-iflow-oauth" data-i18n="upload.providerFilter.iflow">iFlow OAuth</option>
|
<option value="openai-iflow-oauth" data-i18n="upload.providerFilter.iflow">iFlow OAuth</option>
|
||||||
<option value="openai-letta" data-i18n="upload.providerFilter.letta">Letta API</option>
|
|
||||||
<option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option>
|
<option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue