forked from boos/BoosAPI
refactor: share platform file and route helpers
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { inferInputFileMimeType, normalizeInputFileBlock } from '../../transformers/shared/inputFile.js';
|
||||
import { classifyConversationFileMimeType } from '../../../shared/conversationFileTypes.js';
|
||||
|
||||
export type ConversationFileTransport = 'unsupported' | 'inline_only' | 'native';
|
||||
export type ConversationFileEndpoint = 'chat' | 'messages' | 'responses';
|
||||
@@ -25,13 +26,6 @@ function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function classifyMimeFamily(mimeType: string | null): 'image' | 'audio' | 'document' {
|
||||
const normalized = asTrimmedString(mimeType).toLowerCase();
|
||||
if (normalized.startsWith('image/')) return 'image';
|
||||
if (normalized.startsWith('audio/')) return 'audio';
|
||||
return 'document';
|
||||
}
|
||||
|
||||
function appendConversationFileSummary(
|
||||
summary: ConversationFileInputSummary,
|
||||
item: Record<string, unknown>,
|
||||
@@ -39,7 +33,7 @@ function appendConversationFileSummary(
|
||||
const normalizedFile = normalizeInputFileBlock(item);
|
||||
if (normalizedFile) {
|
||||
const mimeType = inferInputFileMimeType(normalizedFile);
|
||||
const family = classifyMimeFamily(mimeType);
|
||||
const family = classifyConversationFileMimeType(mimeType);
|
||||
if (family === 'image') {
|
||||
summary.hasImage = true;
|
||||
return;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
softDeleteProxyFileByPublicIdForOwner,
|
||||
} from '../../services/proxyFileStore.js';
|
||||
import { ensureMultipartBufferParser, parseMultipartFormData } from '../../routes/proxy/multipart.js';
|
||||
import { isSupportedConversationFileMimeType } from '../../../shared/conversationFileTypes.js';
|
||||
|
||||
function invalidRequest(reply: FastifyReply, message: string) {
|
||||
return reply.code(400).send({ error: { message, type: 'invalid_request_error' } });
|
||||
@@ -58,15 +59,6 @@ function inferMimeTypeFromFilename(filename: string): string {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
|
||||
function isSupportedProxyFileMimeType(mimeType: string): boolean {
|
||||
return mimeType === 'application/pdf'
|
||||
|| mimeType === 'text/plain'
|
||||
|| mimeType === 'text/markdown'
|
||||
|| mimeType === 'application/json'
|
||||
|| mimeType.startsWith('image/')
|
||||
|| mimeType.startsWith('audio/');
|
||||
}
|
||||
|
||||
export async function filesProxyRoute(app: FastifyInstance) {
|
||||
ensureMultipartBufferParser(app);
|
||||
|
||||
@@ -88,7 +80,7 @@ export async function filesProxyRoute(app: FastifyInstance) {
|
||||
|
||||
const filename = fileEntry.name || 'upload.bin';
|
||||
const mimeType = (fileEntry.type || inferMimeTypeFromFilename(filename)).trim().toLowerCase();
|
||||
if (!isSupportedProxyFileMimeType(mimeType)) {
|
||||
if (!isSupportedConversationFileMimeType(mimeType)) {
|
||||
return invalidRequest(reply, `unsupported file mime type: ${mimeType || 'application/octet-stream'}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { parseProxyLogMetadata } from '../../../shared/proxyLogMeta.js';
|
||||
|
||||
type ComposeProxyLogMessageArgs = {
|
||||
clientKind?: string | null;
|
||||
sessionId?: string | null;
|
||||
@@ -16,21 +18,7 @@ export type ParsedProxyLogMessageMeta = {
|
||||
};
|
||||
|
||||
export function parseProxyLogMessageMeta(rawMessage: string): ParsedProxyLogMessageMeta {
|
||||
const clientMatch = rawMessage.match(/\[client:([^\]]+)\]/i);
|
||||
const sessionMatch = rawMessage.match(/\[session:([^\]]+)\]/i);
|
||||
const downstreamMatch = rawMessage.match(/\[downstream:([^\]]+)\]/i);
|
||||
const upstreamMatch = rawMessage.match(/\[upstream:([^\]]+)\]/i);
|
||||
const messageText = rawMessage.replace(
|
||||
/^\s*(?:\[(?:client|session|downstream|upstream):[^\]]+\]\s*)+/i,
|
||||
'',
|
||||
).trim();
|
||||
return {
|
||||
clientKind: clientMatch?.[1]?.trim() || null,
|
||||
sessionId: sessionMatch?.[1]?.trim() || null,
|
||||
downstreamPath: downstreamMatch?.[1]?.trim() || null,
|
||||
upstreamPath: upstreamMatch?.[1]?.trim() || null,
|
||||
messageText,
|
||||
};
|
||||
return parseProxyLogMetadata(rawMessage);
|
||||
}
|
||||
|
||||
export function composeProxyLogMessage({
|
||||
|
||||
@@ -4,6 +4,7 @@ import { db, schema } from '../db/index.js';
|
||||
import { upsertSetting } from '../db/upsertSetting.js';
|
||||
import { mergeAccountExtraConfig } from './accountExtraConfig.js';
|
||||
import { getOauthInfoFromAccount } from './oauth/oauthAccount.js';
|
||||
import { PLATFORM_ALIASES, detectPlatformByUrlHint } from '../../shared/platformIdentity.js';
|
||||
|
||||
const BACKUP_VERSION = '2.1';
|
||||
|
||||
@@ -244,38 +245,6 @@ const DIRECT_API_PLATFORMS = new Set([
|
||||
'antigravity',
|
||||
]);
|
||||
|
||||
const IMPORT_PLATFORM_ALIASES: Record<string, string> = {
|
||||
anyrouter: 'anyrouter',
|
||||
'wong-gongyi': 'new-api',
|
||||
'vo-api': 'new-api',
|
||||
'super-api': 'new-api',
|
||||
'rix-api': 'new-api',
|
||||
'neo-api': 'new-api',
|
||||
newapi: 'new-api',
|
||||
'new api': 'new-api',
|
||||
'new-api': 'new-api',
|
||||
oneapi: 'one-api',
|
||||
'one api': 'one-api',
|
||||
'one-api': 'one-api',
|
||||
onehub: 'one-hub',
|
||||
'one-hub': 'one-hub',
|
||||
donehub: 'done-hub',
|
||||
'done-hub': 'done-hub',
|
||||
veloera: 'veloera',
|
||||
sub2api: 'sub2api',
|
||||
openai: 'openai',
|
||||
anthropic: 'claude',
|
||||
claude: 'claude',
|
||||
google: 'gemini',
|
||||
gemini: 'gemini',
|
||||
cliproxyapi: 'cliproxyapi',
|
||||
cpa: 'cliproxyapi',
|
||||
'cli-proxy-api': 'cliproxyapi',
|
||||
codex: 'codex',
|
||||
'gemini-cli': 'gemini-cli',
|
||||
antigravity: 'antigravity',
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
@@ -645,38 +614,14 @@ function normalizeOriginUrl(raw: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function detectLocalPlatformByUrlHint(url: string): string | undefined {
|
||||
const normalized = url.trim().toLowerCase();
|
||||
if (!normalized) return undefined;
|
||||
|
||||
if (normalized.includes('api.openai.com')) return 'openai';
|
||||
if (normalized.includes('chatgpt.com/backend-api/codex')) return 'codex';
|
||||
if (normalized.includes('api.anthropic.com') || normalized.includes('anthropic.com/v1')) return 'claude';
|
||||
if (
|
||||
normalized.includes('generativelanguage.googleapis.com')
|
||||
|| normalized.includes('googleapis.com/v1beta/openai')
|
||||
|| normalized.includes('gemini.google.com')
|
||||
) {
|
||||
return 'gemini';
|
||||
}
|
||||
if (normalized.includes('cloudcode-pa.googleapis.com')) return 'gemini-cli';
|
||||
if (normalized.includes('anyrouter')) return 'anyrouter';
|
||||
if (normalized.includes('donehub') || normalized.includes('done-hub')) return 'done-hub';
|
||||
if (normalized.includes('onehub') || normalized.includes('one-hub')) return 'one-hub';
|
||||
if (normalized.includes('veloera')) return 'veloera';
|
||||
if (normalized.includes('sub2api')) return 'sub2api';
|
||||
if (normalized.includes('127.0.0.1:8317') || normalized.includes('localhost:8317')) return 'cliproxyapi';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveImportedPlatform(rawPlatform: unknown, rawUrl: string): string {
|
||||
const normalizedPlatform = typeof rawPlatform === 'string'
|
||||
? IMPORT_PLATFORM_ALIASES[rawPlatform.trim().toLowerCase()]
|
||||
: undefined;
|
||||
const rawPlatformText = asString(rawPlatform).toLowerCase();
|
||||
const normalizedPlatform = rawPlatformText
|
||||
? (PLATFORM_ALIASES[rawPlatformText] ?? (DIRECT_API_PLATFORMS.has(rawPlatformText) ? rawPlatformText : ''))
|
||||
: '';
|
||||
if (normalizedPlatform) return normalizedPlatform;
|
||||
|
||||
const urlHint = detectLocalPlatformByUrlHint(rawUrl);
|
||||
const urlHint = detectPlatformByUrlHint(rawUrl);
|
||||
if (urlHint) return urlHint;
|
||||
|
||||
return normalizeLegacyPlatform(asString(rawPlatform));
|
||||
@@ -688,9 +633,9 @@ function resolveImportedProfilePlatform(apiType: unknown, baseUrl: string): stri
|
||||
if (normalizedType === 'anthropic') return 'claude';
|
||||
if (normalizedType === 'google') return 'gemini';
|
||||
if (normalizedType === 'openai-compatible') {
|
||||
return detectLocalPlatformByUrlHint(baseUrl) || 'openai';
|
||||
return detectPlatformByUrlHint(baseUrl) || 'openai';
|
||||
}
|
||||
return detectLocalPlatformByUrlHint(baseUrl) || 'openai';
|
||||
return detectPlatformByUrlHint(baseUrl) || 'openai';
|
||||
}
|
||||
|
||||
function pushDefaultImportedToken(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { StandardApiProviderAdapterBase } from './standardApiProvider.js';
|
||||
|
||||
const DEFAULT_ANTHROPIC_VERSION = '2023-06-01';
|
||||
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from '../oauth/claudeProvider.js';
|
||||
|
||||
export class ClaudeAdapter extends StandardApiProviderAdapterBase {
|
||||
readonly platformName = 'claude';
|
||||
@@ -15,7 +14,7 @@ export class ClaudeAdapter extends StandardApiProviderAdapterBase {
|
||||
baseUrl,
|
||||
headers: {
|
||||
'x-api-key': apiToken,
|
||||
'anthropic-version': DEFAULT_ANTHROPIC_VERSION,
|
||||
'anthropic-version': CLAUDE_DEFAULT_ANTHROPIC_VERSION,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { GeminiCliAdapter } from './geminiCli.js';
|
||||
import { AntigravityAdapter } from './antigravity.js';
|
||||
import { CliProxyApiAdapter } from './cliproxyapi.js';
|
||||
import { detectPlatformByTitle } from './titleHint.js';
|
||||
import { detectPlatformByUrlHint, normalizePlatformAlias } from '../../../shared/platformIdentity.js';
|
||||
|
||||
const adapters: PlatformAdapter[] = [
|
||||
// Specific forks before generic adapters for better auto-detection.
|
||||
@@ -33,47 +34,8 @@ const adapters: PlatformAdapter[] = [
|
||||
new OneApiAdapter(),
|
||||
];
|
||||
|
||||
const platformAliases: Record<string, string> = {
|
||||
// NewAPI family aliases
|
||||
anyrouter: 'anyrouter',
|
||||
'wong-gongyi': 'new-api',
|
||||
'vo-api': 'new-api',
|
||||
'super-api': 'new-api',
|
||||
'rix-api': 'new-api',
|
||||
'neo-api': 'new-api',
|
||||
newapi: 'new-api',
|
||||
'new api': 'new-api',
|
||||
// OneAPI family aliases
|
||||
oneapi: 'one-api',
|
||||
'one api': 'one-api',
|
||||
// Keep canonical forms explicit for clarity
|
||||
'new-api': 'new-api',
|
||||
'one-api': 'one-api',
|
||||
veloera: 'veloera',
|
||||
'one-hub': 'one-hub',
|
||||
'done-hub': 'done-hub',
|
||||
sub2api: 'sub2api',
|
||||
// Official upstream APIs
|
||||
openai: 'openai',
|
||||
codex: 'codex',
|
||||
'chatgpt-codex': 'codex',
|
||||
'chatgpt codex': 'codex',
|
||||
anthropic: 'claude',
|
||||
claude: 'claude',
|
||||
gemini: 'gemini',
|
||||
'gemini-cli': 'gemini-cli',
|
||||
antigravity: 'antigravity',
|
||||
'anti-gravity': 'antigravity',
|
||||
google: 'gemini',
|
||||
// CLIProxyAPI aliases
|
||||
cliproxyapi: 'cliproxyapi',
|
||||
cpa: 'cliproxyapi',
|
||||
'cli-proxy-api': 'cliproxyapi',
|
||||
};
|
||||
|
||||
function normalizePlatform(platform: string): string {
|
||||
const raw = (platform || '').trim().toLowerCase();
|
||||
return platformAliases[raw] ?? raw;
|
||||
return normalizePlatformAlias(platform);
|
||||
}
|
||||
|
||||
export function getAdapter(platform: string): PlatformAdapter | undefined {
|
||||
@@ -89,36 +51,6 @@ const titleFirstPlatforms = new Set<string>([
|
||||
'sub2api',
|
||||
]);
|
||||
|
||||
function detectPlatformByUrlHint(url: string): string | undefined {
|
||||
const normalized = (url || '').trim().toLowerCase();
|
||||
if (!normalized) return undefined;
|
||||
|
||||
// Official upstream endpoints.
|
||||
if (normalized.includes('api.openai.com')) return 'openai';
|
||||
if (normalized.includes('chatgpt.com/backend-api/codex')) return 'codex';
|
||||
if (normalized.includes('api.anthropic.com') || normalized.includes('anthropic.com/v1')) return 'claude';
|
||||
if (
|
||||
normalized.includes('generativelanguage.googleapis.com')
|
||||
|| normalized.includes('googleapis.com/v1beta/openai')
|
||||
|| normalized.includes('gemini.google.com')
|
||||
) {
|
||||
return 'gemini';
|
||||
}
|
||||
if (normalized.includes('cloudcode-pa.googleapis.com')) return 'gemini-cli';
|
||||
|
||||
// NewAPI-family forks and common aliases.
|
||||
if (normalized.includes('anyrouter')) return 'anyrouter';
|
||||
if (normalized.includes('donehub') || normalized.includes('done-hub')) return 'done-hub';
|
||||
if (normalized.includes('onehub') || normalized.includes('one-hub')) return 'one-hub';
|
||||
if (normalized.includes('veloera')) return 'veloera';
|
||||
if (normalized.includes('sub2api')) return 'sub2api';
|
||||
|
||||
// CLIProxyAPI default local endpoints.
|
||||
if (normalized.includes('127.0.0.1:8317') || normalized.includes('localhost:8317')) return 'cliproxyapi';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function detectPlatform(url: string): Promise<PlatformAdapter | undefined> {
|
||||
const urlHint = detectPlatformByUrlHint(url);
|
||||
if (urlHint) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ProxyResourceOwner } from '../middleware/auth.js';
|
||||
import { getProxyFileByPublicIdForOwner } from './proxyFileStore.js';
|
||||
import { ensureBase64DataUrl } from '../transformers/shared/inputFile.js';
|
||||
import { summarizeConversationFileInputsInOpenAiBody } from '../proxy-core/capabilities/conversationFileCapabilities.js';
|
||||
import { isSupportedConversationFileMimeType } from '../../shared/conversationFileTypes.js';
|
||||
|
||||
const LOCAL_PROXY_FILE_ID_PREFIX = 'file-metapi-';
|
||||
|
||||
@@ -44,12 +45,7 @@ function inferMimeTypeFromFilename(filename: string): string {
|
||||
}
|
||||
|
||||
function isSupportedMimeType(mimeType: string): boolean {
|
||||
return mimeType === 'application/pdf'
|
||||
|| mimeType === 'text/plain'
|
||||
|| mimeType === 'text/markdown'
|
||||
|| mimeType === 'application/json'
|
||||
|| mimeType.startsWith('image/')
|
||||
|| mimeType.startsWith('audio/');
|
||||
return isSupportedConversationFileMimeType(mimeType);
|
||||
}
|
||||
|
||||
function audioFormatFromMimeType(mimeType: string, filename: string): string {
|
||||
|
||||
@@ -2,11 +2,14 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
geminiGenerateContentTransformer,
|
||||
resolveGeminiGenerateContentUrl,
|
||||
resolveGeminiModelsUrl,
|
||||
resolveGeminiNativeBaseUrl,
|
||||
reasoningEffortToGeminiThinkingConfig,
|
||||
geminiThinkingConfigToReasoning,
|
||||
} from './index.js';
|
||||
import { extractGeminiUsage } from './usage.js';
|
||||
import { serializeGeminiAggregateResponse, extractResponseMetadata } from './outbound.js';
|
||||
import { serializeGeminiAggregateResponse, extractResponseMetadata, geminiGenerateContentOutbound } from './outbound.js';
|
||||
import { resolveGeminiThinkingConfigFromRequest } from './convert.js';
|
||||
import {
|
||||
parseGeminiStreamPayload,
|
||||
@@ -15,6 +18,15 @@ import {
|
||||
} from './stream.js';
|
||||
|
||||
describe('geminiGenerateContentTransformer.inbound', () => {
|
||||
it('reuses the same gemini url resolver helpers across transformer layers', () => {
|
||||
expect(geminiGenerateContentTransformer.resolveBaseUrl).toBe(resolveGeminiNativeBaseUrl);
|
||||
expect(geminiGenerateContentTransformer.resolveModelsUrl).toBe(resolveGeminiModelsUrl);
|
||||
expect(geminiGenerateContentTransformer.resolveActionUrl).toBe(resolveGeminiGenerateContentUrl);
|
||||
expect(geminiGenerateContentOutbound.resolveBaseUrl).toBe(resolveGeminiNativeBaseUrl);
|
||||
expect(geminiGenerateContentOutbound.resolveModelsUrl).toBe(resolveGeminiModelsUrl);
|
||||
expect(geminiGenerateContentOutbound.resolveActionUrl).toBe(resolveGeminiGenerateContentUrl);
|
||||
});
|
||||
|
||||
it('parses native Gemini requests into canonical envelopes', () => {
|
||||
const result = geminiGenerateContentTransformer.parseRequest({
|
||||
model: 'gemini-2.5-pro',
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { canonicalRequestFromOpenAiBody } from '../../canonical/request.js';
|
||||
import type { CanonicalContentPart, CanonicalRequestEnvelope } from '../../canonical/types.js';
|
||||
import type { ProtocolBuildContext, ProtocolParseContext } from '../../contracts.js';
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return (baseUrl || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function baseIncludesVersion(baseUrl: string): boolean {
|
||||
return /\/v\d+(?:beta)?(?:\/|$)/i.test(baseUrl);
|
||||
}
|
||||
import {
|
||||
resolveGeminiGenerateContentUrl,
|
||||
resolveGeminiModelsUrl,
|
||||
resolveGeminiNativeBaseUrl,
|
||||
} from './urlResolver.js';
|
||||
export {
|
||||
resolveGeminiGenerateContentUrl,
|
||||
resolveGeminiModelsUrl,
|
||||
resolveGeminiNativeBaseUrl,
|
||||
} from './urlResolver.js';
|
||||
|
||||
function asTrimmedString(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
@@ -17,37 +20,6 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function resolveGeminiNativeBaseUrl(baseUrl: string, apiVersion: string): string {
|
||||
const normalized = normalizeBaseUrl(baseUrl);
|
||||
if (baseIncludesVersion(normalized)) return normalized;
|
||||
return `${normalized}/${apiVersion}`;
|
||||
}
|
||||
|
||||
export function resolveGeminiModelsUrl(
|
||||
baseUrl: string,
|
||||
apiVersion: string,
|
||||
apiKey: string,
|
||||
): string {
|
||||
const base = resolveGeminiNativeBaseUrl(baseUrl, apiVersion);
|
||||
const separator = base.includes('?') ? '&' : '?';
|
||||
return `${base}/models${separator}key=${encodeURIComponent(apiKey)}`;
|
||||
}
|
||||
|
||||
export function resolveGeminiGenerateContentUrl(
|
||||
baseUrl: string,
|
||||
apiVersion: string,
|
||||
modelActionPath: string,
|
||||
apiKey: string,
|
||||
search: string,
|
||||
): string {
|
||||
const base = resolveGeminiNativeBaseUrl(baseUrl, apiVersion);
|
||||
const normalizedAction = modelActionPath.replace(/^\/+/, '');
|
||||
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
||||
params.set('key', apiKey);
|
||||
const query = params.toString();
|
||||
return `${base}/${normalizedAction}${query ? `?${query}` : ''}`;
|
||||
}
|
||||
|
||||
export function resolveGeminiProxyApiVersion(params: { geminiApiVersion?: unknown } | null | undefined): string {
|
||||
return asTrimmedString(params?.geminiApiVersion) || 'v1beta';
|
||||
}
|
||||
|
||||
@@ -7,45 +7,11 @@ import {
|
||||
toTransformerMetadataRecord,
|
||||
type TransformerMetadata,
|
||||
} from '../../shared/normalized.js';
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return (baseUrl || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function baseIncludesVersion(baseUrl: string): boolean {
|
||||
return /\/v\d+(?:beta)?(?:\/|$)/i.test(baseUrl);
|
||||
}
|
||||
|
||||
function resolveBaseUrl(baseUrl: string, apiVersion: string): string {
|
||||
const normalized = normalizeBaseUrl(baseUrl);
|
||||
if (baseIncludesVersion(normalized)) return normalized;
|
||||
return `${normalized}/${apiVersion}`;
|
||||
}
|
||||
|
||||
function resolveModelsUrl(
|
||||
baseUrl: string,
|
||||
apiVersion: string,
|
||||
apiKey: string,
|
||||
): string {
|
||||
const resolvedBaseUrl = resolveBaseUrl(baseUrl, apiVersion);
|
||||
const separator = resolvedBaseUrl.includes('?') ? '&' : '?';
|
||||
return `${resolvedBaseUrl}/models${separator}key=${encodeURIComponent(apiKey)}`;
|
||||
}
|
||||
|
||||
function resolveActionUrl(
|
||||
baseUrl: string,
|
||||
apiVersion: string,
|
||||
modelActionPath: string,
|
||||
apiKey: string,
|
||||
search: string,
|
||||
): string {
|
||||
const resolvedBaseUrl = resolveBaseUrl(baseUrl, apiVersion);
|
||||
const normalizedAction = modelActionPath.replace(/^\/+/, '');
|
||||
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
||||
params.set('key', apiKey);
|
||||
const query = params.toString();
|
||||
return `${resolvedBaseUrl}/${normalizedAction}${query ? `?${query}` : ''}`;
|
||||
}
|
||||
import {
|
||||
resolveGeminiGenerateContentUrl,
|
||||
resolveGeminiModelsUrl,
|
||||
resolveGeminiNativeBaseUrl,
|
||||
} from './urlResolver.js';
|
||||
|
||||
type GeminiRecord = Record<string, unknown>;
|
||||
|
||||
@@ -304,9 +270,9 @@ export function extractResponseMetadata(payload: unknown, requestPayload?: unkno
|
||||
}
|
||||
|
||||
export const geminiGenerateContentOutbound = {
|
||||
resolveBaseUrl,
|
||||
resolveModelsUrl,
|
||||
resolveActionUrl,
|
||||
resolveBaseUrl: resolveGeminiNativeBaseUrl,
|
||||
resolveModelsUrl: resolveGeminiModelsUrl,
|
||||
resolveActionUrl: resolveGeminiGenerateContentUrl,
|
||||
extractTransformerMetadata,
|
||||
extractResponseMetadata,
|
||||
serializeAggregateResponse: serializeGeminiAggregateResponse,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return (baseUrl || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function baseIncludesVersion(baseUrl: string): boolean {
|
||||
return /\/v\d+(?:beta)?(?:\/|$)/i.test(baseUrl);
|
||||
}
|
||||
|
||||
export function resolveGeminiNativeBaseUrl(baseUrl: string, apiVersion: string): string {
|
||||
const normalized = normalizeBaseUrl(baseUrl);
|
||||
if (baseIncludesVersion(normalized)) return normalized;
|
||||
return `${normalized}/${apiVersion}`;
|
||||
}
|
||||
|
||||
export function resolveGeminiModelsUrl(
|
||||
baseUrl: string,
|
||||
apiVersion: string,
|
||||
apiKey: string,
|
||||
): string {
|
||||
const base = resolveGeminiNativeBaseUrl(baseUrl, apiVersion);
|
||||
const separator = base.includes('?') ? '&' : '?';
|
||||
return `${base}/models${separator}key=${encodeURIComponent(apiKey)}`;
|
||||
}
|
||||
|
||||
export function resolveGeminiGenerateContentUrl(
|
||||
baseUrl: string,
|
||||
apiVersion: string,
|
||||
modelActionPath: string,
|
||||
apiKey: string,
|
||||
search: string,
|
||||
): string {
|
||||
const base = resolveGeminiNativeBaseUrl(baseUrl, apiVersion);
|
||||
const normalizedAction = modelActionPath.replace(/^\/+/, '');
|
||||
const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
|
||||
params.set('key', apiKey);
|
||||
const query = params.toString();
|
||||
return `${base}/${normalizedAction}${query ? `?${query}` : ''}`;
|
||||
}
|
||||
Reference in New Issue
Block a user