diff --git a/.env.example b/.env.example index 014271fb..6c5cbb07 100644 --- a/.env.example +++ b/.env.example @@ -63,6 +63,9 @@ CLAUDE_BETA_HEADER=claude-code-20250219,oauth-2025-04-20,interleaved-thinking-20 # ANTHROPIC_DEBUG_RESPONSE_DUMP_MAX_BYTES=2097152 # ANTHROPIC_DEBUG_TOOLS_DUMP=true # +# (可选)工具失败继续:当 tool_result 标记 is_error=true 时,提示模型不要中断任务(仅 /antigravity/api 分流生效) +# ANTHROPIC_TOOL_ERROR_CONTINUE=true +# # (可选)Antigravity 上游请求 Dump:会在项目根目录写入 jsonl 文件,便于核对最终发往上游的 payload(含 tools/schema 清洗后的结果) # - antigravity-upstream-requests-dump.jsonl # ANTIGRAVITY_DEBUG_UPSTREAM_REQUEST_DUMP=true diff --git a/src/routes/api.js b/src/routes/api.js index 8047a51d..6ec81cbd 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -122,12 +122,18 @@ async function handleMessagesRequest(req, res) { try { const startTime = Date.now() - // Claude 服务权限校验,阻止未授权的 Key - if (!apiKeyService.hasPermission(req.apiKey.permissions, 'claude')) { + const forcedVendor = req._anthropicVendor || null + const requiredService = + forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity' ? 'gemini' : 'claude' + + if (!apiKeyService.hasPermission(req.apiKey?.permissions, requiredService)) { return res.status(403).json({ error: { type: 'permission_error', - message: '此 API Key 无权访问 Claude 服务' + message: + requiredService === 'gemini' + ? '此 API Key 无权访问 Gemini 服务' + : '此 API Key 无权访问 Claude 服务' } }) } @@ -176,7 +182,6 @@ async function handleMessagesRequest(req, res) { } } - const forcedVendor = req._anthropicVendor || null logger.api('📥 /v1/messages request received', { model: req.body.model || null, forcedVendor, @@ -192,34 +197,10 @@ async function handleMessagesRequest(req, res) { // /v1/messages 的扩展:按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱) if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') { - const permissions = req.apiKey?.permissions || 'all' - if (permissions !== 'all' && permissions !== 'gemini') { - return res.status(403).json({ - error: { - type: 'permission_error', - message: '此 API Key 无权访问 Gemini 服务' - } - }) - } - const baseModel = (req.body.model || '').trim() return await handleAnthropicMessagesToGemini(req, res, { vendor: forcedVendor, baseModel }) } - // Claude 服务权限校验,阻止未授权的 Key(默认路径保持不变) - if ( - req.apiKey.permissions && - req.apiKey.permissions !== 'all' && - req.apiKey.permissions !== 'claude' - ) { - return res.status(403).json({ - error: { - type: 'permission_error', - message: '此 API Key 无权访问 Claude 服务' - } - }) - } - // 检查是否为流式请求 const isStream = req.body.stream === true @@ -1250,8 +1231,7 @@ router.get('/v1/models', authenticateApiKey, async (req, res) => { //(通过 v1internal:fetchAvailableModels),避免依赖静态 modelService 列表。 const forcedVendor = req._anthropicVendor || null if (forcedVendor === 'antigravity') { - const permissions = req.apiKey?.permissions || 'all' - if (permissions !== 'all' && permissions !== 'gemini') { + if (!apiKeyService.hasPermission(req.apiKey?.permissions, 'gemini')) { return res.status(403).json({ error: { type: 'permission_error', @@ -1444,34 +1424,25 @@ router.get('/v1/organizations/:org_id/usage', authenticateApiKey, async (req, re router.post('/v1/messages/count_tokens', authenticateApiKey, async (req, res) => { // 按路径强制分流到 Gemini OAuth 账户(避免 model 前缀混乱) const forcedVendor = req._anthropicVendor || null - if (forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity') { - const permissions = req.apiKey?.permissions || 'all' - if (permissions !== 'all' && permissions !== 'gemini') { - return res.status(403).json({ - error: { - type: 'permission_error', - message: 'This API key does not have permission to access Gemini' - } - }) - } + const requiredService = + forcedVendor === 'gemini-cli' || forcedVendor === 'antigravity' ? 'gemini' : 'claude' - return await handleAnthropicCountTokensToGemini(req, res, { vendor: forcedVendor }) - } - - // 检查权限 - if ( - req.apiKey.permissions && - req.apiKey.permissions !== 'all' && - req.apiKey.permissions !== 'claude' - ) { + if (!apiKeyService.hasPermission(req.apiKey?.permissions, requiredService)) { return res.status(403).json({ error: { type: 'permission_error', - message: 'This API key does not have permission to access Claude' + message: + requiredService === 'gemini' + ? 'This API key does not have permission to access Gemini' + : 'This API key does not have permission to access Claude' } }) } + if (requiredService === 'gemini') { + return await handleAnthropicCountTokensToGemini(req, res, { vendor: forcedVendor }) + } + // 🔗 会话绑定验证(与 messages 端点保持一致) const originalSessionId = claudeRelayConfigService.extractOriginalSessionId(req.body) const sessionValidation = await claudeRelayConfigService.validateNewSession( diff --git a/src/routes/unified.js b/src/routes/unified.js index 57c4fe80..c1401137 100644 --- a/src/routes/unified.js +++ b/src/routes/unified.js @@ -46,11 +46,11 @@ async function routeToBackend(req, res, requestedModel) { logger.info(`🔀 Routing request - Model: ${requestedModel}, Backend: ${backend}`) // 检查权限 - const permissions = req.apiKey.permissions || 'all' + const { permissions } = req.apiKey if (backend === 'claude') { // Claude 后端:通过 OpenAI 兼容层 - if (permissions !== 'all' && permissions !== 'claude') { + if (!apiKeyService.hasPermission(permissions, 'claude')) { return res.status(403).json({ error: { message: 'This API key does not have permission to access Claude', @@ -62,7 +62,7 @@ async function routeToBackend(req, res, requestedModel) { await handleChatCompletion(req, res, req.apiKey) } else if (backend === 'openai') { // OpenAI 后端 - if (permissions !== 'all' && permissions !== 'openai') { + if (!apiKeyService.hasPermission(permissions, 'openai')) { return res.status(403).json({ error: { message: 'This API key does not have permission to access OpenAI', diff --git a/src/services/anthropicGeminiBridgeService.js b/src/services/anthropicGeminiBridgeService.js index 5ae77006..a7b805f4 100644 --- a/src/services/anthropicGeminiBridgeService.js +++ b/src/services/anthropicGeminiBridgeService.js @@ -21,7 +21,10 @@ const SYSTEM_REMINDER_PREFIX = '' const TOOLS_DUMP_ENV = 'ANTHROPIC_DEBUG_TOOLS_DUMP' const TOOLS_DUMP_FILENAME = 'anthropic-tools-dump.jsonl' const TEXT_TOOL_FALLBACK_ENV = 'ANTHROPIC_TEXT_TOOL_FALLBACK' +const TOOL_ERROR_CONTINUE_ENV = 'ANTHROPIC_TOOL_ERROR_CONTINUE' const THOUGHT_SIGNATURE_FALLBACK = 'skip_thought_signature_validator' +const TOOL_ERROR_CONTINUE_PROMPT = + 'Tool calls may fail (e.g., missing prerequisites). When a tool result indicates an error, do not stop: briefly explain the cause and continue with an alternative approach or the remaining steps.' function ensureAntigravityProjectId(account) { if (account.projectId) { @@ -710,12 +713,13 @@ function convertAnthropicMessagesToGeminiContents( if (vendor === 'antigravity') { const toolCallId = typeof toolUseId === 'string' && toolUseId ? toolUseId : undefined const result = parsedResponse !== null ? parsedResponse : raw || '' + const response = part.is_error === true ? { result, is_error: true } : { result } parts.push({ functionResponse: { ...(toolCallId ? { id: toolCallId } : {}), name: toolName, - response: { result } + response } }) } else { @@ -761,6 +765,10 @@ function buildGeminiRequestFromAnthropic(body, baseModel, { vendor = null } = {} ) const systemParts = buildSystemParts(body.system) + if (vendor === 'antigravity' && isEnvEnabled(process.env[TOOL_ERROR_CONTINUE_ENV])) { + systemParts.push({ text: TOOL_ERROR_CONTINUE_PROMPT }) + } + const temperature = typeof body.temperature === 'number' ? body.temperature : 1 const maxTokens = Number.isFinite(body.max_tokens) ? body.max_tokens : 4096 diff --git a/web/admin-spa/src/components/accounts/OAuthFlow.vue b/web/admin-spa/src/components/accounts/OAuthFlow.vue index 9ca4c46a..d24c5765 100644 --- a/web/admin-spa/src/components/accounts/OAuthFlow.vue +++ b/web/admin-spa/src/components/accounts/OAuthFlow.vue @@ -287,7 +287,7 @@ -
+
diff --git a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue index 749039bf..0b68528b 100644 --- a/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue +++ b/web/admin-spa/src/components/apikeys/EditApiKeyModal.vue @@ -1233,13 +1233,28 @@ onMounted(async () => { form.totalCostLimit = props.apiKey.totalCostLimit || '' form.weeklyOpusCostLimit = props.apiKey.weeklyOpusCostLimit || '' // 处理权限数据,兼容旧格式(字符串)和新格式(数组) - const perms = props.apiKey.permissions + // 有效的权限值 + const VALID_PERMS = ['claude', 'gemini', 'openai', 'droid'] + let perms = props.apiKey.permissions + // 如果是字符串,尝试 JSON.parse(Redis 可能返回 "[]" 或 "[\"gemini\"]") + if (typeof perms === 'string') { + if (perms === 'all' || perms === '') { + perms = [] + } else if (perms.startsWith('[')) { + try { + perms = JSON.parse(perms) + } catch { + perms = VALID_PERMS.includes(perms) ? [perms] : [] + } + } else if (VALID_PERMS.includes(perms)) { + perms = [perms] + } else { + perms = [] + } + } if (Array.isArray(perms)) { - form.permissions = perms - } else if (perms === 'all' || !perms) { - form.permissions = [] - } else if (typeof perms === 'string') { - form.permissions = [perms] + // 过滤掉无效值(如 "[]") + form.permissions = perms.filter((p) => VALID_PERMS.includes(p)) } else { form.permissions = [] }