Skip to content

Commit ebbbc9e

Browse files
authored
Handle warning and info messages froom SDK (#333851)
* Handle warning and info messages froom SDK * fix ci
1 parent b8b400c commit ebbbc9e

5 files changed

Lines changed: 125 additions & 7 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { IAgentModelInfo } from './agent.js';
7+
import type { SessionModelInfo } from './state/protocol/state.js';
8+
9+
const DATA_RETENTION_WARNING_CODE = 'data_retention';
10+
const PENDING_DEPRECATION_WARNING_CODE = 'model_pending_deprecation';
11+
12+
type ModelMessage = { readonly code: string; readonly message: string };
13+
type ModelNoticeSource = {
14+
readonly warningText?: { readonly dataRetention?: string };
15+
readonly infoMessages?: readonly ModelMessage[];
16+
readonly warningMessages?: readonly ModelMessage[];
17+
};
18+
19+
/** Converts SDK model messages to model-picker metadata. */
20+
export function createAgentModelNoticesMeta(source: ModelNoticeSource): Record<string, unknown> | undefined {
21+
const warningText: Record<string, string> = {};
22+
const infoText: Record<string, string> = {};
23+
if (source.warningText?.dataRetention) {
24+
warningText[DATA_RETENTION_WARNING_CODE] = source.warningText.dataRetention;
25+
}
26+
for (const { code, message } of source.infoMessages ?? []) {
27+
if (message) {
28+
const target = code === PENDING_DEPRECATION_WARNING_CODE ? warningText : infoText;
29+
target[code || 'info'] = message;
30+
}
31+
}
32+
for (const { code, message } of source.warningMessages ?? []) {
33+
if (message) {
34+
warningText[code || 'warning'] = message;
35+
}
36+
}
37+
const rowWarning = source.warningMessages?.find(({ message }) => !!message)?.message
38+
?? warningText[PENDING_DEPRECATION_WARNING_CODE];
39+
const result = {
40+
...(Object.keys(warningText).length > 0 ? { warningText } : {}),
41+
...(Object.keys(infoText).length > 0 ? { infoText } : {}),
42+
...(rowWarning ? { rowWarning } : {}),
43+
};
44+
return Object.keys(result).length > 0 ? result : undefined;
45+
}
46+
47+
/** Reads model-picker messages from Agent Host metadata. */
48+
export function readAgentModelNoticesMeta(model: IAgentModelInfo | SessionModelInfo) {
49+
const meta = model._meta;
50+
return {
51+
warningText: asStringDictionary(meta?.warningText),
52+
infoText: asStringDictionary(meta?.infoText),
53+
rowWarning: typeof meta?.rowWarning === 'string' && meta.rowWarning.length > 0 ? meta.rowWarning : undefined,
54+
};
55+
}
56+
57+
function asStringDictionary(value: unknown): Record<string, string> | undefined {
58+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
59+
return undefined;
60+
}
61+
const entries = Object.entries(value).filter((entry): entry is [string, string] =>
62+
entry[0].length > 0 && typeof entry[1] === 'string' && entry[1].length > 0);
63+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
64+
}

src/vs/platform/agentHost/node/copilot/copilotAgent.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { IAgentHostCheckpointService } from '../../common/agentHostCheckpointSer
3636
import type { IAgentHostClientTelemetryContext } from '../../common/agentHostTelemetry.js';
3737
import { IAgentHostReviewService } from '../../common/agentHostReviewService.js';
3838
import { createPricingMetaFromBilling, hasLongContextSurcharge, normalizeCAPIBilling, type ICAPIModelBilling } from '../../common/agentModelPricing.js';
39+
import { createAgentModelNoticesMeta } from '../../common/agentModelNotices.js';
3940
import { createAgentModelByokMeta } from '../../common/agentModelByokMeta.js';
4041
import { AgentHostConfigKey, agentHostCustomizationConfigSchema, DEFAULT_SESSION_CUSTOMIZATION_DISCOVERY_MODE, toContainerCustomization } from '../../common/agentHostCustomizationConfig.js';
4142
import { CopilotCliConfigKey, CopilotCliVSCodeAssignmentContextKey, copilotCliConfigSchema, DEFAULT_COPILOT_RUBBER_DUCK_ENABLED, type CopilotSdkLogLevelSetting } from '../../common/copilotCliConfig.js';
@@ -2296,11 +2297,10 @@ export class CopilotAgent extends Disposable implements IAgent {
22962297
};
22972298
}
22982299

