mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 20:57:08 +08:00
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:
+129
@@ -0,0 +1,129 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { metrics } from '../../../base';
|
||||
import { Models } from '../../../models';
|
||||
import { type ByokFeatureKind, ByokProviderSource } from '../byok/types';
|
||||
|
||||
export type CopilotRuntimeRouteIdentity = {
|
||||
profileId: string;
|
||||
source: 'server' | 'local' | 'affine_cloud';
|
||||
provider: string;
|
||||
model: string;
|
||||
};
|
||||
|
||||
export type CopilotRuntimeEvent =
|
||||
| { type: 'route_selected'; route: CopilotRuntimeRouteIdentity }
|
||||
| {
|
||||
type: 'route_failed';
|
||||
route: CopilotRuntimeRouteIdentity;
|
||||
errorKind: string;
|
||||
}
|
||||
| {
|
||||
type: 'usage';
|
||||
route: CopilotRuntimeRouteIdentity;
|
||||
usage: {
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
total_tokens?: number;
|
||||
cached_tokens?: number;
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type CopilotRuntimeEventContext = {
|
||||
workspaceId?: string;
|
||||
userId?: string;
|
||||
sessionId?: string;
|
||||
taskId?: string;
|
||||
actionId?: string;
|
||||
billingUnitId?: string;
|
||||
featureKind: ByokFeatureKind;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CopilotRuntimeEventConsumer {
|
||||
private readonly logger = new Logger(CopilotRuntimeEventConsumer.name);
|
||||
|
||||
constructor(private readonly models: Models) {}
|
||||
|
||||
async consume(
|
||||
events: CopilotRuntimeEvent[],
|
||||
context: CopilotRuntimeEventContext
|
||||
) {
|
||||
for (const event of events) {
|
||||
try {
|
||||
if (event.type === 'usage') {
|
||||
await this.recordUsage(event, context);
|
||||
} else if (event.type === 'route_failed') {
|
||||
await this.recordFailure(event, context);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to consume copilot runtime event: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async recordUsage(
|
||||
event: Extract<CopilotRuntimeEvent, { type: 'usage' }>,
|
||||
context: CopilotRuntimeEventContext
|
||||
) {
|
||||
if (!context.workspaceId || event.route.source === 'affine_cloud') {
|
||||
return;
|
||||
}
|
||||
const usage = event.usage;
|
||||
metrics.ai.counter('byok_usage').add(1, {
|
||||
provider: event.route.provider,
|
||||
source: event.route.source,
|
||||
feature: context.featureKind,
|
||||
});
|
||||
await this.models.copilotUsage.create({
|
||||
workspaceId: context.workspaceId,
|
||||
userId: context.userId,
|
||||
provider: event.route.provider,
|
||||
providerSource:
|
||||
event.route.source === 'server'
|
||||
? ByokProviderSource.Server
|
||||
: ByokProviderSource.Local,
|
||||
featureKind: context.featureKind,
|
||||
model: event.route.model,
|
||||
sessionId: context.sessionId,
|
||||
taskId: context.taskId,
|
||||
actionId: context.actionId,
|
||||
billingUnitId: context.billingUnitId,
|
||||
promptTokens: usage.prompt_tokens ?? usage.input_tokens ?? 0,
|
||||
completionTokens: usage.completion_tokens ?? usage.output_tokens ?? 0,
|
||||
totalTokens: usage.total_tokens ?? 0,
|
||||
cachedTokens: usage.cached_tokens ?? 0,
|
||||
});
|
||||
if (event.route.source === 'server') {
|
||||
await this.models.copilotWorkspaceByokConfig.touchUsed(
|
||||
context.workspaceId,
|
||||
event.route.profileId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async recordFailure(
|
||||
event: Extract<CopilotRuntimeEvent, { type: 'route_failed' }>,
|
||||
context: CopilotRuntimeEventContext
|
||||
) {
|
||||
metrics.ai.counter('byok_route_failure').add(1, {
|
||||
provider: event.route.provider,
|
||||
source: event.route.source,
|
||||
feature: context.featureKind,
|
||||
reason: event.errorKind,
|
||||
});
|
||||
if (context.workspaceId && event.route.source === 'server') {
|
||||
await this.models.copilotWorkspaceByokConfig.markFailure(
|
||||
context.workspaceId,
|
||||
event.route.profileId,
|
||||
event.errorKind
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user