mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
fix(provider): 将rust分支的gemini cli端点行为对齐到python分支 (#321)
* fix(provider): 对齐 Vertex/Gemini 上游发包与 Python master - provider-transport: 为 custom+aiplatform 推断 Vertex API key 上下文并统一 URL 构建顺序,复用共享 request_url 构建最终上游地址 - ai-pipeline/gateway: Vertex Gemini 路径改为仅使用 URL query key,不再向上游附带 x-goog-api-key header;同步对齐 standard/admin/test-connection/runtime miss 摘要中的最终 URL - gemini conversion: 按 Python master 输出 Gemini 请求体,补齐 system_instruction / generation_config / tool_config / function_declarations 形态,并移植 Gemini schema 清洗逻辑 - scheduler/executor: 将最终 upstream_url、mapped_model、key_name 写入候选 extra_data,运行时 miss 诊断优先展示真实展开后的上游 URL 便于服务器排障 * fix(provider): 修复 Vertex provider 测试与本地调度链路 * fix(provider): 对齐 Vertex 本地执行与 Rust CI
This commit is contained in:
@@ -190,6 +190,10 @@ export interface TestModelRequest {
|
||||
endpoint_id?: string
|
||||
message?: string
|
||||
api_format?: string
|
||||
request_headers?: Record<string, unknown>
|
||||
request_body?: Record<string, unknown>
|
||||
request_id?: string
|
||||
concurrency?: number
|
||||
}
|
||||
|
||||
export interface TestModelResponse {
|
||||
@@ -210,9 +214,13 @@ export interface TestModelResponse {
|
||||
model?: string
|
||||
}
|
||||
|
||||
export async function testModel(data: TestModelRequest): Promise<TestModelResponse> {
|
||||
export async function testModel(
|
||||
data: TestModelRequest,
|
||||
options: { signal?: AbortSignal } = {},
|
||||
): Promise<TestModelResponse> {
|
||||
const response = await client.post('/api/admin/provider-query/test-model', data, {
|
||||
timeout: 10 * 60 * 1000,
|
||||
signal: options.signal,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@ import { ref, onBeforeUnmount } from 'vue'
|
||||
import { isAxiosError } from 'axios'
|
||||
import { useToast } from './useToast'
|
||||
import {
|
||||
testModel,
|
||||
testModelFailover,
|
||||
type TestAttemptDetail,
|
||||
type TestModelResponse,
|
||||
type TestModelFailoverResponse,
|
||||
} from '@/api/endpoints/providers'
|
||||
import { requestTraceApi, type RequestTrace } from '@/api/requestTrace'
|
||||
@@ -14,6 +17,7 @@ export interface StartTestParams {
|
||||
displayLabel: string
|
||||
apiFormat?: string
|
||||
endpointId?: string
|
||||
endpointBaseUrl?: string
|
||||
message?: string
|
||||
requestHeaders?: Record<string, unknown>
|
||||
requestBody?: Record<string, unknown>
|
||||
@@ -33,6 +37,7 @@ export interface UseModelTestOptions {
|
||||
export function useModelTest(options: UseModelTestOptions) {
|
||||
const { providerId, pollInterval = 800 } = options
|
||||
const { success: showSuccess, error: showError } = useToast()
|
||||
const LOCAL_FAILOVER_UNCONFIGURED_MESSAGE = 'Rust local provider-query failover simulation is not configured'
|
||||
|
||||
const testing = ref(false)
|
||||
const testMode = ref<'global' | 'direct'>('global')
|
||||
@@ -61,6 +66,78 @@ export function useModelTest(options: UseModelTestOptions) {
|
||||
return false
|
||||
}
|
||||
|
||||
function normalizeDirectTestResult(
|
||||
params: StartTestParams,
|
||||
result: TestModelResponse,
|
||||
): TestModelFailoverResponse {
|
||||
const responsePayload = result.data?.response
|
||||
const failureMessage = typeof result.error === 'string' && result.error.trim()
|
||||
? result.error.trim()
|
||||
: (
|
||||
typeof responsePayload?.error === 'string'
|
||||
? responsePayload.error
|
||||
: responsePayload?.error?.message
|
||||
) || null
|
||||
const syntheticAttempt: TestAttemptDetail = {
|
||||
candidate_index: 1,
|
||||
endpoint_api_format: params.apiFormat || '-',
|
||||
endpoint_base_url: params.endpointBaseUrl || '',
|
||||
key_name: null,
|
||||
key_id: '',
|
||||
auth_type: '',
|
||||
effective_model: result.model || params.modelName,
|
||||
status: result.success ? 'success' : 'failed',
|
||||
skip_reason: null,
|
||||
error_message: result.success ? null : failureMessage,
|
||||
status_code: responsePayload?.status_code ?? null,
|
||||
latency_ms: null,
|
||||
request_url: null,
|
||||
request_headers: (params.requestHeaders as Record<string, unknown> | undefined) ?? null,
|
||||
request_body: params.requestBody ?? null,
|
||||
response_headers: null,
|
||||
response_body: (responsePayload as Record<string, unknown> | undefined)
|
||||
?? (result.data as Record<string, unknown> | undefined)
|
||||
?? null,
|
||||
}
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
model: result.model || params.modelName,
|
||||
provider: result.provider || { id: providerId(), name: providerId() },
|
||||
attempts: [syntheticAttempt],
|
||||
total_candidates: 1,
|
||||
total_attempts: 1,
|
||||
data: (result.data as Record<string, unknown> | undefined) ?? null,
|
||||
error: failureMessage,
|
||||
}
|
||||
}
|
||||
|
||||
async function runDirectTest(
|
||||
params: StartTestParams,
|
||||
reqId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TestModelFailoverResponse> {
|
||||
return normalizeDirectTestResult(params, await testModel({
|
||||
provider_id: providerId(),
|
||||
model_name: params.modelName,
|
||||
api_format: params.apiFormat,
|
||||
endpoint_id: params.endpointId,
|
||||
...(normalizedMessage(params.message) ? { message: normalizedMessage(params.message) } : {}),
|
||||
...(params.requestHeaders ? { request_headers: params.requestHeaders } : {}),
|
||||
...(params.requestBody ? { request_body: params.requestBody } : {}),
|
||||
request_id: reqId,
|
||||
concurrency: params.concurrency,
|
||||
}, {
|
||||
signal,
|
||||
}))
|
||||
}
|
||||
|
||||
function normalizedMessage(message?: string): string | undefined {
|
||||
return typeof message === 'string' && message.trim()
|
||||
? message.trim()
|
||||
: undefined
|
||||
}
|
||||
|
||||
async function pollTestTrace(reqId: string, token: number) {
|
||||
try {
|
||||
const trace = await requestTraceApi.getRequestTrace(reqId, { attemptedOnly: false })
|
||||
@@ -136,25 +213,33 @@ export function useModelTest(options: UseModelTestOptions) {
|
||||
startPolling(reqId)
|
||||
|
||||
try {
|
||||
const normalizedMessage = typeof params.message === 'string' && params.message.trim()
|
||||
? params.message.trim()
|
||||
: undefined
|
||||
const message = normalizedMessage(params.message)
|
||||
|
||||
const result = await testModelFailover({
|
||||
provider_id: providerId(),
|
||||
mode: params.mode,
|
||||
model_name: params.modelName,
|
||||
failover_models: [params.modelName],
|
||||
api_format: params.apiFormat,
|
||||
endpoint_id: params.endpointId,
|
||||
...(normalizedMessage ? { message: normalizedMessage } : {}),
|
||||
...(params.requestHeaders ? { request_headers: params.requestHeaders } : {}),
|
||||
...(params.requestBody ? { request_body: params.requestBody } : {}),
|
||||
request_id: reqId,
|
||||
concurrency: params.concurrency,
|
||||
}, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
let result = params.mode === 'direct'
|
||||
? await runDirectTest(params, reqId, abortController.signal)
|
||||
: await testModelFailover({
|
||||
provider_id: providerId(),
|
||||
mode: params.mode,
|
||||
model_name: params.modelName,
|
||||
failover_models: [params.modelName],
|
||||
api_format: params.apiFormat,
|
||||
endpoint_id: params.endpointId,
|
||||
...(message ? { message } : {}),
|
||||
...(params.requestHeaders ? { request_headers: params.requestHeaders } : {}),
|
||||
...(params.requestBody ? { request_body: params.requestBody } : {}),
|
||||
request_id: reqId,
|
||||
concurrency: params.concurrency,
|
||||
}, {
|
||||
signal: abortController.signal,
|
||||
})
|
||||
|
||||
if (
|
||||
params.mode === 'global'
|
||||
&& !result.success
|
||||
&& result.error === LOCAL_FAILOVER_UNCONFIGURED_MESSAGE
|
||||
) {
|
||||
result = await runDirectTest(params, reqId, abortController.signal)
|
||||
}
|
||||
|
||||
const keepTraceContext = resultHasTraceContext(result)
|
||||
if (result.success) {
|
||||
|
||||
@@ -392,28 +392,40 @@ function filterAvailableApiFormats(formats: string[]): string[] {
|
||||
return formats.filter(format => availableFormatSet.has(normalizeApiFormat(format)))
|
||||
}
|
||||
|
||||
function getSelectableApiFormats(authType = form.value.auth_type): string[] {
|
||||
const sorted = sortApiFormats(props.availableApiFormats)
|
||||
if (props.providerType !== 'vertex_ai') {
|
||||
return sorted
|
||||
}
|
||||
|
||||
const allowed = getVertexAllowedFormatsByAuth(authType)
|
||||
return sorted.filter(fmt => allowed.has(normalizeApiFormat(fmt)))
|
||||
}
|
||||
|
||||
function sanitizeApiFormats(formats: string[], authType = form.value.auth_type): string[] {
|
||||
const selectable = new Set(getSelectableApiFormats(authType).map(normalizeApiFormat))
|
||||
if (selectable.size === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return formats.filter(format => selectable.has(normalizeApiFormat(format)))
|
||||
}
|
||||
|
||||
function getDefaultApiFormats(): string[] {
|
||||
const endpointFormat = props.endpoint?.api_format
|
||||
if (endpointFormat) {
|
||||
const endpointFormats = filterAvailableApiFormats([endpointFormat])
|
||||
const endpointFormats = sanitizeApiFormats([endpointFormat])
|
||||
if (endpointFormats.length > 0) {
|
||||
return endpointFormats
|
||||
}
|
||||
}
|
||||
|
||||
const firstAvailableFormat = sortApiFormats(props.availableApiFormats)[0]
|
||||
const firstAvailableFormat = getSelectableApiFormats()[0]
|
||||
return firstAvailableFormat ? [firstAvailableFormat] : []
|
||||
}
|
||||
|
||||
// 按 provider/auth_type 过滤后的可用 API 格式列表
|
||||
const visibleApiFormats = computed(() => {
|
||||
const sorted = sortApiFormats(props.availableApiFormats)
|
||||
if (props.providerType !== 'vertex_ai') {
|
||||
return sorted
|
||||
}
|
||||
const allowed = getVertexAllowedFormatsByAuth(form.value.auth_type)
|
||||
return sorted.filter(fmt => allowed.has(normalizeApiFormat(fmt)))
|
||||
})
|
||||
const visibleApiFormats = computed(() => getSelectableApiFormats())
|
||||
|
||||
const showAuthTypeSelector = computed(() => props.providerType === 'vertex_ai')
|
||||
|
||||
@@ -517,11 +529,7 @@ const form = ref({
|
||||
watch(
|
||||
[() => form.value.auth_type, () => props.providerType, () => props.availableApiFormats],
|
||||
() => {
|
||||
if (props.providerType !== 'vertex_ai') {
|
||||
return
|
||||
}
|
||||
const allowed = getVertexAllowedFormatsByAuth(form.value.auth_type)
|
||||
const filtered = form.value.api_formats.filter(fmt => allowed.has(normalizeApiFormat(fmt)))
|
||||
const filtered = sanitizeApiFormats(form.value.api_formats)
|
||||
if (filtered.length !== form.value.api_formats.length) {
|
||||
form.value.api_formats = [...filtered]
|
||||
}
|
||||
@@ -536,7 +544,7 @@ watch(
|
||||
return
|
||||
}
|
||||
|
||||
const filtered = filterAvailableApiFormats(form.value.api_formats)
|
||||
const filtered = sanitizeApiFormats(form.value.api_formats)
|
||||
if (filtered.length !== form.value.api_formats.length) {
|
||||
form.value.api_formats = [...filtered]
|
||||
return
|
||||
@@ -639,7 +647,10 @@ function loadKeyData() {
|
||||
auth_type: props.editingKey.auth_type === 'service_account' ? 'service_account' : 'api_key',
|
||||
auth_config_text: '', // auth_config 不返回给前端,编辑时需要重新输入
|
||||
api_formats: props.editingKey.api_formats?.length > 0
|
||||
? filterAvailableApiFormats(props.editingKey.api_formats)
|
||||
? sanitizeApiFormats(
|
||||
props.editingKey.api_formats,
|
||||
props.editingKey.auth_type === 'service_account' ? 'service_account' : 'api_key'
|
||||
)
|
||||
: [], // 编辑模式下保持原有选择,不默认全选
|
||||
rate_multipliers: { ...(props.editingKey.rate_multipliers || {}) },
|
||||
internal_priority: props.editingKey.internal_priority ?? 10,
|
||||
@@ -731,6 +742,8 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
form.value.api_formats = sanitizeApiFormats(form.value.api_formats)
|
||||
|
||||
// 验证至少选择一个 API 格式
|
||||
if (form.value.api_formats.length === 0) {
|
||||
showError('请至少选择一个 API 格式', '验证失败')
|
||||
|
||||
@@ -732,6 +732,7 @@ async function handleStartMappingTest() {
|
||||
displayLabel: `[${endpoint.api_format}] 映射 "${testingModelName.value}"`,
|
||||
apiFormat: endpoint.api_format,
|
||||
endpointId: endpoint.id,
|
||||
endpointBaseUrl: endpoint.base_url,
|
||||
requestHeaders,
|
||||
requestBody,
|
||||
concurrency: isPoolManagedProvider.value ? POOL_TEST_CONCURRENCY : SINGLE_TEST_CONCURRENCY,
|
||||
|
||||
@@ -517,6 +517,7 @@ async function handleStartPendingTest() {
|
||||
displayLabel: `${endpointPrefix}${modelName}`,
|
||||
apiFormat: endpoint.api_format,
|
||||
endpointId: endpoint.id,
|
||||
endpointBaseUrl: endpoint.base_url,
|
||||
requestHeaders,
|
||||
requestBody,
|
||||
concurrency: isPoolManagedProvider.value ? POOL_TEST_CONCURRENCY : SINGLE_TEST_CONCURRENCY,
|
||||
|
||||
Reference in New Issue
Block a user