mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
refactor: 统一 API 格式显示函数,补全 Usage/Trace 的 provider 链路信息
- 提取 formatApiFormat 为共享工具函数,替换各组件中分散的 API_FORMAT_LABELS 调用 - Trace API 返回密钥认证类型(key_auth_type)和 OAuth 套餐信息(key_oauth_plan_type) - 请求时间线展示密钥认证方式标签(OAuth/Vertex AI/Kiro 等) - Usage 记录补充 provider_id/endpoint_id/key_id,避免 curl 复现时缺失 provider 信息 - curl 复现兜底从 RequestCandidate 表查找 provider 信息 - Headers Diff 面板左右独立滚动并同步垂直滚动位置
This commit is contained in:
@@ -67,6 +67,13 @@ export const API_FORMAT_ORDER: string[] = [
|
|||||||
API_FORMATS.GEMINI_VIDEO,
|
API_FORMATS.GEMINI_VIDEO,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 工具函数:将 API 格式签名转为友好显示名称
|
||||||
|
export function formatApiFormat(format: string | null | undefined): string {
|
||||||
|
if (!format) return '-'
|
||||||
|
const raw = format.trim()
|
||||||
|
return API_FORMAT_LABELS[raw] || API_FORMAT_LABELS[raw.toLowerCase()] || API_FORMAT_LABELS[raw.toUpperCase()] || raw
|
||||||
|
}
|
||||||
|
|
||||||
// 工具函数:按标准顺序排序 API 格式数组
|
// 工具函数:按标准顺序排序 API 格式数组
|
||||||
export function sortApiFormats(formats: string[]): string[] {
|
export function sortApiFormats(formats: string[]): string[] {
|
||||||
return [...formats].sort((a, b) => {
|
return [...formats].sort((a, b) => {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export interface CandidateRecord {
|
|||||||
endpoint_name?: string // 端点显示名称(api_format)
|
endpoint_name?: string // 端点显示名称(api_format)
|
||||||
key_id?: string
|
key_id?: string
|
||||||
key_name?: string // 密钥名称
|
key_name?: string // 密钥名称
|
||||||
key_preview?: string // 密钥脱敏预览(如 sk-***abc)
|
key_preview?: string // 密钥脱敏预览(如 sk-***abc),OAuth 类型不返回
|
||||||
|
key_auth_type?: string // 密钥认证类型(api_key, oauth, vertex_ai 等)
|
||||||
|
key_oauth_plan_type?: string // OAuth 账号套餐类型(free/plus/team/enterprise)
|
||||||
key_capabilities?: Record<string, boolean> | null // Key 支持的能力
|
key_capabilities?: Record<string, boolean> | null // Key 支持的能力
|
||||||
required_capabilities?: Record<string, boolean> | null // 请求实际需要的能力标签
|
required_capabilities?: Record<string, boolean> | null // 请求实际需要的能力标签
|
||||||
status: 'pending' | 'streaming' | 'success' | 'failed' | 'skipped' | 'cancelled' | 'available' | 'unused' | 'stream_interrupted'
|
status: 'pending' | 'streaming' | 'success' | 'failed' | 'skipped' | 'cancelled' | 'available' | 'unused' | 'stream_interrupted'
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
class="text-xs font-semibold px-2.5 py-1"
|
class="text-xs font-semibold px-2.5 py-1"
|
||||||
>
|
>
|
||||||
{{ formatGroup.api_format }}
|
{{ formatApiFormat(formatGroup.api_format) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -604,6 +604,7 @@ import {
|
|||||||
type RoutingEndpointInfo
|
type RoutingEndpointInfo
|
||||||
} from '@/api/global-models'
|
} from '@/api/global-models'
|
||||||
import { API_FORMAT_ORDER } from '@/api/endpoints/types'
|
import { API_FORMAT_ORDER } from '@/api/endpoints/types'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import { recoverKeyHealth } from '@/api/endpoints/health'
|
import { recoverKeyHealth } from '@/api/endpoints/health'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useCountdownTimer, getProbeCountdown } from '@/composables/useCountdownTimer'
|
import { useCountdownTimer, getProbeCountdown } from '@/composables/useCountdownTimer'
|
||||||
|
|||||||
@@ -169,7 +169,7 @@
|
|||||||
:key="fmt"
|
:key="fmt"
|
||||||
class="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0"
|
class="text-[10px] px-1 py-0.5 rounded bg-muted text-muted-foreground shrink-0"
|
||||||
>
|
>
|
||||||
{{ API_FORMAT_LABELS[fmt] || fmt }}
|
{{ formatApiFormat(fmt) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
@@ -250,9 +250,9 @@ import {
|
|||||||
batchAssignModelsToProvider,
|
batchAssignModelsToProvider,
|
||||||
deleteModel,
|
deleteModel,
|
||||||
importModelsFromUpstream,
|
importModelsFromUpstream,
|
||||||
API_FORMAT_LABELS,
|
|
||||||
type Model
|
type Model
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import { useUpstreamModelsCache, type UpstreamModel } from '../composables/useUpstreamModelsCache'
|
import { useUpstreamModelsCache, type UpstreamModel } from '../composables/useUpstreamModelsCache'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<!-- 卡片头部:格式名称 + 状态 + 操作 -->
|
<!-- 卡片头部:格式名称 + 状态 + 操作 -->
|
||||||
<div class="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b">
|
<div class="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="font-medium">{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
|
<span class="font-medium">{{ formatApiFormat(endpoint.api_format) }}</span>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="!endpoint.is_active"
|
v-if="!endpoint.is_active"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -737,7 +737,6 @@ import {
|
|||||||
createEndpoint,
|
createEndpoint,
|
||||||
updateEndpoint,
|
updateEndpoint,
|
||||||
deleteEndpoint,
|
deleteEndpoint,
|
||||||
API_FORMAT_LABELS,
|
|
||||||
type ProviderEndpoint,
|
type ProviderEndpoint,
|
||||||
type ProviderWithEndpointsSummary,
|
type ProviderWithEndpointsSummary,
|
||||||
type HeaderRule,
|
type HeaderRule,
|
||||||
@@ -747,6 +746,7 @@ import {
|
|||||||
type BodyRuleConditionOp,
|
type BodyRuleConditionOp,
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
// 编辑用的规则类型(统一的可编辑结构)
|
// 编辑用的规则类型(统一的可编辑结构)
|
||||||
interface EditableRule {
|
interface EditableRule {
|
||||||
@@ -1032,7 +1032,7 @@ const availableFormats = computed(() => {
|
|||||||
// 删除确认弹窗描述
|
// 删除确认弹窗描述
|
||||||
const deleteConfirmDescription = computed(() => {
|
const deleteConfirmDescription = computed(() => {
|
||||||
if (!endpointToDelete.value) return ''
|
if (!endpointToDelete.value) return ''
|
||||||
const formatLabel = API_FORMAT_LABELS[endpointToDelete.value.api_format] || endpointToDelete.value.api_format
|
const formatLabel = formatApiFormat(endpointToDelete.value.api_format)
|
||||||
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
|
return `确定要删除 ${formatLabel} 端点吗?关联密钥将移除对该 API 格式的支持。`
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2040,7 +2040,7 @@ async function handleAddEndpoint() {
|
|||||||
custom_path: newEndpoint.value.custom_path || undefined,
|
custom_path: newEndpoint.value.custom_path || undefined,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
})
|
})
|
||||||
success(`已添加 ${API_FORMAT_LABELS[newEndpoint.value.api_format] || newEndpoint.value.api_format} 端点`)
|
success(`已添加 ${formatApiFormat(newEndpoint.value.api_format)} 端点`)
|
||||||
// 重置表单,保留 URL
|
// 重置表单,保留 URL
|
||||||
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
newEndpoint.value = { api_format: '', base_url: baseUrl, custom_path: '' }
|
||||||
emit('endpointCreated')
|
emit('endpointCreated')
|
||||||
@@ -2082,7 +2082,7 @@ async function confirmDeleteEndpoint() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteEndpoint(endpoint.id)
|
await deleteEndpoint(endpoint.id)
|
||||||
success(`已删除 ${API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format} 端点`)
|
success(`已删除 ${formatApiFormat(endpoint.api_format)} 端点`)
|
||||||
emit('endpointUpdated')
|
emit('endpointUpdated')
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
showError(error.response?.data?.detail || '删除失败', '错误')
|
showError(error.response?.data?.detail || '删除失败', '错误')
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="font-mono text-xs"
|
class="font-mono text-xs"
|
||||||
>
|
>
|
||||||
{{ monitor.api_format }}
|
{{ formatApiFormat(monitor.api_format) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="monitor.total_attempts > 0"
|
v-if="monitor.total_attempts > 0"
|
||||||
@@ -142,6 +142,7 @@ import EndpointHealthTimeline from './EndpointHealthTimeline.vue'
|
|||||||
import { getEndpointStatusMonitor, getPublicEndpointStatusMonitor } from '@/api/endpoints/health'
|
import { getEndpointStatusMonitor, getPublicEndpointStatusMonitor } from '@/api/endpoints/health'
|
||||||
import type { EndpointStatusMonitor, PublicEndpointStatusMonitor } from '@/api/endpoints/types'
|
import type { EndpointStatusMonitor, PublicEndpointStatusMonitor } from '@/api/endpoints/types'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
title?: string
|
title?: string
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="text-[10px] px-1.5 py-0 shrink-0"
|
class="text-[10px] px-1.5 py-0 shrink-0"
|
||||||
>
|
>
|
||||||
{{ API_FORMAT_LABELS[fmt] || fmt }}
|
{{ formatApiFormat(fmt) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="isModelExisting(model.id)"
|
v-if="isModelExisting(model.id)"
|
||||||
@@ -197,8 +197,8 @@ import {
|
|||||||
getProviderModels,
|
getProviderModels,
|
||||||
type EndpointAPIKey,
|
type EndpointAPIKey,
|
||||||
type UpstreamModel,
|
type UpstreamModel,
|
||||||
API_FORMAT_LABELS,
|
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
import { useUpstreamModelsCache } from '../composables/useUpstreamModelsCache'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
<span
|
<span
|
||||||
class="text-sm whitespace-nowrap"
|
class="text-sm whitespace-nowrap"
|
||||||
:class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'"
|
:class="form.api_formats.includes(format) ? 'text-primary' : 'text-muted-foreground'"
|
||||||
>{{ API_FORMAT_LABELS[format] || format }}</span>
|
>{{ formatApiFormat(format) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center shrink-0 ml-2 text-xs text-muted-foreground gap-1"
|
class="flex items-center shrink-0 ml-2 text-xs text-muted-foreground gap-1"
|
||||||
@@ -321,7 +321,6 @@ import {
|
|||||||
addProviderKey,
|
addProviderKey,
|
||||||
updateProviderKey,
|
updateProviderKey,
|
||||||
getAllCapabilities,
|
getAllCapabilities,
|
||||||
API_FORMAT_LABELS,
|
|
||||||
sortApiFormats,
|
sortApiFormats,
|
||||||
type EndpointAPIKey,
|
type EndpointAPIKey,
|
||||||
type EndpointAPIKeyUpdate,
|
type EndpointAPIKeyUpdate,
|
||||||
@@ -329,6 +328,7 @@ import {
|
|||||||
type CapabilityDefinition,
|
type CapabilityDefinition,
|
||||||
type ProviderType
|
type ProviderType
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|||||||
@@ -188,7 +188,7 @@
|
|||||||
]"
|
]"
|
||||||
@click="activeFormatTab = format"
|
@click="activeFormatTab = format"
|
||||||
>
|
>
|
||||||
{{ API_FORMAT_LABELS[format] || format }}
|
{{ formatApiFormat(format) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -436,7 +436,8 @@ import { updateProvider, updateProviderKey } from '@/api/endpoints'
|
|||||||
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
import type { ProviderWithEndpointsSummary } from '@/api/endpoints'
|
||||||
import { adminApi } from '@/api/admin'
|
import { adminApi } from '@/api/admin'
|
||||||
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
|
import { batchQueryBalance, type ActionResultResponse, type BalanceInfo } from '@/api/providerOps'
|
||||||
import { API_FORMAT_SHORT, API_FORMAT_LABELS } from '@/api/endpoints/types'
|
import { API_FORMAT_SHORT } from '@/api/endpoints/types'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
interface KeyWithMeta {
|
interface KeyWithMeta {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -98,9 +98,9 @@
|
|||||||
<span
|
<span
|
||||||
class="text-xs px-2 py-0.5 rounded-md border border-border bg-background hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors font-medium"
|
class="text-xs px-2 py-0.5 rounded-md border border-border bg-background hover:bg-accent hover:border-accent-foreground/20 cursor-pointer transition-colors font-medium"
|
||||||
:class="{ 'opacity-40': !endpoint.is_active }"
|
:class="{ 'opacity-40': !endpoint.is_active }"
|
||||||
:title="`编辑 ${API_FORMAT_LABELS[endpoint.api_format]} 端点`"
|
:title="`编辑 ${formatApiFormat(endpoint.api_format)} 端点`"
|
||||||
@click="handleEditEndpoint(endpoint)"
|
@click="handleEditEndpoint(endpoint)"
|
||||||
>{{ API_FORMAT_LABELS[endpoint.api_format] || endpoint.api_format }}</span>
|
>{{ formatApiFormat(endpoint.api_format) }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
v-if="endpoints.length > 0"
|
v-if="endpoints.length > 0"
|
||||||
@@ -1053,12 +1053,12 @@ import {
|
|||||||
type ProviderEndpoint,
|
type ProviderEndpoint,
|
||||||
type EndpointAPIKey,
|
type EndpointAPIKey,
|
||||||
type Model,
|
type Model,
|
||||||
API_FORMAT_LABELS,
|
|
||||||
API_FORMAT_ORDER,
|
API_FORMAT_ORDER,
|
||||||
API_FORMAT_SHORT,
|
API_FORMAT_SHORT,
|
||||||
sortApiFormats,
|
sortApiFormats,
|
||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
import type { UpstreamMetadata, AntigravityModelQuota } from '@/api/endpoints/types'
|
import type { UpstreamMetadata, AntigravityModelQuota } from '@/api/endpoints/types'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
// 扩展端点类型,包含密钥列表
|
// 扩展端点类型,包含密钥列表
|
||||||
interface ProviderEndpointWithKeys extends ProviderEndpoint {
|
interface ProviderEndpointWithKeys extends ProviderEndpoint {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="text-xs"
|
class="text-xs"
|
||||||
>
|
>
|
||||||
{{ API_FORMAT_LABELS[format] || format }}
|
{{ formatApiFormat(format) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<!-- 映射数量 -->
|
<!-- 映射数量 -->
|
||||||
@@ -201,6 +201,7 @@ import {
|
|||||||
} from '@/api/endpoints'
|
} from '@/api/endpoints'
|
||||||
import { updateModel } from '@/api/endpoints/models'
|
import { updateModel } from '@/api/endpoints/models'
|
||||||
import { parseTestModelError } from '@/utils/errorParser'
|
import { parseTestModelError } from '@/utils/errorParser'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
provider: any
|
provider: any
|
||||||
@@ -296,7 +297,7 @@ const deleteConfirmDescription = computed(() => {
|
|||||||
if (!deletingGroup.value) return ''
|
if (!deletingGroup.value) return ''
|
||||||
const { model, aliases, apiFormats } = deletingGroup.value
|
const { model, aliases, apiFormats } = deletingGroup.value
|
||||||
const modelName = model.global_model_display_name || model.provider_model_name
|
const modelName = model.global_model_display_name || model.provider_model_name
|
||||||
const scopeText = apiFormats.length === 0 ? '全部' : apiFormats.map(f => API_FORMAT_LABELS[f] || f).join(', ')
|
const scopeText = apiFormats.length === 0 ? '全部' : apiFormats.map(f => formatApiFormat(f)).join(', ')
|
||||||
const aliasNames = aliases.map(a => a.name).join(', ')
|
const aliasNames = aliases.map(a => a.name).join(', ')
|
||||||
return `确定要删除模型「${modelName}」在作用域「${scopeText}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
return `确定要删除模型「${modelName}」在作用域「${scopeText}」下的 ${aliases.length} 个映射吗?\n\n映射名称:${aliasNames}`
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -92,14 +92,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 格式转换标记(节点下方) -->
|
|
||||||
<div
|
|
||||||
v-if="group.hasConversion"
|
|
||||||
class="conversion-indicator"
|
|
||||||
>
|
|
||||||
{{ group.primary.extra_data?.provider_api_format || '转换' }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 连接线 -->
|
<!-- 连接线 -->
|
||||||
<div
|
<div
|
||||||
v-if="groupIndex < groupedTimeline.length - 1"
|
v-if="groupIndex < groupedTimeline.length - 1"
|
||||||
@@ -202,25 +194,31 @@
|
|||||||
<span class="info-value mono">{{ formatLatency(currentAttempt.extra_data.first_byte_time_ms) }}</span>
|
<span class="info-value mono">{{ formatLatency(currentAttempt.extra_data.first_byte_time_ms) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="currentAttempt.extra_data?.needs_conversion"
|
v-if="currentAttempt.extra_data?.provider_api_format"
|
||||||
class="info-item"
|
class="info-item"
|
||||||
>
|
>
|
||||||
<span class="info-label">格式</span>
|
<span class="info-label">格式</span>
|
||||||
<span class="info-value">
|
<span class="info-value">
|
||||||
<span class="conversion-badge">格式转换</span>
|
<code class="format-code">{{ formatApiFormat(currentAttempt.extra_data.provider_api_format) }}</code>
|
||||||
<code
|
<span
|
||||||
v-if="currentAttempt.extra_data?.provider_api_format"
|
v-if="currentAttempt.extra_data?.needs_conversion"
|
||||||
class="ml-1.5 text-xs"
|
class="conversion-badge ml-1.5"
|
||||||
>{{ currentAttempt.extra_data.provider_api_format }}</code>
|
>格式转换</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="currentAttempt.key_name || currentAttempt.key_id"
|
v-if="currentAttempt.key_name || currentAttempt.key_id"
|
||||||
class="info-item"
|
class="info-item"
|
||||||
>
|
>
|
||||||
<span class="info-label">密钥</span>
|
<span class="info-label">{{ isOAuthType(currentAttempt.key_auth_type) ? '账号' : '密钥' }}</span>
|
||||||
<span class="info-value info-value-stacked">
|
<span class="info-value info-value-stacked">
|
||||||
<span class="key-name">{{ currentAttempt.key_name || '未知' }}</span>
|
<span class="key-name">
|
||||||
|
{{ currentAttempt.key_name || '未知' }}
|
||||||
|
<span
|
||||||
|
v-if="currentAttempt.key_auth_type && currentAttempt.key_auth_type !== 'api_key'"
|
||||||
|
class="auth-type-tag"
|
||||||
|
>{{ formatAuthTypeWithPlan(currentAttempt.key_auth_type, currentAttempt.key_oauth_plan_type) }}</span>
|
||||||
|
</span>
|
||||||
<code
|
<code
|
||||||
v-if="currentAttempt.key_preview"
|
v-if="currentAttempt.key_preview"
|
||||||
class="key-preview"
|
class="key-preview"
|
||||||
@@ -373,6 +371,7 @@ import Skeleton from '@/components/ui/skeleton.vue'
|
|||||||
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-vue-next'
|
import { ChevronLeft, ChevronRight, ExternalLink } from 'lucide-vue-next'
|
||||||
import { requestTraceApi, type RequestTrace, type CandidateRecord } from '@/api/requestTrace'
|
import { requestTraceApi, type RequestTrace, type CandidateRecord } from '@/api/requestTrace'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
// 节点组类型
|
// 节点组类型
|
||||||
interface NodeGroup {
|
interface NodeGroup {
|
||||||
@@ -386,6 +385,7 @@ interface NodeGroup {
|
|||||||
startIndex: number
|
startIndex: number
|
||||||
endIndex: number
|
endIndex: number
|
||||||
hasConversion: boolean // 组内是否有格式转换候选
|
hasConversion: boolean // 组内是否有格式转换候选
|
||||||
|
providerApiFormat: string | null // 提供商 API 格式(如 openai:cli)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用量数据类型
|
// 用量数据类型
|
||||||
@@ -615,6 +615,7 @@ const groupedTimeline = computed<NodeGroup[]>(() => {
|
|||||||
startIndex: index,
|
startIndex: index,
|
||||||
endIndex: index,
|
endIndex: index,
|
||||||
hasConversion: candidate.extra_data?.needs_conversion === true,
|
hasConversion: candidate.extra_data?.needs_conversion === true,
|
||||||
|
providerApiFormat: candidate.extra_data?.provider_api_format || null,
|
||||||
}
|
}
|
||||||
groups.push(currentGroup)
|
groups.push(currentGroup)
|
||||||
}
|
}
|
||||||
@@ -722,6 +723,30 @@ const isCapabilityUsed = (cap: string): boolean => {
|
|||||||
return activeCapabilities.value.includes(cap)
|
return activeCapabilities.value.includes(cap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断是否为 OAuth 类型(provider_type 为具体值时也算 OAuth)
|
||||||
|
const isOAuthType = (authType?: string): boolean => {
|
||||||
|
if (!authType) return false
|
||||||
|
return !['api_key', 'vertex_ai'].includes(authType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化认证类型(合并 plan 信息,避免冗余)
|
||||||
|
const formatAuthTypeWithPlan = (authType: string, planType?: string): string => {
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
'oauth': 'OAuth',
|
||||||
|
'vertex_ai': 'Vertex AI',
|
||||||
|
'kiro': 'Kiro',
|
||||||
|
'codex': 'Codex',
|
||||||
|
'antigravity': 'Antigravity',
|
||||||
|
'claude_code': 'Claude Code',
|
||||||
|
'gemini_cli': 'Gemini CLI',
|
||||||
|
}
|
||||||
|
const typeName = labels[authType] || authType
|
||||||
|
if (planType) {
|
||||||
|
return `${typeName} ${planType}`
|
||||||
|
}
|
||||||
|
return typeName
|
||||||
|
}
|
||||||
|
|
||||||
// 格式化能力标签显示
|
// 格式化能力标签显示
|
||||||
const formatCapabilityLabel = (cap: string): string => {
|
const formatCapabilityLabel = (cap: string): string => {
|
||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
@@ -1143,24 +1168,6 @@ const getStatusColorClass = (status: string) => {
|
|||||||
.node-dot.status-skipped { color: hsl(var(--primary)); }
|
.node-dot.status-skipped { color: hsl(var(--primary)); }
|
||||||
.node-dot.status-available { color: #d1d5db; }
|
.node-dot.status-available { color: #d1d5db; }
|
||||||
|
|
||||||
/* 格式转换标记(节点下方) */
|
|
||||||
.conversion-indicator {
|
|
||||||
position: absolute;
|
|
||||||
top: calc(100% + 6px);
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
font-size: 0.55rem;
|
|
||||||
color: hsl(var(--muted-foreground) / 0.7);
|
|
||||||
white-space: nowrap;
|
|
||||||
max-width: 80px;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
padding: 1px 4px;
|
|
||||||
border: 1px dashed hsl(var(--border));
|
|
||||||
border-radius: 3px;
|
|
||||||
background: hsl(var(--muted) / 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 连接线容器 */
|
/* 连接线容器 */
|
||||||
.node-line-wrapper {
|
.node-line-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1426,6 +1433,16 @@ const getStatusColorClass = (status: string) => {
|
|||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 格式代码 */
|
||||||
|
.format-code {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.1rem 0.3rem;
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
border-radius: 3px;
|
||||||
|
color: hsl(var(--muted-foreground));
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
/* Key 信息 */
|
/* Key 信息 */
|
||||||
.key-name {
|
.key-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -1440,6 +1457,20 @@ const getStatusColorClass = (status: string) => {
|
|||||||
font-family: ui-monospace, monospace;
|
font-family: ui-monospace, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 认证类型标签 */
|
||||||
|
.auth-type-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.1rem 0.35rem;
|
||||||
|
margin-left: 0.375rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: hsl(var(--primary) / 0.8);
|
||||||
|
background: hsl(var(--primary) / 0.08);
|
||||||
|
border: 1px solid hsl(var(--primary) / 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 代理信息 */
|
/* 代理信息 */
|
||||||
.proxy-name {
|
.proxy-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ import Tabs from '@/components/ui/tabs.vue'
|
|||||||
import TabsContent from '@/components/ui/tabs-content.vue'
|
import TabsContent from '@/components/ui/tabs-content.vue'
|
||||||
import { Copy, Check, Maximize2, Minimize2, Columns2, RefreshCw, X, Monitor, Server, MessageSquareText, Code2, Terminal, Play } from 'lucide-vue-next'
|
import { Copy, Check, Maximize2, Minimize2, Columns2, RefreshCw, X, Monitor, Server, MessageSquareText, Code2, Terminal, Play } from 'lucide-vue-next'
|
||||||
import { dashboardApi, type RequestDetail } from '@/api/dashboard'
|
import { dashboardApi, type RequestDetail } from '@/api/dashboard'
|
||||||
import { API_FORMAT_LABELS } from '@/api/endpoints/types'
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
|
|
||||||
// 子组件
|
// 子组件
|
||||||
@@ -1074,17 +1074,6 @@ function getTaskTypeLabel(taskType: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatApiFormat(format: string | null | undefined): string {
|
|
||||||
if (!format) return '-'
|
|
||||||
const raw = (format || '').trim()
|
|
||||||
return (
|
|
||||||
API_FORMAT_LABELS[raw] ||
|
|
||||||
API_FORMAT_LABELS[raw.toLowerCase()] ||
|
|
||||||
API_FORMAT_LABELS[raw.toUpperCase()] ||
|
|
||||||
raw
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatNumber(num: number): string {
|
function formatNumber(num: number): string {
|
||||||
if (num >= 1_000_000) {
|
if (num >= 1_000_000) {
|
||||||
return `${(num / 1_000_000).toFixed(1) }M`
|
return `${(num / 1_000_000).toFixed(1) }M`
|
||||||
|
|||||||
@@ -25,94 +25,100 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 并排 Diff 内容 -->
|
<!-- 并排 Diff 内容 -->
|
||||||
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
|
<div class="flex font-mono text-xs max-h-[500px]">
|
||||||
<div class="flex font-mono text-xs">
|
<!-- 左侧:客户端 -->
|
||||||
<!-- 左侧:客户端 -->
|
<div
|
||||||
<div class="flex-1 border-r">
|
ref="leftPanelRef"
|
||||||
<template
|
class="w-1/2 min-w-0 border-r overflow-x-auto overflow-y-auto"
|
||||||
v-for="entry in sortedEntries"
|
@scroll="onLeftScroll"
|
||||||
:key="'left-' + entry.key"
|
>
|
||||||
|
<template
|
||||||
|
v-for="entry in sortedEntries"
|
||||||
|
:key="'left-' + entry.key"
|
||||||
|
>
|
||||||
|
<!-- 删除的行 -->
|
||||||
|
<div
|
||||||
|
v-if="entry.status === 'removed'"
|
||||||
|
class="flex items-start bg-destructive/10 px-3 py-0.5"
|
||||||
>
|
>
|
||||||
<!-- 删除的行 -->
|
<span class="text-destructive">
|
||||||
<div
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
v-if="entry.status === 'removed'"
|
</span>
|
||||||
class="flex items-start bg-destructive/10 px-3 py-0.5"
|
</div>
|
||||||
>
|
<!-- 修改的行 - 旧值 -->
|
||||||
<span class="text-destructive">
|
<div
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
v-else-if="entry.status === 'modified'"
|
||||||
</span>
|
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
||||||
</div>
|
|
||||||
<!-- 修改的行 - 旧值 -->
|
|
||||||
<div
|
|
||||||
v-else-if="entry.status === 'modified'"
|
|
||||||
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
|
||||||
>
|
|
||||||
<span class="text-amber-600 dark:text-amber-400">
|
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!-- 新增的行 - 左侧空白占位 -->
|
|
||||||
<div
|
|
||||||
v-else-if="entry.status === 'added'"
|
|
||||||
class="flex items-start bg-muted/30 px-3 py-0.5"
|
|
||||||
>
|
|
||||||
<span class="text-muted-foreground/30 italic">(无)</span>
|
|
||||||
</div>
|
|
||||||
<!-- 未变化的行 -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
|
||||||
>
|
|
||||||
<span class="text-muted-foreground">
|
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<!-- 右侧:提供商 -->
|
|
||||||
<div class="flex-1">
|
|
||||||
<template
|
|
||||||
v-for="entry in sortedEntries"
|
|
||||||
:key="'right-' + entry.key"
|
|
||||||
>
|
>
|
||||||
<!-- 删除的行 - 右侧空白占位 -->
|
<span class="text-amber-600 dark:text-amber-400">
|
||||||
<div
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
v-if="entry.status === 'removed'"
|
</span>
|
||||||
class="flex items-start bg-muted/30 px-3 py-0.5"
|
</div>
|
||||||
>
|
<!-- 新增的行 - 左侧空白占位 -->
|
||||||
<span class="text-muted-foreground/50 line-through">
|
<div
|
||||||
"{{ entry.key }}": "{{ entry.clientValue }}"
|
v-else-if="entry.status === 'added'"
|
||||||
</span>
|
class="flex items-start bg-muted/30 px-3 py-0.5"
|
||||||
</div>
|
>
|
||||||
<!-- 修改的行 - 新值 -->
|
<span class="text-muted-foreground/30 italic">(无)</span>
|
||||||
<div
|
</div>
|
||||||
v-else-if="entry.status === 'modified'"
|
<!-- 未变化的行 -->
|
||||||
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
<div
|
||||||
>
|
v-else
|
||||||
<span class="text-amber-600 dark:text-amber-400">
|
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
||||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
>
|
||||||
</span>
|
<span class="text-muted-foreground">
|
||||||
</div>
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
<!-- 新增的行 -->
|
</span>
|
||||||
<div
|
</div>
|
||||||
v-else-if="entry.status === 'added'"
|
</template>
|
||||||
class="flex items-start bg-green-500/10 px-3 py-0.5"
|
</div>
|
||||||
>
|
<!-- 右侧:提供商 -->
|
||||||
<span class="text-green-600 dark:text-green-400">
|
<div
|
||||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
ref="rightPanelRef"
|
||||||
</span>
|
class="w-1/2 min-w-0 overflow-x-auto overflow-y-auto"
|
||||||
</div>
|
@scroll="onRightScroll"
|
||||||
<!-- 未变化的行 -->
|
>
|
||||||
<div
|
<template
|
||||||
v-else
|
v-for="entry in sortedEntries"
|
||||||
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
:key="'right-' + entry.key"
|
||||||
>
|
>
|
||||||
<span class="text-muted-foreground">
|
<!-- 删除的行 - 右侧空白占位 -->
|
||||||
"{{ entry.key }}": "{{ entry.providerValue }}"
|
<div
|
||||||
</span>
|
v-if="entry.status === 'removed'"
|
||||||
</div>
|
class="flex items-start bg-muted/30 px-3 py-0.5"
|
||||||
</template>
|
>
|
||||||
</div>
|
<span class="text-muted-foreground/50 line-through">
|
||||||
|
"{{ entry.key }}": "{{ entry.clientValue }}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 修改的行 - 新值 -->
|
||||||
|
<div
|
||||||
|
v-else-if="entry.status === 'modified'"
|
||||||
|
class="flex items-start bg-amber-500/10 px-3 py-0.5"
|
||||||
|
>
|
||||||
|
<span class="text-amber-600 dark:text-amber-400">
|
||||||
|
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 新增的行 -->
|
||||||
|
<div
|
||||||
|
v-else-if="entry.status === 'added'"
|
||||||
|
class="flex items-start bg-green-500/10 px-3 py-0.5"
|
||||||
|
>
|
||||||
|
<span class="text-green-600 dark:text-green-400">
|
||||||
|
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!-- 未变化的行 -->
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex items-start px-3 py-0.5 hover:bg-muted/50"
|
||||||
|
>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
"{{ entry.key }}": "{{ entry.providerValue }}"
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -150,7 +156,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import Card from '@/components/ui/card.vue'
|
import Card from '@/components/ui/card.vue'
|
||||||
import JsonContent from './JsonContent.vue'
|
import JsonContent from './JsonContent.vue'
|
||||||
import type { RequestDetail } from '@/api/dashboard'
|
import type { RequestDetail } from '@/api/dashboard'
|
||||||
@@ -168,6 +174,28 @@ const props = defineProps<{
|
|||||||
isDark: boolean
|
isDark: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const leftPanelRef = ref<HTMLElement | null>(null)
|
||||||
|
const rightPanelRef = ref<HTMLElement | null>(null)
|
||||||
|
let isSyncingScroll = false
|
||||||
|
|
||||||
|
function onLeftScroll() {
|
||||||
|
if (isSyncingScroll) return
|
||||||
|
isSyncingScroll = true
|
||||||
|
if (leftPanelRef.value && rightPanelRef.value) {
|
||||||
|
rightPanelRef.value.scrollTop = leftPanelRef.value.scrollTop
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => { isSyncingScroll = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRightScroll() {
|
||||||
|
if (isSyncingScroll) return
|
||||||
|
isSyncingScroll = true
|
||||||
|
if (leftPanelRef.value && rightPanelRef.value) {
|
||||||
|
leftPanelRef.value.scrollTop = rightPanelRef.value.scrollTop
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => { isSyncingScroll = false })
|
||||||
|
}
|
||||||
|
|
||||||
// 合并并排序的条目(用于并排显示)
|
// 合并并排序的条目(用于并排显示)
|
||||||
const sortedEntries = computed(() => {
|
const sortedEntries = computed(() => {
|
||||||
const clientHeaders = props.detail.request_headers || {}
|
const clientHeaders = props.detail.request_headers || {}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ import TableRow from '@/components/ui/table-row.vue'
|
|||||||
import TableHead from '@/components/ui/table-head.vue'
|
import TableHead from '@/components/ui/table-head.vue'
|
||||||
import TableCell from '@/components/ui/table-cell.vue'
|
import TableCell from '@/components/ui/table-cell.vue'
|
||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import { API_FORMAT_LABELS } from '@/api/endpoints/types'
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import type { ApiFormatStatsItem } from '../types'
|
import type { ApiFormatStatsItem } from '../types'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -86,15 +86,4 @@ defineProps<{
|
|||||||
isAdmin: boolean
|
isAdmin: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// 格式化 API 格式显示名称
|
|
||||||
function formatApiFormat(format: string): string {
|
|
||||||
const raw = (format || '').trim()
|
|
||||||
return (
|
|
||||||
API_FORMAT_LABELS[raw] ||
|
|
||||||
API_FORMAT_LABELS[raw.toLowerCase()] ||
|
|
||||||
API_FORMAT_LABELS[raw.toUpperCase()] ||
|
|
||||||
raw
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -668,7 +668,7 @@ import { RefreshCcw, Search } from 'lucide-vue-next'
|
|||||||
import { formatTokens, formatCurrency } from '@/utils/format'
|
import { formatTokens, formatCurrency } from '@/utils/format'
|
||||||
import { formatDateTime } from '../composables'
|
import { formatDateTime } from '../composables'
|
||||||
import { useRowClick } from '@/composables/useRowClick'
|
import { useRowClick } from '@/composables/useRowClick'
|
||||||
import { API_FORMAT_LABELS } from '@/api/endpoints/types'
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
import type { DateRangeParams, UsageRecord } from '../types'
|
import type { DateRangeParams, UsageRecord } from '../types'
|
||||||
import { TimeRangePicker } from '@/components/common'
|
import { TimeRangePicker } from '@/components/common'
|
||||||
|
|
||||||
@@ -811,17 +811,6 @@ function handleRowClick(event: MouseEvent, id: string) {
|
|||||||
|
|
||||||
// useIntervalFn 和 useDebounceFn 自动处理清理,无需 onUnmounted
|
// useIntervalFn 和 useDebounceFn 自动处理清理,无需 onUnmounted
|
||||||
|
|
||||||
// 格式化 API 格式显示名称
|
|
||||||
function formatApiFormat(format: string): string {
|
|
||||||
const raw = (format || '').trim()
|
|
||||||
return (
|
|
||||||
API_FORMAT_LABELS[raw] ||
|
|
||||||
API_FORMAT_LABELS[raw.toLowerCase()] ||
|
|
||||||
API_FORMAT_LABELS[raw.toUpperCase()] ||
|
|
||||||
raw
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 判断是否应该显示格式转换信息
|
// 判断是否应该显示格式转换信息
|
||||||
// 包括:1. 跨格式转换(has_format_conversion=true)2. 同族格式差异(如 CLAUDE_CLI → CLAUDE)
|
// 包括:1. 跨格式转换(has_format_conversion=true)2. 同族格式差异(如 CLAUDE_CLI → CLAUDE)
|
||||||
function shouldShowFormatConversion(record: UsageRecord): boolean {
|
function shouldShowFormatConversion(record: UsageRecord): boolean {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
getFrequencyClass
|
getFrequencyClass
|
||||||
} from '@/composables/useTTLAnalysis'
|
} from '@/composables/useTTLAnalysis'
|
||||||
import { log } from '@/utils/logger'
|
import { log } from '@/utils/logger'
|
||||||
|
import { formatApiFormat } from '@/api/endpoints/types/api-format'
|
||||||
|
|
||||||
// ==================== 缓存统计与亲和性列表 ====================
|
// ==================== 缓存统计与亲和性列表 ====================
|
||||||
|
|
||||||
@@ -660,7 +661,7 @@ onBeforeUnmount(() => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ item.api_format || '---' }}
|
{{ formatApiFormat(item.api_format) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-muted-foreground font-mono">
|
<div class="text-xs text-muted-foreground font-mono">
|
||||||
{{ item.key_prefix || '---' }}
|
{{ item.key_prefix || '---' }}
|
||||||
@@ -740,7 +741,7 @@ onBeforeUnmount(() => {
|
|||||||
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
|
<span class="truncate max-w-[100px]">{{ item.model_display_name || '---' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between text-xs">
|
<div class="flex items-center justify-between text-xs">
|
||||||
<span class="text-muted-foreground">{{ item.api_format || '---' }}</span>
|
<span class="text-muted-foreground">{{ formatApiFormat(item.api_format) }}</span>
|
||||||
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}次</span>
|
<span>{{ getRemainingTime(item.expire_at) }} · {{ item.request_count }}次</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -38,7 +39,9 @@ class CandidateResponse(BaseModel):
|
|||||||
endpoint_name: str | None = None # 端点显示名称(api_format)
|
endpoint_name: str | None = None # 端点显示名称(api_format)
|
||||||
key_id: str | None = None
|
key_id: str | None = None
|
||||||
key_name: str | None = None # 密钥名称
|
key_name: str | None = None # 密钥名称
|
||||||
key_preview: str | None = None # 密钥脱敏预览(如 sk-***abc)
|
key_preview: str | None = None # 密钥脱敏预览(如 sk-***abc),OAuth 类型不返回
|
||||||
|
key_auth_type: str | None = None # 密钥认证类型(api_key, oauth, vertex_ai 等)
|
||||||
|
key_oauth_plan_type: str | None = None # OAuth 账号套餐类型(free/plus/team/enterprise)
|
||||||
key_capabilities: dict | None = None # Key 支持的能力
|
key_capabilities: dict | None = None # Key 支持的能力
|
||||||
required_capabilities: dict | None = None # 请求实际需要的能力标签
|
required_capabilities: dict | None = None # 请求实际需要的能力标签
|
||||||
status: str # 'pending', 'success', 'failed', 'skipped'
|
status: str # 'pending', 'success', 'failed', 'skipped'
|
||||||
@@ -208,13 +211,15 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
# 批量加载 provider 信息,避免 N+1 查询
|
# 批量加载 provider 信息,避免 N+1 查询
|
||||||
provider_ids = {c.provider_id for c in candidates if c.provider_id}
|
provider_ids = {c.provider_id for c in candidates if c.provider_id}
|
||||||
provider_map = {}
|
provider_map: dict[str, str] = {}
|
||||||
provider_website_map = {}
|
provider_website_map: dict[str, str | None] = {}
|
||||||
|
provider_type_map: dict[str, str] = {}
|
||||||
if provider_ids:
|
if provider_ids:
|
||||||
providers = db.query(Provider).filter(Provider.id.in_(provider_ids)).all()
|
providers = db.query(Provider).filter(Provider.id.in_(provider_ids)).all()
|
||||||
for p in providers:
|
for p in providers:
|
||||||
provider_map[p.id] = p.name
|
provider_map[p.id] = p.name
|
||||||
provider_website_map[p.id] = p.website
|
provider_website_map[p.id] = p.website
|
||||||
|
provider_type_map[p.id] = getattr(p, "provider_type", "custom") or "custom"
|
||||||
|
|
||||||
# 批量加载 endpoint 信息
|
# 批量加载 endpoint 信息
|
||||||
endpoint_ids = {c.endpoint_id for c in candidates if c.endpoint_id}
|
endpoint_ids = {c.endpoint_id for c in candidates if c.endpoint_id}
|
||||||
@@ -227,15 +232,64 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
|||||||
|
|
||||||
# 批量加载 key 信息
|
# 批量加载 key 信息
|
||||||
key_ids = {c.key_id for c in candidates if c.key_id}
|
key_ids = {c.key_id for c in candidates if c.key_id}
|
||||||
key_map = {}
|
key_map: dict[str, str] = {}
|
||||||
key_preview_map = {}
|
key_preview_map: dict[str, str] = {}
|
||||||
key_capabilities_map = {}
|
key_capabilities_map: dict[str, dict | None] = {}
|
||||||
|
key_auth_type_map: dict[str, str] = {}
|
||||||
|
key_oauth_plan_map: dict[str, str | None] = {}
|
||||||
|
# 建立 key_id -> provider_id 的映射(用于获取 provider_type)
|
||||||
|
key_provider_map: dict[str, str | None] = {
|
||||||
|
c.key_id: c.provider_id for c in candidates if c.key_id
|
||||||
|
}
|
||||||
if key_ids:
|
if key_ids:
|
||||||
keys = db.query(ProviderAPIKey).filter(ProviderAPIKey.id.in_(key_ids)).all()
|
keys = db.query(ProviderAPIKey).filter(ProviderAPIKey.id.in_(key_ids)).all()
|
||||||
for k in keys:
|
for k in keys:
|
||||||
key_map[k.id] = k.name
|
key_map[k.id] = k.name
|
||||||
key_capabilities_map[k.id] = k.capabilities
|
key_capabilities_map[k.id] = k.capabilities
|
||||||
# 生成脱敏预览:先解密再脱敏
|
|
||||||
|
is_oauth = k.auth_type == "oauth"
|
||||||
|
|
||||||
|
if is_oauth:
|
||||||
|
# OAuth: auth_type 使用具体的 provider_type(如 kiro/codex/antigravity)
|
||||||
|
pid = key_provider_map.get(k.id)
|
||||||
|
key_auth_type_map[k.id] = (
|
||||||
|
provider_type_map.get(pid, "oauth") if pid else "oauth"
|
||||||
|
)
|
||||||
|
# 提取 plan_type(不同 provider 存储位置不同)
|
||||||
|
oauth_plan_type = None
|
||||||
|
# 1. Codex: auth_config.plan_type
|
||||||
|
# 2. Antigravity: auth_config.tier
|
||||||
|
if k.auth_config:
|
||||||
|
try:
|
||||||
|
decrypted_config = crypto_service.decrypt(k.auth_config)
|
||||||
|
auth_config = json.loads(decrypted_config)
|
||||||
|
oauth_plan_type = auth_config.get("plan_type")
|
||||||
|
if not oauth_plan_type:
|
||||||
|
ag_tier = auth_config.get("tier")
|
||||||
|
if ag_tier and isinstance(ag_tier, str):
|
||||||
|
oauth_plan_type = ag_tier.lower()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# 3. Kiro: upstream_metadata.kiro.subscription_title
|
||||||
|
# subscription_title 通常为 "KIRO FREE" / "KIRO PRO+" 等,
|
||||||
|
# 去掉 provider 名称前缀,只保留等级部分
|
||||||
|
if not oauth_plan_type:
|
||||||
|
um = getattr(k, "upstream_metadata", None) or {}
|
||||||
|
kiro_meta = um.get("kiro") if isinstance(um, dict) else None
|
||||||
|
if isinstance(kiro_meta, dict):
|
||||||
|
sub_title = kiro_meta.get("subscription_title")
|
||||||
|
if sub_title and isinstance(sub_title, str):
|
||||||
|
# "KIRO FREE" -> "Free", "KIRO PRO+" -> "Pro+"
|
||||||
|
ptype = provider_type_map.get(pid, "") if pid else ""
|
||||||
|
if ptype and sub_title.upper().startswith(ptype.upper()):
|
||||||
|
sub_title = sub_title[len(ptype) :].strip()
|
||||||
|
oauth_plan_type = sub_title
|
||||||
|
key_oauth_plan_map[k.id] = oauth_plan_type
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
key_auth_type_map[k.id] = k.auth_type or "api_key"
|
||||||
|
|
||||||
|
# 非 OAuth:生成脱敏预览
|
||||||
try:
|
try:
|
||||||
decrypted_key = crypto_service.decrypt(k.api_key)
|
decrypted_key = crypto_service.decrypt(k.api_key)
|
||||||
if len(decrypted_key) > 8:
|
if len(decrypted_key) > 8:
|
||||||
@@ -272,6 +326,10 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
|||||||
)
|
)
|
||||||
key_name = key_map.get(candidate.key_id) if candidate.key_id else None
|
key_name = key_map.get(candidate.key_id) if candidate.key_id else None
|
||||||
key_preview = key_preview_map.get(candidate.key_id) if candidate.key_id else None
|
key_preview = key_preview_map.get(candidate.key_id) if candidate.key_id else None
|
||||||
|
key_auth_type = key_auth_type_map.get(candidate.key_id) if candidate.key_id else None
|
||||||
|
key_oauth_plan_type = (
|
||||||
|
key_oauth_plan_map.get(candidate.key_id) if candidate.key_id else None
|
||||||
|
)
|
||||||
key_capabilities = (
|
key_capabilities = (
|
||||||
key_capabilities_map.get(candidate.key_id) if candidate.key_id else None
|
key_capabilities_map.get(candidate.key_id) if candidate.key_id else None
|
||||||
)
|
)
|
||||||
@@ -290,6 +348,8 @@ class AdminGetRequestTraceAdapter(AdminApiAdapter):
|
|||||||
key_id=candidate.key_id,
|
key_id=candidate.key_id,
|
||||||
key_name=key_name,
|
key_name=key_name,
|
||||||
key_preview=key_preview,
|
key_preview=key_preview,
|
||||||
|
key_auth_type=key_auth_type,
|
||||||
|
key_oauth_plan_type=key_oauth_plan_type,
|
||||||
key_capabilities=key_capabilities,
|
key_capabilities=key_capabilities,
|
||||||
required_capabilities=candidate.required_capabilities,
|
required_capabilities=candidate.required_capabilities,
|
||||||
status=candidate.status,
|
status=candidate.status,
|
||||||
|
|||||||
@@ -1559,20 +1559,32 @@ class AdminUsageCurlAdapter(AdminApiAdapter):
|
|||||||
usage_record = _find_usage_record(db, self.usage_id)
|
usage_record = _find_usage_record(db, self.usage_id)
|
||||||
|
|
||||||
# 获取端点和密钥
|
# 获取端点和密钥
|
||||||
|
endpoint_id = usage_record.provider_endpoint_id
|
||||||
|
key_id = usage_record.provider_api_key_id
|
||||||
|
|
||||||
|
# 兜底:Usage 记录缺少 provider 信息时,从 RequestCandidate 表查找
|
||||||
|
if not endpoint_id or not key_id:
|
||||||
|
from src.models.database import RequestCandidate as RC
|
||||||
|
|
||||||
|
candidate = (
|
||||||
|
db.query(RC)
|
||||||
|
.filter(
|
||||||
|
RC.request_id == usage_record.request_id,
|
||||||
|
RC.status.in_(["success", "failed", "streaming"]),
|
||||||
|
)
|
||||||
|
.order_by(RC.candidate_index.desc(), RC.retry_index.desc())
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if candidate:
|
||||||
|
endpoint_id = endpoint_id or candidate.endpoint_id
|
||||||
|
key_id = key_id or candidate.key_id
|
||||||
|
|
||||||
endpoint = None
|
endpoint = None
|
||||||
if usage_record.provider_endpoint_id:
|
if endpoint_id:
|
||||||
endpoint = (
|
endpoint = db.query(ProviderEndpoint).filter(ProviderEndpoint.id == endpoint_id).first()
|
||||||
db.query(ProviderEndpoint)
|
|
||||||
.filter(ProviderEndpoint.id == usage_record.provider_endpoint_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
provider_key = None
|
provider_key = None
|
||||||
if usage_record.provider_api_key_id:
|
if key_id:
|
||||||
provider_key = (
|
provider_key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == key_id).first()
|
||||||
db.query(ProviderAPIKey)
|
|
||||||
.filter(ProviderAPIKey.id == usage_record.provider_api_key_id)
|
|
||||||
.first()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 重建请求 URL
|
# 重建请求 URL
|
||||||
url: str | None = None
|
url: str | None = None
|
||||||
|
|||||||
@@ -255,6 +255,9 @@ class ChatSyncExecutor:
|
|||||||
request_body=actual_request_body,
|
request_body=actual_request_body,
|
||||||
error_message=str(e),
|
error_message=str(e),
|
||||||
is_stream=False,
|
is_stream=False,
|
||||||
|
provider_id=ctx.provider_id,
|
||||||
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
|
provider_api_key_id=ctx.key_id,
|
||||||
request_metadata=request_metadata or None,
|
request_metadata=request_metadata or None,
|
||||||
)
|
)
|
||||||
client_format = (ctx.client_api_format_for_error or "").upper()
|
client_format = (ctx.client_api_format_for_error or "").upper()
|
||||||
@@ -289,6 +292,9 @@ class ChatSyncExecutor:
|
|||||||
provider_request_headers=ctx.provider_request_headers,
|
provider_request_headers=ctx.provider_request_headers,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers={"content-type": "application/json"},
|
client_response_headers={"content-type": "application/json"},
|
||||||
|
provider_id=ctx.provider_id,
|
||||||
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
|
provider_api_key_id=ctx.key_id,
|
||||||
# 格式转换追踪
|
# 格式转换追踪
|
||||||
endpoint_api_format=ctx.provider_api_format_for_error or None,
|
endpoint_api_format=ctx.provider_api_format_for_error or None,
|
||||||
has_format_conversion=is_format_converted(
|
has_format_conversion=is_format_converted(
|
||||||
@@ -347,6 +353,9 @@ class ChatSyncExecutor:
|
|||||||
response_headers=error_response_headers,
|
response_headers=error_response_headers,
|
||||||
# 非流式失败返回给客户端的是 JSON 错误响应
|
# 非流式失败返回给客户端的是 JSON 错误响应
|
||||||
client_response_headers={"content-type": "application/json"},
|
client_response_headers={"content-type": "application/json"},
|
||||||
|
provider_id=ctx.provider_id,
|
||||||
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
|
provider_api_key_id=ctx.key_id,
|
||||||
# 格式转换追踪
|
# 格式转换追踪
|
||||||
endpoint_api_format=ctx.provider_api_format_for_error or None,
|
endpoint_api_format=ctx.provider_api_format_for_error or None,
|
||||||
has_format_conversion=is_format_converted(
|
has_format_conversion=is_format_converted(
|
||||||
@@ -377,6 +386,9 @@ class ChatSyncExecutor:
|
|||||||
ctx = self._ctx
|
ctx = self._ctx
|
||||||
|
|
||||||
ctx.provider_name = str(provider.name)
|
ctx.provider_name = str(provider.name)
|
||||||
|
ctx.provider_id = str(provider.id)
|
||||||
|
ctx.endpoint_id = str(endpoint.id)
|
||||||
|
ctx.key_id = str(key.id)
|
||||||
provider_api_format = str(endpoint.api_format or api_format)
|
provider_api_format = str(endpoint.api_format or api_format)
|
||||||
client_api_format = api_format.value if hasattr(api_format, "value") else str(api_format)
|
client_api_format = api_format.value if hasattr(api_format, "value") else str(api_format)
|
||||||
|
|
||||||
@@ -710,6 +722,9 @@ class ChatSyncExecutor:
|
|||||||
provider_request_headers=ctx.provider_request_headers,
|
provider_request_headers=ctx.provider_request_headers,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
|
provider_id=ctx.provider_id,
|
||||||
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
|
provider_api_key_id=ctx.key_id,
|
||||||
# 格式转换追踪
|
# 格式转换追踪
|
||||||
endpoint_api_format=ctx.provider_api_format or None,
|
endpoint_api_format=ctx.provider_api_format or None,
|
||||||
has_format_conversion=ctx.has_format_conversion,
|
has_format_conversion=ctx.has_format_conversion,
|
||||||
|
|||||||
@@ -269,6 +269,9 @@ class StreamTelemetryRecorder:
|
|||||||
response_body=response_body,
|
response_body=response_body,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
|
provider_id=ctx.provider_id,
|
||||||
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
|
provider_api_key_id=ctx.key_id,
|
||||||
target_model=ctx.mapped_model,
|
target_model=ctx.mapped_model,
|
||||||
request_type="chat",
|
request_type="chat",
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
@@ -318,6 +321,9 @@ class StreamTelemetryRecorder:
|
|||||||
response_body=response_body,
|
response_body=response_body,
|
||||||
response_headers=ctx.response_headers,
|
response_headers=ctx.response_headers,
|
||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
|
provider_id=ctx.provider_id,
|
||||||
|
provider_endpoint_id=ctx.endpoint_id,
|
||||||
|
provider_api_key_id=ctx.key_id,
|
||||||
target_model=ctx.mapped_model,
|
target_model=ctx.mapped_model,
|
||||||
request_type="chat",
|
request_type="chat",
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
|
|||||||
@@ -256,10 +256,13 @@ def update_existing_usage(
|
|||||||
existing_usage.actual_total_cost_usd = usage_params["actual_total_cost_usd"]
|
existing_usage.actual_total_cost_usd = usage_params["actual_total_cost_usd"]
|
||||||
existing_usage.rate_multiplier = usage_params["rate_multiplier"]
|
existing_usage.rate_multiplier = usage_params["rate_multiplier"]
|
||||||
|
|
||||||
# 更新 Provider 侧追踪信息
|
# 更新 Provider 侧追踪信息(仅在有新值时更新,避免覆盖已有数据)
|
||||||
existing_usage.provider_id = usage_params["provider_id"]
|
if usage_params.get("provider_id"):
|
||||||
existing_usage.provider_endpoint_id = usage_params["provider_endpoint_id"]
|
existing_usage.provider_id = usage_params["provider_id"]
|
||||||
existing_usage.provider_api_key_id = usage_params["provider_api_key_id"]
|
if usage_params.get("provider_endpoint_id"):
|
||||||
|
existing_usage.provider_endpoint_id = usage_params["provider_endpoint_id"]
|
||||||
|
if usage_params.get("provider_api_key_id"):
|
||||||
|
existing_usage.provider_api_key_id = usage_params["provider_api_key_id"]
|
||||||
|
|
||||||
# 更新元数据(如 billing_snapshot/dimensions 等)
|
# 更新元数据(如 billing_snapshot/dimensions 等)
|
||||||
if usage_params.get("request_metadata") is not None:
|
if usage_params.get("request_metadata") is not None:
|
||||||
|
|||||||
@@ -172,6 +172,10 @@ class MessageTelemetry:
|
|||||||
response_body: dict[str, Any] | None = None,
|
response_body: dict[str, Any] | None = None,
|
||||||
response_headers: dict[str, Any] | None = None,
|
response_headers: dict[str, Any] | None = None,
|
||||||
client_response_headers: dict[str, Any] | None = None,
|
client_response_headers: dict[str, Any] | None = None,
|
||||||
|
# Provider 侧追踪信息(用于 curl 复现等场景)
|
||||||
|
provider_id: str | None = None,
|
||||||
|
provider_endpoint_id: str | None = None,
|
||||||
|
provider_api_key_id: str | None = None,
|
||||||
# 格式转换追踪
|
# 格式转换追踪
|
||||||
endpoint_api_format: str | None = None,
|
endpoint_api_format: str | None = None,
|
||||||
has_format_conversion: bool = False,
|
has_format_conversion: bool = False,
|
||||||
@@ -183,9 +187,6 @@ class MessageTelemetry:
|
|||||||
"""
|
"""
|
||||||
记录失败请求
|
记录失败请求
|
||||||
|
|
||||||
注意:Provider 链路信息(provider_id, endpoint_id, key_id)不在此处记录,
|
|
||||||
因为 RequestCandidate 表已经记录了完整的请求链路追踪信息。
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
input_tokens: 预估输入 tokens(来自 message_start,用于中断请求的成本估算)
|
input_tokens: 预估输入 tokens(来自 message_start,用于中断请求的成本估算)
|
||||||
output_tokens: 预估输出 tokens(来自已收到的内容)
|
output_tokens: 预估输出 tokens(来自已收到的内容)
|
||||||
@@ -228,6 +229,10 @@ class MessageTelemetry:
|
|||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
response_body=response_body or {"error": error_message},
|
response_body=response_body or {"error": error_message},
|
||||||
request_id=self.request_id,
|
request_id=self.request_id,
|
||||||
|
# Provider 侧追踪信息
|
||||||
|
provider_id=provider_id,
|
||||||
|
provider_endpoint_id=provider_endpoint_id,
|
||||||
|
provider_api_key_id=provider_api_key_id,
|
||||||
# 模型映射信息
|
# 模型映射信息
|
||||||
target_model=target_model,
|
target_model=target_model,
|
||||||
# 请求元数据
|
# 请求元数据
|
||||||
@@ -254,6 +259,10 @@ class MessageTelemetry:
|
|||||||
response_body: dict[str, Any] | None = None,
|
response_body: dict[str, Any] | None = None,
|
||||||
response_headers: dict[str, Any] | None = None,
|
response_headers: dict[str, Any] | None = None,
|
||||||
client_response_headers: dict[str, Any] | None = None,
|
client_response_headers: dict[str, Any] | None = None,
|
||||||
|
# Provider 侧追踪信息
|
||||||
|
provider_id: str | None = None,
|
||||||
|
provider_endpoint_id: str | None = None,
|
||||||
|
provider_api_key_id: str | None = None,
|
||||||
# 格式转换追踪
|
# 格式转换追踪
|
||||||
endpoint_api_format: str | None = None,
|
endpoint_api_format: str | None = None,
|
||||||
has_format_conversion: bool = False,
|
has_format_conversion: bool = False,
|
||||||
@@ -294,6 +303,10 @@ class MessageTelemetry:
|
|||||||
client_response_headers=client_response_headers,
|
client_response_headers=client_response_headers,
|
||||||
response_body=response_body or {},
|
response_body=response_body or {},
|
||||||
request_id=self.request_id,
|
request_id=self.request_id,
|
||||||
|
# Provider 侧追踪信息
|
||||||
|
provider_id=provider_id,
|
||||||
|
provider_endpoint_id=provider_endpoint_id,
|
||||||
|
provider_api_key_id=provider_api_key_id,
|
||||||
target_model=target_model,
|
target_model=target_model,
|
||||||
metadata=request_metadata,
|
metadata=request_metadata,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user