Normalize endpoint API root handling

This commit is contained in:
fawney19
2026-05-29 02:29:33 +08:00
parent ef2953038e
commit b59c724455
46 changed files with 1179 additions and 205 deletions
@@ -42,11 +42,21 @@ function baseUrlEndsWithV1Root(baseUrl?: string | null): boolean {
return parseBaseUrlParts(baseUrl)?.path.endsWith('/v1') ?? false
}
function baseUrlHasVersionedApiRoot(baseUrl?: string | null): boolean {
const path = parseBaseUrlParts(baseUrl)?.path || ''
return /\/v\d+(?:beta\d*)?(?:\/|$)/i.test(path)
}
function isBigModelCodingApiRoot(baseUrl?: string | null): boolean {
const parts = parseBaseUrlParts(baseUrl)
return parts?.host === 'open.bigmodel.cn' && parts.path === '/api/coding/paas/v4'
}
function isDeepSeekApiRoot(baseUrl?: string | null): boolean {
const parts = parseBaseUrlParts(baseUrl)
return parts?.host === 'api.deepseek.com'
}
function isGoogleOpenAiCompatApiRoot(baseUrl?: string | null): boolean {
const parts = parseBaseUrlParts(baseUrl)
return parts?.host === 'generativelanguage.googleapis.com'
@@ -68,20 +78,84 @@ function openAiCompatibleBaseIncludesApiRoot(baseUrl?: string | null): boolean {
|| isVertexOpenAiCompatApiRoot(baseUrl)
}
function v1CompatibleBaseIncludesApiRoot(baseUrl?: string | null): boolean {
return baseUrlEndsWithV1Root(baseUrl)
}
function stripV1PrefixForApiRoot(path: string): string {
return path.replace(/^\/v1(?=\/)/i, '')
function stripVersionPrefixForApiRoot(path: string): string {
return path.replace(/^\/v\d+(?:beta\d*)?(?=\/)/i, '')
}
function isOpenAiCompatibleFormat(apiFormat: string): boolean {
return apiFormat.startsWith('openai:') || apiFormat.startsWith('jina:')
}
function isClaudeCompatibleFormat(apiFormat: string): boolean {
return apiFormat === 'claude:messages'
function usesVersionedApiRootByDefault(apiFormat: string): boolean {
return apiFormat === 'openai:chat'
|| apiFormat === 'openai:responses'
|| apiFormat === 'openai:responses:compact'
|| apiFormat === 'openai:embedding'
|| apiFormat === 'openai:rerank'
|| apiFormat === 'openai:image'
|| apiFormat === 'openai:video'
|| apiFormat === 'jina:embedding'
|| apiFormat === 'jina:rerank'
|| apiFormat === 'claude:messages'
|| apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video'
}
function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
if (
apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video'
) {
return '/v1beta'
}
return '/v1'
}
function skipsVersionedApiRootDefault(apiFormat: string, baseUrl: string): boolean {
if (
apiFormat === 'gemini:generate_content'
|| apiFormat === 'gemini:embedding'
|| apiFormat === 'gemini:video'
) {
return false
}
return isDeepSeekApiRoot(baseUrl)
|| isBigModelCodingApiRoot(baseUrl)
|| isGoogleOpenAiCompatApiRoot(baseUrl)
|| isVertexOpenAiCompatApiRoot(baseUrl)
}
function appendVersionedApiRoot(baseUrl: string, suffix: '/v1' | '/v1beta'): string {
const raw = baseUrl.trim()
if (!raw) return ''
try {
const parsed = new URL(raw)
parsed.pathname = `${parsed.pathname.replace(/\/+$/, '')}${suffix}`
return parsed.toString().replace(/\/$/, '')
} catch {
const [base, query] = raw.split('?', 2)
const normalizedBase = base.replace(/\/+$/, '')
return query === undefined ? `${normalizedBase}${suffix}` : `${normalizedBase}${suffix}?${query}`
}
}
export function getDefaultEndpointBaseUrl(params: {
apiFormat: string
baseUrl?: string | null
}): string {
const normalizedApiFormat = normalizeEndpointApiFormat(params.apiFormat)
const rawBaseUrl = (params.baseUrl || '').trim()
if (!rawBaseUrl) return ''
if (
usesVersionedApiRootByDefault(normalizedApiFormat)
&& !baseUrlHasVersionedApiRoot(rawBaseUrl)
&& !skipsVersionedApiRootDefault(normalizedApiFormat, rawBaseUrl)
) {
return appendVersionedApiRoot(rawBaseUrl, versionedApiRootSuffix(normalizedApiFormat))
}
return rawBaseUrl
}
export function getDefaultEndpointPath(params: {
@@ -117,11 +191,11 @@ export function getDefaultEndpointPath(params: {
if (normalizedApiFormat === 'openai:responses' && isCodex) {
return '/responses'
}
if (openAiCompatibleBaseIncludesApiRoot(params.baseUrl) && isOpenAiCompatibleFormat(normalizedApiFormat)) {
return stripV1PrefixForApiRoot(defaultPath)
if (usesVersionedApiRootByDefault(normalizedApiFormat)) {
return stripVersionPrefixForApiRoot(defaultPath)
}
if (v1CompatibleBaseIncludesApiRoot(params.baseUrl) && isClaudeCompatibleFormat(normalizedApiFormat)) {
return stripV1PrefixForApiRoot(defaultPath)
if (openAiCompatibleBaseIncludesApiRoot(params.baseUrl) && isOpenAiCompatibleFormat(normalizedApiFormat)) {
return stripVersionPrefixForApiRoot(defaultPath)
}
return defaultPath
}