mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
fix(provider): support unversioned API roots in model fetch
This commit is contained in:
@@ -153,7 +153,7 @@
|
||||
<Label class="text-xs text-muted-foreground">自定义路径</Label>
|
||||
<Input
|
||||
:model-value="getDisplayedPath(endpoint)"
|
||||
:placeholder="getDefaultPath(endpoint.api_format, endpoint.base_url) || '留空使用默认'"
|
||||
:placeholder="getDefaultPath(endpoint.api_format, getEndpointEditState(endpoint.id)?.url ?? endpoint.base_url) || '留空使用默认'"
|
||||
:disabled="isFixedProvider"
|
||||
@update:model-value="(v) => updateEndpointField(endpoint.id, 'path', v)"
|
||||
/>
|
||||
|
||||
@@ -212,7 +212,7 @@ const emit = defineEmits<{
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { success, error: showError, warning: showWarning } = useToast()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
const isOpen = computed(() => props.open)
|
||||
@@ -297,7 +297,7 @@ async function fetchUpstreamModels() {
|
||||
hasQueried.value = true
|
||||
// 如果有部分失败,显示警告提示
|
||||
if (result.error) {
|
||||
showError(`部分格式获取失败: ${result.error}`, '警告')
|
||||
showWarning(result.error, '部分格式获取失败')
|
||||
}
|
||||
} else if (result.error) {
|
||||
errorMessage.value = result.error
|
||||
|
||||
@@ -395,7 +395,7 @@ const emit = defineEmits<{
|
||||
saved: []
|
||||
}>()
|
||||
|
||||
const { success, error: showError } = useToast()
|
||||
const { success, error: showError, warning: showWarning } = useToast()
|
||||
const { confirmWarning } = useConfirm()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
@@ -691,6 +691,9 @@ async function fetchUpstreamModels(forceRefresh = false) {
|
||||
// 获取上游模型后,从自定义模型列表中移除已变成已知的模型
|
||||
const upstreamIds = new Set(result.models.map((m: UpstreamModel) => m.id))
|
||||
allCustomModels.value = allCustomModels.value.filter(m => !upstreamIds.has(m))
|
||||
if (result.error) {
|
||||
showWarning(result.error, '部分格式获取失败')
|
||||
}
|
||||
} else if (result.error) {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ const emit = defineEmits<{
|
||||
'saved': []
|
||||
}>()
|
||||
|
||||
const { error: showError, success: showSuccess } = useToast()
|
||||
const { error: showError, success: showSuccess, warning: showWarning } = useToast()
|
||||
const { fetchModels: fetchCachedModels } = useUpstreamModelsCache()
|
||||
|
||||
type EndpointOption = {
|
||||
@@ -565,7 +565,11 @@ async function fetchUpstreamModels() {
|
||||
allCustomNames.value = Array.from(mergedCustom).filter(name => !upstreamIds.has(name))
|
||||
}
|
||||
if (result.error) {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
if (result.models.length > 0) {
|
||||
showWarning(result.error, '部分格式获取失败')
|
||||
} else {
|
||||
showError(result.error, '获取上游模型失败')
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
showError(parseApiError(err, '获取上游模型列表失败'), '错误')
|
||||
|
||||
@@ -3,9 +3,11 @@ import { describe, expect, it } from 'vitest'
|
||||
import { getDefaultEndpointPath } from '../endpoint-default-paths'
|
||||
|
||||
const apiFormats = [
|
||||
{ value: 'openai:chat', default_path: '/v1/chat/completions' },
|
||||
{ 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' },
|
||||
{ value: 'openai:embedding', default_path: '/v1/embeddings' },
|
||||
]
|
||||
|
||||
describe('endpoint default paths', () => {
|
||||
@@ -44,4 +46,20 @@ describe('endpoint default paths', () => {
|
||||
apiFormats,
|
||||
})).toBe('/responses')
|
||||
})
|
||||
|
||||
it('drops /v1 from OpenAI-compatible defaults when base URL ends with /api', () => {
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'openai:chat',
|
||||
providerType: 'custom',
|
||||
baseUrl: 'https://proxy.example.com/api',
|
||||
apiFormats,
|
||||
})).toBe('/chat/completions')
|
||||
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'openai:embedding',
|
||||
providerType: 'custom',
|
||||
baseUrl: 'https://proxy.example.com/api?tenant=demo',
|
||||
apiFormats,
|
||||
})).toBe('/embeddings')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,6 +15,25 @@ function isCodexUrl(baseUrl: string): boolean {
|
||||
return url.includes('/backend-api/codex') || url.endsWith('/codex')
|
||||
}
|
||||
|
||||
function baseUrlEndsWithApiRoot(baseUrl?: string | null): boolean {
|
||||
const raw = (baseUrl || '').trim()
|
||||
if (!raw) return false
|
||||
try {
|
||||
const parsed = new URL(raw)
|
||||
return parsed.pathname.replace(/\/+$/, '').toLowerCase().endsWith('/api')
|
||||
} catch {
|
||||
return raw.split('?')[0].replace(/\/+$/, '').toLowerCase().endsWith('/api')
|
||||
}
|
||||
}
|
||||
|
||||
function stripV1PrefixForApiRoot(path: string): string {
|
||||
return path.replace(/^\/v1(?=\/)/i, '')
|
||||
}
|
||||
|
||||
function isOpenAiCompatibleFormat(apiFormat: string): boolean {
|
||||
return apiFormat.startsWith('openai:') || apiFormat.startsWith('jina:')
|
||||
}
|
||||
|
||||
export function getDefaultEndpointPath(params: {
|
||||
apiFormat: string
|
||||
providerType?: string | null
|
||||
@@ -43,5 +62,8 @@ export function getDefaultEndpointPath(params: {
|
||||
if (normalizedApiFormat === 'openai:responses' && isCodex) {
|
||||
return '/responses'
|
||||
}
|
||||
if (baseUrlEndsWithApiRoot(params.baseUrl) && isOpenAiCompatibleFormat(normalizedApiFormat)) {
|
||||
return stripV1PrefixForApiRoot(defaultPath)
|
||||
}
|
||||
return defaultPath
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user