mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
@@ -1031,6 +1031,7 @@ import { log } from '@/utils/logger'
|
||||
import AlertDialog from '@/components/common/AlertDialog.vue'
|
||||
import EndpointConditionEditor from './EndpointConditionEditor.vue'
|
||||
import ProxyNodeSelect from './ProxyNodeSelect.vue'
|
||||
import { getDefaultEndpointPath, normalizeEndpointApiFormat } from './endpoint-default-paths'
|
||||
import { useProxyNodesStore } from '@/stores/proxy-nodes'
|
||||
import {
|
||||
createEndpoint,
|
||||
@@ -1814,13 +1815,6 @@ function hasDefaultBodyRules(apiFormat: string): boolean {
|
||||
return (defaultBodyRulesByFormat.value[cacheKey]?.length || 0) > 0
|
||||
}
|
||||
|
||||
function normalizeLegacyOpenAIFormatAlias(apiFormat: string): string {
|
||||
switch (apiFormat.trim().toLowerCase()) {
|
||||
default:
|
||||
return apiFormat.trim().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDefaultBodyRulesForFormat(apiFormat: string, force = false): Promise<BodyRule[]> {
|
||||
if (!apiFormat) return []
|
||||
const providerType = (props.provider?.provider_type || '').toLowerCase()
|
||||
@@ -1857,26 +1851,12 @@ async function preloadDefaultBodyRules(endpoints: ProviderEndpoint[]): Promise<v
|
||||
// 获取指定 API 格式的默认路径
|
||||
function getDefaultPath(apiFormat: string, baseUrl?: string): string {
|
||||
const providerType = (props.provider?.provider_type || '').toLowerCase()
|
||||
const normalizedApiFormat = normalizeLegacyOpenAIFormatAlias(apiFormat)
|
||||
if (providerType === 'vertex_ai') {
|
||||
if (normalizedApiFormat === 'gemini:generate_content') {
|
||||
return '/v1/publishers/google/models/{model}:{action}'
|
||||
}
|
||||
if (normalizedApiFormat === 'claude:messages') {
|
||||
return '/v1/projects/{project_id}/locations/{region}/publishers/anthropic/models/{model}:{action}'
|
||||
}
|
||||
}
|
||||
|
||||
const format = apiFormats.value.find(f => f.value === normalizedApiFormat)
|
||||
const defaultPath = format?.default_path || ''
|
||||
// Codex 端点使用 /responses 而非 /v1/responses
|
||||
const isCodex = providerType
|
||||
? providerType === 'codex'
|
||||
: (!!baseUrl && isCodexUrl(baseUrl))
|
||||
if (normalizedApiFormat === 'openai:responses' && isCodex) {
|
||||
return '/responses'
|
||||
}
|
||||
return defaultPath
|
||||
return getDefaultEndpointPath({
|
||||
apiFormat,
|
||||
providerType,
|
||||
baseUrl,
|
||||
apiFormats: apiFormats.value,
|
||||
})
|
||||
}
|
||||
|
||||
function getDisplayedPath(endpoint: ProviderEndpoint): string {
|
||||
@@ -1886,12 +1866,6 @@ function getDisplayedPath(endpoint: ProviderEndpoint): string {
|
||||
return getEndpointEditState(endpoint.id)?.path ?? (endpoint.custom_path || '')
|
||||
}
|
||||
|
||||
// 判断是否是 Codex OAuth 端点
|
||||
function isCodexUrl(baseUrl: string): boolean {
|
||||
const url = baseUrl.replace(/\/+$/, '')
|
||||
return url.includes('/backend-api/codex') || url.endsWith('/codex')
|
||||
}
|
||||
|
||||
// 读取端点的上游流式策略(endpoint.config.upstream_stream_policy)
|
||||
function getEndpointUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
|
||||
const cfg = endpoint.config || {}
|
||||
@@ -3284,7 +3258,7 @@ function getCurrentUpstreamStreamPolicy(endpoint: ProviderEndpoint): string {
|
||||
|
||||
function isUpstreamStreamPolicyLocked(endpoint: ProviderEndpoint): boolean {
|
||||
return (props.provider?.provider_type || '').toLowerCase() === 'codex'
|
||||
&& normalizeLegacyOpenAIFormatAlias(endpoint.api_format) === 'openai:responses'
|
||||
&& normalizeEndpointApiFormat(endpoint.api_format) === 'openai:responses'
|
||||
}
|
||||
|
||||
// 获取上游流式按钮的样式类
|
||||
|
||||
@@ -467,10 +467,10 @@ function getAuthTypeOptions(providerType: ProviderType | null): AuthTypeOption[]
|
||||
|
||||
function getVertexAllowedFormatsByAuth(authType: ProviderKeyFormAuthType): Set<string> {
|
||||
if (authType === 'api_key') {
|
||||
return new Set(['gemini:generate_content'])
|
||||
return new Set(['gemini:generate_content', 'gemini:embedding'])
|
||||
}
|
||||
if (authType === 'service_account') {
|
||||
return new Set(['gemini:generate_content', 'claude:messages'])
|
||||
return new Set(['gemini:generate_content', 'gemini:embedding', 'claude:messages'])
|
||||
}
|
||||
return new Set()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getDefaultEndpointPath } from '../endpoint-default-paths'
|
||||
|
||||
const apiFormats = [
|
||||
{ value: 'gemini:generate_content', default_path: '/v1beta/models/{model}:{action}' },
|
||||
{ value: 'gemini:embedding', default_path: '/v1beta/models/{model}:{action}' },
|
||||
{ value: 'openai:responses', default_path: '/v1/responses' },
|
||||
]
|
||||
|
||||
describe('endpoint default paths', () => {
|
||||
it('uses Gemini Developer API paths for custom Gemini endpoints', () => {
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'gemini:generate_content',
|
||||
providerType: 'custom',
|
||||
apiFormats,
|
||||
})).toBe('/v1beta/models/{model}:{action}')
|
||||
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'gemini:embedding',
|
||||
providerType: 'custom',
|
||||
apiFormats,
|
||||
})).toBe('/v1beta/models/{model}:{action}')
|
||||
})
|
||||
|
||||
it('uses Vertex AI project/location paths for Vertex provider Gemini endpoints', () => {
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'gemini:generate_content',
|
||||
providerType: 'vertex_ai',
|
||||
apiFormats,
|
||||
})).toBe('/v1/projects/{project_id}/locations/{region}/publishers/google/models/{model}:{action}')
|
||||
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'gemini:embedding',
|
||||
providerType: 'vertex_ai',
|
||||
apiFormats,
|
||||
})).toBe('/v1/projects/{project_id}/locations/{region}/publishers/google/models/{model}:predict')
|
||||
})
|
||||
|
||||
it('keeps Codex Responses root path without duplicating /v1', () => {
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'openai:responses',
|
||||
providerType: 'codex',
|
||||
apiFormats,
|
||||
})).toBe('/responses')
|
||||
})
|
||||
})
|
||||
@@ -350,6 +350,27 @@ describe('provider key concurrent_limit form behavior', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('keeps Gemini embedding selectable for Vertex AI keys', async () => {
|
||||
const root = mountDialog(KeyFormDialog, {
|
||||
open: true,
|
||||
endpoint: null,
|
||||
editingKey: null,
|
||||
providerId: 'provider-vertex',
|
||||
providerType: 'vertex_ai',
|
||||
availableApiFormats: ['gemini:generate_content', 'gemini:embedding', 'claude:messages'],
|
||||
})
|
||||
await settle()
|
||||
|
||||
expect(root.textContent).toContain('Gemini Embedding')
|
||||
|
||||
const serviceAccountOption = root.querySelector<HTMLButtonElement>('[data-select-item="service_account"]')
|
||||
expect(serviceAccountOption).not.toBeNull()
|
||||
serviceAccountOption?.click()
|
||||
await settle()
|
||||
|
||||
expect(root.textContent).toContain('Gemini Embedding')
|
||||
})
|
||||
|
||||
it('hydrates and serializes a positive concurrent_limit number from the normal key form', async () => {
|
||||
const root = mountDialog(KeyFormDialog, {
|
||||
open: true,
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
interface ApiFormatPathDefinition {
|
||||
value: string
|
||||
default_path: string
|
||||
}
|
||||
|
||||
export function normalizeEndpointApiFormat(apiFormat: string): string {
|
||||
switch (apiFormat.trim().toLowerCase()) {
|
||||
default:
|
||||
return apiFormat.trim().toLowerCase()
|
||||
}
|
||||
}
|
||||
|
||||
function isCodexUrl(baseUrl: string): boolean {
|
||||
const url = baseUrl.replace(/\/+$/, '')
|
||||
return url.includes('/backend-api/codex') || url.endsWith('/codex')
|
||||
}
|
||||
|
||||
export function getDefaultEndpointPath(params: {
|
||||
apiFormat: string
|
||||
providerType?: string | null
|
||||
baseUrl?: string
|
||||
apiFormats: ApiFormatPathDefinition[]
|
||||
}): string {
|
||||
const providerType = (params.providerType || '').toLowerCase()
|
||||
const normalizedApiFormat = normalizeEndpointApiFormat(params.apiFormat)
|
||||
if (providerType === 'vertex_ai') {
|
||||
if (normalizedApiFormat === 'gemini:generate_content') {
|
||||
return '/v1/projects/{project_id}/locations/{region}/publishers/google/models/{model}:{action}'
|
||||
}
|
||||
if (normalizedApiFormat === 'gemini:embedding') {
|
||||
return '/v1/projects/{project_id}/locations/{region}/publishers/google/models/{model}:predict'
|
||||
}
|
||||
if (normalizedApiFormat === 'claude:messages') {
|
||||
return '/v1/projects/{project_id}/locations/{region}/publishers/anthropic/models/{model}:{action}'
|
||||
}
|
||||
}
|
||||
|
||||
const format = params.apiFormats.find(f => f.value === normalizedApiFormat)
|
||||
const defaultPath = format?.default_path || ''
|
||||
const isCodex = providerType
|
||||
? providerType === 'codex'
|
||||
: (!!params.baseUrl && isCodexUrl(params.baseUrl))
|
||||
if (normalizedApiFormat === 'openai:responses' && isCodex) {
|
||||
return '/responses'
|
||||
}
|
||||
return defaultPath
|
||||
}
|
||||
Reference in New Issue
Block a user