mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 03:52:26 +08:00
feat(gemini): 适配 AI Studio AQ. 格式 Key
- AI Studio 新建 Key 为 AQ.Ab 前缀的 auth key:logredact.RedactText 与 渠道监控 sanitizeErrorMessage 增加 AQ.Ab 规则,长度按 40 位以上放宽以覆盖格式变体 - 创建 / 编辑账号的 Key 占位符改为 AQ.Ab...,提示文案说明新旧两种前缀 与第三方上游 Key
This commit is contained in:
@@ -636,6 +636,8 @@ var monitorAPIKeyPatterns = []struct {
|
||||
{regexp.MustCompile(`xai-[A-Za-z0-9_-]{6,}`), "xai-***REDACTED***"},
|
||||
// Gemini / Google API Key:固定前缀 + 35 位
|
||||
{regexp.MustCompile(`AIza[A-Za-z0-9_-]{35}`), "AIza***REDACTED***"},
|
||||
// Gemini auth key:AQ.Ab 前缀 + 48 位,长度放宽以覆盖格式变体
|
||||
{regexp.MustCompile(`AQ\.Ab[A-Za-z0-9_-]{40,}`), "AQ.***REDACTED***"},
|
||||
// JWT 三段式(Bearer 后常出现):eyJxxx.eyJxxx.signature
|
||||
{regexp.MustCompile(`eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}`), "eyJ***REDACTED.JWT***"},
|
||||
}
|
||||
@@ -643,7 +645,7 @@ var monitorAPIKeyPatterns = []struct {
|
||||
// sanitizeErrorMessage 擦除错误/响应文本中可能泄露的 API key。
|
||||
// 处理两类来源:
|
||||
// 1. URL query 中的 ?key= / ?api_key= 等(Go *url.Error 会回填完整 URL)
|
||||
// 2. 上游 HTTP body 文本里直接出现的 sk-* / xai-* / AIza* / JWT 等密钥碎片
|
||||
// 2. 上游 HTTP body 文本里直接出现的 sk-* / xai-* / AIza* / AQ.* / JWT 等密钥碎片
|
||||
//
|
||||
// 注意:与 gemini_messages_compat_service.go 的 sanitizeUpstreamErrorMessage 关注点类似但参数集更广,
|
||||
// 监控模块独立维护,避免互相耦合。
|
||||
|
||||
@@ -279,6 +279,17 @@ func TestRunCheckForModel_Grok_RedactsXAIKeyFromUpstreamBody(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeErrorMessage_RedactsGeminiAuthKey(t *testing.T) {
|
||||
authKey := "AQ." + "Ab" + strings.Repeat("Xy9_-z", 8)
|
||||
got := sanitizeErrorMessage(`{"error":{"message":"API key not valid: ` + authKey + `"}}`)
|
||||
if strings.Contains(got, authKey) {
|
||||
t.Fatalf("Gemini auth key leaked: %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "AQ.***REDACTED***") {
|
||||
t.Fatalf("expected redaction marker, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCheckForModel_OpenAIResponses_DefaultRequest(t *testing.T) {
|
||||
h := &openAICaptureHandler{}
|
||||
endpoint := setupFakeOpenAI(t, h)
|
||||
|
||||
@@ -42,6 +42,8 @@ type textRedactPatterns struct {
|
||||
var (
|
||||
reGOCSPX = regexp.MustCompile(`GOCSPX-[0-9A-Za-z_-]{24,}`)
|
||||
reAIza = regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`)
|
||||
// Gemini auth key:AQ.Ab 前缀 + 48 位,长度放宽以覆盖格式变体。
|
||||
reGeminiAuthKey = regexp.MustCompile(`AQ\.Ab[0-9A-Za-z_-]{40,}`)
|
||||
|
||||
defaultTextRedactPatterns = compileTextRedactPatterns(nil)
|
||||
extraTextPatternCache sync.Map // map[string]*textRedactPatterns
|
||||
@@ -99,6 +101,7 @@ func RedactText(input string, extraKeys ...string) string {
|
||||
out := input
|
||||
out = reGOCSPX.ReplaceAllString(out, "GOCSPX-***")
|
||||
out = reAIza.ReplaceAllString(out, "AIza***")
|
||||
out = reGeminiAuthKey.ReplaceAllString(out, "AQ.***")
|
||||
out = patterns.reJSONLike.ReplaceAllString(out, `$1***$3`)
|
||||
out = patterns.reQueryLike.ReplaceAllString(out, `$1=***`)
|
||||
out = patterns.rePlain.ReplaceAllString(out, `$1$2***`)
|
||||
|
||||
@@ -38,6 +38,28 @@ func TestRedactText_GOCSPX(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactText_GeminiAPIKeys(t *testing.T) {
|
||||
legacyKey := "AI" + "za" + strings.Repeat("Ab1_-", 7)
|
||||
authKey := "AQ." + "Ab" + strings.Repeat("Xy9_-z", 8)
|
||||
in := "upstream rejected " + legacyKey + " and " + authKey + " for model gemini-3.1-pro"
|
||||
out := RedactText(in)
|
||||
if strings.Contains(out, legacyKey) || strings.Contains(out, authKey) {
|
||||
t.Fatalf("expected Gemini keys redacted, got %q", out)
|
||||
}
|
||||
for _, want := range []string{"AIza***", "AQ.***", "for model gemini-3.1-pro"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("expected %q in %q", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactText_GeminiAuthKeyPrefixAloneUnchanged(t *testing.T) {
|
||||
in := "key format AQ.Ab... is documented"
|
||||
if out := RedactText(in); out != in {
|
||||
t.Fatalf("expected short AQ. prefix unchanged, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedactText_ExtraKeyCacheUsesNormalizedSortedKey(t *testing.T) {
|
||||
clearExtraTextPatternCache()
|
||||
|
||||
|
||||
@@ -4253,7 +4253,7 @@ const apiKeyValuePlaceholder = computed(() => {
|
||||
case 'openai':
|
||||
return 'sk-proj-...'
|
||||
case 'gemini':
|
||||
return 'AIza...'
|
||||
return 'AQ.Ab...'
|
||||
case 'grok':
|
||||
return 'xai-...'
|
||||
case 'kimi':
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
account.platform === 'openai'
|
||||
? 'sk-proj-...'
|
||||
: account.platform === 'gemini'
|
||||
? 'AIza...'
|
||||
? 'AQ.Ab...'
|
||||
: account.platform === 'antigravity'
|
||||
? 'sk-...'
|
||||
: account.platform === 'grok'
|
||||
|
||||
@@ -1422,7 +1422,7 @@ export default {
|
||||
modelPassthroughDesc:
|
||||
'All model requests are forwarded directly to the Gemini API without model restrictions or mappings.',
|
||||
baseUrlHint: 'Leave default for official Gemini API',
|
||||
apiKeyHint: 'Your Gemini API Key (starts with AIza)',
|
||||
apiKeyHint: 'New AI Studio keys start with AQ. (legacy keys start with AIza); for a third-party upstream, use the key it provides',
|
||||
tier: {
|
||||
label: 'Account Tier',
|
||||
hint: 'Tip: The system will try to auto-detect the tier first; if auto-detection is unavailable or fails, your selected tier is used as a fallback (simulated quota).',
|
||||
|
||||
@@ -1513,7 +1513,7 @@ export default {
|
||||
modelPassthrough: 'Gemini 直接转发模型',
|
||||
modelPassthroughDesc: '所有模型请求将直接转发至 Gemini API,不进行模型限制或映射。',
|
||||
baseUrlHint: '留空使用官方 Gemini API',
|
||||
apiKeyHint: '您的 Gemini API Key(以 AIza 开头)',
|
||||
apiKeyHint: 'AI Studio 新建的 Key 以 AQ. 开头(旧版以 AIza 开头);第三方上游填写其提供的 Key',
|
||||
tier: {
|
||||
label: '账号等级',
|
||||
hint: '提示:系统会优先尝试自动识别账号等级;若自动识别不可用或失败,则使用你选择的等级作为回退(本地模拟配额)。',
|
||||
|
||||
Reference in New Issue
Block a user