From c7f4a649dfa9322179e9e9d75196cba0dc6d8f86 Mon Sep 17 00:00:00 2001 From: Wang Lvyuan <74089601+LvyuanW@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:54:59 +0800 Subject: [PATCH 01/26] fix(admin): use custom select for ops log filters --- .../ops/components/OpsSystemLogTable.vue | 63 +++++++++++-------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/frontend/src/views/admin/ops/components/OpsSystemLogTable.vue b/frontend/src/views/admin/ops/components/OpsSystemLogTable.vue index d2aeb3ca..bfc9397d 100644 --- a/frontend/src/views/admin/ops/components/OpsSystemLogTable.vue +++ b/frontend/src/views/admin/ops/components/OpsSystemLogTable.vue @@ -2,6 +2,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue' import { opsAPI, type OpsRuntimeLogConfig, type OpsSystemLog, type OpsSystemLogSinkHealth } from '@/api/admin/ops' import Pagination from '@/components/common/Pagination.vue' +import Select from '@/components/common/Select.vue' import { useAppStore } from '@/stores' const appStore = useAppStore() @@ -56,6 +57,37 @@ const filters = reactive({ q: '' }) +const runtimeLevelOptions = [ + { value: 'debug', label: 'debug' }, + { value: 'info', label: 'info' }, + { value: 'warn', label: 'warn' }, + { value: 'error', label: 'error' } +] + +const stacktraceLevelOptions = [ + { value: 'none', label: 'none' }, + { value: 'error', label: 'error' }, + { value: 'fatal', label: 'fatal' } +] + +const timeRangeOptions = [ + { value: '5m', label: '5m' }, + { value: '30m', label: '30m' }, + { value: '1h', label: '1h' }, + { value: '6h', label: '6h' }, + { value: '24h', label: '24h' }, + { value: '7d', label: '7d' }, + { value: '30d', label: '30d' } +] + +const filterLevelOptions = [ + { value: '', label: '全部' }, + { value: 'debug', label: 'debug' }, + { value: 'info', label: 'info' }, + { value: 'warn', label: 'warn' }, + { value: 'error', label: 'error' } +] + const levelBadgeClass = (level: string) => { const v = String(level || '').toLowerCase() if (v === 'error' || v === 'fatal') return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' @@ -347,20 +379,11 @@ onMounted(async () => {
+
@@ -3095,6 +3130,8 @@ const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([]) const sessionIdMaskingEnabled = ref(false) const cacheTTLOverrideEnabled = ref(false) const cacheTTLOverrideTarget = ref('5m') +const customBaseUrlEnabled = ref(false) +const customBaseUrl = ref('') // Gemini tier selection (used as fallback when auto-detection is unavailable/fails) const geminiTierGoogleOne = ref<'google_one_free' | 'google_ai_pro' | 'google_ai_ultra'>('google_one_free') @@ -3765,6 +3802,8 @@ const resetForm = () => { sessionIdMaskingEnabled.value = false cacheTTLOverrideEnabled.value = false cacheTTLOverrideTarget.value = '5m' + customBaseUrlEnabled.value = false + customBaseUrl.value = '' allowOverages.value = false antigravityAccountType.value = 'oauth' upstreamBaseUrl.value = '' @@ -4856,6 +4895,12 @@ const handleAnthropicExchange = async (authCode: string) => { extra.cache_ttl_override_target = cacheTTLOverrideTarget.value } + // Add custom base URL settings + if (customBaseUrlEnabled.value && customBaseUrl.value.trim()) { + extra.custom_base_url_enabled = true + extra.custom_base_url = customBaseUrl.value.trim() + } + const credentials: Record = { ...tokenInfo } applyInterceptWarmup(credentials, interceptWarmupRequests.value, 'create') await createAccountAndFinish(form.platform, addMethod.value as AccountType, credentials, extra) @@ -4974,6 +5019,12 @@ const handleCookieAuth = async (sessionKey: string) => { extra.cache_ttl_override_target = cacheTTLOverrideTarget.value } + // Add custom base URL settings + if (customBaseUrlEnabled.value && customBaseUrl.value.trim()) { + extra.custom_base_url_enabled = true + extra.custom_base_url = customBaseUrl.value.trim() + } + const accountName = keys.length > 1 ? `${form.name} #${i + 1}` : form.name const credentials: Record = { ...tokenInfo } diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index da6c9715..607e7a69 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -1580,6 +1580,41 @@

+ + +
+
+
+ +

+ {{ t('admin.accounts.quotaControl.customBaseUrl.hint') }} +

+
+ +
+
+ +
+
@@ -1854,6 +1889,8 @@ const tlsFingerprintProfiles = ref<{ id: number; name: string }[]>([]) const sessionIdMaskingEnabled = ref(false) const cacheTTLOverrideEnabled = ref(false) const cacheTTLOverrideTarget = ref('5m') +const customBaseUrlEnabled = ref(false) +const customBaseUrl = ref('') // OpenAI 自动透传开关(OAuth/API Key) const openaiPassthroughEnabled = ref(false) @@ -2482,6 +2519,8 @@ function loadQuotaControlSettings(account: Account) { sessionIdMaskingEnabled.value = false cacheTTLOverrideEnabled.value = false cacheTTLOverrideTarget.value = '5m' + customBaseUrlEnabled.value = false + customBaseUrl.value = '' // Only applies to Anthropic OAuth/SetupToken accounts if (account.platform !== 'anthropic' || (account.type !== 'oauth' && account.type !== 'setup-token')) { @@ -2528,6 +2567,12 @@ function loadQuotaControlSettings(account: Account) { cacheTTLOverrideEnabled.value = true cacheTTLOverrideTarget.value = account.cache_ttl_override_target || '5m' } + + // Load custom base URL setting + if (account.custom_base_url_enabled === true) { + customBaseUrlEnabled.value = true + customBaseUrl.value = account.custom_base_url || '' + } } function formatTempUnschedKeywords(value: unknown) { @@ -2980,6 +3025,15 @@ const handleSubmit = async () => { delete newExtra.cache_ttl_override_target } + // Custom base URL relay setting + if (customBaseUrlEnabled.value && customBaseUrl.value.trim()) { + newExtra.custom_base_url_enabled = true + newExtra.custom_base_url = customBaseUrl.value.trim() + } else { + delete newExtra.custom_base_url_enabled + delete newExtra.custom_base_url + } + updatePayload.extra = newExtra } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 07a0e634..d1f55e58 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2318,6 +2318,11 @@ export default { target: 'Target TTL', targetHint: 'Select the TTL tier for billing' }, + customBaseUrl: { + label: 'Custom Relay URL', + hint: 'Forward requests to a custom relay service. Proxy URL will be passed as a query parameter.', + urlHint: 'Relay service URL (e.g., https://relay.example.com)', + }, clientAffinity: { label: 'Client Affinity Scheduling', hint: 'When enabled, new sessions prefer accounts previously used by this client to reduce account switching' diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index a6b6e8b5..55634bd8 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -2462,6 +2462,11 @@ export default { target: '目标 TTL', targetHint: '选择计费使用的 TTL 类型' }, + customBaseUrl: { + label: '自定义转发地址', + hint: '启用后将请求转发到自定义中继服务,代理地址将作为 URL 参数传递给中继服务', + urlHint: '中继服务地址(如 https://relay.example.com)', + }, clientAffinity: { label: '客户端亲和调度', hint: '启用后,新会话会优先调度到该客户端之前使用过的账号,避免频繁切换账号' diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 8ab48216..f9425ad0 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -734,6 +734,10 @@ export interface Account { cache_ttl_override_enabled?: boolean | null cache_ttl_override_target?: string | null + // 自定义 Base URL 中继转发(仅 Anthropic OAuth/SetupToken 账号有效) + custom_base_url_enabled?: boolean | null + custom_base_url?: string | null + // 客户端亲和调度(仅 Anthropic/Antigravity 平台有效) // 启用后新会话会优先调度到客户端之前使用过的账号 client_affinity_enabled?: boolean | null From 61607990c82c920851e24b1603e901720629ab38 Mon Sep 17 00:00:00 2001 From: QTom Date: Mon, 30 Mar 2026 10:32:59 +0800 Subject: [PATCH 20/26] =?UTF-8?q?fix(lifecycle):=20TokenRefreshService=20S?= =?UTF-8?q?top()=20=E9=98=B2=E9=87=8D=E5=A4=8D=20close?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 sync.Once 包裹 close(stopCh),避免多次调用 Stop() 时 触发 panic: close of closed channel。 --- backend/internal/service/token_refresh_service.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/internal/service/token_refresh_service.go b/backend/internal/service/token_refresh_service.go index eb3e5592..8f949382 100644 --- a/backend/internal/service/token_refresh_service.go +++ b/backend/internal/service/token_refresh_service.go @@ -32,8 +32,9 @@ type TokenRefreshService struct { privacyClientFactory PrivacyClientFactory proxyRepo ProxyRepository - stopCh chan struct{} - wg sync.WaitGroup + stopCh chan struct{} + stopOnce sync.Once + wg sync.WaitGroup } // NewTokenRefreshService 创建token刷新服务 @@ -130,7 +131,9 @@ func (s *TokenRefreshService) Start() { // Stop 停止刷新服务(可安全多次调用) func (s *TokenRefreshService) Stop() { - close(s.stopCh) + s.stopOnce.Do(func() { + close(s.stopCh) + }) s.wg.Wait() slog.Info("token_refresh.service_stopped") } From ab3e44e4bd23265fdaaf74121db3e5f4df4458a8 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 30 Mar 2026 11:28:27 +0800 Subject: [PATCH 21/26] =?UTF-8?q?fix:=20=E9=80=82=E9=85=8DX-Claude-Code-Se?= =?UTF-8?q?ssion-Id=E5=A4=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/service/gateway_service.go | 20 ++++++++++++++++++++ backend/internal/service/header_util.go | 8 ++++++++ 2 files changed, 28 insertions(+) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 44214b65..b54f463b 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -369,6 +369,8 @@ var allowedHeaders = map[string]bool{ "user-agent": true, "content-type": true, "accept-encoding": true, + "x-claude-code-session-id": true, + "x-client-request-id": true, } // GatewayCache 定义网关服务的缓存操作接口。 @@ -5756,6 +5758,15 @@ func (s *GatewayService) buildUpstreamRequest(ctx context.Context, c *gin.Contex } } + // 同步 X-Claude-Code-Session-Id 头:取 body 中已处理的 metadata.user_id 的 session_id 覆盖 + if sessionHeader := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id"); sessionHeader != "" { + if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" { + if parsed := ParseMetadataUserID(uid); parsed != nil { + setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", parsed.SessionID) + } + } + } + // === DEBUG: 打印上游转发请求(headers + body 摘要),与 CLIENT_ORIGINAL 对比 === s.debugLogGatewaySnapshot("UPSTREAM_FORWARD", req.Header, body, map[string]string{ "url": req.URL.String(), @@ -8475,6 +8486,15 @@ func (s *GatewayService) buildCountTokensRequest(ctx context.Context, c *gin.Con } } + // 同步 X-Claude-Code-Session-Id 头:取 body 中已处理的 metadata.user_id 的 session_id 覆盖 + if sessionHeader := getHeaderRaw(req.Header, "X-Claude-Code-Session-Id"); sessionHeader != "" { + if uid := gjson.GetBytes(body, "metadata.user_id").String(); uid != "" { + if parsed := ParseMetadataUserID(uid); parsed != nil { + setHeaderRaw(req.Header, "X-Claude-Code-Session-Id", parsed.SessionID) + } + } + } + if c != nil && tokenType == "oauth" { c.Set(claudeMimicDebugInfoKey, buildClaudeMimicDebugLine(req, body, account, tokenType, mimicClaudeCode)) } diff --git a/backend/internal/service/header_util.go b/backend/internal/service/header_util.go index 6acfee5a..1091070d 100644 --- a/backend/internal/service/header_util.go +++ b/backend/internal/service/header_util.go @@ -36,6 +36,11 @@ var headerWireCasing = map[string]string{ "sec-fetch-mode": "sec-fetch-mode", "accept-encoding": "accept-encoding", "authorization": "authorization", + + // Claude Code 2.1.87+ 新增 header + "x-claude-code-session-id": "X-Claude-Code-Session-Id", + "x-client-request-id": "x-client-request-id", + "content-length": "content-length", } // headerWireOrder 定义真实 Claude CLI 发送 header 的顺序(基于抓包)。 @@ -55,11 +60,14 @@ var headerWireOrder = []string{ "authorization", "x-app", "User-Agent", + "X-Claude-Code-Session-Id", "content-type", "anthropic-beta", + "x-client-request-id", "accept-language", "sec-fetch-mode", "accept-encoding", + "content-length", "x-stainless-helper-method", } From 50288e6b01b806dc2ba57f31bbe5c499b50728be Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 30 Mar 2026 15:36:53 +0800 Subject: [PATCH 22/26] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E4=BB=B7=E6=96=87=E4=BB=B6=E6=9B=B4=E6=96=B0?= =?UTF-8?q?url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/config/config.go | 4 +- backend/internal/service/pricing_service.go | 96 +++++++++++++-------- deploy/config.example.yaml | 4 +- 3 files changed, 62 insertions(+), 42 deletions(-) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index d1cb76db..3ee5d6cd 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -1281,8 +1281,8 @@ func setDefaults() { viper.SetDefault("rate_limit.oauth_401_cooldown_minutes", 10) // Pricing - 从 model-price-repo 同步模型定价和上下文窗口数据(固定到 commit,避免分支漂移) - viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/c7947e9871687e664180bc971d4837f1fc2784a9/model_prices_and_context_window.json") - viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/c7947e9871687e664180bc971d4837f1fc2784a9/model_prices_and_context_window.sha256") + viper.SetDefault("pricing.remote_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.json") + viper.SetDefault("pricing.hash_url", "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/main/model_prices_and_context_window.sha256") viper.SetDefault("pricing.data_dir", "./data") viper.SetDefault("pricing.fallback_file", "./resources/model-pricing/model_prices_and_context_window.json") viper.SetDefault("pricing.update_interval_hours", 24) diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index 10440c60..5623d4b7 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -189,10 +189,38 @@ func (s *PricingService) checkAndUpdatePricing() error { return s.downloadPricingData() } - // 检查文件是否过期 + // 先加载本地文件(确保服务可用),再检查是否需要更新 + if err := s.loadPricingData(pricingFile); err != nil { + logger.LegacyPrintf("service.pricing", "[Pricing] Failed to load local file, downloading: %v", err) + return s.downloadPricingData() + } + + // 如果配置了哈希URL,通过远程哈希检查是否有更新 + if s.cfg.Pricing.HashURL != "" { + remoteHash, err := s.fetchRemoteHash() + if err != nil { + logger.LegacyPrintf("service.pricing", "[Pricing] Failed to fetch remote hash on startup: %v", err) + return nil // 已加载本地文件,哈希获取失败不影响启动 + } + + s.mu.RLock() + localHash := s.localHash + s.mu.RUnlock() + + if localHash == "" || remoteHash != localHash { + logger.LegacyPrintf("service.pricing", "[Pricing] Remote hash differs on startup (local=%s remote=%s), downloading...", + localHash[:min(8, len(localHash))], remoteHash[:min(8, len(remoteHash))]) + if err := s.downloadPricingData(); err != nil { + logger.LegacyPrintf("service.pricing", "[Pricing] Download failed, using existing file: %v", err) + } + } + return nil + } + + // 没有哈希URL时,基于文件年龄检查 info, err := os.Stat(pricingFile) if err != nil { - return s.downloadPricingData() + return nil // 已加载本地文件 } fileAge := time.Since(info.ModTime()) @@ -205,21 +233,11 @@ func (s *PricingService) checkAndUpdatePricing() error { } } - // 加载本地文件 - return s.loadPricingData(pricingFile) + return nil } // syncWithRemote 与远程同步(基于哈希校验) func (s *PricingService) syncWithRemote() error { - pricingFile := s.getPricingFilePath() - - // 计算本地文件哈希 - localHash, err := s.computeFileHash(pricingFile) - if err != nil { - logger.LegacyPrintf("service.pricing", "[Pricing] Failed to compute local hash: %v", err) - return s.downloadPricingData() - } - // 如果配置了哈希URL,从远程获取哈希进行比对 if s.cfg.Pricing.HashURL != "" { remoteHash, err := s.fetchRemoteHash() @@ -228,8 +246,13 @@ func (s *PricingService) syncWithRemote() error { return nil // 哈希获取失败不影响正常使用 } - if remoteHash != localHash { - logger.LegacyPrintf("service.pricing", "%s", "[Pricing] Remote hash differs, downloading new version...") + s.mu.RLock() + localHash := s.localHash + s.mu.RUnlock() + + if localHash == "" || remoteHash != localHash { + logger.LegacyPrintf("service.pricing", "[Pricing] Remote hash differs (local=%s remote=%s), downloading new version...", + localHash[:min(8, len(localHash))], remoteHash[:min(8, len(remoteHash))]) return s.downloadPricingData() } logger.LegacyPrintf("service.pricing", "%s", "[Pricing] Hash check passed, no update needed") @@ -237,6 +260,7 @@ func (s *PricingService) syncWithRemote() error { } // 没有哈希URL时,基于时间检查 + pricingFile := s.getPricingFilePath() info, err := os.Stat(pricingFile) if err != nil { return s.downloadPricingData() @@ -264,11 +288,12 @@ func (s *PricingService) downloadPricingData() error { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - var expectedHash string + // 获取远程哈希(用于同步锚点,不作为完整性校验) + var remoteHash string if strings.TrimSpace(s.cfg.Pricing.HashURL) != "" { - expectedHash, err = s.fetchRemoteHash() + remoteHash, err = s.fetchRemoteHash() if err != nil { - return fmt.Errorf("fetch remote hash: %w", err) + logger.LegacyPrintf("service.pricing", "[Pricing] Failed to fetch remote hash (continuing): %v", err) } } @@ -277,11 +302,13 @@ func (s *PricingService) downloadPricingData() error { return fmt.Errorf("download failed: %w", err) } - if expectedHash != "" { - actualHash := sha256.Sum256(body) - if !strings.EqualFold(expectedHash, hex.EncodeToString(actualHash[:])) { - return fmt.Errorf("pricing hash mismatch") - } + // 哈希校验:不匹配时仅告警,不阻止更新 + // 远程哈希文件可能与数据文件不同步(如维护者更新了数据但未更新哈希文件) + dataHash := sha256.Sum256(body) + dataHashStr := hex.EncodeToString(dataHash[:]) + if remoteHash != "" && !strings.EqualFold(remoteHash, dataHashStr) { + logger.LegacyPrintf("service.pricing", "[Pricing] Hash mismatch warning: remote=%s data=%s (hash file may be out of sync)", + remoteHash[:min(8, len(remoteHash))], dataHashStr[:8]) } // 解析JSON数据(使用灵活的解析方式) @@ -296,11 +323,14 @@ func (s *PricingService) downloadPricingData() error { logger.LegacyPrintf("service.pricing", "[Pricing] Failed to save file: %v", err) } - // 保存哈希 - hash := sha256.Sum256(body) - hashStr := hex.EncodeToString(hash[:]) + // 使用远程哈希作为同步锚点,防止重复下载 + // 当远程哈希不可用时,回退到数据本身的哈希 + syncHash := dataHashStr + if remoteHash != "" { + syncHash = remoteHash + } hashFile := s.getHashFilePath() - if err := os.WriteFile(hashFile, []byte(hashStr+"\n"), 0644); err != nil { + if err := os.WriteFile(hashFile, []byte(syncHash+"\n"), 0644); err != nil { logger.LegacyPrintf("service.pricing", "[Pricing] Failed to save hash: %v", err) } @@ -308,7 +338,7 @@ func (s *PricingService) downloadPricingData() error { s.mu.Lock() s.pricingData = data s.lastUpdated = time.Now() - s.localHash = hashStr + s.localHash = syncHash s.mu.Unlock() logger.LegacyPrintf("service.pricing", "[Pricing] Downloaded %d models successfully", len(data)) @@ -486,16 +516,6 @@ func (s *PricingService) validatePricingURL(raw string) (string, error) { return normalized, nil } -// computeFileHash 计算文件哈希 -func (s *PricingService) computeFileHash(filePath string) (string, error) { - data, err := os.ReadFile(filePath) - if err != nil { - return "", err - } - hash := sha256.Sum256(data) - return hex.EncodeToString(hash[:]), nil -} - // GetModelPricing 获取模型价格(带模糊匹配) func (s *PricingService) GetModelPricing(modelName string) *LiteLLMModelPricing { s.mu.RLock() diff --git a/deploy/config.example.yaml b/deploy/config.example.yaml index 2058ced1..8f60acd5 100644 --- a/deploy/config.example.yaml +++ b/deploy/config.example.yaml @@ -865,10 +865,10 @@ rate_limit: pricing: # URL to fetch model pricing data (default: pinned model-price-repo commit) # 获取模型定价数据的 URL(默认:固定 commit 的 model-price-repo) - remote_url: "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/c7947e9871687e664180bc971d4837f1fc2784a9/model_prices_and_context_window.json" + remote_url: "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/refs/heads/main//model_prices_and_context_window.json" # Hash verification URL (optional) # 哈希校验 URL(可选) - hash_url: "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/c7947e9871687e664180bc971d4837f1fc2784a9/model_prices_and_context_window.sha256" + hash_url: "https://raw.githubusercontent.com/Wei-Shaw/model-price-repo/refs/heads/main//model_prices_and_context_window.sha256" # Local data directory for caching # 本地数据缓存目录 data_dir: "./data" From aa8b9cc5081aa026dc3f8d5f938f49c3f871793a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:13:49 +0000 Subject: [PATCH 23/26] chore: sync VERSION to 0.1.106 [skip ci] --- backend/cmd/server/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index 23175873..9e3db2aa 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.105 +0.1.106 From cc396f59cf6e3c52371161314a88a04adde8cea9 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 30 Mar 2026 16:24:12 +0800 Subject: [PATCH 24/26] chore: update readme --- README.md | 4 ++++ README_CN.md | 4 ++++ README_JA.md | 4 ++++ assets/partners/logos/packycode.png | Bin 0 -> 8329 bytes 4 files changed, 12 insertions(+) create mode 100644 assets/partners/logos/packycode.png diff --git a/README.md b/README.md index 41a5aca1..c4ae61df 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot pincc PinCC is the official relay service built on Sub2API, offering stable access to Claude Code, Codex, Gemini and other popular models — ready to use, no deployment or maintenance required. + +PackyCode +Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using this link and enter the "sub2api" promo code during first recharge to get 10% off. + ## Ecosystem diff --git a/README_CN.md b/README_CN.md index 3380cce7..604b5e7f 100644 --- a/README_CN.md +++ b/README_CN.md @@ -51,6 +51,10 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的 pincc PinCC 是基于 Sub2API 搭建的官方中转服务,提供 Claude Code、Codex、Gemini 等主流模型的稳定中转,开箱即用,免去自建部署与运维烦恼。 + +PackyCode +感谢 PackyCode 赞助了本项目!PackyCode 是一家稳定、高效的API中转服务商,提供 Claude Code、Codex、Gemini 等多种中转服务。PackyCode 为本软件的用户提供了特别优惠,使用此链接注册并在充值时填写"sub2api"优惠码,首次充值可以享受9折优惠! + ## 生态项目 diff --git a/README_JA.md b/README_JA.md index c60b1a8e..eff4c063 100644 --- a/README_JA.md +++ b/README_JA.md @@ -52,6 +52,10 @@ Sub2API は、AI 製品のサブスクリプションから API クォータを pincc PinCC は Sub2API 上に構築された公式リレーサービスで、Claude Code、Codex、Gemini などの人気モデルへの安定したアクセスを提供します。デプロイやメンテナンスは不要で、すぐにご利用いただけます。 + +PackyCode +PackyCode のご支援に感謝します!PackyCode は Claude Code、Codex、Gemini などのリレーサービスを提供する信頼性の高い API 中継プラットフォームです。本ソフト利用者向けに特別割引があります:このリンクで登録し、チャージ時に「sub2api」クーポンを入力すると 10% オフになります。 + ## エコシステム diff --git a/assets/partners/logos/packycode.png b/assets/partners/logos/packycode.png new file mode 100644 index 0000000000000000000000000000000000000000..4fc7eecc75863c1e6ecc19614f90baf9fd9177dc GIT binary patch literal 8329 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 Date: Mon, 30 Mar 2026 16:28:31 +0800 Subject: [PATCH 25/26] chore: update readme --- README.md | 2 +- README_CN.md | 2 +- README_JA.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c4ae61df..99753e45 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Sub2API is an AI API gateway platform designed to distribute and manage API quot - + diff --git a/README_CN.md b/README_CN.md index 604b5e7f..8b6feaba 100644 --- a/README_CN.md +++ b/README_CN.md @@ -48,7 +48,7 @@ Sub2API 是一个 AI API 网关平台,用于分发和管理 AI 产品订阅的
pinccpincc PinCC is the official relay service built on Sub2API, offering stable access to Claude Code, Codex, Gemini and other popular models — ready to use, no deployment or maintenance required.
- + diff --git a/README_JA.md b/README_JA.md index eff4c063..1266bd84 100644 --- a/README_JA.md +++ b/README_JA.md @@ -49,7 +49,7 @@ Sub2API は、AI 製品のサブスクリプションから API クォータを
pinccpincc PinCC 是基于 Sub2API 搭建的官方中转服务,提供 Claude Code、Codex、Gemini 等主流模型的稳定中转,开箱即用,免去自建部署与运维烦恼。
- + From 318aa5e0d37c0d4f0ef7d09f241042d30cf461b1 Mon Sep 17 00:00:00 2001 From: shaw Date: Mon, 30 Mar 2026 21:43:07 +0800 Subject: [PATCH 26/26] feat: add cache hit rate line to token usage trend chart Add a purple dashed line showing cache hit rate percentage (cache_read / (cache_read + cache_creation)) on a secondary right Y-axis (0-100%). Applies to both user and admin dashboards. --- .../src/components/charts/TokenUsageTrend.vue | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/charts/TokenUsageTrend.vue b/frontend/src/components/charts/TokenUsageTrend.vue index a255fb03..4cd126b9 100644 --- a/frontend/src/components/charts/TokenUsageTrend.vue +++ b/frontend/src/components/charts/TokenUsageTrend.vue @@ -64,7 +64,8 @@ const chartColors = computed(() => ({ input: '#3b82f6', output: '#10b981', cacheCreation: '#f59e0b', - cacheRead: '#06b6d4' + cacheRead: '#06b6d4', + cacheHitRate: '#8b5cf6' })) const chartData = computed(() => { @@ -104,6 +105,19 @@ const chartData = computed(() => { backgroundColor: `${chartColors.value.cacheRead}20`, fill: true, tension: 0.3 + }, + { + label: 'Cache Hit Rate', + data: props.trendData.map((d) => { + const total = d.cache_read_tokens + d.cache_creation_tokens + return total > 0 ? (d.cache_read_tokens / total) * 100 : 0 + }), + borderColor: chartColors.value.cacheHitRate, + backgroundColor: `${chartColors.value.cacheHitRate}20`, + borderDash: [5, 5], + fill: false, + tension: 0.3, + yAxisID: 'yPercent' } ] } @@ -132,6 +146,9 @@ const lineOptions = computed(() => ({ tooltip: { callbacks: { label: (context: any) => { + if (context.dataset.yAxisID === 'yPercent') { + return `${context.dataset.label}: ${context.raw.toFixed(1)}%` + } return `${context.dataset.label}: ${formatTokens(context.raw)}` }, footer: (tooltipItems: any) => { @@ -168,6 +185,21 @@ const lineOptions = computed(() => ({ }, callback: (value: string | number) => formatTokens(Number(value)) } + }, + yPercent: { + position: 'right' as const, + min: 0, + max: 100, + grid: { + drawOnChartArea: false + }, + ticks: { + color: chartColors.value.cacheHitRate, + font: { + size: 10 + }, + callback: (value: string | number) => `${value}%` + } } } }))
pinccpincc PinCC は Sub2API 上に構築された公式リレーサービスで、Claude Code、Codex、Gemini などの人気モデルへの安定したアクセスを提供します。デプロイやメンテナンスは不要で、すぐにご利用いただけます。