Compare commits

..

14 Commits

Author SHA1 Message Date
github-actions[bot]
0f5321b0ef chore: sync VERSION file with release v1.1.260 [skip ci] 2026-01-21 02:19:34 +00:00
shaw
c7d7bf47d6 fix: 更新claude账号oauth链接生成规则 2026-01-21 10:06:24 +08:00
Wesley Liddick
ebc30b6026 Merge pull request #906 from 0xRichardH/fix-bedrock-sse-stream-event [skip ci]
Fix bedrock sse stream event
2026-01-21 09:38:19 +08:00
Wesley Liddick
d5a7af2d7d Merge pull request #903 from RedwindA/main [skip ci]
feat(droid): add prompt_cache_retention and safety_identifier to fiel…
2026-01-21 09:37:19 +08:00
Richard Hao
81a3e26e27 fix: correct Bedrock SSE stream event format to match Claude API spec
- message_start: nest fields inside 'message' object with type: 'message'
- content_block_delta: add type field to data
- message_delta: add type field to data
- message_stop: remove usage field, just return type
- Extract usage from message_delta instead of message_stop
2026-01-18 11:38:38 +08:00
Richard Hao
64db4a270d fix: handle bedrock content block start/stop events 2026-01-18 10:58:11 +08:00
RedwindA
ca027ecb90 feat(droid): add prompt_cache_retention and safety_identifier to fieldsToRemove 2026-01-16 04:22:05 +08:00
github-actions[bot]
21e6944abb chore: sync VERSION file with release v1.1.259 [skip ci] 2026-01-15 03:07:53 +00:00
Wesley Liddick
4ea3d4830f Merge pull request #858 from zengqinglei/feature/gemini-retrieve-user-quota
feat: 添加 Gemini retrieveUserQuota 接口支持
2026-01-15 11:07:41 +08:00
github-actions[bot]
3000632d4e chore: sync VERSION file with release v1.1.258 [skip ci] 2026-01-15 01:25:03 +00:00
Wesley Liddick
9e3a4cf45a Merge pull request #899 from UncleJ-h/fix/remove-unused-heapdump
fix: remove unused heapdump dependency
2026-01-15 09:24:51 +08:00
UncleJ-h
eb992697b6 fix: remove unused heapdump dependency
The heapdump package was added in v1.1.257 but is not actually used anywhere in the codebase.

This causes build failures on platforms without Python (e.g., Zeabur) because heapdump requires node-gyp compilation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:43:45 +08:00
曾庆雷
944ef096b3 fix: eslint 代码风格优化 2026-01-08 18:26:45 +08:00
曾庆雷
18a493e805 feat: 添加 Gemini retrieveUserQuota 接口支持
支持 Gemini CLI 0.22.2+ 的配额查询功能
实现与现有 v1internal 接口一致的 projectId 处理逻辑
2025-12-24 22:48:27 +08:00
7 changed files with 157 additions and 17 deletions

View File

@@ -1 +1 @@
1.1.257 1.1.260

View File

@@ -59,7 +59,6 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"google-auth-library": "^10.1.0", "google-auth-library": "^10.1.0",
"heapdump": "^0.3.15",
"helmet": "^7.1.0", "helmet": "^7.1.0",
"https-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.2",
"inquirer": "^8.2.6", "inquirer": "^8.2.6",

View File

