export type FingerprintInput = Omit
+export type FingerprintIdentity = AccountFingerprint['identity']
export const fingerprintProviders = new Set(['codex', 'claude'])
export function fetchAccountFingerprint(id: string) {
return request(
@@ -165,11 +172,15 @@ export function fetchAccountFingerprint(id: string) {
export function saveAccountFingerprint(input: {
id: string
profile: FingerprintInput
+ identity: FingerprintIdentity
}) {
return request(
`/api/accounts/${encodeURIComponent(input.id)}/fingerprint`,
fingerprintSchema,
- { method: 'PUT', body: JSON.stringify(input.profile) },
+ {
+ method: 'PUT',
+ body: JSON.stringify({ ...input.profile, ...input.identity }),
+ },
)
}
export function resetAccountFingerprint(input: {
diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json
index adb03038..c0c02338 100644
--- a/web/src/locales/ar.json
+++ b/web/src/locales/ar.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "مضبوطة يدويًا",
"fingerprintSourceNone": "لم تُتعلَّم بعد. سيضبطها العميل الأصلي التالي.",
"fingerprintCodexPlatform": "المنصة والطرفية",
- "fingerprintCodexPlatformHint": "جزء المنصة من User-Agent الخاص بـ Codex، مثل (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. يدير SubLane إصدار Codex.",
+ "fingerprintCodexPlatformHint": "جزء المنصة من User-Agent الخاص بـ Codex، مثل (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. يدير SubLane إصدار Codex. تستخدم الحسابات التي لم يستخدمها عميل أصلي بعد (Ubuntu 22.4.0; x86_64) xterm-256color.",
"fingerprintClaudeOS": "نظام التشغيل",
"fingerprintClaudeArch": "البنية",
"fingerprintRuntimeVersion": "إصدار بيئة التشغيل",
"fingerprintPackageVersion": "إصدار SDK",
"fingerprintClaudeHint": "تُرسل في ترويسات X-Stainless. يُتعلَّم نظام التشغيل والبنية من أول عميل Claude Code، وتتبع إصدارات بيئة التشغيل وSDK إصدار Claude Code المُدار ما لم تُضبط هنا.",
"fingerprintAutomatic": "تلقائي",
+ "fingerprintConvergence": "توحيد المعرّفات",
+ "fingerprintModeOff": "متوقف",
+ "fingerprintModeDevice": "الجهاز",
+ "fingerprintModeSession": "الجلسة",
+ "fingerprintModeFull": "كامل",
+ "fingerprintModeOffHint": "يحتفظ كل عميل بمعرّفات التثبيت والجلسة والسلسلة الخاصة به (يواصل SubLane عزل الجلسات لكل عضو).",
+ "fingerprintModeDeviceHint": "يتشارك جميع العملاء معرّف تثبيت واحدًا، فيرى المزوّد جهازًا واحدًا بعدة جلسات.",
+ "fingerprintModeSessionHint": "جهاز واحد وجلسة واحدة؛ تحتفظ كل محادثة حقيقية بسلسلتها، كأن مستخدمًا واحدًا يفتح عدة نوافذ أو وكلاء فرعيين.",
+ "fingerprintModeFullHint": "جهاز واحد وجلسة واحدة وسلسلة واحدة لكل العملاء. أكثر الأوضاع تشددًا.",
+ "fingerprintConvergenceWarning": "الإيقاف الافتراضي له سبب: عندما فعّل sub2api التوحيد افتراضيًا أبلغ مستخدمون عن تقلص الحصص. فعّله أولاً على حساب واحد وراقب حصته.",
+ "fingerprintTLS": "بصمة TLS الخاصة بـ Claude Code",
+ "fingerprintTLSHint": "تستخدم الاتصالات مصافحة TLS الخاصة بـ Claude Code على Node.js 24 (JA3 44f88fca…) بدلاً من Go، عبر HTTP/1.1 مثل العميل الحقيقي.",
+ "fingerprintInstallation": "معرّف التثبيت",
+ "fingerprintResetInstallation": "إنشاء معرّف تثبيت جديد أيضًا",
"fingerprintDevice": "معرّف الجهاز",
"fingerprintResetDevice": "إنشاء معرّف جهاز جديد أيضًا",
"fingerprintResetDeviceHint": "سيرى المزوّد هذا الحساب على جهاز جديد.",
diff --git a/web/src/locales/de.json b/web/src/locales/de.json
index 00f3825d..596893e6 100644
--- a/web/src/locales/de.json
+++ b/web/src/locales/de.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "Manuell festgelegt",
"fingerprintSourceNone": "Noch nicht gelernt. Der nächste native Client legt ihn fest.",
"fingerprintCodexPlatform": "Plattform und Terminal",
- "fingerprintCodexPlatformHint": "Der Plattformteil des Codex-User-Agents, zum Beispiel (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane verwaltet die Codex-Version.",
+ "fingerprintCodexPlatformHint": "Der Plattformteil des Codex-User-Agents, zum Beispiel (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane verwaltet die Codex-Version. Konten, die noch kein nativer Client genutzt hat, verwenden (Ubuntu 22.4.0; x86_64) xterm-256color.",
"fingerprintClaudeOS": "Betriebssystem",
"fingerprintClaudeArch": "Architektur",
"fingerprintRuntimeVersion": "Laufzeitversion",
"fingerprintPackageVersion": "SDK-Version",
"fingerprintClaudeHint": "Wird als X-Stainless-Header gesendet. Betriebssystem und Architektur werden vom ersten Claude-Code-Client gelernt; Laufzeit- und SDK-Version folgen der verwalteten Claude-Code-Version, sofern hier nichts festgelegt ist.",
"fingerprintAutomatic": "Automatisch",
+ "fingerprintConvergence": "Kennungs-Konvergenz",
+ "fingerprintModeOff": "Aus",
+ "fingerprintModeDevice": "Gerät",
+ "fingerprintModeSession": "Sitzung",
+ "fingerprintModeFull": "Vollständig",
+ "fingerprintModeOffHint": "Jeder Client behält seine eigenen Installations-, Sitzungs- und Thread-Kennungen (SubLane trennt Sitzungen weiterhin pro Mitglied).",
+ "fingerprintModeDeviceHint": "Alle Clients teilen eine Installations-ID, der Anbieter sieht ein Gerät mit mehreren Sitzungen.",
+ "fingerprintModeSessionHint": "Ein Gerät und eine Sitzung; jede echte Unterhaltung behält ihren eigenen Thread, wie ein Nutzer mit mehreren Fenstern oder Subagenten.",
+ "fingerprintModeFullHint": "Ein Gerät, eine Sitzung und ein Thread für alle Clients. Der aggressivste Modus.",
+ "fingerprintConvergenceWarning": "Standardmäßig aus, aus gutem Grund: Als sub2api die Konvergenz standardmäßig aktivierte, meldeten Nutzer schrumpfende Kontingente. Zuerst bei einem Konto aktivieren und dessen Kontingent beobachten.",
+ "fingerprintTLS": "Claude-Code-TLS-Fingerabdruck",
+ "fingerprintTLSHint": "Verbindungen nutzen den TLS-Handshake von Claude Code unter Node.js 24 (JA3 44f88fca…) statt dem von Go, über HTTP/1.1 wie der echte Client.",
+ "fingerprintInstallation": "Installations-ID",
+ "fingerprintResetInstallation": "Auch eine neue Installations-ID erzeugen",
"fingerprintDevice": "Geräte-ID",
"fingerprintResetDevice": "Auch eine neue Geräte-ID erzeugen",
"fingerprintResetDeviceHint": "Der Anbieter sieht dieses Konto auf einem neuen Gerät.",
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index 72d720b7..7aba02fe 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -937,7 +937,7 @@ export const en = {
fingerprintSourceNone: 'Not learned yet. The next native client sets it.',
fingerprintCodexPlatform: 'Platform and terminal',
fingerprintCodexPlatformHint:
- 'The platform part of the Codex User-Agent, for example (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane manages the Codex version.',
+ 'The platform part of the Codex User-Agent, for example (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane manages the Codex version. Accounts no native client has used yet present (Ubuntu 22.4.0; x86_64) xterm-256color.',
fingerprintClaudeOS: 'Operating system',
fingerprintClaudeArch: 'Architecture',
fingerprintRuntimeVersion: 'Runtime version',
@@ -945,6 +945,26 @@ export const en = {
fingerprintClaudeHint:
'Sent as X-Stainless headers. Operating system and architecture are learned from the first Claude Code client; runtime and SDK versions follow the managed Claude Code release unless set here.',
fingerprintAutomatic: 'Automatic',
+ fingerprintConvergence: 'Identifier convergence',
+ fingerprintModeOff: 'Off',
+ fingerprintModeDevice: 'Device',
+ fingerprintModeSession: 'Session',
+ fingerprintModeFull: 'Full',
+ fingerprintModeOffHint:
+ 'Each client keeps its own installation, session and thread identifiers (SubLane still isolates sessions per member).',
+ fingerprintModeDeviceHint:
+ 'All clients share one installation ID, so the provider sees one device with several sessions.',
+ fingerprintModeSessionHint:
+ 'One device and one session; each real conversation keeps its own thread, like one user running several windows or subagents.',
+ fingerprintModeFullHint:
+ 'One device, one session and one thread for every client. The most aggressive mode.',
+ fingerprintConvergenceWarning:
+ 'Off by default for a reason: when sub2api enabled convergence by default, users reported shrinking quotas. Turn it on for one account first and watch its quota.',
+ fingerprintTLS: 'Claude Code TLS fingerprint',
+ fingerprintTLSHint:
+ 'Connections present the TLS handshake of Claude Code on Node.js 24 (JA3 44f88fca…) instead of Go’s, over HTTP/1.1 like the real client.',
+ fingerprintInstallation: 'Installation ID',
+ fingerprintResetInstallation: 'Also generate a new installation ID',
fingerprintDevice: 'Device ID',
fingerprintResetDevice: 'Also generate a new device ID',
fingerprintResetDeviceHint:
diff --git a/web/src/locales/es.json b/web/src/locales/es.json
index 9aa100ed..cef1f3e0 100644
--- a/web/src/locales/es.json
+++ b/web/src/locales/es.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "Configurada manualmente",
"fingerprintSourceNone": "Aún no aprendida. El siguiente cliente nativo la establecerá.",
"fingerprintCodexPlatform": "Plataforma y terminal",
- "fingerprintCodexPlatformHint": "La parte de plataforma del User-Agent de Codex, por ejemplo (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane gestiona la versión de Codex.",
+ "fingerprintCodexPlatformHint": "La parte de plataforma del User-Agent de Codex, por ejemplo (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane gestiona la versión de Codex. Las cuentas que aún no usó ningún cliente nativo presentan (Ubuntu 22.4.0; x86_64) xterm-256color.",
"fingerprintClaudeOS": "Sistema operativo",
"fingerprintClaudeArch": "Arquitectura",
"fingerprintRuntimeVersion": "Versión del entorno de ejecución",
"fingerprintPackageVersion": "Versión del SDK",
"fingerprintClaudeHint": "Se envía como encabezados X-Stainless. El sistema operativo y la arquitectura se aprenden del primer cliente de Claude Code; las versiones del entorno y del SDK siguen la versión gestionada de Claude Code salvo que se indiquen aquí.",
"fingerprintAutomatic": "Automático",
+ "fingerprintConvergence": "Convergencia de identificadores",
+ "fingerprintModeOff": "Desactivada",
+ "fingerprintModeDevice": "Dispositivo",
+ "fingerprintModeSession": "Sesión",
+ "fingerprintModeFull": "Completa",
+ "fingerprintModeOffHint": "Cada cliente conserva sus propios identificadores de instalación, sesión e hilo (SubLane sigue aislando las sesiones por miembro).",
+ "fingerprintModeDeviceHint": "Todos los clientes comparten un ID de instalación, así que el proveedor ve un dispositivo con varias sesiones.",
+ "fingerprintModeSessionHint": "Un dispositivo y una sesión; cada conversación real conserva su propio hilo, como un usuario con varias ventanas o subagentes.",
+ "fingerprintModeFullHint": "Un dispositivo, una sesión y un hilo para todos los clientes. El modo más agresivo.",
+ "fingerprintConvergenceWarning": "Está desactivada por defecto por un motivo: cuando sub2api la activó por defecto, hubo informes de cuotas reducidas. Actívala primero en una cuenta y vigila su cuota.",
+ "fingerprintTLS": "Huella TLS de Claude Code",
+ "fingerprintTLSHint": "Las conexiones presentan el handshake TLS de Claude Code sobre Node.js 24 (JA3 44f88fca…) en lugar del de Go, por HTTP/1.1 como el cliente real.",
+ "fingerprintInstallation": "ID de instalación",
+ "fingerprintResetInstallation": "Generar también un nuevo ID de instalación",
"fingerprintDevice": "ID de dispositivo",
"fingerprintResetDevice": "Generar también un nuevo ID de dispositivo",
"fingerprintResetDeviceHint": "El proveedor verá esta cuenta en un dispositivo nuevo.",
diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json
index 09db1065..e1f2200c 100644
--- a/web/src/locales/fr.json
+++ b/web/src/locales/fr.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "Définie manuellement",
"fingerprintSourceNone": "Pas encore apprise. Le prochain client natif la définira.",
"fingerprintCodexPlatform": "Plateforme et terminal",
- "fingerprintCodexPlatformHint": "La partie plateforme du User-Agent de Codex, par exemple (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane gère la version de Codex.",
+ "fingerprintCodexPlatformHint": "La partie plateforme du User-Agent de Codex, par exemple (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. SubLane gère la version de Codex. Les comptes encore jamais utilisés par un client natif présentent (Ubuntu 22.4.0; x86_64) xterm-256color.",
"fingerprintClaudeOS": "Système d’exploitation",
"fingerprintClaudeArch": "Architecture",
"fingerprintRuntimeVersion": "Version de l’environnement d’exécution",
"fingerprintPackageVersion": "Version du SDK",
"fingerprintClaudeHint": "Envoyée dans les en-têtes X-Stainless. Le système et l’architecture sont appris du premier client Claude Code ; les versions de l’environnement et du SDK suivent la version gérée de Claude Code sauf si elles sont définies ici.",
"fingerprintAutomatic": "Automatique",
+ "fingerprintConvergence": "Convergence des identifiants",
+ "fingerprintModeOff": "Désactivée",
+ "fingerprintModeDevice": "Appareil",
+ "fingerprintModeSession": "Session",
+ "fingerprintModeFull": "Complète",
+ "fingerprintModeOffHint": "Chaque client conserve ses propres identifiants d’installation, de session et de fil (SubLane isole toujours les sessions par membre).",
+ "fingerprintModeDeviceHint": "Tous les clients partagent un identifiant d’installation : le fournisseur voit un appareil avec plusieurs sessions.",
+ "fingerprintModeSessionHint": "Un appareil et une session ; chaque conversation réelle garde son propre fil, comme un utilisateur avec plusieurs fenêtres ou sous-agents.",
+ "fingerprintModeFullHint": "Un appareil, une session et un fil pour tous les clients. Le mode le plus agressif.",
+ "fingerprintConvergenceWarning": "Désactivée par défaut pour une raison : quand sub2api l’a activée par défaut, des utilisateurs ont signalé des quotas réduits. Activez-la d’abord sur un compte et surveillez son quota.",
+ "fingerprintTLS": "Empreinte TLS de Claude Code",
+ "fingerprintTLSHint": "Les connexions présentent la poignée de main TLS de Claude Code sous Node.js 24 (JA3 44f88fca…) au lieu de celle de Go, en HTTP/1.1 comme le vrai client.",
+ "fingerprintInstallation": "ID d’installation",
+ "fingerprintResetInstallation": "Générer aussi un nouvel ID d’installation",
"fingerprintDevice": "ID d’appareil",
"fingerprintResetDevice": "Générer aussi un nouvel ID d’appareil",
"fingerprintResetDeviceHint": "Le fournisseur verra ce compte sur un nouvel appareil.",
diff --git a/web/src/locales/ja.json b/web/src/locales/ja.json
index 0c9b85e6..76534c56 100644
--- a/web/src/locales/ja.json
+++ b/web/src/locales/ja.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "手動設定",
"fingerprintSourceNone": "未学習です。次のネイティブクライアントが設定します。",
"fingerprintCodexPlatform": "プラットフォームとターミナル",
- "fingerprintCodexPlatformHint": "Codex User-Agent のプラットフォーム部分です(例: (Mac OS 15.5.0; arm64) iTerm.app/3.6.1)。Codex のバージョンは SubLane が管理します。",
+ "fingerprintCodexPlatformHint": "Codex User-Agent のプラットフォーム部分です(例: (Mac OS 15.5.0; arm64) iTerm.app/3.6.1)。Codex のバージョンは SubLane が管理します。まだネイティブクライアントが使っていないアカウントは (Ubuntu 22.4.0; x86_64) xterm-256color を使います。",
"fingerprintClaudeOS": "OS",
"fingerprintClaudeArch": "アーキテクチャ",
"fingerprintRuntimeVersion": "ランタイムバージョン",
"fingerprintPackageVersion": "SDK バージョン",
"fingerprintClaudeHint": "X-Stainless ヘッダーとして送信されます。OS とアーキテクチャは最初の Claude Code クライアントから学習し、ランタイムと SDK のバージョンはここで設定しない限り管理された Claude Code リリースに従います。",
"fingerprintAutomatic": "自動",
+ "fingerprintConvergence": "識別子の収束",
+ "fingerprintModeOff": "オフ",
+ "fingerprintModeDevice": "デバイス",
+ "fingerprintModeSession": "セッション",
+ "fingerprintModeFull": "フル",
+ "fingerprintModeOffHint": "各クライアントが自分のインストール ID、セッション ID、スレッド ID を保持します(SubLane はメンバーごとのセッション分離を続けます)。",
+ "fingerprintModeDeviceHint": "全クライアントが 1 つのインストール ID を共有し、上流からは 1 台のデバイスで複数セッションに見えます。",
+ "fingerprintModeSessionHint": "1 台のデバイスと 1 つのセッション。実際の会話ごとに独自のスレッドを持ち、1 人が複数ウィンドウやサブエージェントを使う形に見えます。",
+ "fingerprintModeFullHint": "全クライアントで 1 台のデバイス、1 つのセッション、1 つのスレッド。最も強いモードです。",
+ "fingerprintConvergenceWarning": "既定でオフなのには理由があります。sub2api が収束を既定で有効にした際、クォータが減ったという報告がありました。まず 1 つのアカウントで有効にしてクォータを確認してください。",
+ "fingerprintTLS": "Claude Code の TLS フィンガープリント",
+ "fingerprintTLSHint": "接続時に Go ではなく Node.js 24 上の Claude Code の TLS ハンドシェイク(JA3 44f88fca…)を使い、実クライアントと同じく HTTP/1.1 で通信します。",
+ "fingerprintInstallation": "インストール ID",
+ "fingerprintResetInstallation": "インストール ID も再生成する",
"fingerprintDevice": "デバイス ID",
"fingerprintResetDevice": "デバイス ID も再生成する",
"fingerprintResetDeviceHint": "上流からは新しいデバイスでの利用に見えます。",
diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json
index d4f0283b..52730d1f 100644
--- a/web/src/locales/ko.json
+++ b/web/src/locales/ko.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "수동 설정",
"fingerprintSourceNone": "아직 학습되지 않았습니다. 다음 네이티브 클라이언트가 설정합니다.",
"fingerprintCodexPlatform": "플랫폼 및 터미널",
- "fingerprintCodexPlatformHint": "Codex User-Agent의 플랫폼 부분입니다(예: (Mac OS 15.5.0; arm64) iTerm.app/3.6.1). Codex 버전은 SubLane이 관리합니다.",
+ "fingerprintCodexPlatformHint": "Codex User-Agent의 플랫폼 부분입니다(예: (Mac OS 15.5.0; arm64) iTerm.app/3.6.1). Codex 버전은 SubLane이 관리합니다. 아직 네이티브 클라이언트가 사용하지 않은 계정은 (Ubuntu 22.4.0; x86_64) xterm-256color를 사용합니다.",
"fingerprintClaudeOS": "운영체제",
"fingerprintClaudeArch": "아키텍처",
"fingerprintRuntimeVersion": "런타임 버전",
"fingerprintPackageVersion": "SDK 버전",
"fingerprintClaudeHint": "X-Stainless 헤더로 전송됩니다. 운영체제와 아키텍처는 첫 Claude Code 클라이언트에서 학습하며, 런타임과 SDK 버전은 여기서 설정하지 않으면 관리되는 Claude Code 릴리스를 따릅니다.",
"fingerprintAutomatic": "자동",
+ "fingerprintConvergence": "식별자 수렴",
+ "fingerprintModeOff": "끔",
+ "fingerprintModeDevice": "기기",
+ "fingerprintModeSession": "세션",
+ "fingerprintModeFull": "전체",
+ "fingerprintModeOffHint": "각 클라이언트가 자체 설치 ID, 세션 ID, 스레드 ID를 유지합니다(SubLane은 계속 구성원별로 세션을 분리합니다).",
+ "fingerprintModeDeviceHint": "모든 클라이언트가 설치 ID 하나를 공유하여 업스트림에는 기기 하나에 세션 여러 개로 보입니다.",
+ "fingerprintModeSessionHint": "기기 하나와 세션 하나. 실제 대화마다 고유한 스레드를 유지해, 한 사용자가 여러 창이나 하위 에이전트를 쓰는 것처럼 보입니다.",
+ "fingerprintModeFullHint": "모든 클라이언트가 기기 하나, 세션 하나, 스레드 하나를 씁니다. 가장 강한 모드입니다.",
+ "fingerprintConvergenceWarning": "기본값이 꺼짐인 데는 이유가 있습니다. sub2api가 수렴을 기본으로 켰을 때 할당량이 줄었다는 보고가 있었습니다. 먼저 계정 하나에서 켜고 할당량을 지켜보세요.",
+ "fingerprintTLS": "Claude Code TLS 지문",
+ "fingerprintTLSHint": "연결할 때 Go 대신 Node.js 24에서 실행되는 Claude Code의 TLS 핸드셰이크(JA3 44f88fca…)를 사용하며, 실제 클라이언트처럼 HTTP/1.1로 통신합니다.",
+ "fingerprintInstallation": "설치 ID",
+ "fingerprintResetInstallation": "설치 ID도 새로 생성",
"fingerprintDevice": "기기 ID",
"fingerprintResetDevice": "기기 ID도 새로 생성",
"fingerprintResetDeviceHint": "업스트림에서는 이 계정이 새 기기에서 사용되는 것으로 보입니다.",
diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json
index e8ab49df..6784554b 100644
--- a/web/src/locales/pt.json
+++ b/web/src/locales/pt.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "Definida manualmente",
"fingerprintSourceNone": "Ainda não aprendida. O próximo cliente nativo vai defini-la.",
"fingerprintCodexPlatform": "Plataforma e terminal",
- "fingerprintCodexPlatformHint": "A parte de plataforma do User-Agent do Codex, por exemplo (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. O SubLane gerencia a versão do Codex.",
+ "fingerprintCodexPlatformHint": "A parte de plataforma do User-Agent do Codex, por exemplo (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. O SubLane gerencia a versão do Codex. Contas ainda não usadas por um cliente nativo apresentam (Ubuntu 22.4.0; x86_64) xterm-256color.",
"fingerprintClaudeOS": "Sistema operacional",
"fingerprintClaudeArch": "Arquitetura",
"fingerprintRuntimeVersion": "Versão do runtime",
"fingerprintPackageVersion": "Versão do SDK",
"fingerprintClaudeHint": "Enviada nos cabeçalhos X-Stainless. Sistema operacional e arquitetura são aprendidos do primeiro cliente Claude Code; as versões de runtime e SDK seguem a versão gerenciada do Claude Code, a menos que sejam definidas aqui.",
"fingerprintAutomatic": "Automático",
+ "fingerprintConvergence": "Convergência de identificadores",
+ "fingerprintModeOff": "Desligada",
+ "fingerprintModeDevice": "Dispositivo",
+ "fingerprintModeSession": "Sessão",
+ "fingerprintModeFull": "Completa",
+ "fingerprintModeOffHint": "Cada cliente mantém seus próprios identificadores de instalação, sessão e thread (o SubLane continua isolando sessões por membro).",
+ "fingerprintModeDeviceHint": "Todos os clientes compartilham um ID de instalação, então o provedor vê um dispositivo com várias sessões.",
+ "fingerprintModeSessionHint": "Um dispositivo e uma sessão; cada conversa real mantém sua própria thread, como um usuário com várias janelas ou subagentes.",
+ "fingerprintModeFullHint": "Um dispositivo, uma sessão e uma thread para todos os clientes. O modo mais agressivo.",
+ "fingerprintConvergenceWarning": "Desligada por padrão por um motivo: quando o sub2api a ativou por padrão, houve relatos de cotas reduzidas. Ative primeiro em uma conta e acompanhe a cota.",
+ "fingerprintTLS": "Impressão TLS do Claude Code",
+ "fingerprintTLSHint": "As conexões usam o handshake TLS do Claude Code no Node.js 24 (JA3 44f88fca…) em vez do Go, via HTTP/1.1 como o cliente real.",
+ "fingerprintInstallation": "ID de instalação",
+ "fingerprintResetInstallation": "Gerar também um novo ID de instalação",
"fingerprintDevice": "ID do dispositivo",
"fingerprintResetDevice": "Gerar também um novo ID de dispositivo",
"fingerprintResetDeviceHint": "O provedor verá esta conta em um novo dispositivo.",
diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json
index 552ff724..a7e1b884 100644
--- a/web/src/locales/ru.json
+++ b/web/src/locales/ru.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "Задан вручную",
"fingerprintSourceNone": "Ещё не получен. Его задаст следующий нативный клиент.",
"fingerprintCodexPlatform": "Платформа и терминал",
- "fingerprintCodexPlatformHint": "Часть User-Agent Codex с платформой, например (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. Версией Codex управляет SubLane.",
+ "fingerprintCodexPlatformHint": "Часть User-Agent Codex с платформой, например (Mac OS 15.5.0; arm64) iTerm.app/3.6.1. Версией Codex управляет SubLane. Аккаунты, которые ещё не использовал нативный клиент, используют (Ubuntu 22.4.0; x86_64) xterm-256color.",
"fingerprintClaudeOS": "Операционная система",
"fingerprintClaudeArch": "Архитектура",
"fingerprintRuntimeVersion": "Версия среды выполнения",
"fingerprintPackageVersion": "Версия SDK",
"fingerprintClaudeHint": "Отправляется в заголовках X-Stainless. ОС и архитектура берутся у первого клиента Claude Code; версии среды выполнения и SDK следуют управляемому выпуску Claude Code, если не заданы здесь.",
"fingerprintAutomatic": "Автоматически",
+ "fingerprintConvergence": "Сведение идентификаторов",
+ "fingerprintModeOff": "Выкл.",
+ "fingerprintModeDevice": "Устройство",
+ "fingerprintModeSession": "Сессия",
+ "fingerprintModeFull": "Полное",
+ "fingerprintModeOffHint": "Каждый клиент сохраняет свои идентификаторы установки, сессии и потока (SubLane по-прежнему изолирует сессии по участникам).",
+ "fingerprintModeDeviceHint": "Все клиенты используют один ID установки, и провайдер видит одно устройство с несколькими сессиями.",
+ "fingerprintModeSessionHint": "Одно устройство и одна сессия; каждый реальный диалог получает свой поток, как у пользователя с несколькими окнами или субагентами.",
+ "fingerprintModeFullHint": "Одно устройство, одна сессия и один поток для всех клиентов. Самый агрессивный режим.",
+ "fingerprintConvergenceWarning": "По умолчанию выключено не случайно: когда sub2api включил сведение по умолчанию, пользователи сообщали об уменьшении квоты. Сначала включите на одном аккаунте и следите за квотой.",
+ "fingerprintTLS": "TLS-отпечаток Claude Code",
+ "fingerprintTLSHint": "Соединения используют TLS-рукопожатие Claude Code на Node.js 24 (JA3 44f88fca…) вместо Go и работают по HTTP/1.1, как настоящий клиент.",
+ "fingerprintInstallation": "ID установки",
+ "fingerprintResetInstallation": "Также создать новый ID установки",
"fingerprintDevice": "ID устройства",
"fingerprintResetDevice": "Также создать новый ID устройства",
"fingerprintResetDeviceHint": "Провайдер увидит этот аккаунт на новом устройстве.",
diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json
index ae091bb3..11e6eea8 100644
--- a/web/src/locales/zh-TW.json
+++ b/web/src/locales/zh-TW.json
@@ -605,13 +605,27 @@
"fingerprintSourceManual": "手動設定",
"fingerprintSourceNone": "尚未學習,下一個原生用戶端會設定它。",
"fingerprintCodexPlatform": "平台與終端",
- "fingerprintCodexPlatformHint": "Codex User-Agent 中的平台部分,例如 (Mac OS 15.5.0; arm64) iTerm.app/3.6.1。Codex 版本號由 SubLane 統一管理。",
+ "fingerprintCodexPlatformHint": "Codex User-Agent 中的平台部分,例如 (Mac OS 15.5.0; arm64) iTerm.app/3.6.1。Codex 版本號由 SubLane 統一管理。尚未有原生用戶端使用過的帳號使用 (Ubuntu 22.4.0; x86_64) xterm-256color。",
"fingerprintClaudeOS": "作業系統",
"fingerprintClaudeArch": "架構",
"fingerprintRuntimeVersion": "執行環境版本",
"fingerprintPackageVersion": "SDK 版本",
"fingerprintClaudeHint": "以 X-Stainless 標頭傳送。作業系統和架構從第一個 Claude Code 用戶端學習;執行環境和 SDK 版本跟隨 SubLane 管理的 Claude Code 版本,除非在此手動設定。",
"fingerprintAutomatic": "自動",
+ "fingerprintConvergence": "識別碼收斂",
+ "fingerprintModeOff": "關閉",
+ "fingerprintModeDevice": "裝置",
+ "fingerprintModeSession": "工作階段",
+ "fingerprintModeFull": "完全",
+ "fingerprintModeOffHint": "每個用戶端保留自己的安裝 ID、工作階段 ID 和執行緒 ID(SubLane 仍依成員隔離工作階段)。",
+ "fingerprintModeDeviceHint": "所有用戶端共用一個安裝 ID,上游看到一台裝置、多個工作階段。",
+ "fingerprintModeSessionHint": "一台裝置、一個工作階段;每個真實對話保留自己的執行緒,就像一個使用者開了多個視窗或子代理。",
+ "fingerprintModeFullHint": "所有用戶端共用一台裝置、一個工作階段、一個執行緒,最激進的模式。",
+ "fingerprintConvergenceWarning": "預設關閉是有原因的:sub2api 曾把收斂設為預設開啟,之後有使用者回報額度縮水。建議先在一個帳號上開啟並觀察額度。",
+ "fingerprintTLS": "Claude Code TLS 指紋",
+ "fingerprintTLSHint": "連線時使用 Node.js 24 上的 Claude Code 的 TLS 握手(JA3 44f88fca…),而非 Go 的握手,並像真實用戶端一樣使用 HTTP/1.1。",
+ "fingerprintInstallation": "安裝 ID",
+ "fingerprintResetInstallation": "同時重新產生安裝 ID",
"fingerprintDevice": "裝置 ID",
"fingerprintResetDevice": "同時重新產生裝置 ID",
"fingerprintResetDeviceHint": "上游會把此帳號視為在一台新裝置上登入。",
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index 0db4cf1a..dc2b1def 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -856,7 +856,7 @@ export const zh: Record = {
fingerprintSourceNone: '尚未学习,下一个原生客户端会设置它。',
fingerprintCodexPlatform: '平台与终端',
fingerprintCodexPlatformHint:
- 'Codex User-Agent 中的平台部分,例如 (Mac OS 15.5.0; arm64) iTerm.app/3.6.1。Codex 版本号由 SubLane 统一管理。',
+ 'Codex User-Agent 中的平台部分,例如 (Mac OS 15.5.0; arm64) iTerm.app/3.6.1。Codex 版本号由 SubLane 统一管理。还没有原生客户端使用过的账号使用 (Ubuntu 22.4.0; x86_64) xterm-256color。',
fingerprintClaudeOS: '操作系统',
fingerprintClaudeArch: '架构',
fingerprintRuntimeVersion: '运行时版本',
@@ -864,6 +864,26 @@ export const zh: Record = {
fingerprintClaudeHint:
'以 X-Stainless 请求头发送。操作系统和架构从第一个 Claude Code 客户端学习;运行时和 SDK 版本跟随 SubLane 管理的 Claude Code 版本,除非在此手动设置。',
fingerprintAutomatic: '自动',
+ fingerprintConvergence: '标识收敛',
+ fingerprintModeOff: '关闭',
+ fingerprintModeDevice: '设备',
+ fingerprintModeSession: '会话',
+ fingerprintModeFull: '完全',
+ fingerprintModeOffHint:
+ '每个客户端保留自己的安装 ID、会话 ID 和线程 ID(SubLane 仍按成员隔离会话)。',
+ fingerprintModeDeviceHint:
+ '所有客户端共用一个安装 ID,上游看到一台设备、多个会话。',
+ fingerprintModeSessionHint:
+ '一台设备、一个会话;每个真实对话保留自己的线程,就像一个用户开了多个窗口或子代理。',
+ fingerprintModeFullHint:
+ '所有客户端共用一台设备、一个会话、一个线程,最激进的模式。',
+ fingerprintConvergenceWarning:
+ '默认关闭是有原因的:sub2api 曾把收敛设为默认开启,随后有用户报告额度缩水。建议先在一个账号上开启并观察额度。',
+ fingerprintTLS: 'Claude Code TLS 指纹',
+ fingerprintTLSHint:
+ '连接时使用 Node.js 24 上的 Claude Code 的 TLS 握手(JA3 44f88fca…),而不是 Go 的握手,并像真实客户端一样走 HTTP/1.1。',
+ fingerprintInstallation: '安装 ID',
+ fingerprintResetInstallation: '同时重新生成安装 ID',
fingerprintDevice: '设备 ID',
fingerprintResetDevice: '同时重新生成设备 ID',
fingerprintResetDeviceHint: '上游会把此账号视为在一台新设备上登录。',
From 4d8615111cef2a6034c3b425df81296d1fa4de1f Mon Sep 17 00:00:00 2001
From: Alumos <1281610535@qq.com>
Date: Thu, 12 Nov 2026 23:45:48 +0800
Subject: [PATCH 2/2] fix: non-blocking overload peek, Codex convergence on by
default
- Peek at most 100 ms for an in-stream overload rejection so slow streams
keep keepalive and idle-timeout behavior (fixes hanging server tests).
- Codex accounts converge identifiers in session mode by default, seeding
existing accounts in migration 039 and new ones on first use; session
mode keeps one prompt cache key per real conversation.
- Dial the TLS fingerprint test by name so SNI is sent.
Co-Authored-By: Claude Opus 5.5
---
PRODUCT.md | 2 +-
internal/accounts/fingerprint.go | 18 ++-
internal/accounts/refresh.go | 5 +-
.../migrations/039_client_identity.sql | 11 +-
internal/upstream/codex_identity.go | 119 ++++++++++++++----
internal/upstream/identity_test.go | 7 +-
web/src/locales/ar.json | 4 +-
web/src/locales/de.json | 4 +-
web/src/locales/en.ts | 4 +-
web/src/locales/es.json | 4 +-
web/src/locales/fr.json | 4 +-
web/src/locales/ja.json | 4 +-
web/src/locales/ko.json | 4 +-
web/src/locales/pt.json | 4 +-
web/src/locales/ru.json | 4 +-
web/src/locales/zh-TW.json | 4 +-
web/src/locales/zh.ts | 4 +-
17 files changed, 149 insertions(+), 57 deletions(-)
diff --git a/PRODUCT.md b/PRODUCT.md
index 8f190419..12170283 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -111,7 +111,7 @@ Resource groups also offer account failover, enabled by default for new and exis
Each upstream account presents one client fingerprint, following CLIProxyAPI; different accounts may present different fingerprints. The profile is stored on the account (`client_profile`) and is learned from the first native client that uses it: Codex keeps that client's User-Agent platform and terminal, and Claude keeps the operating system and architecture its Claude Code client reports in X-Stainless headers. Every later client, on any machine or tool, presents the pinned values. Client versions stay under SubLane's version policy: Codex uses the managed Codex version, and Claude runtime and SDK versions follow the SDK's measured baseline for the managed Claude Code release. Claude accounts also keep one durable device ID, and the SDK's device-profile stabilization keeps the remaining Claude headers consistent per account. Administrators can view, set or clear an account's fingerprint from the account menu (validated values only; empty fields fall back to learning or defaults) and can give a Claude account a new device ID; changes are audited.
-Identity hardening follows sub2api's production-tested rules. Codex requests always present one self-consistent client: originator, User-Agent product and `Version` agree and follow the managed Codex release (the backend rejects mismatches and versions below 0.144.0), and accounts that have not learned a native client present the canonical `(Ubuntu 22.4.0; x86_64) xterm-256color` platform instead of a platform-less agent. The identity headers native clients send (installation, window and turn-state headers and turn metadata) are forwarded, and every identifier carrier is kept consistent with the outgoing session: the `Session_id` header, turn metadata, `client_metadata` and `prompt_cache_key`. Codex accounts can opt in to identifier convergence (`device`: one installation ID; `session`: also one session ID with one thread per real conversation; `full`: one thread), derived from a per-account random seed that a device reset rotates. Convergence is off by default because sub2api users reported shrinking quotas when it was on by default. When the Codex backend sheds load by client identity it answers HTTP 200 with `server_is_overloaded` or `slow_down` as the first stream event; SubLane detects that before anything reaches the client and turns it into a retryable 503 that the gateway retries and, through cooldowns, fails over. Claude accounts can opt in to Claude Code's TLS fingerprint: connections, including through HTTP or SOCKS5 proxies, perform a Node.js 24 ClientHello (JA3 `44f88fca027f27bab4bb08d4af15f23e`, as captured by sub2api) over HTTP/1.1 using uTLS.
+Identity hardening follows sub2api's production-tested rules. Codex requests always present one self-consistent client: originator, User-Agent product and `Version` agree and follow the managed Codex release (the backend rejects mismatches and versions below 0.144.0), and accounts that have not learned a native client present the canonical `(Ubuntu 22.4.0; x86_64) xterm-256color` platform instead of a platform-less agent. The identity headers native clients send (installation, window and turn-state headers and turn metadata) are forwarded, and every identifier carrier is kept consistent with the outgoing session: the `Session_id` header, turn metadata, `client_metadata` and `prompt_cache_key`. Codex accounts converge identifiers by default in `session` mode (one installation ID and one session ID, with one thread per real conversation), sub2api's recommended non-off mode; administrators can switch an account to `off`, `device` (one installation ID only) or `full` (one thread). Identifiers derive from a per-account random seed that a device reset rotates. sub2api users reported shrinking quotas with convergence; collapsing every conversation into one prompt cache key is a likely cause, so `session` mode keeps one `prompt_cache_key` per real conversation (its thread) and only `full` mode collapses it. When the Codex backend sheds load by client identity it answers HTTP 200 with `server_is_overloaded` or `slow_down` as the first stream event; SubLane checks the first stream events for at most 100 ms (so slow-starting streams keep their keepalive and idle-timeout policies), and when the rejection is there, before anything reaches the client, turns it into a retryable 503 that the gateway retries and, through cooldowns, fails over. Claude accounts can opt in to Claude Code's TLS fingerprint: connections, including through HTTP or SOCKS5 proxies, perform a Node.js 24 ClientHello (JA3 `44f88fca027f27bab4bb08d4af15f23e`, as captured by sub2api) over HTTP/1.1 using uTLS.
Provider requests whose connection fails before any byte is sent (DNS, TCP dial or proxy CONNECT) are retried up to twice after 250 ms and 1 s on the same account; failures after a request may have reached the provider are never replayed.
diff --git a/internal/accounts/fingerprint.go b/internal/accounts/fingerprint.go
index e01d6102..aa2ad206 100644
--- a/internal/accounts/fingerprint.go
+++ b/internal/accounts/fingerprint.go
@@ -192,12 +192,28 @@ func identityOf(row db.Account) ClientIdentity {
if row.FingerprintSeed != nil {
identity.Seed = *row.FingerprintSeed
}
- if !convergenceModes[identity.Mode] {
+ // Convergence only exists for Codex identifiers; other providers always report off.
+ if !convergenceModes[identity.Mode] || row.Provider != "codex" {
identity.Mode = "off"
}
return identity
}
+// ensureSeed gives a converging Codex account its seed on first use, so every creation path
+// (authorization, import, restore) converges without a separate setup step.
+func (s *Service) ensureSeed(ctx context.Context, row db.Account, identity ClientIdentity) (ClientIdentity, error) {
+ if row.Provider != "codex" || identity.Mode == "off" || identity.Seed != "" {
+ return identity, nil
+ }
+ seed, err := newSeed()
+ if err != nil {
+ return identity, err
+ }
+ identity.Seed = seed
+ _, err = s.queries.SetClientIdentity(ctx, db.SetClientIdentityParams{ID: row.ID, TenantID: s.tenantID, FingerprintMode: row.FingerprintMode, FingerprintSeed: &seed, TlsFingerprint: row.TlsFingerprint, UpdatedAt: row.UpdatedAt})
+ return identity, err
+}
+
func newSeed() (string, error) {
raw := make([]byte, 16)
if _, err := rand.Read(raw); err != nil {
diff --git a/internal/accounts/refresh.go b/internal/accounts/refresh.go
index 46d92c95..ec94b25d 100644
--- a/internal/accounts/refresh.go
+++ b/internal/accounts/refresh.go
@@ -73,7 +73,10 @@ func (s *Service) prepare(ctx context.Context, id, rejectedToken string, refresh
return Credential{}, err
}
}
- credential.Profile, credential.Identity = decodeProfile(row.ClientProfile), identityOf(row)
+ credential.Profile = decodeProfile(row.ClientProfile)
+ if credential.Identity, err = s.ensureSeed(ctx, row, identityOf(row)); err != nil {
+ return Credential{}, err
+ }
if row.ProxyID != nil {
credential.ProxyURL, err = s.proxyURL(ctx, *row.ProxyID)
if err != nil {
diff --git a/internal/storage/migrations/039_client_identity.sql b/internal/storage/migrations/039_client_identity.sql
index ead82e67..aa50b755 100644
--- a/internal/storage/migrations/039_client_identity.sql
+++ b/internal/storage/migrations/039_client_identity.sql
@@ -1,6 +1,9 @@
--- Opt-in identity hardening per account, following sub2api. fingerprint_mode converges Codex device
--- and session identifiers (off, device, session, full); fingerprint_seed is the system-managed random
--- seed they derive from; tls_fingerprint makes Claude connections present Claude Code's TLS handshake.
-ALTER TABLE accounts ADD COLUMN fingerprint_mode TEXT NOT NULL DEFAULT 'off' CHECK(fingerprint_mode IN ('off','device','session','full'));
+-- Identity hardening per account, following sub2api. fingerprint_mode converges Codex device and
+-- session identifiers (off, device, session, full) and defaults to session, sub2api's recommended
+-- non-off mode; fingerprint_seed is the system-managed random seed converged identifiers derive from;
+-- tls_fingerprint makes Claude connections present Claude Code's TLS handshake (opt-in).
+ALTER TABLE accounts ADD COLUMN fingerprint_mode TEXT NOT NULL DEFAULT 'session' CHECK(fingerprint_mode IN ('off','device','session','full'));
ALTER TABLE accounts ADD COLUMN fingerprint_seed TEXT CHECK(fingerprint_seed IS NULL OR length(fingerprint_seed) = 36);
ALTER TABLE accounts ADD COLUMN tls_fingerprint INTEGER NOT NULL DEFAULT 0 CHECK(tls_fingerprint IN (0,1));
+-- Existing Codex accounts get their own seed now; newer accounts are seeded on first use.
+UPDATE accounts SET fingerprint_seed = lower(hex(randomblob(18))) WHERE provider = 'codex';
diff --git a/internal/upstream/codex_identity.go b/internal/upstream/codex_identity.go
index 2012da29..f20a3bf3 100644
--- a/internal/upstream/codex_identity.go
+++ b/internal/upstream/codex_identity.go
@@ -9,6 +9,7 @@ import (
"io"
"net/http"
"strings"
+ "sync"
"time"
"unicode/utf16"
@@ -24,10 +25,15 @@ import (
type codexIdentity struct {
headers http.Header
fields map[string]any
- // session is the outgoing session ID that prompt_cache_key and client_metadata must match.
+ // session is the outgoing session ID that client_metadata and turn metadata must match.
session string
- mode string
- ids map[string]string
+ // cacheKey is the outgoing prompt_cache_key: the conversation's thread when convergence keeps
+ // one, otherwise the session. Session mode deliberately keeps one cache key per real
+ // conversation: collapsing every conversation into one key (as full mode does) defeats the
+ // provider's prompt cache and spends more uncached tokens.
+ cacheKey string
+ mode string
+ ids map[string]string
}
// Identity headers Codex sends on Responses requests; everything else from the client is dropped.
@@ -63,11 +69,15 @@ func newCodexIdentity(credential accounts.Credential, client http.Header) *codex
id.fields["thread_id"], id.fields["turn_id"], id.fields["window_id"] = thread, turn, window
id.fields["turn_started_at_unix_ms"] = time.Now().UnixMilli()
id.ids["thread_id"], id.ids["turn_id"], id.ids["x-codex-window-id"] = thread, turn, window
+ id.cacheKey = thread
}
}
if conversation != "" {
id.headers.Set("Session_id", conversation)
id.fields["session_id"], id.ids["session_id"], id.session = conversation, conversation, conversation
+ if id.cacheKey == "" {
+ id.cacheKey = conversation
+ }
}
if raw := id.headers.Get("X-Codex-Turn-Metadata"); raw != "" {
if rebuilt, ok := rewriteTurnMetadata(raw, id.fields); ok {
@@ -104,9 +114,9 @@ func (id *codexIdentity) rewriteBody(body []byte) []byte {
return body
}
changed := false
- // Native Codex sends its session ID as prompt_cache_key.
- if id.session != "" && len(input["prompt_cache_key"]) > 0 {
- input["prompt_cache_key"], _ = json.Marshal(id.session)
+ // Native Codex sends its conversation ID as prompt_cache_key.
+ if id.cacheKey != "" && len(input["prompt_cache_key"]) > 0 {
+ input["prompt_cache_key"], _ = json.Marshal(id.cacheKey)
changed = true
}
var metadata map[string]any
@@ -172,16 +182,86 @@ func randomUUID() string {
// When capacity is short, the Codex backend sheds load by client identity: it answers HTTP 200 and
// the stream's first real event reports server_is_overloaded (or slow_down). Nothing has reached the
// client yet, so the response becomes a retryable 503 that the gateway retries and fails over.
+//
+// The rejection arrives with the response headers, so the peek waits at most overloadPeekWindow:
+// a stream that is merely slow to start reaches the gateway at once, keeping its keepalive and
+// idle-timeout policies intact.
const overloadPeekLimit = 64 << 10
+var overloadPeekWindow = 100 * time.Millisecond
+
var codexOverloadCodes = map[string]bool{"server_is_overloaded": true, "slow_down": true}
+type streamChunk struct {
+ data []byte
+ err error
+}
+
+// peekedBody replays what the peek consumed, then continues from the background reader.
+type peekedBody struct {
+ pending []byte
+ err error
+ chunks <-chan streamChunk
+ stop chan struct{}
+ once sync.Once
+ body io.Closer
+}
+
+func (b *peekedBody) Read(p []byte) (int, error) {
+ for len(b.pending) == 0 {
+ if b.err != nil {
+ return 0, b.err
+ }
+ chunk := <-b.chunks
+ b.pending, b.err = chunk.data, chunk.err
+ }
+ n := copy(p, b.pending)
+ b.pending = b.pending[n:]
+ return n, nil
+}
+
+func (b *peekedBody) Close() error {
+ b.once.Do(func() { close(b.stop) })
+ return b.body.Close()
+}
+
func detectCodexOverload(response *http.Response) (*http.Response, bool) {
- reader := bufio.NewReaderSize(response.Body, 4096)
+ chunks, stop := make(chan streamChunk), make(chan struct{})
+ go func(reader *bufio.Reader) {
+ for {
+ line, err := reader.ReadBytes('\n')
+ select {
+ case chunks <- streamChunk{line, err}:
+ case <-stop:
+ return
+ }
+ if err != nil {
+ return
+ }
+ }
+ }(bufio.NewReaderSize(response.Body, 4096))
+ body := &peekedBody{chunks: chunks, stop: stop, body: response.Body}
var seen bytes.Buffer
+ timer := time.NewTimer(overloadPeekWindow)
+ defer timer.Stop()
+ finish := func() (*http.Response, bool) {
+ body.pending = seen.Bytes()
+ response.Body = body
+ return response, false
+ }
for seen.Len() < overloadPeekLimit {
- line, err := reader.ReadBytes('\n')
- seen.Write(line)
+ var line []byte
+ select {
+ case chunk := <-chunks:
+ seen.Write(chunk.data)
+ if chunk.err != nil {
+ body.err = chunk.err
+ return finish()
+ }
+ line = chunk.data
+ case <-timer.C:
+ return finish()
+ }
if data, ok := bytes.CutPrefix(bytes.TrimSpace(line), []byte("data:")); ok {
var event struct {
Type string `json:"type"`
@@ -201,29 +281,16 @@ func detectCodexOverload(response *http.Response) (*http.Response, bool) {
// Handshake events carry nothing the client needs before the first real event.
case "error", "response.failed":
if codexOverloadCodes[event.Code] || codexOverloadCodes[event.Error.Code] || codexOverloadCodes[event.Response.Error.Code] {
- response.Body.Close()
+ body.Close()
return &http.Response{StatusCode: http.StatusServiceUnavailable, Status: "503 Service Unavailable", Header: http.Header{"Retry-After": {"1"}, "Content-Type": {"application/json"}},
Body: io.NopCloser(strings.NewReader(`{"error":{"code":"server_is_overloaded"}}`)), Request: response.Request}, true
}
- return replayBody(response, &seen, reader), false
+ return finish()
default:
- return replayBody(response, &seen, reader), false
+ return finish()
}
}
}
- if err != nil {
- break
- }
}
- return replayBody(response, &seen, reader), false
-}
-
-func replayBody(response *http.Response, seen *bytes.Buffer, rest io.Reader) *http.Response {
- response.Body = &replayedBody{Reader: io.MultiReader(bytes.NewReader(seen.Bytes()), rest), Closer: response.Body}
- return response
-}
-
-type replayedBody struct {
- io.Reader
- io.Closer
+ return finish()
}
diff --git a/internal/upstream/identity_test.go b/internal/upstream/identity_test.go
index 95323149..13cdfa1e 100644
--- a/internal/upstream/identity_test.go
+++ b/internal/upstream/identity_test.go
@@ -84,7 +84,8 @@ func TestCodexIdentityConvergesEveryCarrier(t *testing.T) {
if metadata["session_id"] != session || metadata["thread_id"] != thread || metadata["window_id"] != thread+":0" || metadata["turn_id"] == "" {
t.Fatal("turn metadata not converged", metadata)
}
- if !strings.Contains(body, `"prompt_cache_key":"`+session+`"`) || !strings.Contains(body, `"thread_id":"`+thread+`"`) {
+ // Each real conversation keeps its own cache key (its thread); only full mode collapses it.
+ if !strings.Contains(body, `"prompt_cache_key":"`+thread+`"`) || !strings.Contains(body, `"thread_id":"`+thread+`"`) {
t.Fatal("body session identifiers not converged", body)
}
})
@@ -153,7 +154,9 @@ func captureHello(t *testing.T) (string, <-chan *tls.ClientHelloInfo) {
}}
server.StartTLS()
t.Cleanup(server.Close)
- return server.Listener.Addr().String(), hellos
+ // TLS never sends SNI for an IP address, so dial by name as a real client does.
+ _, port, _ := net.SplitHostPort(server.Listener.Addr().String())
+ return net.JoinHostPort("localhost", port), hellos
}
func TestClaudeTLSFingerprintMatchesClaudeCode(t *testing.T) {
diff --git a/web/src/locales/ar.json b/web/src/locales/ar.json
index c0c02338..a92516ca 100644
--- a/web/src/locales/ar.json
+++ b/web/src/locales/ar.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "توحيد المعرّفات",
"fingerprintModeOff": "متوقف",
"fingerprintModeDevice": "الجهاز",
- "fingerprintModeSession": "الجلسة",
+ "fingerprintModeSession": "الجلسة (افتراضي)",
"fingerprintModeFull": "كامل",
"fingerprintModeOffHint": "يحتفظ كل عميل بمعرّفات التثبيت والجلسة والسلسلة الخاصة به (يواصل SubLane عزل الجلسات لكل عضو).",
"fingerprintModeDeviceHint": "يتشارك جميع العملاء معرّف تثبيت واحدًا، فيرى المزوّد جهازًا واحدًا بعدة جلسات.",
"fingerprintModeSessionHint": "جهاز واحد وجلسة واحدة؛ تحتفظ كل محادثة حقيقية بسلسلتها، كأن مستخدمًا واحدًا يفتح عدة نوافذ أو وكلاء فرعيين.",
"fingerprintModeFullHint": "جهاز واحد وجلسة واحدة وسلسلة واحدة لكل العملاء. أكثر الأوضاع تشددًا.",
- "fingerprintConvergenceWarning": "الإيقاف الافتراضي له سبب: عندما فعّل sub2api التوحيد افتراضيًا أبلغ مستخدمون عن تقلص الحصص. فعّله أولاً على حساب واحد وراقب حصته.",
+ "fingerprintConvergenceWarning": "التوحيد مفعّل افتراضيًا (وضع الجلسة). أبلغ مستخدمو sub2api عن تقلص الحصص مع التوحيد، والسبب المرجّح هو مفتاح ذاكرة تخزين مؤقت واحد لكل المحادثات؛ لذا يحتفظ وضع الجلسة بمفتاح لكل محادثة حقيقية، ولا يدمجها إلا الوضع الكامل. إذا استُهلكت حصة حساب بسرعة غير معتادة فغيّره إلى الجهاز أو متوقف.",
"fingerprintTLS": "بصمة TLS الخاصة بـ Claude Code",
"fingerprintTLSHint": "تستخدم الاتصالات مصافحة TLS الخاصة بـ Claude Code على Node.js 24 (JA3 44f88fca…) بدلاً من Go، عبر HTTP/1.1 مثل العميل الحقيقي.",
"fingerprintInstallation": "معرّف التثبيت",
diff --git a/web/src/locales/de.json b/web/src/locales/de.json
index 596893e6..1adb1b9f 100644
--- a/web/src/locales/de.json
+++ b/web/src/locales/de.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "Kennungs-Konvergenz",
"fingerprintModeOff": "Aus",
"fingerprintModeDevice": "Gerät",
- "fingerprintModeSession": "Sitzung",
+ "fingerprintModeSession": "Sitzung (Standard)",
"fingerprintModeFull": "Vollständig",
"fingerprintModeOffHint": "Jeder Client behält seine eigenen Installations-, Sitzungs- und Thread-Kennungen (SubLane trennt Sitzungen weiterhin pro Mitglied).",
"fingerprintModeDeviceHint": "Alle Clients teilen eine Installations-ID, der Anbieter sieht ein Gerät mit mehreren Sitzungen.",
"fingerprintModeSessionHint": "Ein Gerät und eine Sitzung; jede echte Unterhaltung behält ihren eigenen Thread, wie ein Nutzer mit mehreren Fenstern oder Subagenten.",
"fingerprintModeFullHint": "Ein Gerät, eine Sitzung und ein Thread für alle Clients. Der aggressivste Modus.",
- "fingerprintConvergenceWarning": "Standardmäßig aus, aus gutem Grund: Als sub2api die Konvergenz standardmäßig aktivierte, meldeten Nutzer schrumpfende Kontingente. Zuerst bei einem Konto aktivieren und dessen Kontingent beobachten.",
+ "fingerprintConvergenceWarning": "Konvergenz ist standardmäßig aktiv (Sitzungsmodus). sub2api-Nutzer meldeten mit Konvergenz schrumpfende Kontingente; eine wahrscheinliche Ursache ist ein einziger Prompt-Cache-Schlüssel für alle Unterhaltungen. Der Sitzungsmodus behält daher pro echter Unterhaltung einen Schlüssel, nur der vollständige Modus fasst sie zusammen. Sinkt das Kontingent eines Kontos ungewöhnlich schnell, auf Gerät oder Aus stellen.",
"fingerprintTLS": "Claude-Code-TLS-Fingerabdruck",
"fingerprintTLSHint": "Verbindungen nutzen den TLS-Handshake von Claude Code unter Node.js 24 (JA3 44f88fca…) statt dem von Go, über HTTP/1.1 wie der echte Client.",
"fingerprintInstallation": "Installations-ID",
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index 7aba02fe..dee97385 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -948,7 +948,7 @@ export const en = {
fingerprintConvergence: 'Identifier convergence',
fingerprintModeOff: 'Off',
fingerprintModeDevice: 'Device',
- fingerprintModeSession: 'Session',
+ fingerprintModeSession: 'Session (default)',
fingerprintModeFull: 'Full',
fingerprintModeOffHint:
'Each client keeps its own installation, session and thread identifiers (SubLane still isolates sessions per member).',
@@ -959,7 +959,7 @@ export const en = {
fingerprintModeFullHint:
'One device, one session and one thread for every client. The most aggressive mode.',
fingerprintConvergenceWarning:
- 'Off by default for a reason: when sub2api enabled convergence by default, users reported shrinking quotas. Turn it on for one account first and watch its quota.',
+ 'Convergence is on by default (session mode). sub2api users reported shrinking quotas with convergence; a likely cause is one prompt cache key for every conversation, so session mode keeps one cache key per real conversation and only full mode collapses it. If an account’s quota drains unusually fast, switch it to device or off.',
fingerprintTLS: 'Claude Code TLS fingerprint',
fingerprintTLSHint:
'Connections present the TLS handshake of Claude Code on Node.js 24 (JA3 44f88fca…) instead of Go’s, over HTTP/1.1 like the real client.',
diff --git a/web/src/locales/es.json b/web/src/locales/es.json
index cef1f3e0..45493596 100644
--- a/web/src/locales/es.json
+++ b/web/src/locales/es.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "Convergencia de identificadores",
"fingerprintModeOff": "Desactivada",
"fingerprintModeDevice": "Dispositivo",
- "fingerprintModeSession": "Sesión",
+ "fingerprintModeSession": "Sesión (predeterminado)",
"fingerprintModeFull": "Completa",
"fingerprintModeOffHint": "Cada cliente conserva sus propios identificadores de instalación, sesión e hilo (SubLane sigue aislando las sesiones por miembro).",
"fingerprintModeDeviceHint": "Todos los clientes comparten un ID de instalación, así que el proveedor ve un dispositivo con varias sesiones.",
"fingerprintModeSessionHint": "Un dispositivo y una sesión; cada conversación real conserva su propio hilo, como un usuario con varias ventanas o subagentes.",
"fingerprintModeFullHint": "Un dispositivo, una sesión y un hilo para todos los clientes. El modo más agresivo.",
- "fingerprintConvergenceWarning": "Está desactivada por defecto por un motivo: cuando sub2api la activó por defecto, hubo informes de cuotas reducidas. Actívala primero en una cuenta y vigila su cuota.",
+ "fingerprintConvergenceWarning": "La convergencia está activada por defecto (modo sesión). Usuarios de sub2api informaron cuotas reducidas con convergencia; una causa probable es una sola clave de caché de prompt para todas las conversaciones, por eso el modo sesión mantiene una clave por conversación real y solo el modo completo las une. Si la cuota de una cuenta baja demasiado rápido, cámbiala a dispositivo o desactivada.",
"fingerprintTLS": "Huella TLS de Claude Code",
"fingerprintTLSHint": "Las conexiones presentan el handshake TLS de Claude Code sobre Node.js 24 (JA3 44f88fca…) en lugar del de Go, por HTTP/1.1 como el cliente real.",
"fingerprintInstallation": "ID de instalación",
diff --git a/web/src/locales/fr.json b/web/src/locales/fr.json
index e1f2200c..aaac414d 100644
--- a/web/src/locales/fr.json
+++ b/web/src/locales/fr.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "Convergence des identifiants",
"fingerprintModeOff": "Désactivée",
"fingerprintModeDevice": "Appareil",
- "fingerprintModeSession": "Session",
+ "fingerprintModeSession": "Session (par défaut)",
"fingerprintModeFull": "Complète",
"fingerprintModeOffHint": "Chaque client conserve ses propres identifiants d’installation, de session et de fil (SubLane isole toujours les sessions par membre).",
"fingerprintModeDeviceHint": "Tous les clients partagent un identifiant d’installation : le fournisseur voit un appareil avec plusieurs sessions.",
"fingerprintModeSessionHint": "Un appareil et une session ; chaque conversation réelle garde son propre fil, comme un utilisateur avec plusieurs fenêtres ou sous-agents.",
"fingerprintModeFullHint": "Un appareil, une session et un fil pour tous les clients. Le mode le plus agressif.",
- "fingerprintConvergenceWarning": "Désactivée par défaut pour une raison : quand sub2api l’a activée par défaut, des utilisateurs ont signalé des quotas réduits. Activez-la d’abord sur un compte et surveillez son quota.",
+ "fingerprintConvergenceWarning": "La convergence est activée par défaut (mode session). Des utilisateurs de sub2api ont signalé des quotas réduits avec la convergence ; une cause probable est une seule clé de cache de prompt pour toutes les conversations. Le mode session garde donc une clé par conversation réelle et seul le mode complet les fusionne. Si le quota d’un compte baisse anormalement vite, passez-le en appareil ou désactivé.",
"fingerprintTLS": "Empreinte TLS de Claude Code",
"fingerprintTLSHint": "Les connexions présentent la poignée de main TLS de Claude Code sous Node.js 24 (JA3 44f88fca…) au lieu de celle de Go, en HTTP/1.1 comme le vrai client.",
"fingerprintInstallation": "ID d’installation",
diff --git a/web/src/locales/ja.json b/web/src/locales/ja.json
index 76534c56..465c9e7a 100644
--- a/web/src/locales/ja.json
+++ b/web/src/locales/ja.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "識別子の収束",
"fingerprintModeOff": "オフ",
"fingerprintModeDevice": "デバイス",
- "fingerprintModeSession": "セッション",
+ "fingerprintModeSession": "セッション(既定)",
"fingerprintModeFull": "フル",
"fingerprintModeOffHint": "各クライアントが自分のインストール ID、セッション ID、スレッド ID を保持します(SubLane はメンバーごとのセッション分離を続けます)。",
"fingerprintModeDeviceHint": "全クライアントが 1 つのインストール ID を共有し、上流からは 1 台のデバイスで複数セッションに見えます。",
"fingerprintModeSessionHint": "1 台のデバイスと 1 つのセッション。実際の会話ごとに独自のスレッドを持ち、1 人が複数ウィンドウやサブエージェントを使う形に見えます。",
"fingerprintModeFullHint": "全クライアントで 1 台のデバイス、1 つのセッション、1 つのスレッド。最も強いモードです。",
- "fingerprintConvergenceWarning": "既定でオフなのには理由があります。sub2api が収束を既定で有効にした際、クォータが減ったという報告がありました。まず 1 つのアカウントで有効にしてクォータを確認してください。",
+ "fingerprintConvergenceWarning": "収束は既定で有効です(セッションモード)。sub2api のユーザーから収束時にクォータが減ったという報告があり、全会話で 1 つのプロンプトキャッシュキーを使うことが原因と考えられます。そのためセッションモードは実際の会話ごとにキャッシュキーを分け、フルモードだけがまとめます。クォータの減りが異常に速い場合はデバイスまたはオフにしてください。",
"fingerprintTLS": "Claude Code の TLS フィンガープリント",
"fingerprintTLSHint": "接続時に Go ではなく Node.js 24 上の Claude Code の TLS ハンドシェイク(JA3 44f88fca…)を使い、実クライアントと同じく HTTP/1.1 で通信します。",
"fingerprintInstallation": "インストール ID",
diff --git a/web/src/locales/ko.json b/web/src/locales/ko.json
index 52730d1f..8e99926f 100644
--- a/web/src/locales/ko.json
+++ b/web/src/locales/ko.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "식별자 수렴",
"fingerprintModeOff": "끔",
"fingerprintModeDevice": "기기",
- "fingerprintModeSession": "세션",
+ "fingerprintModeSession": "세션(기본)",
"fingerprintModeFull": "전체",
"fingerprintModeOffHint": "각 클라이언트가 자체 설치 ID, 세션 ID, 스레드 ID를 유지합니다(SubLane은 계속 구성원별로 세션을 분리합니다).",
"fingerprintModeDeviceHint": "모든 클라이언트가 설치 ID 하나를 공유하여 업스트림에는 기기 하나에 세션 여러 개로 보입니다.",
"fingerprintModeSessionHint": "기기 하나와 세션 하나. 실제 대화마다 고유한 스레드를 유지해, 한 사용자가 여러 창이나 하위 에이전트를 쓰는 것처럼 보입니다.",
"fingerprintModeFullHint": "모든 클라이언트가 기기 하나, 세션 하나, 스레드 하나를 씁니다. 가장 강한 모드입니다.",
- "fingerprintConvergenceWarning": "기본값이 꺼짐인 데는 이유가 있습니다. sub2api가 수렴을 기본으로 켰을 때 할당량이 줄었다는 보고가 있었습니다. 먼저 계정 하나에서 켜고 할당량을 지켜보세요.",
+ "fingerprintConvergenceWarning": "수렴은 기본으로 켜져 있습니다(세션 모드). sub2api 사용자들이 수렴 시 할당량이 줄었다고 보고했는데, 모든 대화가 하나의 프롬프트 캐시 키를 쓰는 것이 원인으로 보입니다. 그래서 세션 모드는 실제 대화마다 캐시 키를 따로 유지하고 전체 모드만 합칩니다. 계정 할당량이 비정상적으로 빨리 줄면 기기 또는 끔으로 바꾸세요.",
"fingerprintTLS": "Claude Code TLS 지문",
"fingerprintTLSHint": "연결할 때 Go 대신 Node.js 24에서 실행되는 Claude Code의 TLS 핸드셰이크(JA3 44f88fca…)를 사용하며, 실제 클라이언트처럼 HTTP/1.1로 통신합니다.",
"fingerprintInstallation": "설치 ID",
diff --git a/web/src/locales/pt.json b/web/src/locales/pt.json
index 6784554b..be8bd47e 100644
--- a/web/src/locales/pt.json
+++ b/web/src/locales/pt.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "Convergência de identificadores",
"fingerprintModeOff": "Desligada",
"fingerprintModeDevice": "Dispositivo",
- "fingerprintModeSession": "Sessão",
+ "fingerprintModeSession": "Sessão (padrão)",
"fingerprintModeFull": "Completa",
"fingerprintModeOffHint": "Cada cliente mantém seus próprios identificadores de instalação, sessão e thread (o SubLane continua isolando sessões por membro).",
"fingerprintModeDeviceHint": "Todos os clientes compartilham um ID de instalação, então o provedor vê um dispositivo com várias sessões.",
"fingerprintModeSessionHint": "Um dispositivo e uma sessão; cada conversa real mantém sua própria thread, como um usuário com várias janelas ou subagentes.",
"fingerprintModeFullHint": "Um dispositivo, uma sessão e uma thread para todos os clientes. O modo mais agressivo.",
- "fingerprintConvergenceWarning": "Desligada por padrão por um motivo: quando o sub2api a ativou por padrão, houve relatos de cotas reduzidas. Ative primeiro em uma conta e acompanhe a cota.",
+ "fingerprintConvergenceWarning": "A convergência vem ativada por padrão (modo sessão). Usuários do sub2api relataram cotas reduzidas com convergência; uma causa provável é uma única chave de cache de prompt para todas as conversas, então o modo sessão mantém uma chave por conversa real e só o modo completo as junta. Se a cota de uma conta cair rápido demais, mude para dispositivo ou desligada.",
"fingerprintTLS": "Impressão TLS do Claude Code",
"fingerprintTLSHint": "As conexões usam o handshake TLS do Claude Code no Node.js 24 (JA3 44f88fca…) em vez do Go, via HTTP/1.1 como o cliente real.",
"fingerprintInstallation": "ID de instalação",
diff --git a/web/src/locales/ru.json b/web/src/locales/ru.json
index a7e1b884..d99e26cd 100644
--- a/web/src/locales/ru.json
+++ b/web/src/locales/ru.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "Сведение идентификаторов",
"fingerprintModeOff": "Выкл.",
"fingerprintModeDevice": "Устройство",
- "fingerprintModeSession": "Сессия",
+ "fingerprintModeSession": "Сессия (по умолчанию)",
"fingerprintModeFull": "Полное",
"fingerprintModeOffHint": "Каждый клиент сохраняет свои идентификаторы установки, сессии и потока (SubLane по-прежнему изолирует сессии по участникам).",
"fingerprintModeDeviceHint": "Все клиенты используют один ID установки, и провайдер видит одно устройство с несколькими сессиями.",
"fingerprintModeSessionHint": "Одно устройство и одна сессия; каждый реальный диалог получает свой поток, как у пользователя с несколькими окнами или субагентами.",
"fingerprintModeFullHint": "Одно устройство, одна сессия и один поток для всех клиентов. Самый агрессивный режим.",
- "fingerprintConvergenceWarning": "По умолчанию выключено не случайно: когда sub2api включил сведение по умолчанию, пользователи сообщали об уменьшении квоты. Сначала включите на одном аккаунте и следите за квотой.",
+ "fingerprintConvergenceWarning": "Сведение включено по умолчанию (режим сессии). Пользователи sub2api сообщали об уменьшении квоты при сведении; вероятная причина — один ключ кэша промптов для всех диалогов. Поэтому режим сессии сохраняет отдельный ключ для каждого реального диалога, и только полный режим их объединяет. Если квота аккаунта тает слишком быстро, переключите его на «Устройство» или «Выкл.».",
"fingerprintTLS": "TLS-отпечаток Claude Code",
"fingerprintTLSHint": "Соединения используют TLS-рукопожатие Claude Code на Node.js 24 (JA3 44f88fca…) вместо Go и работают по HTTP/1.1, как настоящий клиент.",
"fingerprintInstallation": "ID установки",
diff --git a/web/src/locales/zh-TW.json b/web/src/locales/zh-TW.json
index 11e6eea8..20f4d0d0 100644
--- a/web/src/locales/zh-TW.json
+++ b/web/src/locales/zh-TW.json
@@ -615,13 +615,13 @@
"fingerprintConvergence": "識別碼收斂",
"fingerprintModeOff": "關閉",
"fingerprintModeDevice": "裝置",
- "fingerprintModeSession": "工作階段",
+ "fingerprintModeSession": "工作階段(預設)",
"fingerprintModeFull": "完全",
"fingerprintModeOffHint": "每個用戶端保留自己的安裝 ID、工作階段 ID 和執行緒 ID(SubLane 仍依成員隔離工作階段)。",
"fingerprintModeDeviceHint": "所有用戶端共用一個安裝 ID,上游看到一台裝置、多個工作階段。",
"fingerprintModeSessionHint": "一台裝置、一個工作階段;每個真實對話保留自己的執行緒,就像一個使用者開了多個視窗或子代理。",
"fingerprintModeFullHint": "所有用戶端共用一台裝置、一個工作階段、一個執行緒,最激進的模式。",
- "fingerprintConvergenceWarning": "預設關閉是有原因的:sub2api 曾把收斂設為預設開啟,之後有使用者回報額度縮水。建議先在一個帳號上開啟並觀察額度。",
+ "fingerprintConvergenceWarning": "收斂預設開啟(工作階段模式)。sub2api 使用者曾回報開啟收斂後額度縮水,可能原因是所有對話共用一個提示詞快取鍵;因此工作階段模式為每個真實對話保留獨立的快取鍵,只有完全模式才會合併。若某個帳號額度消耗異常快,可改為「裝置」或「關閉」。",
"fingerprintTLS": "Claude Code TLS 指紋",
"fingerprintTLSHint": "連線時使用 Node.js 24 上的 Claude Code 的 TLS 握手(JA3 44f88fca…),而非 Go 的握手,並像真實用戶端一樣使用 HTTP/1.1。",
"fingerprintInstallation": "安裝 ID",
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index dc2b1def..d9059041 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -867,7 +867,7 @@ export const zh: Record = {
fingerprintConvergence: '标识收敛',
fingerprintModeOff: '关闭',
fingerprintModeDevice: '设备',
- fingerprintModeSession: '会话',
+ fingerprintModeSession: '会话(默认)',
fingerprintModeFull: '完全',
fingerprintModeOffHint:
'每个客户端保留自己的安装 ID、会话 ID 和线程 ID(SubLane 仍按成员隔离会话)。',
@@ -878,7 +878,7 @@ export const zh: Record = {
fingerprintModeFullHint:
'所有客户端共用一台设备、一个会话、一个线程,最激进的模式。',
fingerprintConvergenceWarning:
- '默认关闭是有原因的:sub2api 曾把收敛设为默认开启,随后有用户报告额度缩水。建议先在一个账号上开启并观察额度。',
+ '收敛默认开启(会话模式)。sub2api 用户曾报告开启收敛后额度缩水,可能原因是所有对话共用一个提示词缓存键;因此会话模式为每个真实对话保留独立的缓存键,只有完全模式才会合并。如果某个账号额度消耗异常快,可改为“设备”或“关闭”。',
fingerprintTLS: 'Claude Code TLS 指纹',
fingerprintTLSHint:
'连接时使用 Node.js 24 上的 Claude Code 的 TLS 握手(JA3 44f88fca…),而不是 Go 的握手,并像真实客户端一样走 HTTP/1.1。',