mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
feat(vertex-ai): 重构 Vertex AI 为插件化 adapter,支持 service_account 认证与动态路由
将 Vertex AI 从 transport.py 的硬编码逻辑重构为独立的 plugin adapter, 支持 service_account/oauth 认证类型、模型格式自动识别、区域路由和 URL 构建。 前端新增 Key 认证类型选择和 Service Account 配置表单。 Co-authored-by: NyaDoo <65238336+NyaDoo@users.noreply.github.com> Closes #194
This commit is contained in:
@@ -775,14 +775,14 @@ const isCapabilityUsed = (cap: string): boolean => {
|
||||
// 判断是否为 OAuth 类型(provider_type 为具体值时也算 OAuth)
|
||||
const isOAuthType = (authType?: string): boolean => {
|
||||
if (!authType) return false
|
||||
return !['api_key', 'vertex_ai'].includes(authType)
|
||||
return !['api_key', 'service_account'].includes(authType)
|
||||
}
|
||||
|
||||
// 格式化认证类型(合并 plan 信息,避免冗余)
|
||||
const formatAuthTypeWithPlan = (authType: string, planType?: string): string => {
|
||||
const labels: Record<string, string> = {
|
||||
'oauth': 'OAuth',
|
||||
'vertex_ai': 'Vertex AI',
|
||||
'service_account': 'Service Account',
|
||||
'kiro': 'Kiro',
|
||||
'codex': 'Codex',
|
||||
'antigravity': 'Antigravity',
|
||||
|
||||
@@ -131,17 +131,22 @@ export class GeminiParser implements ApiFormatParser {
|
||||
apiFormat: 'gemini',
|
||||
}
|
||||
|
||||
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (candidateContent?.parts && Array.isArray(candidateContent.parts)) {
|
||||
const contentBlocks = this.parseParts(candidateContent.parts as RawObject[])
|
||||
// Gemini 原生响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidateParts = this.getCandidateParts(body)
|
||||
if (candidateParts.length > 0) {
|
||||
const contentBlocks = this.parseParts(candidateParts)
|
||||
if (contentBlocks.length > 0) {
|
||||
result.messages.push(createMessage('assistant', contentBlocks))
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 统一响应格式(例如 status/output/output_text)
|
||||
const unifiedMessages = this.parseUnifiedOutputMessages(body)
|
||||
if (unifiedMessages.length > 0) {
|
||||
result.messages.push(...unifiedMessages)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (e) {
|
||||
return createEmptyConversation('gemini', `解析失败: ${e}`)
|
||||
@@ -363,12 +368,10 @@ export class GeminiParser implements ApiFormatParser {
|
||||
const body = responseBody as RawObject
|
||||
const blocks: RenderBlock[] = []
|
||||
|
||||
// Gemini 响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (candidateContent?.parts && Array.isArray(candidateContent.parts)) {
|
||||
const parts = candidateContent.parts as RawObject[]
|
||||
// Gemini 原生响应格式: { candidates: [{ content: { parts: [...] } }] }
|
||||
const candidateParts = this.getCandidateParts(body)
|
||||
if (candidateParts.length > 0) {
|
||||
const parts = candidateParts
|
||||
const contentBlocks = this.renderParts(parts)
|
||||
if (contentBlocks.length > 0) {
|
||||
const badges = this.getBadgesForParts(parts)
|
||||
@@ -376,9 +379,13 @@ export class GeminiParser implements ApiFormatParser {
|
||||
roleLabel: 'Assistant',
|
||||
badges: badges.length > 0 ? badges : undefined,
|
||||
}))
|
||||
return { blocks, isStream: false }
|
||||
}
|
||||
}
|
||||
|
||||
// 统一响应格式(例如 status/output/output_text)
|
||||
blocks.push(...this.renderUnifiedOutputMessages(body))
|
||||
|
||||
return { blocks, isStream: false }
|
||||
} catch (e) {
|
||||
return createEmptyRenderResult(`渲染失败: ${e}`)
|
||||
@@ -582,6 +589,117 @@ export class GeminiParser implements ApiFormatParser {
|
||||
return badges
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取 Gemini candidates 中的 parts
|
||||
*/
|
||||
private getCandidateParts(body: RawObject): RawObject[] {
|
||||
const candidates = body.candidates as RawObject[] | undefined
|
||||
const candidate = candidates?.[0] as RawObject | undefined
|
||||
const candidateContent = candidate?.content as RawObject | undefined
|
||||
if (Array.isArray(candidateContent?.parts)) {
|
||||
return candidateContent.parts as RawObject[]
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析统一响应格式中的 output 消息
|
||||
*/
|
||||
private parseUnifiedOutputMessages(body: RawObject): ParsedMessage[] {
|
||||
const output = body.output
|
||||
if (!Array.isArray(output)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const messages: ParsedMessage[] = []
|
||||
for (const rawItem of output) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type !== 'message') {
|
||||
continue
|
||||
}
|
||||
|
||||
const texts = this.extractUnifiedOutputTexts(item.content)
|
||||
if (texts.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
messages.push(createMessage(
|
||||
this.mapUnifiedOutputRole(item.role),
|
||||
texts.map(text => createTextBlock(text))
|
||||
))
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染统一响应格式中的 output 消息
|
||||
*/
|
||||
private renderUnifiedOutputMessages(body: RawObject): RenderBlock[] {
|
||||
const output = body.output
|
||||
if (!Array.isArray(output)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const blocks: RenderBlock[] = []
|
||||
for (const rawItem of output) {
|
||||
const item = rawItem as RawObject
|
||||
if (item?.type !== 'message') {
|
||||
continue
|
||||
}
|
||||
|
||||
const texts = this.extractUnifiedOutputTexts(item.content)
|
||||
if (texts.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const role = this.mapUnifiedOutputRole(item.role)
|
||||
blocks.push(createMessageBlock(
|
||||
role,
|
||||
texts.map(text => createTextRenderBlock(text)),
|
||||
{ roleLabel: this.getRoleLabel(role) }
|
||||
))
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取统一响应格式的文本内容
|
||||
*/
|
||||
private extractUnifiedOutputTexts(content: unknown): string[] {
|
||||
if (typeof content === 'string') {
|
||||
return content ? [content] : []
|
||||
}
|
||||
|
||||
if (!Array.isArray(content)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const texts: string[] = []
|
||||
for (const rawPart of content) {
|
||||
const part = rawPart as RawObject
|
||||
if (
|
||||
(part.type === 'output_text' || part.type === 'text' || part.type === 'input_text') &&
|
||||
typeof part.text === 'string'
|
||||
) {
|
||||
texts.push(part.text)
|
||||
}
|
||||
}
|
||||
return texts
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射统一 output 结构的角色
|
||||
*/
|
||||
private mapUnifiedOutputRole(role: unknown): MessageRole {
|
||||
if (role === 'assistant' || role === 'model') return 'assistant'
|
||||
if (role === 'user') return 'user'
|
||||
if (role === 'system') return 'system'
|
||||
if (role === 'tool') return 'tool'
|
||||
return 'assistant'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已解析内容的徽章
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user