Files
AFFiNE-Mirror/packages/backend/server/src/plugins/copilot/runtime/copilot-runtime-event-consumer.ts
T
DarkSky 965f4590ff 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 -->
2026-08-05 00:40:13 +08:00

130 lines
3.7 KiB
TypeScript

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
);
}
}
}