2299-
/**
2300-
* Builds the open `_meta` model picker bag from the SDK's billing and picker metadata.
2301-
*/
23022300
private _createModelPickerMeta(modelInfo: CopilotModelInfo, billing: ICAPIModelBilling | undefined): Record<string, unknown> | undefined {
2303-
return createPricingMetaFromBilling(billing, modelInfo.modelPickerPriceCategory, modelInfo.modelPickerCategory);
2301+
const pricing = createPricingMetaFromBilling(billing, modelInfo.modelPickerPriceCategory, modelInfo.modelPickerCategory);
2302+
const notices = isAutoModel(modelInfo.id) ? undefined : createAgentModelNoticesMeta(modelInfo);
2303+
return pricing || notices ? { ...pricing, ...notices } : undefined;
23042304
}
23052305

23062306
private _createModelConfigSchema(m: CopilotModelInfo, billing: ICAPIModelBilling | undefined): ConfigSchema | undefined {

src/vs/platform/agentHost/test/node/copilotAgent.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,9 @@ interface ITestCopilotModelInfo {
429429
readonly billing?: CopilotModelInfo['billing'];
430430
readonly modelPickerCategory?: CopilotModelInfo['modelPickerCategory'];
431431
readonly modelPickerPriceCategory?: CopilotModelInfo['modelPickerPriceCategory'];
432+
readonly warningText?: CopilotModelInfo['warningText'];
433+
readonly infoMessages?: CopilotModelInfo['infoMessages'];
434+
readonly warningMessages?: CopilotModelInfo['warningMessages'];
432435
readonly supportedReasoningEfforts?: CopilotModelInfo['supportedReasoningEfforts'];
433436
}
434437

@@ -469,6 +472,9 @@ function toSdkModelInfo(model: ITestCopilotModelInfo): CopilotModelInfo {
469472
...(model.billing ? { billing: model.billing } : {}),
470473
...(model.modelPickerCategory ? { modelPickerCategory: model.modelPickerCategory } : {}),
471474
...(model.modelPickerPriceCategory ? { modelPickerPriceCategory: model.modelPickerPriceCategory } : {}),
475+
...(model.warningText ? { warningText: model.warningText } : {}),
476+
...(model.infoMessages ? { infoMessages: model.infoMessages } : {}),
477+
...(model.warningMessages ? { warningMessages: model.warningMessages } : {}),
472478
...(model.supportedReasoningEfforts ? { supportedReasoningEfforts: model.supportedReasoningEfforts } : {}),
473479
};
474480
}
@@ -4853,7 +4859,7 @@ suite('CopilotAgent', () => {
48534859
}
48544860
});
48554861

