chatEntitlementService.ts ×30

Frontier kind: Joint frontier

unlabeled · c_08f3afbea88c

1 test · 25541 LOC · 136 files · introduces 1 test · 149 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
30 ranges149 lines · 1 files
Tests
1 test

Contains — complete concept membership

All code (extent)
3021 ranges25541 lines · 136 files · Browse complete extent
All tests (intent)
1 testBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

1 test introduced at this concept.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 149 introduced LOC across 30 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/services/chat/common/chatEntitlementService.ts 149 introduced LOC · 30 ranges

Open complete file

314 const CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY = 'chat.allowAnonymousAccess';
315
316 > function isAnonymous(configurationService: IConfigurationService, entitlement: ChatEntitlement, sentiment: IChatSentiment): boolean { chatEntitlementService.ts
317 > if (configurationService.getValue(CHAT_ALLOW_ANONYMOUS_CONFIGURATION_KEY) !== true) {
318 > return false; // only enabled behind an experimental setting
319 > }
320
321 if (entitlement !== ChatEntitlement.Unknown) {
323 }
324
325 > if (sentiment.hidden || sentiment.disabledInWorkspace) { chatEntitlementService.ts
326 return false; // only consider enabled scenarios
327 }
389
390 constructor(
391 > @IInstantiationService instantiationService: IInstantiationService, chatEntitlementService.ts
392 > @IProductService productService: IProductService,
393 > @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
394 > @IContextKeyService private readonly contextKeyService: IContextKeyService,
395 > @IConfigurationService private readonly configurationService: IConfigurationService,
396 > @ITelemetryService private readonly telemetryService: ITelemetryService,
397 > @ILogService private readonly logService: ILogService,
398 > @IStorageService private readonly storageService: IStorageService,
399 > ) {
400 > super();
401 >
402 > const cachedUBB = this.storageService.getBoolean(ChatEntitlementService.CACHED_UBB_STORAGE_KEY, StorageScope.PROFILE);
403 > this._quotas = cachedUBB !== undefined ? { usageBasedBilling: cachedUBB } : {};
404 >
405 > this.chatQuotaExceededContextKey = ChatEntitlementContextKeys.chatQuotaExceeded.bindTo(this.contextKeyService);
406 > this.completionsQuotaExceededContextKey = ChatEntitlementContextKeys.completionsQuotaExceeded.bindTo(this.contextKeyService);
407 >
408 > this.anonymousContextKey = ChatEntitlementContextKeys.chatAnonymous.bindTo(this.contextKeyService);
409 > this.anonymousContextKey.set(this.anonymous);
410 >
411 > // Only apply the workbench-side default if no other source (e.g. the Copilot extension)
412 > // has already set this key; binding would otherwise reset it to the declared default.
413 > if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.clientByokEnabled.key) === undefined) {
414 > ChatEntitlementContextKeys.clientByokEnabled.bindTo(this.contextKeyService);
415 > }
416 >
417 > this.onDidChangeEntitlement = Event.map(
418 > Event.filter(
419 > this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([
420 ChatEntitlementContextKeys.Entitlement.planEdu.key,
421 ChatEntitlementContextKeys.Entitlement.planPro.key,
430 ChatEntitlementContextKeys.Entitlement.internal.key,
431 ChatEntitlementContextKeys.Entitlement.sku.key
432 > ])), this._store chatEntitlementService.ts
433 > ), () => { }, this._store
434 > );
435 > this.entitlementObs = observableFromEvent(this.onDidChangeEntitlement, () => this.entitlement);
436 >
437 > this.onDidChangeSentiment = Event.map(
438 > Event.filter(
439 > this.contextKeyService.onDidChangeContext, e => e.affectsSome(new Set([
440 ChatEntitlementContextKeys.Setup.completed.key,
441 ChatEntitlementContextKeys.Setup.hidden.key,
445 ChatEntitlementContextKeys.Setup.later.key,
446 ChatEntitlementContextKeys.Setup.registered.key
447 > ])), this._store chatEntitlementService.ts
448 > ), () => { }, this._store
449 > );
450 > this.sentimentObs = observableFromEvent(this.onDidChangeSentiment, () => this.sentiment);
451 >
452 > if ((isWeb && !environmentService.remoteAuthority && !environmentService.isSessionsWindow)) {
453 ChatEntitlementContextKeys.Setup.hidden.bindTo(this.contextKeyService).set(true); // hide copilot UI on web if unsupported
454 return;
455 }
457 > if (!productService.defaultChatAgent) {
458 > return; // we need a default chat agent configured going forward from here
459 > }
460
461 const context = this.context = new Lazy(() => this._register(instantiationService.createInstance(ChatEntitlementContext)));
474
475 get entitlement(): ChatEntitlement {
476 > if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planEdu.key) === true) { chatEntitlementService.ts
477 return ChatEntitlement.EDU;
478 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planPro.key) === true) { chatEntitlementService.ts
479 return ChatEntitlement.Pro;
480 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planBusiness.key) === true) { chatEntitlementService.ts
481 return ChatEntitlement.Business;
482 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planEnterprise.key) === true) { chatEntitlementService.ts
483 return ChatEntitlement.Enterprise;
484 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planProPlus.key) === true) { chatEntitlementService.ts
485 return ChatEntitlement.ProPlus;
486 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planMax.key) === true) { chatEntitlementService.ts
487 return ChatEntitlement.Max;
488 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.planFree.key) === true) { chatEntitlementService.ts
489 return ChatEntitlement.Free;
490 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.canSignUp.key) === true) { chatEntitlementService.ts
491 return ChatEntitlement.Available;
492 > } else if (this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Entitlement.signedOut.key) === true) { chatEntitlementService.ts
493 return ChatEntitlement.Unknown;
494 }
496 > return ChatEntitlement.Unresolved;
497 > }
498
499 get isInternal(): boolean {
510
511 get copilotTrackingId(): string | undefined {
512 > return this.context?.value.state.copilotTrackingId; chatEntitlementService.ts
513 > }
514
515 get clientByokEnabled(): boolean {
587
588 acceptQuotas(incomingQuotas: IQuotas): void {
589 > const oldQuota = this._quotas; chatEntitlementService.ts
590 > const cachedQuota = this.quotaCopilotTrackingId === this.copilotTrackingId ? oldQuota : {};
591 > const quotas: IQuotas = {
592 > ...incomingQuotas,
593 > chat: incomingQuotas.chat ? mergeDefinedSnapshot(cachedQuota.chat, incomingQuotas.chat) : undefined,
594 > completions: incomingQuotas.completions ? mergeDefinedSnapshot(cachedQuota.completions, incomingQuotas.completions) : undefined,
595 > premiumChat: incomingQuotas.premiumChat ? mergeDefinedSnapshot(cachedQuota.premiumChat, incomingQuotas.premiumChat) : undefined,
596 > sessionRateLimit: incomingQuotas.sessionRateLimit ? mergeDefinedSnapshot(cachedQuota.sessionRateLimit, incomingQuotas.sessionRateLimit) : undefined,
597 > weeklyRateLimit: incomingQuotas.weeklyRateLimit ? mergeDefinedSnapshot(cachedQuota.weeklyRateLimit, incomingQuotas.weeklyRateLimit) : undefined,
598 > };
599 > this.quotaCopilotTrackingId = this.copilotTrackingId;
600 > this._quotas = quotas;
601 > this.updateContextKeys();
602 >
603 > if (oldQuota.usageBasedBilling !== quotas.usageBasedBilling) {
604 if (quotas.usageBasedBilling !== undefined) {
605 this.storageService.store(ChatEntitlementService.CACHED_UBB_STORAGE_KEY, quotas.usageBasedBilling, StorageScope.PROFILE, StorageTarget.MACHINE);
608 }
609 }
611 > if (this.logService.getLevel() === LogLevel.Trace) {
612 this.logService.trace(`[chat entitlement]: acceptQuotas: ${JSON.stringify(quotas)}`);
613 }
615 > const { changed: chatChanged } = this.compareQuotas(oldQuota.chat, quotas.chat);
616 > const { changed: completionsChanged } = this.compareQuotas(oldQuota.completions, quotas.completions);
617 > const { changed: premiumChatChanged } = this.compareQuotas(oldQuota.premiumChat, quotas.premiumChat);
618 >
619 > if (chatChanged.exceeded || completionsChanged.exceeded || premiumChatChanged.exceeded) {
620 this._onDidChangeQuotaExceeded.fire();
621 }
623 > const sessionRateLimitChanged = oldQuota.sessionRateLimit?.percentRemaining !== quotas.sessionRateLimit?.percentRemaining;
624 > const weeklyRateLimitChanged = oldQuota.weeklyRateLimit?.percentRemaining !== quotas.weeklyRateLimit?.percentRemaining;
625 >
626 > if (chatChanged.remaining || completionsChanged.remaining || premiumChatChanged.remaining || sessionRateLimitChanged || weeklyRateLimitChanged || oldQuota.usageBasedBilling !== quotas.usageBasedBilling) {
627 > this._onDidChangeQuotaRemaining.fire();
628 > }
629 >
630 > if (oldQuota.usageBasedBilling !== quotas.usageBasedBilling) {
631 this._onDidChangeUsageBasedBilling.fire();
632 }
634 > // Track additional spend configuration changes (only when both values come from server snapshots)
635 > if (oldQuota.additionalUsageEnabled !== undefined && quotas.additionalUsageEnabled !== undefined && oldQuota.additionalUsageEnabled !== quotas.additionalUsageEnabled) {
636 this.telemetryService.publicLog2<ChatAdditionalSpendConfigurationEvent, ChatAdditionalSpendConfigurationClassification>('chatAdditionalSpendConfiguration', {
637 enabled: quotas.additionalUsageEnabled ?? false,
639 });
640 }
642 > // Track entering additional spend: included quota just exhausted while additional spend is enabled
643 > if (quotas.additionalUsageEnabled && quotas.premiumChat?.percentRemaining === 0
644 > && oldQuota.premiumChat?.percentRemaining !== undefined && oldQuota.premiumChat.percentRemaining > 0) {
645 this.telemetryService.publicLog2<ChatAdditionalSpendActiveEvent, ChatAdditionalSpendActiveClassification>('chatAdditionalSpendActive', {
646 entitlement: this.entitlement,
648 });
649 }
651
652 private compareQuotas(oldQuota: IQuotaSnapshot | undefined, newQuota: IQuotaSnapshot | undefined): { changed: { exceeded: boolean; remaining: boolean } } {
653 > return { chatEntitlementService.ts
654 > changed: {
655 > exceeded: (oldQuota?.percentRemaining === 0) !== (newQuota?.percentRemaining === 0),
656 > remaining: oldQuota?.percentRemaining !== newQuota?.percentRemaining
657 > || oldQuota?.usageBasedBilling !== newQuota?.usageBasedBilling
658 > }
659 > };
660 > }
661
662 clearQuotas(): void {
665
666 private updateContextKeys(): void {
667 > const chatExhausted = this._quotas.chat?.percentRemaining === 0; chatEntitlementService.ts
668 > const premiumChatExhausted = this._quotas.premiumChat?.unlimited
669 > ? this._quotas.premiumChat.hasQuota === false
670 > : this._quotas.premiumChat?.percentRemaining === 0;
671 > const additionalUsageEnabled = this._quotas.additionalUsageEnabled ?? false;
672 > const isManagedPlan = this.entitlement === ChatEntitlement.Business || this.entitlement === ChatEntitlement.Enterprise;
673 >
674 > // For Business/Enterprise users, hasQuota === false is the authoritative signal
675 > // that the org has blocked usage, regardless of additionalUsageEnabled.
676 > this.chatQuotaExceededContextKey.set(chatExhausted || (premiumChatExhausted && (isManagedPlan || !additionalUsageEnabled)));
677 > this.completionsQuotaExceededContextKey.set(this._quotas.completions?.percentRemaining === 0);
678 > }
679
680 //#endregion
686
687 get sentiment(): IChatSentiment {
688 > return { chatEntitlementService.ts
689 > completed: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.completed.key) === true,
690 > installed: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.installed.key) === true,
691 > hidden: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.hidden.key) === true,
692 > disabledInWorkspace: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.disabledInWorkspace.key) === true,
693 > disabled: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.disabled.key) === true,
694 > untrusted: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.untrusted.key) === true,
695 > later: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.later.key) === true,
696 > registered: this.contextKeyService.getContextKeyValue<boolean>(ChatEntitlementContextKeys.Setup.registered.key) === true
697 > };
698 > }
699
700 //#endregion
710
711 get anonymous(): boolean {
712 > return isAnonymous(this.configurationService, this.entitlement, this.sentiment); chatEntitlementService.ts
713 > }
714
715 //#endregion
836 }
837
838 > function mergeDefinedSnapshot<T extends object>(previous: T | undefined, current: T): T { chatEntitlementService.ts
839 > const result = { ...previous, ...current };
840 > for (const key of Object.keys(current) as (keyof T)[]) {
841 > if (current[key] === undefined && previous?.[key] !== undefined) {
842 > result[key] = previous[key];
843 > }
844 > }
845 > return result;
846 > }
847
848 export function parseQuotas(entitlementsData: IEntitlementsData): IQuotas {