feat(forward): 新增通用转发API提供商支持
- 添加 forward-api 提供商类型,支持将请求透明转发到任意API端点 - 实现 ForwardStrategy、ForwardApiService 和适配器,支持流式和非流式响应 - 在转换逻辑中跳过 forward 协议的数据转换以保持透明性 - 更新UI支持:添加提供商配置字段、多语言标签和显示名称 - 扩展提供商状态检查和健康监测配置 - 为转发请求保留原始路径作为端点参数
This commit is contained in:
parent
d3e83949cf
commit
da8ad6cddb
15 changed files with 446 additions and 69 deletions
|
|
@ -44,6 +44,12 @@ export function convertData(data, type, fromProvider, toProvider, model) {
|
|||
const fromProtocol = getProtocolPrefix(fromProvider);
|
||||
const toProtocol = getProtocolPrefix(toProvider);
|
||||
|
||||
// 如果目标协议为 forward,直接返回原始数据,无需转换
|
||||
if (toProtocol === MODEL_PROTOCOL_PREFIX.FORWARD || fromProtocol === MODEL_PROTOCOL_PREFIX.FORWARD) {
|
||||
console.log(`[Convert] Target protocol is forward, skipping conversion`);
|
||||
return data;
|
||||
}
|
||||
|
||||
// 从工厂获取转换器
|
||||
const converter = ConverterFactory.getConverter(fromProtocol);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiServic
|
|||
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 { ForwardApiService } from './forward/forward-core.js'; // 导入ForwardApiService
|
||||
import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER
|
||||
|
||||
// 定义AI服务适配器接口
|
||||
|
|
@ -568,6 +569,38 @@ export class CodexApiServiceAdapter extends ApiServiceAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
// Forward API 服务适配器
|
||||
export class ForwardApiServiceAdapter extends ApiServiceAdapter {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.forwardApiService = new ForwardApiService(config);
|
||||
}
|
||||
|
||||
async generateContent(model, requestBody) {
|
||||
return this.forwardApiService.generateContent(model, requestBody);
|
||||
}
|
||||
|
||||
async *generateContentStream(model, requestBody) {
|
||||
yield* this.forwardApiService.generateContentStream(model, requestBody);
|
||||
}
|
||||
|
||||
async listModels() {
|
||||
return this.forwardApiService.listModels();
|
||||
}
|
||||
|
||||
async refreshToken() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async forceRefreshToken() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
isExpiryDateNear() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 用于存储服务适配器单例的映射
|
||||
export const serviceInstances = {};
|
||||
|
||||
|
|
@ -606,6 +639,9 @@ export function getServiceAdapter(config) {
|
|||
case MODEL_PROVIDER.CODEX_API:
|
||||
serviceInstances[providerKey] = new CodexApiServiceAdapter(config);
|
||||
break;
|
||||
case MODEL_PROVIDER.FORWARD_API:
|
||||
serviceInstances[providerKey] = new ForwardApiServiceAdapter(config);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported model provider: ${provider}`);
|
||||
}
|
||||
|
|
|
|||
165
src/providers/forward/forward-core.js
Normal file
165
src/providers/forward/forward-core.js
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import axios from 'axios';
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import { configureAxiosProxy } from '../../utils/proxy-utils.js';
|
||||
import { isRetryableNetworkError } from '../../utils/common.js';
|
||||
|
||||
/**
|
||||
* ForwardApiService - A provider that forwards requests to a specified API endpoint.
|
||||
* Transparently passes all parameters and includes an API key in the headers.
|
||||
*/
|
||||
export class ForwardApiService {
|
||||
constructor(config) {
|
||||
if (!config.FORWARD_API_KEY) {
|
||||
throw new Error("API Key is required for ForwardApiService (FORWARD_API_KEY).");
|
||||
}
|
||||
if (!config.FORWARD_BASE_URL) {
|
||||
throw new Error("Base URL is required for ForwardApiService (FORWARD_BASE_URL).");
|
||||
}
|
||||
|
||||
this.config = config;
|
||||
this.apiKey = config.FORWARD_API_KEY;
|
||||
this.baseUrl = config.FORWARD_BASE_URL;
|
||||
this.useSystemProxy = config?.USE_SYSTEM_PROXY_FORWARD ?? false;
|
||||
this.headerName = config?.FORWARD_HEADER_NAME || 'Authorization';
|
||||
this.headerValuePrefix = config?.FORWARD_HEADER_VALUE_PREFIX || 'Bearer ';
|
||||
|
||||
console.log(`[Forward] Base URL: ${this.baseUrl}, System proxy ${this.useSystemProxy ? 'enabled' : 'disabled'}`);
|
||||
|
||||
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 headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
headers[this.headerName] = `${this.headerValuePrefix}${this.apiKey}`;
|
||||
|
||||
const axiosConfig = {
|
||||
baseURL: this.baseUrl,
|
||||
httpAgent,
|
||||
httpsAgent,
|
||||
headers,
|
||||
};
|
||||
|
||||
if (!this.useSystemProxy) {
|
||||
axiosConfig.proxy = false;
|
||||
}
|
||||
|
||||
configureAxiosProxy(axiosConfig, config, 'forward-custom');
|
||||
|
||||
this.axiosInstance = axios.create(axiosConfig);
|
||||
}
|
||||
|
||||
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;
|
||||
const errorMessage = error.message || '';
|
||||
const isNetworkError = isRetryableNetworkError(error);
|
||||
|
||||
if (status === 401 || status === 403) {
|
||||
console.error(`[Forward API] Received ${status}. API Key might be invalid or expired.`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`[Forward API] Error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
return this.callApi(endpoint, body, isRetry, retryCount + 1);
|
||||
}
|
||||
|
||||
console.error(`[Forward API] Error calling API (Status: ${status}, Code: ${errorCode}):`, data || error.message);
|
||||
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, {
|
||||
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();
|
||||
if (jsonData === '[DONE]') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const parsedChunk = JSON.parse(jsonData);
|
||||
yield parsedChunk;
|
||||
} catch (e) {
|
||||
// If it's not JSON, it might be a different format, but for a forwarder we try to parse common SSE formats
|
||||
console.warn("[ForwardApiService] Failed to parse stream chunk JSON:", e.message, "Data:", jsonData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const status = error.response?.status;
|
||||
const errorCode = error.code;
|
||||
const isNetworkError = isRetryableNetworkError(error);
|
||||
|
||||
if ((status === 429 || (status >= 500 && status < 600) || isNetworkError) && retryCount < maxRetries) {
|
||||
const delay = baseDelay * Math.pow(2, retryCount);
|
||||
console.log(`[Forward API] Stream error ${status || errorCode}. Retrying in ${delay}ms... (attempt ${retryCount + 1}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
yield* this.streamApi(endpoint, body, isRetry, retryCount + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async generateContent(model, requestBody) {
|
||||
// Transparently pass the endpoint if provided in requestBody, otherwise use default
|
||||
const endpoint = requestBody.endpoint || '';
|
||||
return this.callApi(endpoint, requestBody);
|
||||
}
|
||||
|
||||
async *generateContentStream(model, requestBody) {
|
||||
const endpoint = requestBody.endpoint || '';
|
||||
yield* this.streamApi(endpoint, requestBody);
|
||||
}
|
||||
|
||||
async listModels() {
|
||||
try {
|
||||
const response = await this.axiosInstance.get('/models');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(`Error listing Forward models:`, error.message);
|
||||
return { data: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/providers/forward/forward-strategy.js
Normal file
53
src/providers/forward/forward-strategy.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { ProviderStrategy } from '../../utils/provider-strategy.js';
|
||||
|
||||
/**
|
||||
* Forward provider strategy implementation.
|
||||
* Designed to be as transparent as possible.
|
||||
*/
|
||||
class ForwardStrategy extends ProviderStrategy {
|
||||
extractModelAndStreamInfo(req, requestBody) {
|
||||
const model = requestBody.model || 'default';
|
||||
const isStream = requestBody.stream === true;
|
||||
return { model, isStream };
|
||||
}
|
||||
|
||||
extractResponseText(response) {
|
||||
// Attempt to extract text using common patterns (OpenAI, Claude, etc.)
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
const choice = response.choices[0];
|
||||
if (choice.message && choice.message.content) {
|
||||
return choice.message.content;
|
||||
} else if (choice.delta && choice.delta.content) {
|
||||
return choice.delta.content;
|
||||
}
|
||||
}
|
||||
if (response.content && Array.isArray(response.content)) {
|
||||
return response.content.map(c => c.text || '').join('');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
extractPromptText(requestBody) {
|
||||
if (requestBody.messages && requestBody.messages.length > 0) {
|
||||
const lastMessage = requestBody.messages[requestBody.messages.length - 1];
|
||||
let content = lastMessage.content;
|
||||
if (typeof content === 'object' && content !== null) {
|
||||
return JSON.stringify(content);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async applySystemPromptFromFile(config, requestBody) {
|
||||
// For forwarder, we might want to skip automatic system prompt application
|
||||
// to keep it transparent, but let's follow the base implementation just in case.
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
async manageSystemPrompt(requestBody) {
|
||||
// No-op for transparency
|
||||
}
|
||||
}
|
||||
|
||||
export { ForwardStrategy };
|
||||
|
|
@ -72,7 +72,8 @@ export const PROVIDER_MODELS = {
|
|||
'gpt-5.1-codex-max',
|
||||
'gpt-5.2',
|
||||
'gpt-5.2-codex'
|
||||
]
|
||||
],
|
||||
'forward-api': []
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,13 +14,14 @@ export class ProviderPoolManager {
|
|||
static DEFAULT_HEALTH_CHECK_MODELS = {
|
||||
'gemini-cli-oauth': 'gemini-2.5-flash',
|
||||
'gemini-antigravity': 'gemini-2.5-flash',
|
||||
'openai-custom': 'gpt-3.5-turbo',
|
||||
'openai-custom': 'gpt-4o-mini',
|
||||
'claude-custom': 'claude-3-7-sonnet-20250219',
|
||||
'claude-kiro-oauth': 'claude-haiku-4-5',
|
||||
'openai-qwen-oauth': 'qwen3-coder-flash',
|
||||
'openai-iflow': 'qwen3-coder-plus',
|
||||
'openai-codex-oauth': 'gpt-5-codex-mini',
|
||||
'openaiResponses-custom': 'gpt-4o-mini'
|
||||
'openaiResponses-custom': 'gpt-4o-mini',
|
||||
'forward-api': 'gpt-4o-mini',
|
||||
};
|
||||
|
||||
constructor(providerPools, options = {}) {
|
||||
|
|
|
|||
|
|
@ -36,20 +36,20 @@ export async function handleAPIRequests(method, path, req, res, currentConfig, a
|
|||
// Route content generation requests
|
||||
if (method === 'POST') {
|
||||
if (path === '/v1/chat/completions') {
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.OPENAI_CHAT, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid);
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.OPENAI_CHAT, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid, path);
|
||||
return true;
|
||||
}
|
||||
if (path === '/v1/responses') {
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.OPENAI_RESPONSES, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid);
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.OPENAI_RESPONSES, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid, path);
|
||||
return true;
|
||||
}
|
||||
const geminiUrlPattern = new RegExp(`/v1beta/models/(.+?):(${API_ACTIONS.GENERATE_CONTENT}|${API_ACTIONS.STREAM_GENERATE_CONTENT})`);
|
||||
if (geminiUrlPattern.test(path)) {
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.GEMINI_CONTENT, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid);
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.GEMINI_CONTENT, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid, path);
|
||||
return true;
|
||||
}
|
||||
if (path === '/v1/messages') {
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.CLAUDE_MESSAGE, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid);
|
||||
await handleContentGenerationRequest(req, res, apiService, ENDPOINT_TYPE.CLAUDE_MESSAGE, currentConfig, promptLogFilename, providerPoolManager, currentConfig.uuid, path);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -395,7 +395,8 @@ 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-iflow': 'IFLOW_TOKEN_FILE_PATH',
|
||||
'forward-api': 'FORWARD_BASE_URL'
|
||||
};
|
||||
let providerPoolsSlim = [];
|
||||
let unhealthyProvideIdentifyList = [];
|
||||
|
|
|
|||
|
|
@ -425,14 +425,16 @@ export async function handleResetProviderHealth(req, res, currentConfig, provide
|
|||
|
||||
let resetCount = 0;
|
||||
providers.forEach(provider => {
|
||||
// 统计 isHealthy 从 false 变为 true 的节点数量
|
||||
if (!provider.isHealthy) {
|
||||
provider.isHealthy = true;
|
||||
provider.errorCount = 0;
|
||||
provider.refreshCount = 0;
|
||||
provider.needsRefresh = false;
|
||||
provider.lastErrorTime = null;
|
||||
resetCount++;
|
||||
}
|
||||
// 重置所有节点的状态
|
||||
provider.isHealthy = true;
|
||||
provider.errorCount = 0;
|
||||
provider.refreshCount = 0;
|
||||
provider.needsRefresh = false;
|
||||
provider.lastErrorTime = null;
|
||||
});
|
||||
|
||||
// Save to file
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export const MODEL_PROTOCOL_PREFIX = {
|
|||
CLAUDE: 'claude',
|
||||
OLLAMA: 'ollama',
|
||||
CODEX: 'codex',
|
||||
FORWARD: 'forward',
|
||||
}
|
||||
|
||||
export const MODEL_PROVIDER = {
|
||||
|
|
@ -69,6 +70,7 @@ export const MODEL_PROVIDER = {
|
|||
QWEN_API: 'openai-qwen-oauth',
|
||||
IFLOW_API: 'openai-iflow',
|
||||
CODEX_API: 'openai-codex-oauth',
|
||||
FORWARD_API: 'forward-api',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -646,8 +648,9 @@ export async function handleModelListRequest(req, res, service, endpointType, CO
|
|||
* @param {Object} CONFIG - The server configuration object.
|
||||
* @param {string} PROMPT_LOG_FILENAME - The prompt log filename.
|
||||
*/
|
||||
export async function handleContentGenerationRequest(req, res, service, endpointType, CONFIG, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid) {
|
||||
export async function handleContentGenerationRequest(req, res, service, endpointType, CONFIG, PROMPT_LOG_FILENAME, providerPoolManager, pooluuid, requestPath = null) {
|
||||
const originalRequestBody = await getRequestBody(req);
|
||||
|
||||
if (!originalRequestBody) {
|
||||
throw new Error("Request body is missing for content generation.");
|
||||
}
|
||||
|
|
@ -711,6 +714,12 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
|
|||
} else {
|
||||
console.log(`[Request Convert] Request format matches backend provider. No conversion needed.`);
|
||||
}
|
||||
|
||||
// 为 forward provider 添加原始请求路径作为 endpoint
|
||||
if (requestPath && toProvider === MODEL_PROVIDER.FORWARD_API) {
|
||||
console.log(`[Forward API] Request path: ${requestPath}`);
|
||||
processedRequestBody.endpoint = requestPath;
|
||||
}
|
||||
|
||||
// 3. Apply system prompt from file if configured.
|
||||
processedRequestBody = await _applySystemPromptFromFile(CONFIG, processedRequestBody, toProvider);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { GeminiStrategy } from '../providers/gemini/gemini-strategy.js';
|
|||
import { OpenAIStrategy } from '../providers/openai/openai-strategy.js';
|
||||
import { ClaudeStrategy } from '../providers/claude/claude-strategy.js';
|
||||
import { ResponsesAPIStrategy } from '../providers/openai/openai-responses-strategy.js';
|
||||
import { ForwardStrategy } from '../providers/forward/forward-strategy.js';
|
||||
|
||||
/**
|
||||
* Strategy factory that returns the appropriate strategy instance based on the provider protocol.
|
||||
|
|
@ -21,6 +22,8 @@ class ProviderStrategyFactory {
|
|||
case MODEL_PROTOCOL_PREFIX.CODEX:
|
||||
// Codex 使用 OpenAI 策略(因为它基于 OpenAI 格式)
|
||||
return new OpenAIStrategy();
|
||||
case MODEL_PROTOCOL_PREFIX.FORWARD:
|
||||
return new ForwardStrategy();
|
||||
default:
|
||||
throw new Error(`Unsupported provider protocol: ${providerProtocol}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -428,6 +428,29 @@ const translations = {
|
|||
'modal.provider.refreshUuidConfirm': '确定要刷新此提供商的uuid吗?\n\n旧uuid: {oldUuid}\n\n刷新后将生成新的uuid,请确保没有其他系统依赖此uuid。',
|
||||
'modal.provider.refreshUuid.success': 'uuid刷新成功\n\n旧uuid: {oldUuid}\n新uuid: {newUuid}',
|
||||
'modal.provider.refreshUuid.failed': 'uuid刷新失败',
|
||||
'modal.provider.field.projectId': '项目 ID',
|
||||
'modal.provider.field.oauthPath': 'OAuth 凭据文件路径',
|
||||
'modal.provider.field.baseUrl': 'Base URL',
|
||||
'modal.provider.field.refreshUrl': 'Refresh URL',
|
||||
'modal.provider.field.refreshIdcUrl': 'Refresh IDC URL',
|
||||
'modal.provider.field.oauthBaseUrl': 'OAuth Base URL',
|
||||
'modal.provider.field.dailyBaseUrl': 'Daily Base URL',
|
||||
'modal.provider.field.autopushBaseUrl': 'Autopush Base URL',
|
||||
'modal.provider.field.headerName': 'Header 名称',
|
||||
'modal.provider.field.headerPrefix': 'Header 值前缀',
|
||||
'modal.provider.field.useSystemProxy': '使用系统代理',
|
||||
'modal.provider.field.apiKey': 'API 密钥',
|
||||
'modal.provider.field.apiKey.placeholder': '请输入 API 密钥',
|
||||
'modal.provider.field.projectId.placeholder': 'Google Cloud 项目 ID',
|
||||
'modal.provider.field.projectId.optional.placeholder': 'Google Cloud 项目 ID (留空自动发现)',
|
||||
'modal.provider.field.oauthPath.gemini.placeholder': '例如: ~/.gemini/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.kiro.placeholder': '例如: ~/.aws/sso/cache/kiro-auth-token.json',
|
||||
'modal.provider.field.oauthPath.qwen.placeholder': '例如: ~/.qwen/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.antigravity.placeholder': '例如: ~/.antigravity/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.iflow.placeholder': '例如: configs/iflow/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.codex.placeholder': '例如: configs/codex/oauth_creds.json',
|
||||
'modal.provider.field.email': '邮箱',
|
||||
'modal.provider.field.email.placeholder': '你的邮箱@example.com',
|
||||
|
||||
'modal.provider.load.failed': '加载提供商详情失败',
|
||||
'modal.provider.auth.initializing': '正在初始化凭据生成...',
|
||||
|
|
@ -1112,6 +1135,29 @@ const translations = {
|
|||
'modal.provider.refreshUuidConfirm': 'Are you sure you want to refresh the uuid for this provider?\n\nOld uuid: {oldUuid}\n\nA new uuid will be generated. Make sure no other systems depend on this uuid.',
|
||||
'modal.provider.refreshUuid.success': 'uuid refreshed successfully\n\nOld uuid: {oldUuid}\nNew uuid: {newUuid}',
|
||||
'modal.provider.refreshUuid.failed': 'Failed to refresh uuid',
|
||||
'modal.provider.field.projectId': 'Project ID',
|
||||
'modal.provider.field.oauthPath': 'OAuth Credentials File Path',
|
||||
'modal.provider.field.baseUrl': 'Base URL',
|
||||
'modal.provider.field.refreshUrl': 'Refresh URL',
|
||||
'modal.provider.field.refreshIdcUrl': 'Refresh IDC URL',
|
||||
'modal.provider.field.oauthBaseUrl': 'OAuth Base URL',
|
||||
'modal.provider.field.dailyBaseUrl': 'Daily Base URL',
|
||||
'modal.provider.field.autopushBaseUrl': 'Autopush Base URL',
|
||||
'modal.provider.field.headerName': 'Header Name',
|
||||
'modal.provider.field.headerPrefix': 'Header Value Prefix',
|
||||
'modal.provider.field.useSystemProxy': 'Use System Proxy',
|
||||
'modal.provider.field.apiKey': 'API Key',
|
||||
'modal.provider.field.apiKey.placeholder': 'Please enter API Key',
|
||||
'modal.provider.field.projectId.placeholder': 'Google Cloud Project ID',
|
||||
'modal.provider.field.projectId.optional.placeholder': 'Google Cloud Project ID (Leave blank for discovery)',
|
||||
'modal.provider.field.oauthPath.gemini.placeholder': 'e.g.: ~/.gemini/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.kiro.placeholder': 'e.g.: ~/.aws/sso/cache/kiro-auth-token.json',
|
||||
'modal.provider.field.oauthPath.qwen.placeholder': 'e.g.: ~/.qwen/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.antigravity.placeholder': 'e.g.: ~/.antigravity/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.iflow.placeholder': 'e.g.: configs/iflow/oauth_creds.json',
|
||||
'modal.provider.field.oauthPath.codex.placeholder': 'e.g.: configs/codex/oauth_creds.json',
|
||||
'modal.provider.field.email': 'Email',
|
||||
'modal.provider.field.email.placeholder': 'your-email@example.com',
|
||||
|
||||
'modal.provider.load.failed': 'Failed to load provider details',
|
||||
'modal.provider.auth.initializing': 'Initializing credential generation...',
|
||||
|
|
|
|||
|
|
@ -686,7 +686,8 @@ function getFieldOrder(provider) {
|
|||
'claude-kiro-oauth': ['KIRO_OAUTH_CREDS_FILE_PATH', 'KIRO_BASE_URL', 'KIRO_REFRESH_URL', 'KIRO_REFRESH_IDC_URL'],
|
||||
'openai-qwen-oauth': ['QWEN_OAUTH_CREDS_FILE_PATH', 'QWEN_BASE_URL', 'QWEN_OAUTH_BASE_URL'],
|
||||
'gemini-antigravity': ['PROJECT_ID', 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH', 'ANTIGRAVITY_BASE_URL_DAILY', 'ANTIGRAVITY_BASE_URL_AUTOPUSH'],
|
||||
'openai-iflow': ['IFLOW_OAUTH_CREDS_FILE_PATH', 'IFLOW_BASE_URL']
|
||||
'openai-iflow': ['IFLOW_OAUTH_CREDS_FILE_PATH', 'IFLOW_BASE_URL'],
|
||||
'forward-api': ['FORWARD_API_KEY', 'FORWARD_BASE_URL', 'FORWARD_HEADER_NAME', 'FORWARD_HEADER_VALUE_PREFIX']
|
||||
};
|
||||
|
||||
// 尝试从全局或当前模态框上下文中推断提供商类型
|
||||
|
|
@ -706,6 +707,8 @@ function getFieldOrder(provider) {
|
|||
providerType = 'gemini-antigravity';
|
||||
} else if (provider.IFLOW_OAUTH_CREDS_FILE_PATH) {
|
||||
providerType = 'openai-iflow';
|
||||
} else if (provider.FORWARD_API_KEY) {
|
||||
providerType = 'forward-api';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -205,31 +205,44 @@ function renderProviders(providers) {
|
|||
// 始终显示统计卡片
|
||||
if (statsGrid) statsGrid.style.display = 'grid';
|
||||
|
||||
// 定义所有支持的提供商显示顺序
|
||||
const providerDisplayOrder = [
|
||||
'gemini-cli-oauth',
|
||||
'gemini-antigravity',
|
||||
'openai-custom',
|
||||
'claude-custom',
|
||||
'claude-kiro-oauth',
|
||||
'openai-qwen-oauth',
|
||||
'openaiResponses-custom',
|
||||
'openai-iflow',
|
||||
'openai-codex-oauth'
|
||||
// 定义所有支持的提供商配置(顺序、显示名称、是否显示)
|
||||
const providerConfigs = [
|
||||
{ id: 'forward-api', name: 'NewAPI', visible: false },
|
||||
{ id: 'gemini-cli-oauth', name: 'Gemini CLI OAuth', visible: true },
|
||||
{ id: 'gemini-antigravity', name: 'Gemini Antigravity', visible: true },
|
||||
{ id: 'openai-custom', name: 'OpenAI Custom', visible: true },
|
||||
{ id: 'claude-custom', name: 'Claude Custom', visible: true },
|
||||
{ id: 'claude-kiro-oauth', name: 'Claude Kiro OAuth', visible: true },
|
||||
{ id: 'openai-qwen-oauth', name: 'OpenAI Qwen OAuth', visible: true },
|
||||
{ id: 'openaiResponses-custom', name: 'OpenAI Responses', visible: true },
|
||||
{ id: 'openai-iflow', name: 'OpenAI iFlow', visible: true },
|
||||
{ id: 'openai-codex-oauth', name: 'OpenAI Codex OAuth', visible: true },
|
||||
];
|
||||
|
||||
// 提取显示的 ID 顺序
|
||||
const providerDisplayOrder = providerConfigs.filter(c => c.visible !== false).map(c => c.id);
|
||||
|
||||
// 建立 ID 到配置的映射,方便获取显示名称
|
||||
const configMap = providerConfigs.reduce((map, config) => {
|
||||
map[config.id] = config;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// 获取所有提供商类型并按指定顺序排序
|
||||
// 优先显示预定义的所有提供商类型,即使某些提供商没有数据也要显示
|
||||
let allProviderTypes;
|
||||
if (hasProviders) {
|
||||
// 合并预定义类型和实际存在的类型,确保显示所有预定义提供商
|
||||
const actualProviderTypes = Object.keys(providers);
|
||||
// 只保留配置中标记为 visible 的,或者不在配置中的(默认显示)
|
||||
allProviderTypes = [...new Set([...providerDisplayOrder, ...actualProviderTypes])];
|
||||
} else {
|
||||
allProviderTypes = providerDisplayOrder;
|
||||
}
|
||||
|
||||
// 过滤掉明确设置为不显示的提供商
|
||||
const sortedProviderTypes = providerDisplayOrder.filter(type => allProviderTypes.includes(type))
|
||||
.concat(allProviderTypes.filter(type => !providerDisplayOrder.includes(type)));
|
||||
.concat(allProviderTypes.filter(type => !providerDisplayOrder.some(t => t === type) && !configMap[type]?.visible === false));
|
||||
|
||||
// 计算总统计
|
||||
let totalAccounts = 0;
|
||||
|
|
@ -237,6 +250,11 @@ function renderProviders(providers) {
|
|||
|
||||
// 按照排序后的提供商类型渲染
|
||||
sortedProviderTypes.forEach((providerType) => {
|
||||
// 如果配置中明确设置为不显示,则跳过
|
||||
if (configMap[providerType] && configMap[providerType].visible === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accounts = hasProviders ? providers[providerType] || [] : [];
|
||||
const providerDiv = document.createElement('div');
|
||||
providerDiv.className = 'provider-item';
|
||||
|
|
@ -275,10 +293,13 @@ function renderProviders(providers) {
|
|||
const statusIcon = isEmptyState ? 'fa-info-circle' : (healthyCount === totalCount ? 'fa-check-circle' : 'fa-exclamation-triangle');
|
||||
const statusText = isEmptyState ? t('providers.status.empty') : t('providers.status.healthy', { healthy: healthyCount, total: totalCount });
|
||||
|
||||
// 获取显示名称
|
||||
const displayName = configMap[providerType]?.name || providerType;
|
||||
|
||||
providerDiv.innerHTML = `
|
||||
<div class="provider-header">
|
||||
<div class="provider-name">
|
||||
<span class="provider-type-text">${providerType}</span>
|
||||
<span class="provider-type-text">${displayName}</span>
|
||||
</div>
|
||||
<div class="provider-header-right">
|
||||
${generateAuthButton(providerType)}
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ function showToast(title, message, type = 'info') {
|
|||
* @returns {string} 显示文案
|
||||
*/
|
||||
function getFieldLabel(key) {
|
||||
const isEn = getCurrentLanguage() === 'en-US';
|
||||
const labelMap = {
|
||||
'customName': t('modal.provider.customName') + ' ' + t('config.optional'),
|
||||
'checkModelName': t('modal.provider.checkModelName') + ' ' + t('config.optional'),
|
||||
|
|
@ -77,21 +76,27 @@ function getFieldLabel(key) {
|
|||
'OPENAI_BASE_URL': 'OpenAI Base URL',
|
||||
'CLAUDE_API_KEY': 'Claude API Key',
|
||||
'CLAUDE_BASE_URL': 'Claude Base URL',
|
||||
'PROJECT_ID': isEn ? 'Project ID' : '项目ID',
|
||||
'GEMINI_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
'KIRO_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凭据文件路径',
|
||||
'IFLOW_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
'PROJECT_ID': t('modal.provider.field.projectId'),
|
||||
'GEMINI_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
|
||||
'KIRO_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
|
||||
'QWEN_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
|
||||
'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
|
||||
'IFLOW_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
|
||||
'CODEX_OAUTH_CREDS_FILE_PATH': t('modal.provider.field.oauthPath'),
|
||||
'GEMINI_BASE_URL': 'Gemini Base URL',
|
||||
'KIRO_BASE_URL': 'Base URL',
|
||||
'KIRO_REFRESH_URL': 'Refresh URL',
|
||||
'KIRO_REFRESH_IDC_URL': 'Refresh IDC URL',
|
||||
'KIRO_BASE_URL': t('modal.provider.field.baseUrl'),
|
||||
'KIRO_REFRESH_URL': t('modal.provider.field.refreshUrl'),
|
||||
'KIRO_REFRESH_IDC_URL': t('modal.provider.field.refreshIdcUrl'),
|
||||
'QWEN_BASE_URL': 'Qwen Base URL',
|
||||
'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'
|
||||
'QWEN_OAUTH_BASE_URL': t('modal.provider.field.oauthBaseUrl'),
|
||||
'ANTIGRAVITY_BASE_URL_DAILY': t('modal.provider.field.dailyBaseUrl'),
|
||||
'ANTIGRAVITY_BASE_URL_AUTOPUSH': t('modal.provider.field.autopushBaseUrl'),
|
||||
'IFLOW_BASE_URL': 'iFlow Base URL',
|
||||
'FORWARD_API_KEY': 'Forward API Key',
|
||||
'FORWARD_BASE_URL': 'Forward Base URL',
|
||||
'FORWARD_HEADER_NAME': t('modal.provider.field.headerName'),
|
||||
'FORWARD_HEADER_VALUE_PREFIX': t('modal.provider.field.headerPrefix'),
|
||||
'USE_SYSTEM_PROXY_FORWARD': t('modal.provider.field.useSystemProxy')
|
||||
};
|
||||
|
||||
return labelMap[key] || key;
|
||||
|
|
@ -103,12 +108,11 @@ function getFieldLabel(key) {
|
|||
* @returns {Array} 字段配置数组
|
||||
*/
|
||||
function getProviderTypeFields(providerType) {
|
||||
const isEn = getCurrentLanguage() === 'en-US';
|
||||
const fieldConfigs = {
|
||||
'openai-custom': [
|
||||
{
|
||||
id: 'OPENAI_API_KEY',
|
||||
label: 'OpenAI API Key',
|
||||
label: t('modal.provider.field.apiKey'),
|
||||
type: 'password',
|
||||
placeholder: 'sk-...'
|
||||
},
|
||||
|
|
@ -122,7 +126,7 @@ function getProviderTypeFields(providerType) {
|
|||
'openaiResponses-custom': [
|
||||
{
|
||||
id: 'OPENAI_API_KEY',
|
||||
label: 'OpenAI API Key',
|
||||
label: t('modal.provider.field.apiKey'),
|
||||
type: 'password',
|
||||
placeholder: 'sk-...'
|
||||
},
|
||||
|
|
@ -150,15 +154,15 @@ function getProviderTypeFields(providerType) {
|
|||
'gemini-cli-oauth': [
|
||||
{
|
||||
id: 'PROJECT_ID',
|
||||
label: isEn ? 'Project ID' : '项目ID',
|
||||
label: t('modal.provider.field.projectId'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'Google Cloud Project ID' : 'Google Cloud项目ID'
|
||||
placeholder: t('modal.provider.field.projectId.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'GEMINI_OAUTH_CREDS_FILE_PATH',
|
||||
label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
label: t('modal.provider.field.oauthPath'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'e.g.: ~/.gemini/oauth_creds.json' : '例如: ~/.gemini/oauth_creds.json'
|
||||
placeholder: t('modal.provider.field.oauthPath.gemini.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'GEMINI_BASE_URL',
|
||||
|
|
@ -170,25 +174,25 @@ function getProviderTypeFields(providerType) {
|
|||
'claude-kiro-oauth': [
|
||||
{
|
||||
id: 'KIRO_OAUTH_CREDS_FILE_PATH',
|
||||
label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
label: t('modal.provider.field.oauthPath'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'e.g.: ~/.aws/sso/cache/kiro-auth-token.json' : '例如: ~/.aws/sso/cache/kiro-auth-token.json'
|
||||
placeholder: t('modal.provider.field.oauthPath.kiro.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'KIRO_BASE_URL',
|
||||
label: `Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
label: `${t('modal.provider.field.baseUrl')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_REFRESH_URL',
|
||||
label: `Refresh URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
label: `${t('modal.provider.field.refreshUrl')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_REFRESH_IDC_URL',
|
||||
label: `Refresh IDC URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
label: `${t('modal.provider.field.refreshIdcUrl')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://oidc.{{region}}.amazonaws.com/token'
|
||||
}
|
||||
|
|
@ -196,9 +200,9 @@ function getProviderTypeFields(providerType) {
|
|||
'openai-qwen-oauth': [
|
||||
{
|
||||
id: 'QWEN_OAUTH_CREDS_FILE_PATH',
|
||||
label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
label: t('modal.provider.field.oauthPath'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'e.g.: ~/.qwen/oauth_creds.json' : '例如: ~/.qwen/oauth_creds.json'
|
||||
placeholder: t('modal.provider.field.oauthPath.qwen.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'QWEN_BASE_URL',
|
||||
|
|
@ -208,7 +212,7 @@ function getProviderTypeFields(providerType) {
|
|||
},
|
||||
{
|
||||
id: 'QWEN_OAUTH_BASE_URL',
|
||||
label: `OAuth Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
label: `${t('modal.provider.field.oauthBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://chat.qwen.ai'
|
||||
}
|
||||
|
|
@ -216,25 +220,25 @@ function getProviderTypeFields(providerType) {
|
|||
'gemini-antigravity': [
|
||||
{
|
||||
id: 'PROJECT_ID',
|
||||
label: isEn ? 'Project ID (Optional)' : '项目ID (选填)',
|
||||
label: `${t('modal.provider.field.projectId')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'Google Cloud Project ID (Leave blank for discovery)' : 'Google Cloud项目ID (留空自动发现)'
|
||||
placeholder: t('modal.provider.field.projectId.optional.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
|
||||
label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
label: t('modal.provider.field.oauthPath'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'e.g.: ~/.antigravity/oauth_creds.json' : '例如: ~/.antigravity/oauth_creds.json'
|
||||
placeholder: t('modal.provider.field.oauthPath.antigravity.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'ANTIGRAVITY_BASE_URL_DAILY',
|
||||
label: `Daily Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
label: `${t('modal.provider.field.dailyBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://daily-cloudcode-pa.sandbox.googleapis.com'
|
||||
},
|
||||
{
|
||||
id: 'ANTIGRAVITY_BASE_URL_AUTOPUSH',
|
||||
label: `Autopush Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
label: `${t('modal.provider.field.autopushBaseUrl')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com'
|
||||
}
|
||||
|
|
@ -242,9 +246,9 @@ function getProviderTypeFields(providerType) {
|
|||
'openai-iflow': [
|
||||
{
|
||||
id: 'IFLOW_OAUTH_CREDS_FILE_PATH',
|
||||
label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
label: t('modal.provider.field.oauthPath'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'e.g.: configs/iflow/oauth_creds.json' : '例如: configs/iflow/oauth_creds.json'
|
||||
placeholder: t('modal.provider.field.oauthPath.iflow.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'IFLOW_BASE_URL',
|
||||
|
|
@ -256,15 +260,15 @@ function getProviderTypeFields(providerType) {
|
|||
'openai-codex-oauth': [
|
||||
{
|
||||
id: 'CODEX_OAUTH_CREDS_FILE_PATH',
|
||||
label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
|
||||
label: t('modal.provider.field.oauthPath'),
|
||||
type: 'text',
|
||||
placeholder: isEn ? 'e.g.: configs/codex/oauth_creds.json' : '例如: configs/codex/oauth_creds.json'
|
||||
placeholder: t('modal.provider.field.oauthPath.codex.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'CODEX_EMAIL',
|
||||
label: isEn ? 'Email (Optional)' : '邮箱 (选填)',
|
||||
label: `${t('modal.provider.field.email')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'email',
|
||||
placeholder: isEn ? 'your-email@example.com' : '你的邮箱@example.com'
|
||||
placeholder: t('modal.provider.field.email.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'CODEX_BASE_URL',
|
||||
|
|
@ -272,6 +276,32 @@ function getProviderTypeFields(providerType) {
|
|||
type: 'text',
|
||||
placeholder: 'https://api.openai.com/v1/codex'
|
||||
}
|
||||
],
|
||||
'forward-api': [
|
||||
{
|
||||
id: 'FORWARD_API_KEY',
|
||||
label: t('modal.provider.field.apiKey'),
|
||||
type: 'password',
|
||||
placeholder: t('modal.provider.field.apiKey.placeholder')
|
||||
},
|
||||
{
|
||||
id: 'FORWARD_BASE_URL',
|
||||
label: t('modal.provider.field.baseUrl'),
|
||||
type: 'text',
|
||||
placeholder: 'https://api.example.com'
|
||||
},
|
||||
{
|
||||
id: 'FORWARD_HEADER_NAME',
|
||||
label: `${t('modal.provider.field.headerName')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'Authorization'
|
||||
},
|
||||
{
|
||||
id: 'FORWARD_HEADER_VALUE_PREFIX',
|
||||
label: `${t('modal.provider.field.headerPrefix')} <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'Bearer '
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue