mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-08-28 16:31:13 +08:00
feat(provider): add fingerprint profile support and update related logic
This commit is contained in:
@@ -89,8 +89,9 @@ function providerKeyToResource(
|
||||
flags.websockets = (config as ProviderKeyConfig).websockets === true;
|
||||
}
|
||||
if (brand === 'claude' || brand === 'claudeApi') {
|
||||
const cloak = (config as ProviderKeyConfig).cloak;
|
||||
flags.cloakEnabled = Boolean(cloak?.mode?.trim());
|
||||
const claudeConfig = config as ProviderKeyConfig;
|
||||
flags.cloakEnabled = Boolean(claudeConfig.cloak?.mode?.trim());
|
||||
flags.claudeCodeCliProfile = claudeConfig.fingerprintProfile === 'claude-code-cli';
|
||||
}
|
||||
|
||||
const selector: ProviderResourceSelector = {
|
||||
|
||||
@@ -130,9 +130,15 @@ export function ProviderResourceTable({
|
||||
if ((r.brand === 'codex' || r.brand === 'xai') && r.flags.websockets) {
|
||||
items.push(renderFlagTag('ws', t('providersPage.table.websocketsTag')));
|
||||
}
|
||||
if (r.brand === 'claude' && r.flags.cloakEnabled) {
|
||||
if ((r.brand === 'claude' || r.brand === 'claudeApi') && r.flags.cloakEnabled) {
|
||||
items.push(renderFlagTag('cloak', t('providersPage.table.cloakTag')));
|
||||
}
|
||||
if (
|
||||
(r.brand === 'claude' || r.brand === 'claudeApi') &&
|
||||
r.flags.claudeCodeCliProfile
|
||||
) {
|
||||
items.push(renderFlagTag('cli-profile', t('providersPage.table.cliProfileTag')));
|
||||
}
|
||||
}
|
||||
return <div className={styles.metricsCell}>{items}</div>;
|
||||
};
|
||||
|
||||
@@ -96,7 +96,7 @@ function buildInitialForm(
|
||||
cloak: isClaudeLikeBrand(brand)
|
||||
? { mode: '', strictMode: false, sensitiveWordsText: '', cacheUserId: false }
|
||||
: undefined,
|
||||
experimentalCchSigning: isClaudeLikeBrand(brand) ? false : undefined,
|
||||
fingerprintProfile: isClaudeLikeBrand(brand) ? '' : undefined,
|
||||
testModel:
|
||||
brand === 'openaiCompatibility' ||
|
||||
brand === 'codex' ||
|
||||
@@ -193,8 +193,8 @@ function buildInitialForm(
|
||||
cacheUserId: (cfg as ProviderKeyConfig).cloak?.cacheUserId === true,
|
||||
}
|
||||
: undefined,
|
||||
experimentalCchSigning: isClaudeLikeBrand(brand)
|
||||
? (cfg as ProviderKeyConfig).experimentalCchSigning === true
|
||||
fingerprintProfile: isClaudeLikeBrand(brand)
|
||||
? ((cfg as ProviderKeyConfig).fingerprintProfile ?? '')
|
||||
: undefined,
|
||||
testModel:
|
||||
brand === 'codex' ||
|
||||
@@ -937,6 +937,34 @@ export function BaseProviderForm({
|
||||
</Collapsible>
|
||||
) : null}
|
||||
|
||||
{isClaudeLikeBrand(brand) ? (
|
||||
<div className={styles.field}>
|
||||
<label id={`${fid}-fingerprint-profile-label`} className={styles.label}>
|
||||
{t('providersPage.form.fingerprintProfile')}
|
||||
</label>
|
||||
<Select
|
||||
id={`${fid}-fingerprint-profile`}
|
||||
value={form.fingerprintProfile ?? ''}
|
||||
options={[
|
||||
{
|
||||
value: '',
|
||||
label: t('providersPage.form.fingerprintProfileDefault'),
|
||||
},
|
||||
{
|
||||
value: 'claude-code-cli',
|
||||
label: t('providersPage.form.fingerprintProfileClaudeCodeCli'),
|
||||
},
|
||||
]}
|
||||
onChange={(value) => updateField('fingerprintProfile', value)}
|
||||
disabled={mutating}
|
||||
ariaLabelledBy={`${fid}-fingerprint-profile-label`}
|
||||
/>
|
||||
<small className={styles.labelHint}>
|
||||
{t('providersPage.form.fingerprintProfileHint')}
|
||||
</small>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{descriptor.supportsCloak && form.cloak ? (
|
||||
<Collapsible label={t('providersPage.form.cloakSection')}>
|
||||
<div className={styles.section}>
|
||||
@@ -975,19 +1003,6 @@ export function BaseProviderForm({
|
||||
<small>{t('providersPage.form.cloakCacheUserIdHint')}</small>
|
||||
</span>
|
||||
</label>
|
||||
<label className={styles.checkboxRow}>
|
||||
<input
|
||||
type="checkbox"
|
||||
className={styles.checkboxBox}
|
||||
checked={form.experimentalCchSigning ?? false}
|
||||
disabled={mutating}
|
||||
onChange={(e) => updateField('experimentalCchSigning', e.target.checked)}
|
||||
/>
|
||||
<span className={styles.checkboxText}>
|
||||
<span>{t('providersPage.form.experimentalCchSigning')}</span>
|
||||
<small>{t('providersPage.form.experimentalCchSigningHint')}</small>
|
||||
</span>
|
||||
</label>
|
||||
<div className={styles.field}>
|
||||
<label className={styles.label}>{t('providersPage.form.cloakSensitiveWords')}</label>
|
||||
<textarea
|
||||
|
||||
@@ -92,6 +92,7 @@ export type ProviderResourceSelector =
|
||||
|
||||
export interface ProviderResourceFlags {
|
||||
cloakEnabled?: boolean;
|
||||
claudeCodeCliProfile?: boolean;
|
||||
websockets?: boolean;
|
||||
protocols?: string[];
|
||||
}
|
||||
@@ -219,7 +220,7 @@ export interface ProviderEntryFormInput {
|
||||
websockets?: boolean;
|
||||
/** Claude 专属 */
|
||||
cloak?: CloakInput;
|
||||
experimentalCchSigning?: boolean;
|
||||
fingerprintProfile?: string;
|
||||
/** OpenAI persists this; Gemini/Claude use it for one-off connectivity tests. */
|
||||
testModel?: string;
|
||||
apiKeyEntries?: ApiKeyEntryInput[];
|
||||
|
||||
@@ -209,7 +209,7 @@ const buildProviderKeyConfig = (
|
||||
};
|
||||
}
|
||||
if (brand === 'claude') {
|
||||
next.experimentalCchSigning = input.experimentalCchSigning === true;
|
||||
next.fingerprintProfile = input.fingerprintProfile?.trim() || undefined;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
@@ -1490,6 +1490,7 @@
|
||||
},
|
||||
"websocketsTag": "WebSockets",
|
||||
"cloakTag": "Cloak",
|
||||
"cliProfileTag": "CLI Profile",
|
||||
"empty": "No resources yet, click \"New\" to add.",
|
||||
"filterPlaceholder": "Search keys, URLs, prefixes…",
|
||||
"description": "Manage resources under {{route}}"
|
||||
@@ -1648,8 +1649,10 @@
|
||||
},
|
||||
"cloakCacheUserId": "Cache user_id",
|
||||
"cloakCacheUserIdHint": "Reuse the Claude cloak user_id per API key",
|
||||
"experimentalCchSigning": "Experimental CCH signing",
|
||||
"experimentalCchSigningHint": "Sign the final cloaked Claude /v1/messages body with CCH",
|
||||
"fingerprintProfile": "Request fingerprint",
|
||||
"fingerprintProfileDefault": "Default (caller-owned)",
|
||||
"fingerprintProfileClaudeCodeCli": "Claude Code CLI",
|
||||
"fingerprintProfileHint": "Keep the caller's prompt and headers by default, or opt in to the Claude Code CLI request fingerprint.",
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"apiKeyRequired": "At least one API key is required",
|
||||
|
||||
@@ -1468,6 +1468,7 @@
|
||||
},
|
||||
"websocketsTag": "WebSockets",
|
||||
"cloakTag": "Cloak",
|
||||
"cliProfileTag": "CLI-профиль",
|
||||
"empty": "Нет ресурсов, нажмите \"Создать\".",
|
||||
"filterPlaceholder": "Поиск по ключам, URL, префиксам…",
|
||||
"description": "Управление ресурсами {{route}}"
|
||||
@@ -1626,8 +1627,10 @@
|
||||
},
|
||||
"cloakCacheUserId": "Кэшировать user_id",
|
||||
"cloakCacheUserIdHint": "Переиспользовать Claude cloak user_id для каждого API-ключа",
|
||||
"experimentalCchSigning": "Экспериментальная CCH-подпись",
|
||||
"experimentalCchSigningHint": "Подписывать финальное тело cloaked Claude /v1/messages через CCH",
|
||||
"fingerprintProfile": "Отпечаток запроса",
|
||||
"fingerprintProfileDefault": "По умолчанию (управляет клиент)",
|
||||
"fingerprintProfileClaudeCodeCli": "Claude Code CLI",
|
||||
"fingerprintProfileHint": "По умолчанию сохраняются prompt и заголовки клиента; можно включить отпечаток запросов Claude Code CLI.",
|
||||
"validation": {
|
||||
"nameRequired": "Название обязательно",
|
||||
"apiKeyRequired": "Нужен хотя бы один API-ключ",
|
||||
|
||||
@@ -1490,6 +1490,7 @@
|
||||
},
|
||||
"websocketsTag": "WebSockets",
|
||||
"cloakTag": "Cloak",
|
||||
"cliProfileTag": "CLI 指纹",
|
||||
"empty": "尚未添加配置,点击右上角新建。",
|
||||
"filterPlaceholder": "搜索密钥、地址、前缀…",
|
||||
"description": "在 {{route}} 下管理资源"
|
||||
@@ -1648,8 +1649,10 @@
|
||||
},
|
||||
"cloakCacheUserId": "缓存 user_id",
|
||||
"cloakCacheUserIdHint": "按 API key 复用 Claude cloak 生成的 user_id",
|
||||
"experimentalCchSigning": "实验性 CCH 签名",
|
||||
"experimentalCchSigningHint": "对 cloaked Claude /v1/messages 最终请求体启用 CCH 签名",
|
||||
"fingerprintProfile": "请求指纹",
|
||||
"fingerprintProfileDefault": "默认(由调用方控制)",
|
||||
"fingerprintProfileClaudeCodeCli": "Claude Code CLI",
|
||||
"fingerprintProfileHint": "默认保留调用方传入的提示词和请求头;也可选择使用 Claude Code CLI 请求指纹。",
|
||||
"validation": {
|
||||
"nameRequired": "名称必填",
|
||||
"apiKeyRequired": "至少填写一个 API 密钥",
|
||||
|
||||
@@ -1516,6 +1516,7 @@
|
||||
},
|
||||
"websocketsTag": "WebSockets",
|
||||
"cloakTag": "Cloak",
|
||||
"cliProfileTag": "CLI 指紋",
|
||||
"empty": "尚未新增設定,點擊右上角新增。",
|
||||
"filterPlaceholder": "搜尋金鑰、位址、前綴…",
|
||||
"description": "在 {{route}} 下管理資源"
|
||||
@@ -1674,8 +1675,10 @@
|
||||
},
|
||||
"cloakCacheUserId": "快取 user_id",
|
||||
"cloakCacheUserIdHint": "按 API key 複用 Claude cloak 產生的 user_id",
|
||||
"experimentalCchSigning": "實驗性 CCH 簽名",
|
||||
"experimentalCchSigningHint": "對 cloaked Claude /v1/messages 最終請求體啟用 CCH 簽名",
|
||||
"fingerprintProfile": "請求指紋",
|
||||
"fingerprintProfileDefault": "預設(由呼叫端控制)",
|
||||
"fingerprintProfileClaudeCodeCli": "Claude Code CLI",
|
||||
"fingerprintProfileHint": "預設保留呼叫端傳入的提示詞與請求標頭;也可選擇使用 Claude Code CLI 請求指紋。",
|
||||
"validation": {
|
||||
"nameRequired": "名稱必填",
|
||||
"apiKeyRequired": "至少填寫一個 API 金鑰",
|
||||
|
||||
@@ -38,6 +38,8 @@ const XAI_KEY_FIELDS = CODEX_KEY_FIELDS;
|
||||
const CLAUDE_KEY_FIELDS = [
|
||||
...PROVIDER_COMMON_KEY_FIELDS,
|
||||
'cloak',
|
||||
'fingerprint-profile',
|
||||
// Keep stripping the deprecated field when a Claude entry is saved.
|
||||
'experimental-cch-signing',
|
||||
] as const;
|
||||
const VERTEX_KEY_FIELDS = [
|
||||
@@ -352,8 +354,8 @@ const serializeProviderKey = (config: ProviderKeyConfig) => {
|
||||
payload.cloak = cloakPayload;
|
||||
}
|
||||
}
|
||||
if (config.experimentalCchSigning) {
|
||||
payload['experimental-cch-signing'] = true;
|
||||
if (config.fingerprintProfile?.trim()) {
|
||||
payload['fingerprint-profile'] = config.fingerprintProfile.trim();
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -181,9 +181,9 @@ const normalizeProviderKeyConfig = (item: unknown): ProviderKeyConfig | null =>
|
||||
config.cloak = cloak;
|
||||
}
|
||||
}
|
||||
const experimentalCchSigning = normalizeBoolean(record?.['experimental-cch-signing']);
|
||||
if (experimentalCchSigning !== undefined) {
|
||||
config.experimentalCchSigning = experimentalCchSigning;
|
||||
const fingerprintProfile = record?.['fingerprint-profile'];
|
||||
if (typeof fingerprintProfile === 'string' && fingerprintProfile.trim()) {
|
||||
config.fingerprintProfile = fingerprintProfile.trim();
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface ProviderKeyConfig {
|
||||
excludedModels?: string[];
|
||||
disableCooling?: boolean;
|
||||
cloak?: CloakConfig;
|
||||
experimentalCchSigning?: boolean;
|
||||
fingerprintProfile?: string;
|
||||
authIndex?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { claudeApiToResource, claudeToResource } from '../src/features/providers/adapters';
|
||||
import { apiClient } from '../src/services/api/client';
|
||||
import { providersApi } from '../src/services/api/providers';
|
||||
import { normalizeConfigResponse } from '../src/services/api/transformers';
|
||||
|
||||
const originalGet = apiClient.get;
|
||||
const originalPut = apiClient.put;
|
||||
|
||||
const callerOwnedConfig = {
|
||||
apiKey: 'claude-secret',
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
apiClient.get = originalGet;
|
||||
apiClient.put = originalPut;
|
||||
});
|
||||
|
||||
describe('Claude fingerprint profile', () => {
|
||||
test('normalizes the backend field and exposes the CLI profile resource flag', () => {
|
||||
const config = normalizeConfigResponse({
|
||||
'claude-api-key': [
|
||||
{
|
||||
'api-key': 'claude-secret',
|
||||
'base-url': 'https://api.anthropic.com',
|
||||
'fingerprint-profile': 'claude-code-cli',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(config.claudeApiKeys).toEqual([
|
||||
{
|
||||
apiKey: 'claude-secret',
|
||||
baseUrl: 'https://api.anthropic.com',
|
||||
fingerprintProfile: 'claude-code-cli',
|
||||
},
|
||||
]);
|
||||
expect(claudeToResource(config.claudeApiKeys![0], 0).flags.claudeCodeCliProfile).toBe(
|
||||
true
|
||||
);
|
||||
expect(claudeApiToResource(config.claudeApiKeys![0], 0).flags.claudeCodeCliProfile).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test('serializes the opt-in profile when creating a Claude key', async () => {
|
||||
const calls: Array<{ url: string; data?: unknown }> = [];
|
||||
apiClient.get = (async () => ({ 'claude-api-key': [] })) as typeof apiClient.get;
|
||||
apiClient.put = (async (url: string, data?: unknown) => {
|
||||
calls.push({ url, data });
|
||||
return undefined;
|
||||
}) as typeof apiClient.put;
|
||||
|
||||
await providersApi.createClaudeConfig({
|
||||
...callerOwnedConfig,
|
||||
fingerprintProfile: 'claude-code-cli',
|
||||
});
|
||||
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
url: '/claude-api-key',
|
||||
data: [
|
||||
{
|
||||
'api-key': 'claude-secret',
|
||||
'base-url': 'https://api.anthropic.com',
|
||||
'fingerprint-profile': 'claude-code-cli',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('clears the profile and deprecated CCH field when saving caller-owned mode', async () => {
|
||||
const calls: Array<{ url: string; data?: unknown }> = [];
|
||||
apiClient.get = (async () => ({
|
||||
'claude-api-key': [
|
||||
{
|
||||
'api-key': 'claude-secret',
|
||||
'base-url': 'https://api.anthropic.com',
|
||||
'fingerprint-profile': 'claude-code-cli',
|
||||
'experimental-cch-signing': true,
|
||||
'future-field': 'preserved',
|
||||
},
|
||||
],
|
||||
})) as typeof apiClient.get;
|
||||
apiClient.put = (async (url: string, data?: unknown) => {
|
||||
calls.push({ url, data });
|
||||
return undefined;
|
||||
}) as typeof apiClient.put;
|
||||
|
||||
await providersApi.updateClaudeConfig(
|
||||
'claude-secret',
|
||||
'https://api.anthropic.com',
|
||||
callerOwnedConfig
|
||||
);
|
||||
|
||||
expect(calls).toEqual([
|
||||
{
|
||||
url: '/claude-api-key',
|
||||
data: [
|
||||
{
|
||||
'api-key': 'claude-secret',
|
||||
'base-url': 'https://api.anthropic.com',
|
||||
'future-field': 'preserved',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user