4856-
test('models include picker and promo metadata when the SDK provides it', async () => {
4862+
test('models include picker, notice, and promo metadata when the SDK provides it', async () => {
48574863
const agent = createTestAgent(disposables, {
48584864
copilotClient: new TestCopilotClient([], [{
48594865
id: 'claude-sonnet',
@@ -4878,6 +4884,16 @@ suite('CopilotAgent', () => {
48784884
},
48794885
modelPickerCategory: 'powerful',
48804886
modelPickerPriceCategory: 'medium',
4887+
warningText: {
4888+
dataRetention: 'Prompts are retained for 30 days.',
4889+
},
4890+
infoMessages: [
4891+
{ code: 'model_pending_deprecation', message: 'Claude Sonnet will be retired soon.' },
4892+
{ code: 'model_relocated', message: 'Claude Sonnet now serves from a new region.' },
4893+
],
4894+
warningMessages: [
4895+
{ code: 'model_degraded', message: 'Claude Sonnet is currently degraded.' },
4896+
],
48814897
}]),
48824898
});
48834899
try {
@@ -4894,6 +4910,15 @@ suite('CopilotAgent', () => {
48944910
longContextOutputCost: 22.5,
48954911
priceCategory: 'medium',
48964912
category: 'powerful',
4913+
warningText: {
4914+
data_retention: 'Prompts are retained for 30 days.',
4915+
model_pending_deprecation: 'Claude Sonnet will be retired soon.',
4916+
model_degraded: 'Claude Sonnet is currently degraded.',
4917+
},
4918+
infoText: {
4919+
model_relocated: 'Claude Sonnet now serves from a new region.',
4920+
},
4921+
rowWarning: 'Claude Sonnet is currently degraded.',
48974922
promo: {
48984923
id: 'summer-sale',
48994924
discountPercent: 25,

src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLanguageModelProvider.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
7+
import { Codicon } from '../../../../../../base/common/codicons.js';
78
import { Emitter } from '../../../../../../base/common/event.js';
89
import { Disposable } from '../../../../../../base/common/lifecycle.js';
910
import { localize } from '../../../../../../nls.js';
11+
import { readAgentModelNoticesMeta } from '../../../../../../platform/agentHost/common/agentModelNotices.js';
1012
import { ConfigSchema, SessionModelInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js';
1113
import { readAgentModelPricingMeta } from '../../../../../../platform/agentHost/common/agentModelPricing.js';
1214
import { readAgentModelByokIdentifier } from '../../../../../../platform/agentHost/common/agentModelByokMeta.js';
@@ -68,16 +70,17 @@ export class AgentHostLanguageModelProvider extends Disposable implements ILangu
6870
const multiplierNumeric = pricing.multiplierNumeric;
6971
// "Auto" advertises the auto-mode discount (detail) + description (tooltip). microsoft/vscode#321778, #321659.
7072
const isAuto = m.id === AUTO_RAW_MODEL_ID;
73+
const notices = isAuto ? undefined : readAgentModelNoticesMeta(m);
7174
const discountPercent = pricing.discountPercent;
7275
// Guard against a non-finite or out-of-range value from the open `_meta` bag so we never render
7376
// nonsense like "Infinity% discount"; the documented range is a whole number in (0, 100].
7477
const hasDiscount = typeof discountPercent === 'number' && discountPercent > 0 && discountPercent <= 100;
7578
const detail = isAuto && hasDiscount
7679
? localize('agentHost.auto.discount', "{0}% discount", discountPercent)
7780
: undefined;
78-
const tooltip = isAuto
81+
const tooltip = notices?.rowWarning ?? (isAuto
7982
? ILanguageModelChatMetadata.getAutoModelDescription(hasDiscount ? discountPercent : undefined)
80-
: undefined;
83+
: undefined);
8184
const modelGroup = this._modelGroupFor(m);
8285
const byokModelIdentifier = readAgentModelByokIdentifier(m);
8386
return {
@@ -95,6 +98,9 @@ export class AgentHostLanguageModelProvider extends Disposable implements ILangu
9598
maxOutputTokens: m.maxOutputTokens ?? 0,
9699
isDefaultForLocation: {},
97100
isUserSelectable: true,
101+
statusIcon: notices?.rowWarning ? Codicon.warning : undefined,
102+
warningText: notices?.warningText,
103+
infoText: notices?.infoText,
98104
pricing: multiplierNumeric !== undefined ? `${multiplierNumeric}x` : undefined,
99105
multiplierNumeric,
100106
inputCost: pricing.inputCost,

src/vs/workbench/contrib/chat/test/browser/agentSessions/agentHostLanguageModelProvider.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import assert from 'assert';
77
import { CancellationToken } from '../../../../../../base/common/cancellation.js';
8+
import { Codicon } from '../../../../../../base/common/codicons.js';
89
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
910
import { SessionModelInfo } from '../../../../../../platform/agentHost/common/state/sessionState.js';
1011
import { ILanguageModelChatMetadata } from '../../../common/languageModels.js';
@@ -136,6 +137,28 @@ suite('AgentHostLanguageModelProvider', () => {
136137
]);
137138
});
138139

140+
test('carries model notices and flags row warnings', async () => {
141+
const provider = createProvider();
142+
provider.updateModels([makeModel('gpt-5', {
143+
warningText: { model_degraded: 'GPT-5 is currently degraded.' },
144+
infoText: { model_relocated: 'GPT-5 now serves from a new region.' },
145+
rowWarning: 'GPT-5 is currently degraded.',
146+
})]);
147+
148+
const metadata = (await provider.provideLanguageModelChatInfo(undefined, CancellationToken.None))[0].metadata;
149+
assert.deepStrictEqual({
150+
tooltip: metadata.tooltip,
151+
statusIcon: metadata.statusIcon?.id,
152+
warningText: metadata.warningText,
153+
infoText: metadata.infoText,
154+
}, {
155+
tooltip: 'GPT-5 is currently degraded.',
156+
statusIcon: Codicon.warning.id,
157+
warningText: { model_degraded: 'GPT-5 is currently degraded.' },
158+
infoText: { model_relocated: 'GPT-5 now serves from a new region.' },
159+
});
160+
});
161+
139162
test('derives the picker group from the model-id prefix, not the harness provider', async () => {
140163
const provider = createProvider();
141164
// The agent host reports every model under the harness provider (`copilotcli`);

0 commit comments

Comments
 (0)