Compare commits

...

2 Commits

Author SHA1 Message Date
Supra4E8C
68974ffc68 feat(ai-providers): add prefix editing for provider configs 2025-12-21 23:46:39 +08:00
Supra4E8C
f8ed787f92 fix(splash): prevent login flicker on startup 2025-12-21 20:22:22 +08:00
9 changed files with 117 additions and 23 deletions

View File

@@ -17,18 +17,24 @@ import { MainLayout } from '@/components/layout/MainLayout';
import { ProtectedRoute } from '@/router/ProtectedRoute'; import { ProtectedRoute } from '@/router/ProtectedRoute';
import { useAuthStore, useLanguageStore, useThemeStore } from '@/stores'; import { useAuthStore, useLanguageStore, useThemeStore } from '@/stores';
const SPLASH_DURATION = 1500;
const SPLASH_FADE_DURATION = 400;
function App() { function App() {
const initializeTheme = useThemeStore((state) => state.initializeTheme); const initializeTheme = useThemeStore((state) => state.initializeTheme);
const language = useLanguageStore((state) => state.language); const language = useLanguageStore((state) => state.language);
const setLanguage = useLanguageStore((state) => state.setLanguage); const setLanguage = useLanguageStore((state) => state.setLanguage);
const restoreSession = useAuthStore((state) => state.restoreSession); const restoreSession = useAuthStore((state) => state.restoreSession);
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
const [splashReadyToFade, setSplashReadyToFade] = useState(false);
const [showSplash, setShowSplash] = useState(true); const [showSplash, setShowSplash] = useState(true);
const [authReady, setAuthReady] = useState(false);
useEffect(() => { useEffect(() => {
initializeTheme(); initializeTheme();
restoreSession(); void restoreSession().finally(() => {
setAuthReady(true);
});
}, [initializeTheme, restoreSession]); }, [initializeTheme, restoreSession]);
useEffect(() => { useEffect(() => {
@@ -36,13 +42,25 @@ function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 仅用于首屏同步 i18n 语言 }, []); // 仅用于首屏同步 i18n 语言
useEffect(() => {
const timer = setTimeout(() => {
setSplashReadyToFade(true);
}, SPLASH_DURATION - SPLASH_FADE_DURATION);
return () => clearTimeout(timer);
}, []);
const handleSplashFinish = useCallback(() => { const handleSplashFinish = useCallback(() => {
setShowSplash(false); setShowSplash(false);
}, []); }, []);
// 仅在已认证时显示闪屏 if (showSplash) {
if (showSplash && isAuthenticated) { return (
return <SplashScreen onFinish={handleSplashFinish} duration={1500} />; <SplashScreen
fadeOut={splashReadyToFade && authReady}
onFinish={handleSplashFinish}
/>
);
} }
return ( return (

View File

@@ -1,29 +1,25 @@
import { useEffect, useState } from 'react'; import { useEffect } from 'react';
import { INLINE_LOGO_JPEG } from '@/assets/logoInline'; import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
import './SplashScreen.scss'; import './SplashScreen.scss';
interface SplashScreenProps { interface SplashScreenProps {
onFinish: () => void; onFinish: () => void;
duration?: number; fadeOut?: boolean;
} }
export function SplashScreen({ onFinish, duration = 1500 }: SplashScreenProps) { const FADE_OUT_DURATION = 400;
const [fadeOut, setFadeOut] = useState(false);
export function SplashScreen({ onFinish, fadeOut = false }: SplashScreenProps) {
useEffect(() => { useEffect(() => {
const fadeTimer = setTimeout(() => { if (!fadeOut) return;
setFadeOut(true);
}, duration - 400);
const finishTimer = setTimeout(() => { const finishTimer = setTimeout(() => {
onFinish(); onFinish();
}, duration); }, FADE_OUT_DURATION);
return () => { return () => {
clearTimeout(fadeTimer);
clearTimeout(finishTimer); clearTimeout(finishTimer);
}; };
}, [duration, onFinish]); }, [fadeOut, onFinish]);
return ( return (
<div className={`splash-screen ${fadeOut ? 'fade-out' : ''}`}> <div className={`splash-screen ${fadeOut ? 'fade-out' : ''}`}>

View File

@@ -29,6 +29,7 @@
"required": "Required", "required": "Required",
"api_key": "Key", "api_key": "Key",
"base_url": "Address", "base_url": "Address",
"prefix": "Prefix",
"proxy_url": "Proxy", "proxy_url": "Proxy",
"alias": "Alias", "alias": "Alias",
"failure": "Failure", "failure": "Failure",
@@ -171,6 +172,9 @@
"excluded_models_placeholder": "Comma or newline separated, e.g. gemini-1.5-pro, gemini-1.5-flash", "excluded_models_placeholder": "Comma or newline separated, e.g. gemini-1.5-pro, gemini-1.5-flash",
"excluded_models_hint": "Leave empty to allow all models; values are trimmed and deduplicated automatically.", "excluded_models_hint": "Leave empty to allow all models; values are trimmed and deduplicated automatically.",
"excluded_models_count": "Excluding {{count}} models", "excluded_models_count": "Excluding {{count}} models",
"prefix_label": "Prefix (Optional):",
"prefix_placeholder": "e.g.: team-a",
"prefix_hint": "When set, call models as prefix/<model> to target this entry.",
"config_toggle_label": "Enabled", "config_toggle_label": "Enabled",
"config_disabled_badge": "Disabled", "config_disabled_badge": "Disabled",
"codex_title": "Codex API Configuration", "codex_title": "Codex API Configuration",

View File

@@ -29,6 +29,7 @@
"required": "必填", "required": "必填",
"api_key": "密钥", "api_key": "密钥",
"base_url": "地址", "base_url": "地址",
"prefix": "前缀",
"proxy_url": "代理", "proxy_url": "代理",
"alias": "别名", "alias": "别名",
"failure": "失败", "failure": "失败",
@@ -171,6 +172,9 @@
"excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash", "excluded_models_placeholder": "用逗号或换行分隔,例如: gemini-1.5-pro, gemini-1.5-flash",
"excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。", "excluded_models_hint": "留空表示不过滤;保存时会自动去重并忽略空白。",
"excluded_models_count": "排除 {{count}} 个模型", "excluded_models_count": "排除 {{count}} 个模型",
"prefix_label": "前缀 (可选):",
"prefix_placeholder": "例如: team-a",
"prefix_hint": "设置后可用 prefix/<model> 选择该条目。",
"config_toggle_label": "启用", "config_toggle_label": "启用",
"config_disabled_badge": "已停用", "config_disabled_badge": "已停用",
"codex_title": "Codex API 配置", "codex_title": "Codex API 配置",

View File

@@ -39,6 +39,7 @@ interface ModelEntry {
interface OpenAIFormState { interface OpenAIFormState {
name: string; name: string;
prefix: string;
baseUrl: string; baseUrl: string;
headers: HeaderEntry[]; headers: HeaderEntry[];
testModel?: string; testModel?: string;
@@ -200,6 +201,7 @@ export function AiProvidersPage() {
const [geminiForm, setGeminiForm] = useState<GeminiKeyConfig & { excludedText: string }>({ const [geminiForm, setGeminiForm] = useState<GeminiKeyConfig & { excludedText: string }>({
apiKey: '', apiKey: '',
prefix: '',
baseUrl: '', baseUrl: '',
headers: {}, headers: {},
excludedModels: [], excludedModels: [],
@@ -209,6 +211,7 @@ export function AiProvidersPage() {
ProviderKeyConfig & { modelEntries: ModelEntry[]; excludedText: string } ProviderKeyConfig & { modelEntries: ModelEntry[]; excludedText: string }
>({ >({
apiKey: '', apiKey: '',
prefix: '',
baseUrl: '', baseUrl: '',
proxyUrl: '', proxyUrl: '',
headers: {}, headers: {},
@@ -219,6 +222,7 @@ export function AiProvidersPage() {
}); });
const [openaiForm, setOpenaiForm] = useState<OpenAIFormState>({ const [openaiForm, setOpenaiForm] = useState<OpenAIFormState>({
name: '', name: '',
prefix: '',
baseUrl: '', baseUrl: '',
headers: [], headers: [],
apiKeyEntries: [buildApiKeyEntry()], apiKeyEntries: [buildApiKeyEntry()],
@@ -317,6 +321,7 @@ export function AiProvidersPage() {
setModal(null); setModal(null);
setGeminiForm({ setGeminiForm({
apiKey: '', apiKey: '',
prefix: '',
baseUrl: '', baseUrl: '',
headers: {}, headers: {},
excludedModels: [], excludedModels: [],
@@ -324,6 +329,7 @@ export function AiProvidersPage() {
}); });
setProviderForm({ setProviderForm({
apiKey: '', apiKey: '',
prefix: '',
baseUrl: '', baseUrl: '',
proxyUrl: '', proxyUrl: '',
headers: {}, headers: {},
@@ -334,6 +340,7 @@ export function AiProvidersPage() {
}); });
setOpenaiForm({ setOpenaiForm({
name: '', name: '',
prefix: '',
baseUrl: '', baseUrl: '',
headers: [], headers: [],
apiKeyEntries: [buildApiKeyEntry()], apiKeyEntries: [buildApiKeyEntry()],
@@ -410,6 +417,7 @@ export function AiProvidersPage() {
const modelEntries = modelsToEntries(entry.models); const modelEntries = modelsToEntries(entry.models);
setOpenaiForm({ setOpenaiForm({
name: entry.name, name: entry.name,
prefix: entry.prefix ?? '',
baseUrl: entry.baseUrl, baseUrl: entry.baseUrl,
headers: headersToEntries(entry.headers), headers: headersToEntries(entry.headers),
testModel: entry.testModel, testModel: entry.testModel,
@@ -757,6 +765,7 @@ export function AiProvidersPage() {
try { try {
const payload: GeminiKeyConfig = { const payload: GeminiKeyConfig = {
apiKey: geminiForm.apiKey.trim(), apiKey: geminiForm.apiKey.trim(),
prefix: geminiForm.prefix?.trim() || undefined,
baseUrl: geminiForm.baseUrl?.trim() || undefined, baseUrl: geminiForm.baseUrl?.trim() || undefined,
headers: buildHeaderObject(headersToEntries(geminiForm.headers as any)), headers: buildHeaderObject(headersToEntries(geminiForm.headers as any)),
excludedModels: parseExcludedModels(geminiForm.excludedText), excludedModels: parseExcludedModels(geminiForm.excludedText),
@@ -900,6 +909,7 @@ export function AiProvidersPage() {
const payload: ProviderKeyConfig = { const payload: ProviderKeyConfig = {
apiKey: providerForm.apiKey.trim(), apiKey: providerForm.apiKey.trim(),
prefix: providerForm.prefix?.trim() || undefined,
baseUrl, baseUrl,
proxyUrl: providerForm.proxyUrl?.trim() || undefined, proxyUrl: providerForm.proxyUrl?.trim() || undefined,
headers: buildHeaderObject(headersToEntries(providerForm.headers as any)), headers: buildHeaderObject(headersToEntries(providerForm.headers as any)),
@@ -970,6 +980,7 @@ export function AiProvidersPage() {
try { try {
const payload: OpenAIProviderConfig = { const payload: OpenAIProviderConfig = {
name: openaiForm.name.trim(), name: openaiForm.name.trim(),
prefix: openaiForm.prefix?.trim() || undefined,
baseUrl: openaiForm.baseUrl.trim(), baseUrl: openaiForm.baseUrl.trim(),
headers: buildHeaderObject(openaiForm.headers), headers: buildHeaderObject(openaiForm.headers),
apiKeyEntries: openaiForm.apiKeyEntries.map((entry) => ({ apiKeyEntries: openaiForm.apiKeyEntries.map((entry) => ({
@@ -1176,6 +1187,12 @@ export function AiProvidersPage() {
<span className={styles.fieldLabel}>{t('common.api_key')}:</span> <span className={styles.fieldLabel}>{t('common.api_key')}:</span>
<span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span> <span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span>
</div> </div>
{item.prefix && (
<div className={styles.fieldRow}>
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
<span className={styles.fieldValue}>{item.prefix}</span>
</div>
)}
{/* Base URL 行 */} {/* Base URL 行 */}
{item.baseUrl && ( {item.baseUrl && (
<div className={styles.fieldRow}> <div className={styles.fieldRow}>
@@ -1274,6 +1291,12 @@ export function AiProvidersPage() {
<span className={styles.fieldLabel}>{t('common.api_key')}:</span> <span className={styles.fieldLabel}>{t('common.api_key')}:</span>
<span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span> <span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span>
</div> </div>
{item.prefix && (
<div className={styles.fieldRow}>
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
<span className={styles.fieldValue}>{item.prefix}</span>
</div>
)}
{/* Base URL 行 */} {/* Base URL 行 */}
{item.baseUrl && ( {item.baseUrl && (
<div className={styles.fieldRow}> <div className={styles.fieldRow}>
@@ -1379,6 +1402,12 @@ export function AiProvidersPage() {
<span className={styles.fieldLabel}>{t('common.api_key')}:</span> <span className={styles.fieldLabel}>{t('common.api_key')}:</span>
<span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span> <span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span>
</div> </div>
{item.prefix && (
<div className={styles.fieldRow}>
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
<span className={styles.fieldValue}>{item.prefix}</span>
</div>
)}
{/* Base URL 行 */} {/* Base URL 行 */}
{item.baseUrl && ( {item.baseUrl && (
<div className={styles.fieldRow}> <div className={styles.fieldRow}>
@@ -1567,6 +1596,12 @@ export function AiProvidersPage() {
return ( return (
<Fragment> <Fragment>
<div className="item-title">{item.name}</div> <div className="item-title">{item.name}</div>
{item.prefix && (
<div className={styles.fieldRow}>
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
<span className={styles.fieldValue}>{item.prefix}</span>
</div>
)}
{/* Base URL 行 */} {/* Base URL 行 */}
<div className={styles.fieldRow}> <div className={styles.fieldRow}>
<span className={styles.fieldLabel}>{t('common.base_url')}:</span> <span className={styles.fieldLabel}>{t('common.base_url')}:</span>
@@ -1785,6 +1820,13 @@ export function AiProvidersPage() {
value={geminiForm.apiKey} value={geminiForm.apiKey}
onChange={(e) => setGeminiForm((prev) => ({ ...prev, apiKey: e.target.value }))} onChange={(e) => setGeminiForm((prev) => ({ ...prev, apiKey: e.target.value }))}
/> />
<Input
label={t('ai_providers.prefix_label')}
placeholder={t('ai_providers.prefix_placeholder')}
value={geminiForm.prefix ?? ''}
onChange={(e) => setGeminiForm((prev) => ({ ...prev, prefix: e.target.value }))}
hint={t('ai_providers.prefix_hint')}
/>
<Input <Input
label={t('ai_providers.gemini_base_url_label')} label={t('ai_providers.gemini_base_url_label')}
placeholder={t('ai_providers.gemini_base_url_placeholder')} placeholder={t('ai_providers.gemini_base_url_placeholder')}
@@ -1849,6 +1891,13 @@ export function AiProvidersPage() {
value={providerForm.apiKey} value={providerForm.apiKey}
onChange={(e) => setProviderForm((prev) => ({ ...prev, apiKey: e.target.value }))} onChange={(e) => setProviderForm((prev) => ({ ...prev, apiKey: e.target.value }))}
/> />
<Input
label={t('ai_providers.prefix_label')}
placeholder={t('ai_providers.prefix_placeholder')}
value={providerForm.prefix ?? ''}
onChange={(e) => setProviderForm((prev) => ({ ...prev, prefix: e.target.value }))}
hint={t('ai_providers.prefix_hint')}
/>
<Input <Input
label={ label={
modal?.type === 'codex' modal?.type === 'codex'
@@ -1931,6 +1980,13 @@ export function AiProvidersPage() {
value={openaiForm.name} value={openaiForm.name}
onChange={(e) => setOpenaiForm((prev) => ({ ...prev, name: e.target.value }))} onChange={(e) => setOpenaiForm((prev) => ({ ...prev, name: e.target.value }))}
/> />
<Input
label={t('ai_providers.prefix_label')}
placeholder={t('ai_providers.prefix_placeholder')}
value={openaiForm.prefix ?? ''}
onChange={(e) => setOpenaiForm((prev) => ({ ...prev, prefix: e.target.value }))}
hint={t('ai_providers.prefix_hint')}
/>
<Input <Input
label={t('ai_providers.openai_add_modal_url_label')} label={t('ai_providers.openai_add_modal_url_label')}
value={openaiForm.baseUrl} value={openaiForm.baseUrl}

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; import { Navigate, useNavigate, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
@@ -44,12 +44,10 @@ export function LoginPage() {
init(); init();
}, [detectedBase, restoreSession, storedBase, storedKey]); }, [detectedBase, restoreSession, storedBase, storedKey]);
useEffect(() => { if (isAuthenticated) {
if (isAuthenticated) { const redirect = (location.state as any)?.from?.pathname || '/';
const redirect = (location.state as any)?.from?.pathname || '/'; return <Navigate to={redirect} replace />;
navigate(redirect, { replace: true }); }
}
}, [isAuthenticated, navigate, location.state]);
const handleUseCurrent = () => { const handleUseCurrent = () => {
setApiBase(detectedBase); setApiBase(detectedBase);

View File

@@ -48,6 +48,7 @@ const serializeApiKeyEntry = (entry: ApiKeyEntry) => {
const serializeProviderKey = (config: ProviderKeyConfig) => { const serializeProviderKey = (config: ProviderKeyConfig) => {
const payload: Record<string, any> = { 'api-key': config.apiKey }; const payload: Record<string, any> = { 'api-key': config.apiKey };
if (config.prefix?.trim()) payload.prefix = config.prefix.trim();
if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.baseUrl) payload['base-url'] = config.baseUrl;
if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl; if (config.proxyUrl) payload['proxy-url'] = config.proxyUrl;
const headers = serializeHeaders(config.headers); const headers = serializeHeaders(config.headers);
@@ -62,6 +63,7 @@ const serializeProviderKey = (config: ProviderKeyConfig) => {
const serializeGeminiKey = (config: GeminiKeyConfig) => { const serializeGeminiKey = (config: GeminiKeyConfig) => {
const payload: Record<string, any> = { 'api-key': config.apiKey }; const payload: Record<string, any> = { 'api-key': config.apiKey };
if (config.prefix?.trim()) payload.prefix = config.prefix.trim();
if (config.baseUrl) payload['base-url'] = config.baseUrl; if (config.baseUrl) payload['base-url'] = config.baseUrl;
const headers = serializeHeaders(config.headers); const headers = serializeHeaders(config.headers);
if (headers) payload.headers = headers; if (headers) payload.headers = headers;
@@ -79,6 +81,7 @@ const serializeOpenAIProvider = (provider: OpenAIProviderConfig) => {
? provider.apiKeyEntries.map((entry) => serializeApiKeyEntry(entry)) ? provider.apiKeyEntries.map((entry) => serializeApiKeyEntry(entry))
: [] : []
}; };
if (provider.prefix?.trim()) payload.prefix = provider.prefix.trim();
const headers = serializeHeaders(provider.headers); const headers = serializeHeaders(provider.headers);
if (headers) payload.headers = headers; if (headers) payload.headers = headers;
const models = serializeModelAliases(provider.models); const models = serializeModelAliases(provider.models);

View File

@@ -70,6 +70,12 @@ const normalizeExcludedModels = (input: any): string[] => {
return normalized; return normalized;
}; };
const normalizePrefix = (value: any): string | undefined => {
if (value === undefined || value === null) return undefined;
const trimmed = String(value).trim();
return trimmed ? trimmed : undefined;
};
const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => { const normalizeApiKeyEntry = (entry: any): ApiKeyEntry | null => {
if (!entry) return null; if (!entry) return null;
const apiKey = entry['api-key'] ?? entry.apiKey ?? entry.key ?? (typeof entry === 'string' ? entry : ''); const apiKey = entry['api-key'] ?? entry.apiKey ?? entry.key ?? (typeof entry === 'string' ? entry : '');
@@ -93,6 +99,8 @@ const normalizeProviderKeyConfig = (item: any): ProviderKeyConfig | null => {
if (!trimmed) return null; if (!trimmed) return null;
const config: ProviderKeyConfig = { apiKey: trimmed }; const config: ProviderKeyConfig = { apiKey: trimmed };
const prefix = normalizePrefix(item.prefix ?? item['prefix']);
if (prefix) config.prefix = prefix;
const baseUrl = item['base-url'] ?? item.baseUrl; const baseUrl = item['base-url'] ?? item.baseUrl;
const proxyUrl = item['proxy-url'] ?? item.proxyUrl; const proxyUrl = item['proxy-url'] ?? item.proxyUrl;
if (baseUrl) config.baseUrl = String(baseUrl); if (baseUrl) config.baseUrl = String(baseUrl);
@@ -118,6 +126,8 @@ const normalizeGeminiKeyConfig = (item: any): GeminiKeyConfig | null => {
if (!trimmed) return null; if (!trimmed) return null;
const config: GeminiKeyConfig = { apiKey: trimmed }; const config: GeminiKeyConfig = { apiKey: trimmed };
const prefix = normalizePrefix(item.prefix ?? item['prefix']);
if (prefix) config.prefix = prefix;
const baseUrl = item['base-url'] ?? item.baseUrl ?? item['base_url']; const baseUrl = item['base-url'] ?? item.baseUrl ?? item['base_url'];
if (baseUrl) config.baseUrl = String(baseUrl); if (baseUrl) config.baseUrl = String(baseUrl);
const headers = normalizeHeaders(item.headers); const headers = normalizeHeaders(item.headers);
@@ -155,6 +165,8 @@ const normalizeOpenAIProvider = (provider: any): OpenAIProviderConfig | null =>
apiKeyEntries apiKeyEntries
}; };
const prefix = normalizePrefix(provider.prefix ?? provider['prefix']);
if (prefix) result.prefix = prefix;
if (headers) result.headers = headers; if (headers) result.headers = headers;
if (models.length) result.models = models; if (models.length) result.models = models;
if (priority !== undefined) result.priority = Number(priority); if (priority !== undefined) result.priority = Number(priority);

View File

@@ -18,6 +18,7 @@ export interface ApiKeyEntry {
export interface GeminiKeyConfig { export interface GeminiKeyConfig {
apiKey: string; apiKey: string;
prefix?: string;
baseUrl?: string; baseUrl?: string;
headers?: Record<string, string>; headers?: Record<string, string>;
excludedModels?: string[]; excludedModels?: string[];
@@ -25,6 +26,7 @@ export interface GeminiKeyConfig {
export interface ProviderKeyConfig { export interface ProviderKeyConfig {
apiKey: string; apiKey: string;
prefix?: string;
baseUrl?: string; baseUrl?: string;
proxyUrl?: string; proxyUrl?: string;
headers?: Record<string, string>; headers?: Record<string, string>;
@@ -34,6 +36,7 @@ export interface ProviderKeyConfig {
export interface OpenAIProviderConfig { export interface OpenAIProviderConfig {
name: string; name: string;
prefix?: string;
baseUrl: string; baseUrl: string;
apiKeyEntries: ApiKeyEntry[]; apiKeyEntries: ApiKeyEntry[];
headers?: Record<string, string>; headers?: Record<string, string>;