diff --git a/src/server/routes/proxy/chat.ts b/src/server/routes/proxy/chat.ts index 556e44d..de186e4 100644 --- a/src/server/routes/proxy/chat.ts +++ b/src/server/routes/proxy/chat.ts @@ -8,7 +8,8 @@ import { isTokenExpiredError } from '../../services/alertRules.js'; import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js'; import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js'; -import { resolveProxyUrlForSite, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; import { type DownstreamFormat } from '../../transformers/shared/normalized.js'; import { buildUpstreamEndpointRequest, @@ -168,7 +169,7 @@ async function handleChatProxyRequest( method: 'POST', headers: compatibilityRequest.headers, body: JSON.stringify(compatibilityRequest.body), - }), + }, getProxyUrlFromExtraConfig(selected.account.extraConfig)), ), }); let startTime = Date.now(); @@ -176,7 +177,7 @@ async function handleChatProxyRequest( try { const endpointResult = await executeEndpointFlow({ siteUrl: selected.site.url, - proxyUrl: resolveProxyUrlForSite(selected.site), + proxyUrl: resolveChannelProxyUrl(selected.site, selected.account.extraConfig), endpointCandidates, buildRequest: (endpoint) => buildEndpointRequest(endpoint), tryRecover: endpointStrategy.tryRecover, diff --git a/src/server/routes/proxy/completions.ts b/src/server/routes/proxy/completions.ts index 17686e3..65eb286 100644 --- a/src/server/routes/proxy/completions.ts +++ b/src/server/routes/proxy/completions.ts @@ -1,45 +1,46 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { fetch } from 'undici'; import { db, schema } from '../../db/index.js'; -import { tokenRouter } from '../../services/tokenRouter.js'; -import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; -import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js'; -import { isTokenExpiredError } from '../../services/alertRules.js'; -import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; -import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js'; -import { mergeProxyUsage, parseProxyUsage, pullSseDataEvents } from '../../services/proxyUsageParser.js'; -import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; -import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; -import { composeProxyLogMessage } from './logPathMeta.js'; -import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; -import { resolveProxyLogBilling } from './proxyBilling.js'; -import { buildUpstreamUrl } from './upstreamUrl.js'; +import { tokenRouter } from '../../services/tokenRouter.js'; +import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; +import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js'; +import { isTokenExpiredError } from '../../services/alertRules.js'; +import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; +import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js'; +import { mergeProxyUsage, parseProxyUsage, pullSseDataEvents } from '../../services/proxyUsageParser.js'; +import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; +import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; +import { composeProxyLogMessage } from './logPathMeta.js'; +import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; +import { resolveProxyLogBilling } from './proxyBilling.js'; +import { buildUpstreamUrl } from './upstreamUrl.js'; const MAX_RETRIES = 2; export async function completionsProxyRoute(app: FastifyInstance) { app.post('/v1/completions', async (request: FastifyRequest, reply: FastifyReply) => { const body = request.body as any; - const requestedModel = body?.model; - if (!requestedModel) { - return reply.code(400).send({ error: { message: 'model is required', type: 'invalid_request_error' } }); - } - if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; - const downstreamPolicy = getDownstreamRoutingPolicy(request); - - const isStream = body.stream === true; + const requestedModel = body?.model; + if (!requestedModel) { + return reply.code(400).send({ error: { message: 'model is required', type: 'invalid_request_error' } }); + } + if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; + const downstreamPolicy = getDownstreamRoutingPolicy(request); + + const isStream = body.stream === true; const excludeChannelIds: number[] = []; let retryCount = 0; - while (retryCount <= MAX_RETRIES) { - let selected = retryCount === 0 - ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) - : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); - - if (!selected && retryCount === 0) { - await refreshModelsAndRebuildRoutes(); - selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); - } + while (retryCount <= MAX_RETRIES) { + let selected = retryCount === 0 + ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) + : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); + + if (!selected && retryCount === 0) { + await refreshModelsAndRebuildRoutes(); + selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); + } if (!selected) { await reportProxyAllFailed({ @@ -51,21 +52,21 @@ export async function completionsProxyRoute(app: FastifyInstance) { }); } - excludeChannelIds.push(selected.channel.id); - - const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/completions'); - const forwardBody = { ...body, model: selected.actualModel }; - const startTime = Date.now(); + excludeChannelIds.push(selected.channel.id); + + const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/completions'); + const forwardBody = { ...body, model: selected.actualModel }; + const startTime = Date.now(); try { - const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${selected.tokenValue}`, - }, - body: JSON.stringify(forwardBody), - })); + const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${selected.tokenValue}`, + }, + body: JSON.stringify(forwardBody), + }, getProxyUrlFromExtraConfig(selected.account.extraConfig))); if (!upstream.ok) { const errText = await upstream.text().catch(() => 'unknown error'); @@ -81,10 +82,10 @@ export async function completionsProxyRoute(app: FastifyInstance) { }); } - if (shouldRetryProxyRequest(upstream.status, errText) && retryCount < MAX_RETRIES) { - retryCount++; - continue; - } + if (shouldRetryProxyRequest(upstream.status, errText) && retryCount < MAX_RETRIES) { + retryCount++; + continue; + } await reportProxyAllFailed({ model: requestedModel, @@ -106,110 +107,110 @@ export async function completionsProxyRoute(app: FastifyInstance) { return; } - const decoder = new TextDecoder(); - let parsedUsage: ReturnType = { - promptTokens: 0, - completionTokens: 0, - totalTokens: 0, - cacheReadTokens: 0, - cacheCreationTokens: 0, - promptTokensIncludeCache: null, - }; - let sseBuffer = ''; - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - const chunk = decoder.decode(value, { stream: true }); - reply.raw.write(chunk); - - sseBuffer += chunk; - const pulled = pullSseDataEvents(sseBuffer); - sseBuffer = pulled.rest; - for (const eventPayload of pulled.events) { - try { - parsedUsage = mergeProxyUsage(parsedUsage, parseProxyUsage(JSON.parse(eventPayload))); - } catch {} - } - } - if (sseBuffer.trim().length > 0) { - const pulled = pullSseDataEvents(`${sseBuffer}\n\n`); - for (const eventPayload of pulled.events) { - try { - parsedUsage = mergeProxyUsage(parsedUsage, parseProxyUsage(JSON.parse(eventPayload))); - } catch {} - } - } - } finally { - reader.releaseLock(); - reply.raw.end(); - } + const decoder = new TextDecoder(); + let parsedUsage: ReturnType = { + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + cacheReadTokens: 0, + cacheCreationTokens: 0, + promptTokensIncludeCache: null, + }; + let sseBuffer = ''; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + const chunk = decoder.decode(value, { stream: true }); + reply.raw.write(chunk); - const latency = Date.now() - startTime; - const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({ - site: selected.site, - account: selected.account, - tokenValue: selected.tokenValue, - tokenName: selected.tokenName, - modelName: selected.actualModel || requestedModel, - requestStartedAtMs: startTime, - requestEndedAtMs: startTime + latency, - localLatencyMs: latency, - usage: { - promptTokens: parsedUsage.promptTokens, - completionTokens: parsedUsage.completionTokens, - totalTokens: parsedUsage.totalTokens, - }, - }); - const { estimatedCost, billingDetails } = await resolveProxyLogBilling({ - site: selected.site, - account: selected.account, - modelName: selected.actualModel || requestedModel, - parsedUsage, - resolvedUsage, - }); - tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); - recordDownstreamCostUsage(request, estimatedCost); - logProxy( - selected, requestedModel, 'success', 200, latency, null, retryCount, - resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails, - ); - return; - } - - const data = await upstream.json() as any; - const latency = Date.now() - startTime; - const parsedUsage = parseProxyUsage(data); - const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({ - site: selected.site, - account: selected.account, - tokenValue: selected.tokenValue, - tokenName: selected.tokenName, - modelName: selected.actualModel || requestedModel, - requestStartedAtMs: startTime, - requestEndedAtMs: startTime + latency, - localLatencyMs: latency, - usage: { - promptTokens: parsedUsage.promptTokens, - completionTokens: parsedUsage.completionTokens, - totalTokens: parsedUsage.totalTokens, - }, - }); - const { estimatedCost, billingDetails } = await resolveProxyLogBilling({ - site: selected.site, - account: selected.account, - modelName: selected.actualModel || requestedModel, - parsedUsage, - resolvedUsage, - }); - - tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); - recordDownstreamCostUsage(request, estimatedCost); - logProxy( - selected, requestedModel, 'success', 200, latency, null, retryCount, - resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails, - ); - return reply.send(data); + sseBuffer += chunk; + const pulled = pullSseDataEvents(sseBuffer); + sseBuffer = pulled.rest; + for (const eventPayload of pulled.events) { + try { + parsedUsage = mergeProxyUsage(parsedUsage, parseProxyUsage(JSON.parse(eventPayload))); + } catch {} + } + } + if (sseBuffer.trim().length > 0) { + const pulled = pullSseDataEvents(`${sseBuffer}\n\n`); + for (const eventPayload of pulled.events) { + try { + parsedUsage = mergeProxyUsage(parsedUsage, parseProxyUsage(JSON.parse(eventPayload))); + } catch {} + } + } + } finally { + reader.releaseLock(); + reply.raw.end(); + } + + const latency = Date.now() - startTime; + const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({ + site: selected.site, + account: selected.account, + tokenValue: selected.tokenValue, + tokenName: selected.tokenName, + modelName: selected.actualModel || requestedModel, + requestStartedAtMs: startTime, + requestEndedAtMs: startTime + latency, + localLatencyMs: latency, + usage: { + promptTokens: parsedUsage.promptTokens, + completionTokens: parsedUsage.completionTokens, + totalTokens: parsedUsage.totalTokens, + }, + }); + const { estimatedCost, billingDetails } = await resolveProxyLogBilling({ + site: selected.site, + account: selected.account, + modelName: selected.actualModel || requestedModel, + parsedUsage, + resolvedUsage, + }); + tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); + recordDownstreamCostUsage(request, estimatedCost); + logProxy( + selected, requestedModel, 'success', 200, latency, null, retryCount, + resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails, + ); + return; + } + + const data = await upstream.json() as any; + const latency = Date.now() - startTime; + const parsedUsage = parseProxyUsage(data); + const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({ + site: selected.site, + account: selected.account, + tokenValue: selected.tokenValue, + tokenName: selected.tokenName, + modelName: selected.actualModel || requestedModel, + requestStartedAtMs: startTime, + requestEndedAtMs: startTime + latency, + localLatencyMs: latency, + usage: { + promptTokens: parsedUsage.promptTokens, + completionTokens: parsedUsage.completionTokens, + totalTokens: parsedUsage.totalTokens, + }, + }); + const { estimatedCost, billingDetails } = await resolveProxyLogBilling({ + site: selected.site, + account: selected.account, + modelName: selected.actualModel || requestedModel, + parsedUsage, + resolvedUsage, + }); + + tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); + recordDownstreamCostUsage(request, estimatedCost); + logProxy( + selected, requestedModel, 'success', 200, latency, null, retryCount, + resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails, + ); + return reply.send(data); } catch (err: any) { tokenRouter.recordFailure(selected.channel.id); logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount); @@ -237,38 +238,38 @@ async function logProxy( latencyMs: number, errorMessage: string | null, retryCount: number, - promptTokens = 0, - completionTokens = 0, - totalTokens = 0, - estimatedCost = 0, - billingDetails: unknown = null, -) { - try { - const createdAt = formatUtcSqlDateTime(new Date()); - const normalizedErrorMessage = composeProxyLogMessage({ - downstreamPath: '/v1/completions', - errorMessage, - }); - await db.insert(schema.proxyLogs).values({ - routeId: selected.channel.routeId, - channelId: selected.channel.id, - accountId: selected.account.id, - modelRequested, - modelActual: selected.actualModel, - status, - httpStatus, - latencyMs, - promptTokens, - completionTokens, - totalTokens, - estimatedCost, - billingDetails: billingDetails ? JSON.stringify(billingDetails) : null, - errorMessage: normalizedErrorMessage, - retryCount, - createdAt, - }).run(); - } catch (error) { - console.warn('[proxy/completions] failed to write proxy log', error); - } -} + promptTokens = 0, + completionTokens = 0, + totalTokens = 0, + estimatedCost = 0, + billingDetails: unknown = null, +) { + try { + const createdAt = formatUtcSqlDateTime(new Date()); + const normalizedErrorMessage = composeProxyLogMessage({ + downstreamPath: '/v1/completions', + errorMessage, + }); + await db.insert(schema.proxyLogs).values({ + routeId: selected.channel.routeId, + channelId: selected.channel.id, + accountId: selected.account.id, + modelRequested, + modelActual: selected.actualModel, + status, + httpStatus, + latencyMs, + promptTokens, + completionTokens, + totalTokens, + estimatedCost, + billingDetails: billingDetails ? JSON.stringify(billingDetails) : null, + errorMessage: normalizedErrorMessage, + retryCount, + createdAt, + }).run(); + } catch (error) { + console.warn('[proxy/completions] failed to write proxy log', error); + } +} diff --git a/src/server/routes/proxy/embeddings.ts b/src/server/routes/proxy/embeddings.ts index 2299512..10412a7 100644 --- a/src/server/routes/proxy/embeddings.ts +++ b/src/server/routes/proxy/embeddings.ts @@ -1,44 +1,45 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; import { fetch } from 'undici'; import { db, schema } from '../../db/index.js'; -import { tokenRouter } from '../../services/tokenRouter.js'; -import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; -import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js'; -import { isTokenExpiredError } from '../../services/alertRules.js'; -import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; -import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js'; -import { parseProxyUsage } from '../../services/proxyUsageParser.js'; -import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; -import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; -import { composeProxyLogMessage } from './logPathMeta.js'; -import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; -import { resolveProxyLogBilling } from './proxyBilling.js'; -import { buildUpstreamUrl } from './upstreamUrl.js'; +import { tokenRouter } from '../../services/tokenRouter.js'; +import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; +import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js'; +import { isTokenExpiredError } from '../../services/alertRules.js'; +import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; +import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js'; +import { parseProxyUsage } from '../../services/proxyUsageParser.js'; +import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; +import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; +import { composeProxyLogMessage } from './logPathMeta.js'; +import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; +import { resolveProxyLogBilling } from './proxyBilling.js'; +import { buildUpstreamUrl } from './upstreamUrl.js'; const MAX_RETRIES = 2; export async function embeddingsProxyRoute(app: FastifyInstance) { app.post('/v1/embeddings', async (request: FastifyRequest, reply: FastifyReply) => { const body = request.body as any; - const requestedModel = body?.model; - if (!requestedModel) { - return reply.code(400).send({ error: { message: 'model is required', type: 'invalid_request_error' } }); - } - if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; - const downstreamPolicy = getDownstreamRoutingPolicy(request); - - const excludeChannelIds: number[] = []; + const requestedModel = body?.model; + if (!requestedModel) { + return reply.code(400).send({ error: { message: 'model is required', type: 'invalid_request_error' } }); + } + if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; + const downstreamPolicy = getDownstreamRoutingPolicy(request); + + const excludeChannelIds: number[] = []; let retryCount = 0; - while (retryCount <= MAX_RETRIES) { - let selected = retryCount === 0 - ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) - : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); - - if (!selected && retryCount === 0) { - await refreshModelsAndRebuildRoutes(); - selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); - } + while (retryCount <= MAX_RETRIES) { + let selected = retryCount === 0 + ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) + : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); + + if (!selected && retryCount === 0) { + await refreshModelsAndRebuildRoutes(); + selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); + } if (!selected) { await reportProxyAllFailed({ @@ -48,21 +49,21 @@ export async function embeddingsProxyRoute(app: FastifyInstance) { return reply.code(503).send({ error: { message: 'No available channels', type: 'server_error' } }); } - excludeChannelIds.push(selected.channel.id); - - const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/embeddings'); - const forwardBody = { ...body, model: selected.actualModel }; - const startTime = Date.now(); + excludeChannelIds.push(selected.channel.id); + + const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/embeddings'); + const forwardBody = { ...body, model: selected.actualModel }; + const startTime = Date.now(); try { - const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${selected.tokenValue}`, - }, - body: JSON.stringify(forwardBody), - })); + const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${selected.tokenValue}`, + }, + body: JSON.stringify(forwardBody), + }, getProxyUrlFromExtraConfig(selected.account.extraConfig))); const text = await upstream.text(); if (!upstream.ok) { @@ -78,10 +79,10 @@ export async function embeddingsProxyRoute(app: FastifyInstance) { }); } - if (shouldRetryProxyRequest(upstream.status, text) && retryCount < MAX_RETRIES) { - retryCount++; - continue; - } + if (shouldRetryProxyRequest(upstream.status, text) && retryCount < MAX_RETRIES) { + retryCount++; + continue; + } await reportProxyAllFailed({ model: requestedModel, @@ -90,40 +91,40 @@ export async function embeddingsProxyRoute(app: FastifyInstance) { return reply.code(upstream.status).send({ error: { message: text, type: 'upstream_error' } }); } - let data: any = {}; - try { data = JSON.parse(text); } catch { data = {}; } - const latency = Date.now() - startTime; - const parsedUsage = parseProxyUsage(data); - const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({ - site: selected.site, - account: selected.account, - tokenValue: selected.tokenValue, - tokenName: selected.tokenName, - modelName: selected.actualModel || requestedModel, - requestStartedAtMs: startTime, - requestEndedAtMs: startTime + latency, - localLatencyMs: latency, - usage: { - promptTokens: parsedUsage.promptTokens, - completionTokens: parsedUsage.completionTokens, - totalTokens: parsedUsage.totalTokens, - }, - }); - const { estimatedCost, billingDetails } = await resolveProxyLogBilling({ - site: selected.site, - account: selected.account, - modelName: selected.actualModel || requestedModel, - parsedUsage, - resolvedUsage, - }); - - tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); - recordDownstreamCostUsage(request, estimatedCost); - logProxy( - selected, requestedModel, 'success', upstream.status, latency, null, retryCount, - resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails, - ); - return reply.code(upstream.status).send(data); + let data: any = {}; + try { data = JSON.parse(text); } catch { data = {}; } + const latency = Date.now() - startTime; + const parsedUsage = parseProxyUsage(data); + const resolvedUsage = await resolveProxyUsageWithSelfLogFallback({ + site: selected.site, + account: selected.account, + tokenValue: selected.tokenValue, + tokenName: selected.tokenName, + modelName: selected.actualModel || requestedModel, + requestStartedAtMs: startTime, + requestEndedAtMs: startTime + latency, + localLatencyMs: latency, + usage: { + promptTokens: parsedUsage.promptTokens, + completionTokens: parsedUsage.completionTokens, + totalTokens: parsedUsage.totalTokens, + }, + }); + const { estimatedCost, billingDetails } = await resolveProxyLogBilling({ + site: selected.site, + account: selected.account, + modelName: selected.actualModel || requestedModel, + parsedUsage, + resolvedUsage, + }); + + tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); + recordDownstreamCostUsage(request, estimatedCost); + logProxy( + selected, requestedModel, 'success', upstream.status, latency, null, retryCount, + resolvedUsage.promptTokens, resolvedUsage.completionTokens, resolvedUsage.totalTokens, estimatedCost, billingDetails, + ); + return reply.code(upstream.status).send(data); } catch (err: any) { tokenRouter.recordFailure(selected.channel.id); logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount); @@ -149,38 +150,38 @@ async function logProxy( latencyMs: number, errorMessage: string | null, retryCount: number, - promptTokens = 0, - completionTokens = 0, - totalTokens = 0, - estimatedCost = 0, - billingDetails: unknown = null, -) { - try { - const createdAt = formatUtcSqlDateTime(new Date()); - const normalizedErrorMessage = composeProxyLogMessage({ - downstreamPath: '/v1/embeddings', - errorMessage, - }); - await db.insert(schema.proxyLogs).values({ - routeId: selected.channel.routeId, - channelId: selected.channel.id, - accountId: selected.account.id, - modelRequested, - modelActual: selected.actualModel, - status, - httpStatus, - latencyMs, - promptTokens, - completionTokens, - totalTokens, - estimatedCost, - billingDetails: billingDetails ? JSON.stringify(billingDetails) : null, - errorMessage: normalizedErrorMessage, - retryCount, - createdAt, - }).run(); - } catch (error) { - console.warn('[proxy/embeddings] failed to write proxy log', error); - } -} + promptTokens = 0, + completionTokens = 0, + totalTokens = 0, + estimatedCost = 0, + billingDetails: unknown = null, +) { + try { + const createdAt = formatUtcSqlDateTime(new Date()); + const normalizedErrorMessage = composeProxyLogMessage({ + downstreamPath: '/v1/embeddings', + errorMessage, + }); + await db.insert(schema.proxyLogs).values({ + routeId: selected.channel.routeId, + channelId: selected.channel.id, + accountId: selected.account.id, + modelRequested, + modelActual: selected.actualModel, + status, + httpStatus, + latencyMs, + promptTokens, + completionTokens, + totalTokens, + estimatedCost, + billingDetails: billingDetails ? JSON.stringify(billingDetails) : null, + errorMessage: normalizedErrorMessage, + retryCount, + createdAt, + }).run(); + } catch (error) { + console.warn('[proxy/embeddings] failed to write proxy log', error); + } +} diff --git a/src/server/routes/proxy/images.ts b/src/server/routes/proxy/images.ts index 27ca200..4cc0eaa 100644 --- a/src/server/routes/proxy/images.ts +++ b/src/server/routes/proxy/images.ts @@ -2,40 +2,41 @@ import { fetch } from 'undici'; import { db, schema } from '../../db/index.js'; import { tokenRouter } from '../../services/tokenRouter.js'; -import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; -import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js'; -import { isTokenExpiredError } from '../../services/alertRules.js'; -import { estimateProxyCost } from '../../services/modelPricingService.js'; -import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; -import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; -import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; -import { composeProxyLogMessage } from './logPathMeta.js'; -import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; -import { cloneFormDataWithOverrides, ensureMultipartBufferParser, parseMultipartFormData } from './multipart.js'; -import { buildUpstreamUrl } from './upstreamUrl.js'; +import { refreshModelsAndRebuildRoutes } from '../../services/modelService.js'; +import { reportProxyAllFailed, reportTokenExpired } from '../../services/alertService.js'; +import { isTokenExpiredError } from '../../services/alertRules.js'; +import { estimateProxyCost } from '../../services/modelPricingService.js'; +import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; +import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; +import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; +import { composeProxyLogMessage } from './logPathMeta.js'; +import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; +import { cloneFormDataWithOverrides, ensureMultipartBufferParser, parseMultipartFormData } from './multipart.js'; +import { buildUpstreamUrl } from './upstreamUrl.js'; const MAX_RETRIES = 2; -export async function imagesProxyRoute(app: FastifyInstance) { - ensureMultipartBufferParser(app); - - app.post('/v1/images/generations', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as any; - const requestedModel = body?.model || 'gpt-image-1'; - if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; - const downstreamPolicy = getDownstreamRoutingPolicy(request); - const excludeChannelIds: number[] = []; - let retryCount = 0; - - while (retryCount <= MAX_RETRIES) { - let selected = retryCount === 0 - ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) - : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); - - if (!selected && retryCount === 0) { - await refreshModelsAndRebuildRoutes(); - selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); - } +export async function imagesProxyRoute(app: FastifyInstance) { + ensureMultipartBufferParser(app); + + app.post('/v1/images/generations', async (request: FastifyRequest, reply: FastifyReply) => { + const body = request.body as any; + const requestedModel = body?.model || 'gpt-image-1'; + if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; + const downstreamPolicy = getDownstreamRoutingPolicy(request); + const excludeChannelIds: number[] = []; + let retryCount = 0; + + while (retryCount <= MAX_RETRIES) { + let selected = retryCount === 0 + ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) + : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); + + if (!selected && retryCount === 0) { + await refreshModelsAndRebuildRoutes(); + selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); + } if (!selected) { await reportProxyAllFailed({ @@ -49,19 +50,19 @@ export async function imagesProxyRoute(app: FastifyInstance) { excludeChannelIds.push(selected.channel.id); - const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/images/generations'); + const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/images/generations'); const forwardBody = { ...body, model: selected.actualModel }; const startTime = Date.now(); try { - const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${selected.tokenValue}`, - }, - body: JSON.stringify(forwardBody), - })); + const upstream = await fetch(targetUrl, withSiteRecordProxyRequestInit(selected.site, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${selected.tokenValue}`, + }, + body: JSON.stringify(forwardBody), + }, getProxyUrlFromExtraConfig(selected.account.extraConfig))); const text = await upstream.text(); if (!upstream.ok) { @@ -75,10 +76,10 @@ export async function imagesProxyRoute(app: FastifyInstance) { detail: `HTTP ${upstream.status}`, }); } - if (shouldRetryProxyRequest(upstream.status, text) && retryCount < MAX_RETRIES) { - retryCount++; - continue; - } + if (shouldRetryProxyRequest(upstream.status, text) && retryCount < MAX_RETRIES) { + retryCount++; + continue; + } await reportProxyAllFailed({ model: requestedModel, reason: `upstream returned HTTP ${upstream.status}`, @@ -90,18 +91,18 @@ export async function imagesProxyRoute(app: FastifyInstance) { try { data = JSON.parse(text); } catch { data = { data: [] }; } const latency = Date.now() - startTime; - const estimatedCost = await estimateProxyCost({ - site: selected.site, - account: selected.account, - modelName: selected.actualModel || requestedModel, + const estimatedCost = await estimateProxyCost({ + site: selected.site, + account: selected.account, + modelName: selected.actualModel || requestedModel, promptTokens: 0, completionTokens: 0, totalTokens: 0, - }); - tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); - recordDownstreamCostUsage(request, estimatedCost); - logProxy(selected, requestedModel, 'success', upstream.status, latency, null, retryCount, estimatedCost); - return reply.code(upstream.status).send(data); + }); + tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); + recordDownstreamCostUsage(request, estimatedCost); + logProxy(selected, requestedModel, 'success', upstream.status, latency, null, retryCount, estimatedCost); + return reply.code(upstream.status).send(data); } catch (err: any) { tokenRouter.recordFailure(selected.channel.id); logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount); @@ -116,175 +117,176 @@ export async function imagesProxyRoute(app: FastifyInstance) { return reply.code(502).send({ error: { message: `Upstream error: ${err.message}`, type: 'upstream_error' }, }); - } - } - }); - - app.post('/v1/images/edits', async (request: FastifyRequest, reply: FastifyReply) => { - const multipartForm = await parseMultipartFormData(request); - const jsonBody = (!multipartForm && request.body && typeof request.body === 'object') - ? request.body as Record - : null; - const requestedModel = typeof multipartForm?.get('model') === 'string' - ? String(multipartForm.get('model')).trim() - : (typeof jsonBody?.model === 'string' ? jsonBody.model.trim() : '') || 'gpt-image-1'; - - if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; - const downstreamPolicy = getDownstreamRoutingPolicy(request); - const excludeChannelIds: number[] = []; - let retryCount = 0; - - while (retryCount <= MAX_RETRIES) { - let selected = retryCount === 0 - ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) - : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); - - if (!selected && retryCount === 0) { - await refreshModelsAndRebuildRoutes(); - selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); - } - - if (!selected) { - await reportProxyAllFailed({ - model: requestedModel, - reason: 'No available channels after retries', - }); - return reply.code(503).send({ - error: { message: 'No available channels for this model', type: 'server_error' }, - }); - } - - excludeChannelIds.push(selected.channel.id); - const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/images/edits'); - const startTime = Date.now(); - - try { - const requestInit = multipartForm - ? withSiteRecordProxyRequestInit(selected.site, { - method: 'POST', - headers: { - Authorization: `Bearer ${selected.tokenValue}`, - }, - body: cloneFormDataWithOverrides(multipartForm, { - model: selected.actualModel || requestedModel, - }) as any, - }) - : withSiteRecordProxyRequestInit(selected.site, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${selected.tokenValue}`, - }, - body: JSON.stringify({ - ...(jsonBody || {}), - model: selected.actualModel || requestedModel, - }), - }); - - const upstream = await fetch(targetUrl, requestInit); - const text = await upstream.text(); - if (!upstream.ok) { - tokenRouter.recordFailure(selected.channel.id); - logProxy(selected, requestedModel, 'failed', upstream.status, Date.now() - startTime, text, retryCount, 0, '/v1/images/edits'); - if (isTokenExpiredError({ status: upstream.status, message: text })) { - await reportTokenExpired({ - accountId: selected.account.id, - username: selected.account.username, - siteName: selected.site.name, - detail: `HTTP ${upstream.status}`, - }); - } - if (shouldRetryProxyRequest(upstream.status, text) && retryCount < MAX_RETRIES) { - retryCount++; - continue; - } - await reportProxyAllFailed({ - model: requestedModel, - reason: `upstream returned HTTP ${upstream.status}`, - }); - return reply.code(upstream.status).send({ error: { message: text, type: 'upstream_error' } }); - } - - let data: any = {}; - try { data = JSON.parse(text); } catch { data = { data: [] }; } - - const latency = Date.now() - startTime; - const estimatedCost = await estimateProxyCost({ - site: selected.site, - account: selected.account, - modelName: selected.actualModel || requestedModel, - promptTokens: 0, - completionTokens: 0, - totalTokens: 0, - }); - tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); - recordDownstreamCostUsage(request, estimatedCost); - logProxy(selected, requestedModel, 'success', upstream.status, latency, null, retryCount, estimatedCost, '/v1/images/edits'); - return reply.code(upstream.status).send(data); - } catch (err: any) { - tokenRouter.recordFailure(selected.channel.id); - logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount, 0, '/v1/images/edits'); - if (retryCount < MAX_RETRIES) { - retryCount++; - continue; - } - await reportProxyAllFailed({ - model: requestedModel, - reason: err.message || 'network failure', - }); - return reply.code(502).send({ - error: { message: `Upstream error: ${err.message}`, type: 'upstream_error' }, - }); - } - } - }); - - app.post('/v1/images/variations', async (_request: FastifyRequest, reply: FastifyReply) => { - return reply.code(400).send({ - error: { - message: 'Image variations are not supported', - type: 'invalid_request_error', - }, - }); - }); -} + } + } + }); + + app.post('/v1/images/edits', async (request: FastifyRequest, reply: FastifyReply) => { + const multipartForm = await parseMultipartFormData(request); + const jsonBody = (!multipartForm && request.body && typeof request.body === 'object') + ? request.body as Record + : null; + const requestedModel = typeof multipartForm?.get('model') === 'string' + ? String(multipartForm.get('model')).trim() + : (typeof jsonBody?.model === 'string' ? jsonBody.model.trim() : '') || 'gpt-image-1'; + + if (!await ensureModelAllowedForDownstreamKey(request, reply, requestedModel)) return; + const downstreamPolicy = getDownstreamRoutingPolicy(request); + const excludeChannelIds: number[] = []; + let retryCount = 0; + + while (retryCount <= MAX_RETRIES) { + let selected = retryCount === 0 + ? await tokenRouter.selectChannel(requestedModel, downstreamPolicy) + : await tokenRouter.selectNextChannel(requestedModel, excludeChannelIds, downstreamPolicy); + + if (!selected && retryCount === 0) { + await refreshModelsAndRebuildRoutes(); + selected = await tokenRouter.selectChannel(requestedModel, downstreamPolicy); + } + + if (!selected) { + await reportProxyAllFailed({ + model: requestedModel, + reason: 'No available channels after retries', + }); + return reply.code(503).send({ + error: { message: 'No available channels for this model', type: 'server_error' }, + }); + } + + excludeChannelIds.push(selected.channel.id); + const targetUrl = buildUpstreamUrl(selected.site.url, '/v1/images/edits'); + const startTime = Date.now(); + + try { + const accountProxy = getProxyUrlFromExtraConfig(selected.account.extraConfig); + const requestInit = multipartForm + ? withSiteRecordProxyRequestInit(selected.site, { + method: 'POST', + headers: { + Authorization: `Bearer ${selected.tokenValue}`, + }, + body: cloneFormDataWithOverrides(multipartForm, { + model: selected.actualModel || requestedModel, + }) as any, + }, accountProxy) + : withSiteRecordProxyRequestInit(selected.site, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${selected.tokenValue}`, + }, + body: JSON.stringify({ + ...(jsonBody || {}), + model: selected.actualModel || requestedModel, + }), + }, accountProxy); + + const upstream = await fetch(targetUrl, requestInit); + const text = await upstream.text(); + if (!upstream.ok) { + tokenRouter.recordFailure(selected.channel.id); + logProxy(selected, requestedModel, 'failed', upstream.status, Date.now() - startTime, text, retryCount, 0, '/v1/images/edits'); + if (isTokenExpiredError({ status: upstream.status, message: text })) { + await reportTokenExpired({ + accountId: selected.account.id, + username: selected.account.username, + siteName: selected.site.name, + detail: `HTTP ${upstream.status}`, + }); + } + if (shouldRetryProxyRequest(upstream.status, text) && retryCount < MAX_RETRIES) { + retryCount++; + continue; + } + await reportProxyAllFailed({ + model: requestedModel, + reason: `upstream returned HTTP ${upstream.status}`, + }); + return reply.code(upstream.status).send({ error: { message: text, type: 'upstream_error' } }); + } + + let data: any = {}; + try { data = JSON.parse(text); } catch { data = { data: [] }; } + + const latency = Date.now() - startTime; + const estimatedCost = await estimateProxyCost({ + site: selected.site, + account: selected.account, + modelName: selected.actualModel || requestedModel, + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }); + tokenRouter.recordSuccess(selected.channel.id, latency, estimatedCost); + recordDownstreamCostUsage(request, estimatedCost); + logProxy(selected, requestedModel, 'success', upstream.status, latency, null, retryCount, estimatedCost, '/v1/images/edits'); + return reply.code(upstream.status).send(data); + } catch (err: any) { + tokenRouter.recordFailure(selected.channel.id); + logProxy(selected, requestedModel, 'failed', 0, Date.now() - startTime, err.message, retryCount, 0, '/v1/images/edits'); + if (retryCount < MAX_RETRIES) { + retryCount++; + continue; + } + await reportProxyAllFailed({ + model: requestedModel, + reason: err.message || 'network failure', + }); + return reply.code(502).send({ + error: { message: `Upstream error: ${err.message}`, type: 'upstream_error' }, + }); + } + } + }); + + app.post('/v1/images/variations', async (_request: FastifyRequest, reply: FastifyReply) => { + return reply.code(400).send({ + error: { + message: 'Image variations are not supported', + type: 'invalid_request_error', + }, + }); + }); +} async function logProxy( selected: any, modelRequested: string, status: string, httpStatus: number, - latencyMs: number, - errorMessage: string | null, - retryCount: number, - estimatedCost = 0, - downstreamPath = '/v1/images/generations', -) { - try { - const createdAt = formatUtcSqlDateTime(new Date()); - const normalizedErrorMessage = composeProxyLogMessage({ - downstreamPath, - errorMessage, - }); - await db.insert(schema.proxyLogs).values({ - routeId: selected.channel.routeId, - channelId: selected.channel.id, - accountId: selected.account.id, - modelRequested, - modelActual: selected.actualModel, - status, - httpStatus, - latencyMs, - promptTokens: 0, - completionTokens: 0, - totalTokens: 0, - estimatedCost, - errorMessage: normalizedErrorMessage, - retryCount, - createdAt, - }).run(); - } catch (error) { - console.warn('[proxy/images] failed to write proxy log', error); - } -} + latencyMs: number, + errorMessage: string | null, + retryCount: number, + estimatedCost = 0, + downstreamPath = '/v1/images/generations', +) { + try { + const createdAt = formatUtcSqlDateTime(new Date()); + const normalizedErrorMessage = composeProxyLogMessage({ + downstreamPath, + errorMessage, + }); + await db.insert(schema.proxyLogs).values({ + routeId: selected.channel.routeId, + channelId: selected.channel.id, + accountId: selected.account.id, + modelRequested, + modelActual: selected.actualModel, + status, + httpStatus, + latencyMs, + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + estimatedCost, + errorMessage: normalizedErrorMessage, + retryCount, + createdAt, + }).run(); + } catch (error) { + console.warn('[proxy/images] failed to write proxy log', error); + } +} diff --git a/src/server/routes/proxy/responses.ts b/src/server/routes/proxy/responses.ts index 0cd79aa..c336416 100644 --- a/src/server/routes/proxy/responses.ts +++ b/src/server/routes/proxy/responses.ts @@ -8,7 +8,8 @@ import { isTokenExpiredError } from '../../services/alertRules.js'; import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; import { resolveProxyUsageWithSelfLogFallback } from '../../services/proxyUsageFallbackService.js'; import { mergeProxyUsage, parseProxyUsage } from '../../services/proxyUsageParser.js'; -import { resolveProxyUrlForSite, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { resolveChannelProxyUrl, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; import { openAiResponsesTransformer } from '../../transformers/openai/responses/index.js'; import { buildUpstreamEndpointRequest, @@ -259,7 +260,7 @@ export async function responsesProxyRoute(app: FastifyInstance) { method: 'POST', headers: compatibilityRequest.headers, body: JSON.stringify(compatibilityRequest.body), - }), + }, getProxyUrlFromExtraConfig(selected.account.extraConfig)), ), }); @@ -268,7 +269,7 @@ export async function responsesProxyRoute(app: FastifyInstance) { try { const endpointResult = await executeEndpointFlow({ siteUrl: selected.site.url, - proxyUrl: resolveProxyUrlForSite(selected.site), + proxyUrl: resolveChannelProxyUrl(selected.site, selected.account.extraConfig), endpointCandidates, buildRequest: (endpoint) => buildEndpointRequest(endpoint), tryRecover: endpointStrategy.tryRecover, diff --git a/src/server/routes/proxy/search.ts b/src/server/routes/proxy/search.ts index 4a3c8b0..5c161c2 100644 --- a/src/server/routes/proxy/search.ts +++ b/src/server/routes/proxy/search.ts @@ -8,6 +8,7 @@ import { isTokenExpiredError } from '../../services/alertRules.js'; import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; import { withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; import { composeProxyLogMessage } from './logPathMeta.js'; import { formatUtcSqlDateTime } from '../../services/localTimeService.js'; import { buildUpstreamUrl } from './upstreamUrl.js'; @@ -92,7 +93,7 @@ export async function searchProxyRoute(app: FastifyInstance) { Authorization: `Bearer ${selected.tokenValue}`, }, body: JSON.stringify(forwardBody), - })); + }, getProxyUrlFromExtraConfig(selected.account.extraConfig))); const text = await upstream.text(); if (!upstream.ok) { diff --git a/src/server/routes/proxy/videos.ts b/src/server/routes/proxy/videos.ts index a5b1438..7f52344 100644 --- a/src/server/routes/proxy/videos.ts +++ b/src/server/routes/proxy/videos.ts @@ -8,6 +8,7 @@ import { estimateProxyCost } from '../../services/modelPricingService.js'; import { shouldRetryProxyRequest } from '../../services/proxyRetryPolicy.js'; import { ensureModelAllowedForDownstreamKey, getDownstreamRoutingPolicy, recordDownstreamCostUsage } from './downstreamPolicy.js'; import { withSiteProxyRequestInit, withSiteRecordProxyRequestInit } from '../../services/siteProxy.js'; +import { getProxyUrlFromExtraConfig } from '../../services/accountExtraConfig.js'; import { cloneFormDataWithOverrides, ensureMultipartBufferParser, parseMultipartFormData } from './multipart.js'; import { buildUpstreamUrl } from './upstreamUrl.js'; import { @@ -75,6 +76,7 @@ export async function videosProxyRoute(app: FastifyInstance) { const startTime = Date.now(); try { + const accountProxy = getProxyUrlFromExtraConfig(selected.account.extraConfig); const requestInit = multipartForm ? withSiteRecordProxyRequestInit(selected.site, { method: 'POST', @@ -84,7 +86,7 @@ export async function videosProxyRoute(app: FastifyInstance) { body: cloneFormDataWithOverrides(multipartForm, { model: selected.actualModel || requestedModel, }) as any, - }) + }, accountProxy) : withSiteRecordProxyRequestInit(selected.site, { method: 'POST', headers: { @@ -95,7 +97,7 @@ export async function videosProxyRoute(app: FastifyInstance) { ...(jsonBody || {}), model: selected.actualModel || requestedModel, }), - }); + }, accountProxy); const upstream = await fetch(targetUrl, requestInit); const text = await upstream.text();