@@ -1188,6 +1188,110 @@ async function handleOnboardUser(req, res) {
} }
} }
/**
* 处理 retrieveUserQuota 请求
* POST /v1internal:retrieveUserQuota
*
* 功能查询用户在各个Gemini模型上的配额使用情况
* 请求体:{ "project": "项目ID" }
* 响应:{ "buckets": [...] }
*/
async function handleRetrieveUserQuota(req, res) {
try {
// 1. 权限检查
if (!ensureGeminiPermission(req, res)) {
return undefined
}
// 2. 会话哈希
const sessionHash = sessionHelper.generateSessionHash(req.body)
// 3. 账户选择
const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash'
const schedulerResult = await unifiedGeminiScheduler.selectAccountForApiKey(
req.apiKey,
sessionHash,
requestedModel
)
const { accountId, accountType } = schedulerResult
// 4. 账户类型验证 - v1internal 路由只支持 OAuth 账户
if (accountType === 'gemini-api') {
logger.error(`❌ v1internal routes do not support Gemini API accounts. Account: ${accountId}`)
return res.status(400).json({
error: {
message:
'This endpoint only supports Gemini OAuth accounts. Gemini API Key accounts are not compatible with v1internal format.',
type: 'invalid_account_type'
}
})
}
// 5. 获取账户
const account = await geminiAccountService.getAccount(accountId)
if (!account) {
return res.status(404).json({
error: {
message: 'Gemini account not found',
type: 'account_not_found'
}
})
}
const { accessToken, refreshToken, projectId } = account
// 6. 从请求体提取项目字段(注意:字段名是 "project",不是 "cloudaicompanionProject"
const requestProject = req.body.project
const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal'
logger.info(`RetrieveUserQuota request (${version})`, {
requestedProject: requestProject || null,
accountProject: projectId || null,
apiKeyId: req.apiKey?.id || 'unknown'
})
// 7. 解析账户的代理配置
const proxyConfig = parseProxyConfig(account)
// 8. 获取OAuth客户端
const client = await geminiAccountService.getOauthClient(accessToken, refreshToken, proxyConfig)
// 9. 智能处理项目ID与其他 v1internal 接口保持一致)
const effectiveProject = projectId || requestProject || null
logger.info('📋 retrieveUserQuota项目ID处理逻辑', {
accountProjectId: projectId,
requestProject,
effectiveProject,
decision: projectId ? '使用账户配置' : requestProject ? '使用请求参数' : '不使用项目ID'
})
// 10. 构建请求体(注入 effectiveProject
const requestBody = { ...req.body }
if (effectiveProject) {
requestBody.project = effectiveProject
}
// 11. 调用底层服务转发请求
const response = await geminiAccountService.forwardToCodeAssist(
client,
'retrieveUserQuota',
requestBody,
proxyConfig
)
res.json(response)
} catch (error) {
const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal'
logger.error(`Error in retrieveUserQuota endpoint (${version})`, {
error: error.message
})
res.status(500).json({
error: 'Internal server error',
message: error.message
})
}
}
/** /**
* 处理 countTokens 请求 * 处理 countTokens 请求
*/ */
@@ -2698,6 +2802,7 @@ module.exports = {
handleSimpleEndpoint, handleSimpleEndpoint,
handleLoadCodeAssist, handleLoadCodeAssist,
handleOnboardUser, handleOnboardUser,
handleRetrieveUserQuota,
handleCountTokens, handleCountTokens,
handleGenerateContent, handleGenerateContent,
handleStreamGenerateContent, handleStreamGenerateContent,

View File

@@ -29,6 +29,7 @@ const {
handleStreamGenerateContent, handleStreamGenerateContent,
handleLoadCodeAssist, handleLoadCodeAssist,
handleOnboardUser, handleOnboardUser,
handleRetrieveUserQuota,
handleCountTokens, handleCountTokens,
handleStandardGenerateContent, handleStandardGenerateContent,
handleStandardStreamGenerateContent, handleStandardStreamGenerateContent,
@@ -68,7 +69,7 @@ router.get('/usage', authenticateApiKey, handleUsage)
router.get('/key-info', authenticateApiKey, handleKeyInfo) router.get('/key-info', authenticateApiKey, handleKeyInfo)
// ============================================================================ // ============================================================================
// v1internal 独有路由listExperiments // v1internal 独有路由
// ============================================================================ // ============================================================================
/** /**
@@ -81,6 +82,12 @@ router.post(
handleSimpleEndpoint('listExperiments') handleSimpleEndpoint('listExperiments')
) )
/**
* POST /v1internal:retrieveUserQuota
* 获取用户配额信息Gemini CLI 0.22.2+ 需要)
*/
router.post('/v1internal\\:retrieveUserQuota', authenticateApiKey, handleRetrieveUserQuota)
/** /**
* POST /v1beta/models/:modelName:listExperiments * POST /v1beta/models/:modelName:listExperiments
* 带模型参数的实验列表(只有 geminiRoutes 定义此路由) * 带模型参数的实验列表(只有 geminiRoutes 定义此路由)

View File

@@ -274,7 +274,9 @@ const handleResponses = async (req, res) => {
'text_formatting', 'text_formatting',
'truncation', 'truncation',
'text', 'text',
'service_tier' 'service_tier',
'prompt_cache_retention',
'safety_identifier'
] ]
fieldsToRemove.forEach((field) => { fieldsToRemove.forEach((field) => {
delete req.body[field] delete req.body[field]

View File

@@ -343,8 +343,8 @@ class BedrockRelayService {
res.write(`event: ${claudeEvent.type}\n`) res.write(`event: ${claudeEvent.type}\n`)
res.write(`data: ${JSON.stringify(claudeEvent.data)}\n\n`) res.write(`data: ${JSON.stringify(claudeEvent.data)}\n\n`)
// 提取使用统计 // 提取使用统计 (usage is reported in message_delta per Claude API spec)
if (claudeEvent.type === 'message_stop' && claudeEvent.data.usage) { if (claudeEvent.type === 'message_delta' && claudeEvent.data.usage) {
totalUsage = claudeEvent.data.usage totalUsage = claudeEvent.data.usage
} }
@@ -576,14 +576,28 @@ class BedrockRelayService {
return { return {
type: 'message_start', type: 'message_start',
data: { data: {
type: 'message', type: 'message_start',
id: `msg_${Date.now()}_bedrock`, message: {
role: 'assistant', id: `msg_${Date.now()}_bedrock`,
content: [], type: 'message',
model: this.defaultModel, role: 'assistant',
stop_reason: null, content: [],
stop_sequence: null, model: this.defaultModel,
usage: bedrockChunk.message?.usage || { input_tokens: 0, output_tokens: 0 } stop_reason: null,
stop_sequence: null,
usage: bedrockChunk.message?.usage || { input_tokens: 0, output_tokens: 0 }
}
}
}
}
if (bedrockChunk.type === 'content_block_start') {
return {
type: 'content_block_start',
data: {
type: 'content_block_start',
index: bedrockChunk.index || 0,
content_block: bedrockChunk.content_block || { type: 'text', text: '' }
} }
} }
} }
@@ -592,16 +606,28 @@ class BedrockRelayService {
return { return {
type: 'content_block_delta', type: 'content_block_delta',
data: { data: {
type: 'content_block_delta',
index: bedrockChunk.index || 0, index: bedrockChunk.index || 0,
delta: bedrockChunk.delta || {} delta: bedrockChunk.delta || {}
} }
} }
} }
if (bedrockChunk.type === 'content_block_stop') {
return {
type: 'content_block_stop',
data: {
type: 'content_block_stop',
index: bedrockChunk.index || 0
}
}
}
if (bedrockChunk.type === 'message_delta') { if (bedrockChunk.type === 'message_delta') {
return { return {
type: 'message_delta', type: 'message_delta',
data: { data: {
type: 'message_delta',
delta: bedrockChunk.delta || {}, delta: bedrockChunk.delta || {},
usage: bedrockChunk.usage || {} usage: bedrockChunk.usage || {}
} }
@@ -612,7 +638,7 @@ class BedrockRelayService {
return { return {
type: 'message_stop', type: 'message_stop',
data: { data: {
usage: bedrockChunk.usage || {} type: 'message_stop'
} }
} }
} }

View File

@@ -13,8 +13,8 @@ const OAUTH_CONFIG = {
AUTHORIZE_URL: 'https://claude.ai/oauth/authorize', AUTHORIZE_URL: 'https://claude.ai/oauth/authorize',
TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token', TOKEN_URL: 'https://console.anthropic.com/v1/oauth/token',
CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e', CLIENT_ID: '9d1c250a-e61b-44d9-88ed-5944d1962f5e',
REDIRECT_URI: 'https://console.anthropic.com/oauth/code/callback', REDIRECT_URI: 'https://platform.claude.com/oauth/code/callback',
SCOPES: 'org:create_api_key user:profile user:inference', SCOPES: 'org:create_api_key user:profile user:inference user:sessions:claude_code',
SCOPES_SETUP: 'user:inference' // Setup Token 只需要推理权限 SCOPES_SETUP: 'user:inference' // Setup Token 只需要推理权限
} }
@@ -35,6 +35,7 @@ function generateState() {
/** /**
* 生成随机的 code verifierPKCE * 生成随机的 code verifierPKCE
* 符合 RFC 7636 标准32字节随机数 → base64url编码 → 43字符
* @returns {string} base64url 编码的随机字符串 * @returns {string} base64url 编码的随机字符串
*/ */
function generateCodeVerifier() { function generateCodeVerifier() {