feat(provider): add fingerprint profile support and update related logic

This commit is contained in:
Supra4E8C
2026-08-27 13:52:28 +08:00
parent 966abff9e9
commit d249ff008e
13 changed files with 183 additions and 35 deletions
+3 -2
View File
@@ -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
+2 -1
View File
@@ -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;
};
+5 -2
View File
@@ -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",
+5 -2
View File
@@ -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-ключ",
+5 -2
View File
@@ -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 密钥",
+5 -2
View File
@@ -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 金鑰",
+4 -2
View File
@@ -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;
};
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -53,7 +53,7 @@ export interface ProviderKeyConfig {
excludedModels?: string[];
disableCooling?: boolean;
cloak?: CloakConfig;
experimentalCchSigning?: boolean;
fingerprintProfile?: string;
authIndex?: string;
}
+111
View File
@@ -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',
},
],
},
]);
});
});