feat(server): converge legacy compatibility (#15426)
#### PR Dependency Tree * **PR #15426** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added workspace BYOK profiles with provider/model catalogs, capability validation, connection probing, credential rotation, reordering, and secure local leases. * Added Copilot route options, selectable targets, managed tiers, explicit profile/model overrides, and improved streaming with tool callbacks and abort support. * Added Copilot availability controls to prevent access when the feature is disabled. * **Changes** * Simplified Copilot configuration and removed legacy provider-specific settings. * Removed obsolete model, token-cost, transcript strategy, and provider metadata fields from public responses. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,135 +1,19 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
|
||||
import { BaseModel } from './base';
|
||||
|
||||
export type UpsertAiWorkspaceByokConfigInput = {
|
||||
id?: string | null;
|
||||
workspaceId: string;
|
||||
provider: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
encryptedApiKey?: string;
|
||||
endpoint: string | null;
|
||||
sortOrder: number;
|
||||
enabled: boolean;
|
||||
userId?: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CopilotWorkspaceByokConfigModel extends BaseModel {
|
||||
async list(workspaceId: string) {
|
||||
return await this.db.aiWorkspaceByokConfig.findMany({
|
||||
where: { workspaceId },
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
async listEnabled(workspaceId: string) {
|
||||
return await this.db.aiWorkspaceByokConfig.findMany({
|
||||
where: { workspaceId, enabled: true },
|
||||
orderBy: [{ sortOrder: 'asc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
}
|
||||
|
||||
async get(id: string) {
|
||||
return await this.db.aiWorkspaceByokConfig.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async upsert(input: UpsertAiWorkspaceByokConfigInput) {
|
||||
const data = {
|
||||
provider: input.provider,
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
endpoint: input.endpoint,
|
||||
sortOrder: input.sortOrder,
|
||||
enabled: input.enabled,
|
||||
updatedBy: input.userId,
|
||||
...(input.encryptedApiKey
|
||||
? {
|
||||
encryptedApiKey: input.encryptedApiKey,
|
||||
lastValidatedAt: new Date(),
|
||||
lastValidationError: null,
|
||||
disabledReason: null,
|
||||
lastError: null,
|
||||
lastErrorAt: null,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
return input.id
|
||||
? await this.db.aiWorkspaceByokConfig.update({
|
||||
where: { id: input.id, workspaceId: input.workspaceId },
|
||||
data,
|
||||
})
|
||||
: await this.db.aiWorkspaceByokConfig.create({
|
||||
data: {
|
||||
...data,
|
||||
encryptedApiKey: input.encryptedApiKey ?? '',
|
||||
workspaceId: input.workspaceId,
|
||||
createdBy: input.userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async reorder(workspaceId: string, ids: string[], userId?: string) {
|
||||
await Promise.all(
|
||||
ids.map((id, sortOrder) =>
|
||||
this.db.aiWorkspaceByokConfig.update({
|
||||
where: { id, workspaceId },
|
||||
data: { sortOrder, updatedBy: userId },
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async delete(workspaceId: string, id: string) {
|
||||
await this.db.aiWorkspaceByokConfig.delete({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async clear(workspaceId: string, provider?: string | null) {
|
||||
await this.db.aiWorkspaceByokConfig.deleteMany({
|
||||
where: { workspaceId, ...(provider ? { provider } : {}) },
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async markValidated(workspaceId: string, id: string, userId?: string) {
|
||||
await this.db.aiWorkspaceByokConfig.update({
|
||||
where: { id, workspaceId },
|
||||
data: {
|
||||
enabled: true,
|
||||
disabledReason: null,
|
||||
lastValidatedAt: new Date(),
|
||||
lastValidationError: null,
|
||||
lastError: null,
|
||||
lastErrorAt: null,
|
||||
updatedBy: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async markFailure(workspaceId: string, id: string, message: string) {
|
||||
await this.db.aiWorkspaceByokConfig.update({
|
||||
await this.db.aiWorkspaceByokConfig.updateMany({
|
||||
where: { id, workspaceId },
|
||||
data: {
|
||||
enabled: false,
|
||||
disabledReason: 'recent_failure',
|
||||
lastValidationError: message,
|
||||
lastError: message,
|
||||
lastErrorAt: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async touchUsed(workspaceId: string, id: string) {
|
||||
await this.db.aiWorkspaceByokConfig.updateMany({
|
||||
where: { id, workspaceId },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Transactional } from '@nestjs-cls/transactional';
|
||||
import { AiPromptRole, Prisma } from '@prisma/client';
|
||||
import { AiSessionMessageRole, Prisma } from '@prisma/client';
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import {
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
CopilotSessionInvalidInput,
|
||||
CopilotSessionNotFound,
|
||||
} from '../base';
|
||||
import { getTokenEncoder } from '../native';
|
||||
import type { PromptAttachment } from '../plugins/copilot/providers/types';
|
||||
import {
|
||||
type ChatMessage as CopilotChatMessage,
|
||||
@@ -26,7 +25,6 @@ export enum SessionType {
|
||||
type ChatPrompt = {
|
||||
name: string;
|
||||
action?: string | null;
|
||||
model: string;
|
||||
};
|
||||
|
||||
type ChatAttachment = PromptAttachment;
|
||||
@@ -95,12 +93,11 @@ export type ForkSessionOptions = Omit<
|
||||
ChatSession,
|
||||
'messages' | 'promptName' | 'promptAction'
|
||||
> & {
|
||||
prompt: { name: string; action: string | null | undefined; model: string };
|
||||
prompt: { name: string; action: string | null | undefined };
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
type UpdateChatSessionMessage = ChatSessionBaseState & {
|
||||
prompt: { model: string };
|
||||
messages: ChatMessage[];
|
||||
};
|
||||
|
||||
@@ -108,7 +105,7 @@ export type UpdateChatSessionOptions = ChatSessionBaseState &
|
||||
Pick<
|
||||
Partial<ChatSession>,
|
||||
'docId' | 'pinned' | 'promptName' | 'promptAction' | 'title'
|
||||
> & { promptModel?: string };
|
||||
>;
|
||||
|
||||
export type UpdateChatSession = ChatSessionBaseState & UpdateChatSessionOptions;
|
||||
|
||||
@@ -144,20 +141,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
};
|
||||
}
|
||||
|
||||
private async ensurePromptCompatRecord(prompt: ChatPrompt) {
|
||||
await this.db.aiPrompt.upsert({
|
||||
where: { name: prompt.name },
|
||||
update: {},
|
||||
create: {
|
||||
name: prompt.name,
|
||||
action: prompt.action,
|
||||
model: prompt.model,
|
||||
optionalModels: [],
|
||||
config: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private sanitizeString<T extends string | null | undefined>(value: T): T {
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
@@ -354,7 +337,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
private isCountedUserMessage(
|
||||
message: Pick<StoredChatMessage, 'role'>
|
||||
): boolean {
|
||||
return message.role === AiPromptRole.user;
|
||||
return message.role === AiSessionMessageRole.user;
|
||||
}
|
||||
|
||||
getSessionType(session: Pick<ChatSession, 'docId' | 'pinned'>): SessionType {
|
||||
@@ -418,7 +401,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
reuseChat = false
|
||||
): Promise<string> {
|
||||
const { prompt, ...rest } = state;
|
||||
await this.ensurePromptCompatRecord(prompt);
|
||||
return await this.models.copilotSession.create(
|
||||
{ ...rest, promptName: prompt.name, promptAction: prompt.action ?? null },
|
||||
reuseChat
|
||||
@@ -507,7 +489,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
pinned: true,
|
||||
title: true,
|
||||
promptName: true,
|
||||
tokenCost: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
messages: {
|
||||
@@ -536,7 +517,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
pinned: true,
|
||||
title: true,
|
||||
promptName: true,
|
||||
tokenCost: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
});
|
||||
@@ -605,7 +585,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
pinned: true,
|
||||
title: true,
|
||||
promptName: true,
|
||||
tokenCost: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
messages: options.withMessages
|
||||
@@ -682,25 +661,11 @@ export class CopilotSessionModel extends BaseModel {
|
||||
|
||||
let nextPromptAction: string | null | undefined;
|
||||
if (promptName) {
|
||||
if (options.promptModel) {
|
||||
await this.ensurePromptCompatRecord({
|
||||
name: promptName,
|
||||
action: options.promptAction,
|
||||
model: options.promptModel,
|
||||
});
|
||||
}
|
||||
nextPromptAction = options.promptAction;
|
||||
if (nextPromptAction === undefined) {
|
||||
const prompt = await this.db.aiPrompt.findFirst({
|
||||
where: { name: promptName },
|
||||
select: { action: true },
|
||||
});
|
||||
if (!prompt) {
|
||||
throw new CopilotSessionInvalidInput(
|
||||
`Prompt ${promptName} not found or not available for session ${sessionId}`
|
||||
);
|
||||
}
|
||||
nextPromptAction = prompt.action ?? null;
|
||||
throw new CopilotSessionInvalidInput(
|
||||
`Prompt action is required when changing prompt ${promptName}`
|
||||
);
|
||||
}
|
||||
if (nextPromptAction) {
|
||||
throw new CopilotSessionInvalidInput(
|
||||
@@ -809,12 +774,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
return message ? this.toPublicMessage(message) : null;
|
||||
}
|
||||
|
||||
private calculateTokenSize(messages: any[], model: string): number {
|
||||
const encoder = getTokenEncoder(model);
|
||||
const content = messages.map(m => m.content).join('');
|
||||
return encoder?.count(content) || 0;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async updateMessages(state: UpdateChatSessionMessage) {
|
||||
const { sessionId, userId, messages } = state;
|
||||
@@ -825,10 +784,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
|
||||
if (messages.length) {
|
||||
const sanitizedMessages = messages.map(m => this.sanitizeMessage(m));
|
||||
const tokenCost = this.calculateTokenSize(
|
||||
sanitizedMessages,
|
||||
state.prompt.model
|
||||
);
|
||||
await this.db.aiSessionMessage.createMany({
|
||||
data: sanitizedMessages.map(m => ({
|
||||
compatSubmissionId: m.compatSubmissionId || undefined,
|
||||
@@ -848,7 +803,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
where: { id: sessionId },
|
||||
data: {
|
||||
messageCost: { increment: userMessages.length },
|
||||
tokenCost: { increment: tokenCost },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -858,7 +812,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
async appendMessage(state: {
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
prompt: { model: string };
|
||||
message: ChatMessage;
|
||||
}) {
|
||||
const haveSession = await this.has(state.sessionId, state.userId);
|
||||
@@ -867,8 +820,6 @@ export class CopilotSessionModel extends BaseModel {
|
||||
}
|
||||
|
||||
const message = this.sanitizeMessage(state.message);
|
||||
const tokenCost = this.calculateTokenSize([message], state.prompt.model);
|
||||
|
||||
const created = await this.db.aiSessionMessage.create({
|
||||
data: {
|
||||
sessionId: state.sessionId,
|
||||
@@ -896,8 +847,9 @@ export class CopilotSessionModel extends BaseModel {
|
||||
where: { id: state.sessionId },
|
||||
data: {
|
||||
messageCost:
|
||||
message.role === AiPromptRole.user ? { increment: 1 } : undefined,
|
||||
tokenCost: { increment: tokenCost },
|
||||
message.role === AiSessionMessageRole.user
|
||||
? { increment: 1 }
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -970,8 +922,9 @@ export class CopilotSessionModel extends BaseModel {
|
||||
});
|
||||
const ids = messages
|
||||
.slice(
|
||||
messages.findLastIndex(({ role }) => role === AiPromptRole.user) +
|
||||
(removeLatestUserMessage ? 0 : 1)
|
||||
messages.findLastIndex(
|
||||
({ role }) => role === AiSessionMessageRole.user
|
||||
) + (removeLatestUserMessage ? 0 : 1)
|
||||
)
|
||||
.map(({ id }) => id);
|
||||
|
||||
|
||||
@@ -24,12 +24,7 @@ export class CopilotTranscriptTaskModel extends BaseModel {
|
||||
async create(
|
||||
input: Pick<
|
||||
Prisma.AiTranscriptTaskCreateArgs['data'],
|
||||
| 'userId'
|
||||
| 'workspaceId'
|
||||
| 'blobId'
|
||||
| 'strategy'
|
||||
| 'recipeId'
|
||||
| 'recipeVersion'
|
||||
'userId' | 'workspaceId' | 'blobId' | 'recipeId' | 'recipeVersion'
|
||||
> &
|
||||
Partial<Prisma.AiTranscriptTaskCreateArgs['data']>
|
||||
) {
|
||||
@@ -39,7 +34,6 @@ export class CopilotTranscriptTaskModel extends BaseModel {
|
||||
workspaceId: input.workspaceId,
|
||||
blobId: input.blobId,
|
||||
status: 'pending',
|
||||
strategy: input.strategy,
|
||||
recipeId: input.recipeId,
|
||||
recipeVersion: input.recipeVersion,
|
||||
inputSnapshot: nullableJson(input.inputSnapshot),
|
||||
|
||||
Reference in New Issue
Block a user