mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-08 00:33:03 +01:00
Support showing promo messages with no end date and no % discount (#329193)
* Support showing promo messages with no end date and no % discount * Address PR feedback on promo message support - Tighten promo JSDoc on the metadata contract, notification contribution, and helpers to match repository JSDoc limits - Document the positive/zero/negative discount cases so message-only promos are not omitted by agent implementations - Cover open-ended, message-only promos through the untyped _meta read and the CAPI billing normalization Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: edb3b42a-1bec-45f8-9453-51ad10583f06 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: edb3b42a-1bec-45f8-9453-51ad10583f06
This commit is contained in:
@@ -105,7 +105,8 @@ export interface IModelTokenPrices {
|
||||
export interface IModelPromo {
|
||||
id: string;
|
||||
discount_percent: number;
|
||||
ends_at: string;
|
||||
/** ISO 8601 end date; absent for open-ended promotions. */
|
||||
ends_at?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ export class ChatEndpoint implements IChatEndpoint {
|
||||
public readonly customModel?: CustomModel | undefined;
|
||||
public readonly maxPromptImages?: number | undefined;
|
||||
public readonly warningText?: Record<string, string> | undefined;
|
||||
public readonly promo?: { id: string; discountPercent: number; endsAt: string; message: string } | undefined;
|
||||
public readonly promo?: { id: string; discountPercent: number; endsAt?: string; message: string } | undefined;
|
||||
|
||||
private readonly _supportsStreaming: boolean;
|
||||
|
||||
|
||||
@@ -342,7 +342,7 @@ export interface IChatEndpoint extends IEndpoint {
|
||||
readonly isPremium?: boolean;
|
||||
readonly degradationReason?: string;
|
||||
readonly warningText?: Record<string, string>;
|
||||
readonly promo?: { id: string; discountPercent: number; endsAt: string; message: string };
|
||||
readonly promo?: { id: string; discountPercent: number; endsAt?: string; message: string };
|
||||
readonly multiplier?: number;
|
||||
readonly restrictedToSkus?: string[];
|
||||
/**
|
||||
|
||||
@@ -39,11 +39,12 @@ export interface IAgentModelPricingMeta {
|
||||
readonly category?: string;
|
||||
/** Whole-number percentage discount (0-100) for the synthetic `auto` model; shown as a "{n}% discount" detail. */
|
||||
readonly discountPercent?: number;
|
||||
/** Promotional information when the model is experiencing a discount. */
|
||||
/** Promotional information for the model. A `discountPercent` of `0` is a valid message-only promo. */
|
||||
readonly promo?: {
|
||||
readonly id: string;
|
||||
readonly discountPercent: number;
|
||||
readonly endsAt: string;
|
||||
/** ISO 8601 end date; absent for open-ended promotions. */
|
||||
readonly endsAt?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
}
|
||||
@@ -87,8 +88,13 @@ export function readAgentModelPricingMeta(model: IAgentModelInfo | SessionModelI
|
||||
const rawPromo = meta.promo;
|
||||
if (rawPromo && typeof rawPromo === 'object' && !Array.isArray(rawPromo)) {
|
||||
const p = rawPromo as Record<string, unknown>;
|
||||
if (typeof p.id === 'string' && typeof p.discountPercent === 'number' && typeof p.endsAt === 'string' && typeof p.message === 'string') {
|
||||
result.promo = { id: p.id, discountPercent: p.discountPercent, endsAt: p.endsAt, message: p.message };
|
||||
if (typeof p.id === 'string' && typeof p.discountPercent === 'number' && typeof p.message === 'string') {
|
||||
result.promo = {
|
||||
id: p.id,
|
||||
discountPercent: p.discountPercent,
|
||||
message: p.message,
|
||||
...(typeof p.endsAt === 'string' ? { endsAt: p.endsAt } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -175,8 +181,8 @@ function normalizePromo(billing: Record<string, unknown>): ICAPIModelBilling['pr
|
||||
: typeof raw.ends_at === 'string' ? raw.ends_at
|
||||
: undefined;
|
||||
const message = typeof raw.message === 'string' ? raw.message : undefined;
|
||||
if (id && typeof discountPercent === 'number' && endsAt && message) {
|
||||
return { id, discountPercent, endsAt, message };
|
||||
if (id && typeof discountPercent === 'number' && message) {
|
||||
return { id, discountPercent, message, ...(endsAt ? { endsAt } : {}) };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -191,11 +197,12 @@ export interface ICAPIModelBilling {
|
||||
readonly priceCategory?: string;
|
||||
/** Whole-number percentage discount (0-100) for the synthetic `auto` model; rendered as a "{n}% discount" detail. */
|
||||
readonly discountPercent?: number;
|
||||
/** Promotional info when the model is experiencing a promotional discount. */
|
||||
/** Promotional information for the model. A `discountPercent` of `0` is a valid message-only promo. */
|
||||
readonly promo?: {
|
||||
readonly id: string;
|
||||
readonly discountPercent: number;
|
||||
readonly endsAt: string;
|
||||
/** ISO 8601 end date; absent for open-ended promotions. */
|
||||
readonly endsAt?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
readonly tokenPrices?: {
|
||||
|
||||
@@ -2129,6 +2129,26 @@ suite('CopilotAgent', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('models keep an open-ended, message-only promotion', async () => {
|
||||
const agent = createTestAgent(disposables, {
|
||||
copilotClient: new TestCopilotClient([], [{
|
||||
id: 'claude-sonnet',
|
||||
name: 'Claude Sonnet',
|
||||
capabilities: { limits: { max_context_window_tokens: 200_000 } },
|
||||
// No `endsAt` and a zero discount: the promo must survive normalization.
|
||||
billing: { multiplier: 1, promo: { id: 'featured', discountPercent: 0, message: 'Now available' } },
|
||||
}]),
|
||||
});
|
||||
try {
|
||||
await agent.authenticate('https://api.github.com', 'token');
|
||||
const models = await waitForState(agent.models, models => models.length > 0);
|
||||
|
||||
assert.deepStrictEqual(models[0]._meta?.promo, { id: 'featured', discountPercent: 0, message: 'Now available' });
|
||||
} finally {
|
||||
await disposeAgent(agent);
|
||||
}
|
||||
});
|
||||
|
||||
test('configSchema emits a thinkingLevel property when the model advertises reasoning efforts', async () => {
|
||||
const agent = createTestAgent(disposables, {
|
||||
copilotClient: new TestCopilotClient([], [{
|
||||
|
||||
@@ -15,12 +15,8 @@ const PROMO_NOTIFICATION_ID = 'copilot.promoNotification';
|
||||
const DISMISSED_PROMOS_STORAGE_KEY = 'chat.dismissedPromoIds';
|
||||
|
||||
/**
|
||||
* Watches for models with active promotions and surfaces a chat input
|
||||
* notification per harness (chat session type) the first time each promo
|
||||
* appears. Each notification is scoped to the session type of the model that
|
||||
* carries the promo, so a chat input only advertises a model it can actually
|
||||
* switch to. Dismissals are persisted by promo id so the same promo is never
|
||||
* shown again.
|
||||
* Surfaces a model's promo as a chat input notification, scoped to the harness
|
||||
* (chat session type) of the model that carries it. Dismissals are persisted by promo id.
|
||||
*/
|
||||
export class ChatPromoNotificationContribution extends Disposable implements IWorkbenchContribution {
|
||||
|
||||
@@ -50,18 +46,17 @@ export class ChatPromoNotificationContribution extends Disposable implements IWo
|
||||
const dismissed = this._getDismissedPromoIds();
|
||||
const modelIds = this._languageModelsService.getLanguageModelIds();
|
||||
|
||||
// A promo can appear in several harnesses at once (e.g. the same model
|
||||
// offered in the Local, Copilot, and Codex sessions). Bucket the first
|
||||
// non-dismissed promo per harness (a model's `targetChatSessionType`,
|
||||
// or the local pool when unset).
|
||||
// Bucket one non-dismissed promo per harness (a model's `targetChatSessionType`,
|
||||
// or the local pool when unset), preferring a discounted promo over a message-only one.
|
||||
const promoByHarness = new Map<string, ILanguageModelChatMetadataAndIdentifier>();
|
||||
for (const id of modelIds) {
|
||||
const meta = this._languageModelsService.lookupLanguageModel(id);
|
||||
if (!meta || !ILanguageModelChatMetadata.hasPromoDiscount(meta) || dismissed.has(meta.promo.id)) {
|
||||
if (!meta || !ILanguageModelChatMetadata.hasPromoMessage(meta) || dismissed.has(meta.promo.id)) {
|
||||
continue;
|
||||
}
|
||||
const harness = meta.targetChatSessionType ?? localChatSessionType;
|
||||
if (!promoByHarness.has(harness)) {
|
||||
const current = promoByHarness.get(harness);
|
||||
if (!current || (!ILanguageModelChatMetadata.hasPromoDiscount(current.metadata) && ILanguageModelChatMetadata.hasPromoDiscount(meta))) {
|
||||
promoByHarness.set(harness, { identifier: id, metadata: meta });
|
||||
}
|
||||
}
|
||||
@@ -82,15 +77,12 @@ export class ChatPromoNotificationContribution extends Disposable implements IWo
|
||||
}
|
||||
this._shownNotifications.set(notificationId, { promoId: promo.id, modelIdentifier: model.identifier });
|
||||
|
||||
const endsAtDate = new Date(promo.endsAt);
|
||||
const formattedDate = endsAtDate.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
|
||||
this._chatInputNotificationService.setNotification({
|
||||
id: notificationId,
|
||||
telemetryId: promo.id,
|
||||
severity: ChatInputNotificationSeverity.Info,
|
||||
message: promo.message,
|
||||
description: localize('chat.promo.endsAt', "Ends {0}.", formattedDate),
|
||||
description: ILanguageModelChatMetadata.getPromoEndsAtLabel(promo.endsAt),
|
||||
actions: [{
|
||||
label: localize('chat.promo.tryModel', "Try {0}", model.metadata.name),
|
||||
kind: ChatInputNotificationActionKind.SwitchToModel,
|
||||
|
||||
@@ -78,9 +78,8 @@ export function getModelHoverContent(
|
||||
if (promo) {
|
||||
const promoContainer = dom.$('.chat-model-hover-promo-text');
|
||||
promoContainer.appendChild(renderIcon(Codicon.info));
|
||||
const endsAtDate = new Date(promo.endsAt);
|
||||
const formattedDate = endsAtDate.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const promoMessage = promo.message + ' ' + localize('chat.promo.endsAt', "Ends {0}.", formattedDate);
|
||||
const endsAtLabel = ILanguageModelChatMetadata.getPromoEndsAtLabel(promo.endsAt);
|
||||
const promoMessage = endsAtLabel ? promo.message + ' ' + endsAtLabel : promo.message;
|
||||
const promoMd = new MarkdownString(promoMessage, { isTrusted: false, supportThemeIcons: true });
|
||||
const rendered = disposables.add(renderMarkdown(promoMd, {
|
||||
actionHandler: link => { void openerService.open(link, { allowCommands: false, fromUserGesture: true }); },
|
||||
|
||||
@@ -86,7 +86,7 @@ export interface IChatSessionProviderOptionModelMetadata {
|
||||
readonly promo?: {
|
||||
readonly id: string;
|
||||
readonly discountPercent: number;
|
||||
readonly endsAt: string;
|
||||
readonly endsAt?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
readonly maxInputTokens?: number;
|
||||
|
||||
@@ -322,13 +322,15 @@ export interface ILanguageModelChatMetadata {
|
||||
*/
|
||||
readonly warningText?: IStringDictionary<string>;
|
||||
/**
|
||||
* Optional promotional information for this model. Positive discounts surface
|
||||
* promotional UI; non-positive discounts only feature the model in the picker.
|
||||
* Optional promotional information for this model. A positive `discountPercent`
|
||||
* surfaces the full promotional UI; `0` is a message-only promo that features the
|
||||
* model without a price change; a negative value is malformed and is ignored.
|
||||
* `endsAt` is optional — open-ended promos omit it and render no end date.
|
||||
*/
|
||||
readonly promo?: {
|
||||
readonly id: string;
|
||||
readonly discountPercent: number;
|
||||
readonly endsAt: string;
|
||||
readonly endsAt?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
}
|
||||
@@ -354,6 +356,24 @@ export namespace ILanguageModelChatMetadata {
|
||||
return !!metadata.promo && metadata.promo.discountPercent > 0;
|
||||
}
|
||||
|
||||
/** Whether the model has a promo message to surface, including message-only (0%) promos. */
|
||||
export function hasPromoMessage(metadata: ILanguageModelChatMetadata): metadata is ILanguageModelChatMetadata & { readonly promo: NonNullable<ILanguageModelChatMetadata['promo']> } {
|
||||
return !!metadata.promo && metadata.promo.discountPercent >= 0 && !!metadata.promo.message;
|
||||
}
|
||||
|
||||
/** The localized "Ends {date}." sentence, or `undefined` for a missing or unparsable end date. */
|
||||
export function getPromoEndsAtLabel(endsAt: string | undefined): string | undefined {
|
||||
if (!endsAt) {
|
||||
return undefined;
|
||||
}
|
||||
const endsAtDate = new Date(endsAt);
|
||||
if (isNaN(endsAtDate.getTime())) {
|
||||
return undefined;
|
||||
}
|
||||
const formattedDate = endsAtDate.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
return localize('chat.promo.endsAt', "Ends {0}.", formattedDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Documentation link explaining how Auto model selection works.
|
||||
* NOTE: Also defined in extensions/copilot/src/extension/conversation/common/languageModelAccess.ts — keep in sync.
|
||||
|
||||
+42
-24
@@ -56,32 +56,50 @@ suite('AgentHostLanguageModelProvider', () => {
|
||||
|
||||
test('carries picker category, price category, and promo from model metadata', async () => {
|
||||
const provider = createProvider();
|
||||
provider.updateModels([makeModel('claude-sonnet', {
|
||||
category: 'powerful',
|
||||
priceCategory: 'medium',
|
||||
promo: {
|
||||
id: 'summer-sale',
|
||||
discountPercent: 25,
|
||||
endsAt: '2026-08-01T00:00:00Z',
|
||||
message: 'Save on Claude Sonnet',
|
||||
},
|
||||
})]);
|
||||
provider.updateModels([
|
||||
makeModel('claude-sonnet', {
|
||||
category: 'powerful',
|
||||
priceCategory: 'medium',
|
||||
promo: {
|
||||
id: 'summer-sale',
|
||||
discountPercent: 25,
|
||||
endsAt: '2026-08-01T00:00:00Z',
|
||||
message: 'Save on Claude Sonnet',
|
||||
},
|
||||
}),
|
||||
// Open-ended, message-only promo: the untyped `_meta` read must keep it
|
||||
// rather than drop the promo for the missing `endsAt` / zero discount.
|
||||
makeModel('gpt-5', {
|
||||
promo: {
|
||||
id: 'featured',
|
||||
discountPercent: 0,
|
||||
message: 'Now available',
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
const metadata = (await provider.provideLanguageModelChatInfo(undefined, CancellationToken.None))[0].metadata;
|
||||
assert.deepStrictEqual({
|
||||
category: metadata.category,
|
||||
priceCategory: metadata.priceCategory,
|
||||
promo: metadata.promo,
|
||||
}, {
|
||||
category: 'powerful',
|
||||
priceCategory: 'medium',
|
||||
promo: {
|
||||
id: 'summer-sale',
|
||||
discountPercent: 25,
|
||||
endsAt: '2026-08-01T00:00:00Z',
|
||||
message: 'Save on Claude Sonnet',
|
||||
const infos = await provider.provideLanguageModelChatInfo(undefined, CancellationToken.None);
|
||||
assert.deepStrictEqual(infos.map(info => ({
|
||||
category: info.metadata.category,
|
||||
priceCategory: info.metadata.priceCategory,
|
||||
promo: info.metadata.promo,
|
||||
})), [
|
||||
{
|
||||
category: 'powerful',
|
||||
priceCategory: 'medium',
|
||||
promo: {
|
||||
id: 'summer-sale',
|
||||
discountPercent: 25,
|
||||
endsAt: '2026-08-01T00:00:00Z',
|
||||
message: 'Save on Claude Sonnet',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
category: undefined,
|
||||
priceCategory: undefined,
|
||||
promo: { id: 'featured', discountPercent: 0, message: 'Now available' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('derives the picker group from the model-id prefix, not the harness provider', async () => {
|
||||
|
||||
@@ -117,6 +117,7 @@ suite('ChatPromoNotificationContribution', () => {
|
||||
const notification = notifService.getNotification();
|
||||
assert.ok(notification, 'Expected a notification to be shown');
|
||||
assert.ok(notification.message.toString().includes('20% off'));
|
||||
assert.ok(notification.description?.toString().includes('2026'), 'Expected the end date to be rendered');
|
||||
assert.deepStrictEqual(notification.actions, [{
|
||||
label: 'Try GPT-5.5',
|
||||
kind: ChatInputNotificationActionKind.SwitchToModel,
|
||||
@@ -124,17 +125,30 @@ suite('ChatPromoNotificationContribution', () => {
|
||||
}]);
|
||||
});
|
||||
|
||||
test('does not show notification for non-positive promo discounts', () => {
|
||||
test('renders the server message for a 0% promo', () => {
|
||||
const notifService = createMockNotificationService(disposables);
|
||||
const { service: lmService } = createMockLanguageModelsService([{
|
||||
identifier: 'copilot:zero-discount',
|
||||
metadata: { name: 'Zero Discount', id: 'zero-discount', promo: { id: 'promo-zero', discountPercent: 0, endsAt: '2026-07-20T23:59:59Z', message: 'Featured model' } },
|
||||
}], disposables);
|
||||
const storageService = disposables.add(new InMemoryStorageService());
|
||||
|
||||
disposables.add(new ChatPromoNotificationContribution(
|
||||
lmService,
|
||||
notifService.service,
|
||||
storageService,
|
||||
));
|
||||
|
||||
const notification = notifService.getNotification();
|
||||
assert.ok(notification, 'Expected a notification for the 0% promo');
|
||||
assert.strictEqual(notification.message, 'Featured model');
|
||||
});
|
||||
|
||||
test('prefers a discounted promo over a 0% one in the same harness', () => {
|
||||
const notifService = createMockNotificationService(disposables);
|
||||
const { service: lmService } = createMockLanguageModelsService([
|
||||
{
|
||||
identifier: 'copilot:zero-discount',
|
||||
metadata: { name: 'Zero Discount', id: 'zero-discount', promo: { id: 'promo-zero', discountPercent: 0, endsAt: '2026-07-20T23:59:59Z', message: 'Featured model' } },
|
||||
},
|
||||
{
|
||||
identifier: 'copilot:negative-discount',
|
||||
metadata: { name: 'Negative Discount', id: 'negative-discount', promo: { id: 'promo-negative', discountPercent: -10, endsAt: '2026-07-20T23:59:59Z', message: 'Featured model' } },
|
||||
},
|
||||
{ identifier: 'copilot:featured', metadata: { name: 'Featured', id: 'featured', promo: { id: 'promo-zero', discountPercent: 0, message: 'Featured model' } } },
|
||||
{ identifier: 'copilot:discounted', metadata: { name: 'Discounted', id: 'discounted', promo: { id: 'promo-discount', discountPercent: 20, message: 'Get 20% off' } } },
|
||||
], disposables);
|
||||
const storageService = disposables.add(new InMemoryStorageService());
|
||||
|
||||
@@ -144,9 +158,51 @@ suite('ChatPromoNotificationContribution', () => {
|
||||
storageService,
|
||||
));
|
||||
|
||||
const notification = notifService.getNotification();
|
||||
assert.ok(notification);
|
||||
assert.strictEqual(notification.message, 'Get 20% off');
|
||||
});
|
||||
|
||||
test('does not show notification for negative promo discounts', () => {
|
||||
const notifService = createMockNotificationService(disposables);
|
||||
const { service: lmService } = createMockLanguageModelsService([{
|
||||
identifier: 'copilot:negative-discount',
|
||||
metadata: { name: 'Negative Discount', id: 'negative-discount', promo: { id: 'promo-negative', discountPercent: -10, endsAt: '2026-07-20T23:59:59Z', message: 'Featured model' } },
|
||||
}], disposables);
|
||||
const storageService = disposables.add(new InMemoryStorageService());
|
||||
|
||||
disposables.add(new ChatPromoNotificationContribution(
|
||||
lmService,
|
||||
notifService.service,
|
||||
storageService,
|
||||
));
|
||||
|
||||
assert.strictEqual(notifService.getNotification(), undefined);
|
||||
});
|
||||
|
||||
test('omits the end date when the promo has none', () => {
|
||||
const notifService = createMockNotificationService(disposables);
|
||||
const { service: lmService } = createMockLanguageModelsService([
|
||||
{ identifier: 'local:no-end-date', metadata: { name: 'Open Ended', id: 'no-end-date', promo: { id: 'promo-open', discountPercent: 20, message: 'Get 20% off' } } },
|
||||
{ identifier: 'copilot:bad-end-date', metadata: { name: 'Bad Date', id: 'bad-end-date', targetChatSessionType: 'copilotcli', promo: { id: 'promo-bad-date', discountPercent: 20, endsAt: 'not a date', message: 'Get 20% off' } } },
|
||||
], disposables);
|
||||
const storageService = disposables.add(new InMemoryStorageService());
|
||||
|
||||
disposables.add(new ChatPromoNotificationContribution(
|
||||
lmService,
|
||||
notifService.service,
|
||||
storageService,
|
||||
));
|
||||
|
||||
assert.deepStrictEqual(
|
||||
notifService.getAllNotifications().map(n => ({ message: n.message, description: n.description })),
|
||||
[
|
||||
{ message: 'Get 20% off', description: undefined },
|
||||
{ message: 'Get 20% off', description: undefined },
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('does not show notification for already-dismissed promo', () => {
|
||||
const notifService = createMockNotificationService(disposables);
|
||||
const { service: lmService } = createMockLanguageModelsService([{
|
||||
|
||||
+22
@@ -54,4 +54,26 @@ suite('ModelPickerHover', () => {
|
||||
{ discountPercent: -10, category: 'Powerful', badges: ['High cost'], promoText: undefined },
|
||||
]);
|
||||
});
|
||||
|
||||
test('promo hover text omits the end date when the promo has none', () => {
|
||||
const results = ['2026-07-20T23:59:59Z', 'not a date', undefined].map(endsAt => {
|
||||
const model = createModel(`promo-${endsAt}`, `Promo ${endsAt}`);
|
||||
model.metadata = {
|
||||
...model.metadata,
|
||||
promo: { id: `test-promo-${endsAt}`, discountPercent: 20, endsAt, message: 'Limited time offer' },
|
||||
} as ILanguageModelChatMetadata;
|
||||
const hover = getModelHoverContent(model, false, undefined, NullOpenerService);
|
||||
assert.ok(hover);
|
||||
disposables.add(hover.disposable);
|
||||
const promoText = hover.element.querySelector('.chat-model-hover-promo-text')?.textContent?.trim();
|
||||
// The formatted date is locale/timezone dependent, so only assert on the sentence around it.
|
||||
return promoText?.replace(/Ends .+\.$/, 'Ends <date>.');
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(results, [
|
||||
'Limited time offer Ends <date>.',
|
||||
'Limited time offer',
|
||||
'Limited time offer',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -105,8 +105,8 @@ declare module 'vscode' {
|
||||
readonly id: string;
|
||||
/** The discount percentage (e.g. 20 for 20% off). */
|
||||
readonly discountPercent: number;
|
||||
/** ISO 8601 date string indicating when the promotion ends. */
|
||||
readonly endsAt: string;
|
||||
/** ISO 8601 date string indicating when the promotion ends. Omit for open-ended promotions. */
|
||||
readonly endsAt?: string;
|
||||
/** A human-readable message about the promotion. */
|
||||
readonly message: string;
|
||||
};
|
||||
|
||||
@@ -717,7 +717,7 @@ declare module 'vscode' {
|
||||
readonly promo?: {
|
||||
readonly id: string;
|
||||
readonly discountPercent: number;
|
||||
readonly endsAt: string;
|
||||
readonly endsAt?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
readonly maxInputTokens?: number;
|
||||
|
||||
Reference in New Issue
Block a user