From fb471123fca9c5df3b7f5c886a5c8bab626e564c Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Mon, 20 Apr 2026 09:38:56 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20support=20model=20alias=20m?= =?UTF-8?q?apping=20for=20image=20and=20video=20runtimes=20(#13896)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + packages/business/model-runtime/src/index.ts | 1 + .../model-runtime/src/model-mapping.ts | 35 +++++++++++++++++++ .../api/webhooks/video/[provider]/route.ts | 28 +++++++++++---- src/server/routers/async/image.ts | 16 +++++++-- src/server/routers/async/video.ts | 22 +++++++++--- src/server/routers/lambda/video/index.ts | 15 ++++++-- 7 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 packages/business/model-runtime/src/model-mapping.ts diff --git a/package.json b/package.json index 3c6f101624..173cf77565 100644 --- a/package.json +++ b/package.json @@ -234,6 +234,7 @@ "@lobechat/builtin-tools": "workspace:*", "@lobechat/business-config": "workspace:*", "@lobechat/business-const": "workspace:*", + "@lobechat/business-model-runtime": "workspace:*", "@lobechat/chat-adapter-feishu": "workspace:*", "@lobechat/chat-adapter-qq": "workspace:*", "@lobechat/chat-adapter-wechat": "workspace:*", diff --git a/packages/business/model-runtime/src/index.ts b/packages/business/model-runtime/src/index.ts index b05a65eae8..436566c50d 100644 --- a/packages/business/model-runtime/src/index.ts +++ b/packages/business/model-runtime/src/index.ts @@ -1 +1,2 @@ +export * from './model-mapping'; export * from './router-runtime-options'; diff --git a/packages/business/model-runtime/src/model-mapping.ts b/packages/business/model-runtime/src/model-mapping.ts new file mode 100644 index 0000000000..2a427daea7 --- /dev/null +++ b/packages/business/model-runtime/src/model-mapping.ts @@ -0,0 +1,35 @@ +export interface MappedBusinessModelFields { + modelId: string; + providerId: string; + requestedModelId?: string; +} + +export interface ResolvedBusinessModel { + requestedModelId?: string; + resolvedModelId: string; +} + +interface BuildMappedBusinessModelFieldsParams { + provider: string; + requestedModelId?: string; + resolvedModelId: string; +} + +export const buildMappedBusinessModelFields = ({ + provider, + requestedModelId, + resolvedModelId, +}: BuildMappedBusinessModelFieldsParams): MappedBusinessModelFields => ({ + modelId: resolvedModelId, + providerId: provider, + ...(requestedModelId ? { requestedModelId } : {}), +}); + +export const resolveBusinessModelMapping = async ( + _provider: string, + model: string, +): Promise => { + return { + resolvedModelId: model, + }; +}; diff --git a/src/app/(backend)/api/webhooks/video/[provider]/route.ts b/src/app/(backend)/api/webhooks/video/[provider]/route.ts index de12432737..ae72af5c02 100644 --- a/src/app/(backend)/api/webhooks/video/[provider]/route.ts +++ b/src/app/(backend)/api/webhooks/video/[provider]/route.ts @@ -1,5 +1,9 @@ import { timingSafeEqual } from 'node:crypto'; +import { + buildMappedBusinessModelFields, + resolveBusinessModelMapping, +} from '@lobechat/business-model-runtime'; import { ModelRuntime } from '@lobechat/model-runtime'; import { AsyncTaskError, @@ -136,8 +140,18 @@ export const POST = async (req: Request, { params }: { params: Promise<{ provide const batch = await db.query.generationBatches.findFirst({ where: eq(generationBatches.id, generation.generationBatchId!), }); - const resolvedModel = - result.status === 'success' ? (result.model ?? batch?.model ?? '') : (batch?.model ?? ''); + const requestedModel = batch?.model ?? ''; + // Resolve mapping so spend log metadata and pricing lookup use the billed model id, + // not the user-facing alias nor the provider-reported internal name. + const { resolvedModelId } = requestedModel + ? await resolveBusinessModelMapping(provider, requestedModel) + : { resolvedModelId: '' }; + + const mappedModelFields = buildMappedBusinessModelFields({ + provider, + requestedModelId: resolvedModelId === requestedModel ? undefined : requestedModel, + resolvedModelId, + }); // Handle error result: refund precharge and mark task as error if (result.status === 'error') { @@ -153,10 +167,10 @@ export const POST = async (req: Request, { params }: { params: Promise<{ provide metadata: { asyncTaskId: asyncTask.id, generationBatchId: generation.generationBatchId!, - modelId: resolvedModel, topicId: batch?.generationTopicId, + ...mappedModelFields, }, - model: resolvedModel, + model: resolvedModelId, prechargeResult: metadata?.precharge as any, provider, userId: asyncTask.userId, @@ -206,7 +220,7 @@ export const POST = async (req: Request, { params }: { params: Promise<{ provide // TODO: temporarily disabled until notification UI is polished // notifyVideoCompleted({ // generationBatchId: generation.generationBatchId!, - // model: resolvedModel, + // model: requestedModel, // prompt: batch?.prompt ?? '', // topicId: batch?.generationTopicId, // userId: asyncTask.userId, @@ -222,10 +236,10 @@ export const POST = async (req: Request, { params }: { params: Promise<{ provide metadata: { asyncTaskId: asyncTask.id, generationBatchId: generation.generationBatchId!, - modelId: resolvedModel, topicId: batch?.generationTopicId, + ...mappedModelFields, }, - model: resolvedModel, + model: resolvedModelId, prechargeResult: metadata?.precharge as any, provider, usage: result.usage, diff --git a/src/server/routers/async/image.ts b/src/server/routers/async/image.ts index ddf77f3fac..2e773b35d5 100644 --- a/src/server/routers/async/image.ts +++ b/src/server/routers/async/image.ts @@ -1,5 +1,9 @@ import { ASYNC_TASK_TIMEOUT } from '@lobechat/business-config/server'; import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const'; +import { + buildMappedBusinessModelFields, + resolveBusinessModelMapping, +} from '@lobechat/business-model-runtime'; import { AgentRuntimeErrorType } from '@lobechat/model-runtime'; import { AsyncTaskError, AsyncTaskErrorType, AsyncTaskStatus } from '@lobechat/types'; import { TRPCError } from '@trpc/server'; @@ -257,6 +261,10 @@ export const imageRouter = router({ try { const imageGenerationPromise = async (signal: AbortSignal) => { log('Initializing agent runtime for provider: %s', provider); + const { requestedModelId, resolvedModelId } = await resolveBusinessModelMapping( + provider, + model, + ); // Read user's provider config from database const modelRuntime = await initModelRuntimeFromDB(ctx.serverDB, ctx.userId, provider); @@ -265,7 +273,7 @@ export const imageRouter = router({ checkAbortSignal(signal); log('Agent runtime initialized, calling createImage'); const response = await modelRuntime.createImage!({ - model, + model: resolvedModelId, params: params as unknown as RuntimeImageGenParams, }); @@ -376,8 +384,12 @@ export const imageRouter = router({ metadata: { asyncTaskId: taskId, generationBatchId, - modelId: model, topicId: generationTopicId, + ...buildMappedBusinessModelFields({ + provider, + requestedModelId, + resolvedModelId, + }), }, modelUsage, provider, diff --git a/src/server/routers/async/video.ts b/src/server/routers/async/video.ts index 74f31aa620..c02ba1e748 100644 --- a/src/server/routers/async/video.ts +++ b/src/server/routers/async/video.ts @@ -1,5 +1,9 @@ import { ASYNC_TASK_TIMEOUT } from '@lobechat/business-config/server'; import { ENABLE_BUSINESS_FEATURES } from '@lobechat/business-const'; +import { + buildMappedBusinessModelFields, + resolveBusinessModelMapping, +} from '@lobechat/business-model-runtime'; import { AsyncTaskError, AsyncTaskErrorType, AsyncTaskStatus } from '@lobechat/types'; import debug from 'debug'; import { z } from 'zod'; @@ -133,6 +137,8 @@ export const videoRouter = router({ provider, }); + const { resolvedModelId } = await resolveBusinessModelMapping(provider, model); + const abortController = new AbortController(); let timeoutId: ReturnType | null = null; @@ -208,10 +214,14 @@ export const videoRouter = router({ metadata: { asyncTaskId, generationBatchId, - modelId: model, topicId: generationTopicId, + ...buildMappedBusinessModelFields({ + provider, + requestedModelId: resolvedModelId === model ? undefined : model, + resolvedModelId, + }), }, - model, + model: resolvedModelId, prechargeResult, provider, usage: undefined, @@ -270,10 +280,14 @@ export const videoRouter = router({ metadata: { asyncTaskId, generationBatchId, - modelId: model, topicId: generationTopicId, + ...buildMappedBusinessModelFields({ + provider, + requestedModelId: resolvedModelId === model ? undefined : model, + resolvedModelId, + }), }, - model, + model: resolvedModelId, prechargeResult, provider, userId: ctx.userId, diff --git a/src/server/routers/lambda/video/index.ts b/src/server/routers/lambda/video/index.ts index a5079fd065..a4bafb15a0 100644 --- a/src/server/routers/lambda/video/index.ts +++ b/src/server/routers/lambda/video/index.ts @@ -1,5 +1,9 @@ import { randomBytes } from 'node:crypto'; +import { + buildMappedBusinessModelFields, + resolveBusinessModelMapping, +} from '@lobechat/business-model-runtime'; import debug from 'debug'; import { and, eq } from 'drizzle-orm'; import { after } from 'next/server'; @@ -67,6 +71,7 @@ export const videoRouter = router({ createVideo: videoProcedure.input(createVideoInputSchema).mutation(async ({ input, ctx }) => { const { userId, serverDB, asyncTaskModel, fileService } = ctx; const { generationTopicId, provider, model, params } = input; + const { resolvedModelId } = await resolveBusinessModelMapping(provider, model); log('Starting video creation process, input: %O', input); @@ -214,7 +219,7 @@ export const videoRouter = router({ const response = await modelRuntime.createVideo({ callbackUrl, - model, + model: resolvedModelId, params: generationParams, }); @@ -289,10 +294,14 @@ export const videoRouter = router({ metadata: { asyncTaskId, generationBatchId: createdBatch.id, - modelId: model, topicId: generationTopicId, + ...buildMappedBusinessModelFields({ + provider, + requestedModelId: resolvedModelId === model ? undefined : model, + resolvedModelId, + }), }, - model, + model: resolvedModelId, prechargeResult, provider, userId,