feat: support model alias mapping for image and video runtimes (#13896)

This commit is contained in:
YuTengjing 2026-04-20 09:38:56 +08:00 committed by GitHub
parent a0471d5906
commit fb471123fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 102 additions and 16 deletions

View file

@ -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:*",

View file

@ -1 +1,2 @@
export * from './model-mapping';
export * from './router-runtime-options';

View file

@ -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<ResolvedBusinessModel> => {
return {
resolvedModelId: model,
};
};

View file

@ -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,

View file

@ -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,

View file

@ -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<typeof setTimeout> | 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,

View file

@ -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,