src/vs/platform/agentHost/common/agentModelPricing.ts

275 LOC · 243 covered · 32 uncovered · 30 ranges · 792 concepts · 10 introducers · 384 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

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 related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- agentModelPricing.ts ×7
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 { SessionModelInfo } from './state/protocol/state.js';
7 > import type { IAgentModelInfo } from './agentService.js';
8 >
9 > /**
10 > * Well-known model picker metadata carried under a model's open `_meta` bag (see {@link IAgentModelInfo._meta} /
11 > * {@link SessionModelInfo._meta}). Agents populate these keys so the chat model picker can render pricing,
12 > * capability categories, and promotions.
13 > *
14 > * All cost values are expressed as credits per 1M tokens — the same unit the model picker hover renders (see
15 > * `getModelHoverContent` in `modelPicker/modelPickerHover.ts`). Fields are optional; agents omit what they don't know.
16 > */
17 > export interface IAgentModelPricingMeta {
18 > /** Request multiplier (e.g. `1.5` rendered as "1.5x"). */
19 > readonly multiplierNumeric?: number;
20 > /** Default-tier input cost in credits per 1M tokens. */
21 > readonly inputCost?: number;
22 > /** Default-tier cached-input (read) cost in credits per 1M tokens. */
23 > readonly cacheCost?: number;
24 > /** Default-tier cache-write cost in credits per 1M tokens. */
25 > readonly cacheWriteCost?: number;
26 > /** Default-tier output cost in credits per 1M tokens. */
27 > readonly outputCost?: number;
28 > /** Long-context-tier input cost in credits per 1M tokens. */
29 > readonly longContextInputCost?: number;
30 > /** Long-context-tier cached-input (read) cost in credits per 1M tokens. */
31 > readonly longContextCacheCost?: number;
32 > /** Long-context-tier cache-write cost in credits per 1M tokens. */
33 > readonly longContextCacheWriteCost?: number;
34 > /** Long-context-tier output cost in credits per 1M tokens. */
35 > readonly longContextOutputCost?: number;
36 > /** Coarse price bucket (e.g. `low`, `medium`, `high`) for an at-a-glance tag. */
37 > readonly priceCategory?: string;
38 > /** Capability category (e.g. `lightweight`, `versatile`, `powerful`) shown in the model picker hover. */
39 > readonly category?: string;
40 > /** Whole-number percentage discount (0-100) for the synthetic `auto` model; shown as a "{n}% discount" detail. */
41 > readonly discountPercent?: number;
42 > /** Promotional information when the model is experiencing a discount. */
43 > readonly promo?: {
44 > readonly id: string;
45 > readonly discountPercent: number;
46 > readonly endsAt: string;
47 > readonly message: string;
48 > };
49 > }
50 >
51 > const NUMBER_KEYS = [
52 > 'multiplierNumeric',
53 > 'inputCost',
54 > 'cacheCost',
55 > 'cacheWriteCost',
56 > 'outputCost',
57 > 'longContextInputCost',
58 > 'longContextCacheCost',
59 > 'longContextCacheWriteCost',
60 > 'longContextOutputCost',
61 > 'discountPercent',
62 > ] as const satisfies readonly (keyof IAgentModelPricingMeta)[];
63 >
64 > /**
65 > * Reads the well-known {@link IAgentModelPricingMeta} keys from a model's open `_meta` bag, ignoring any unrelated
66 > * provider-specific keys and values of the wrong type. Returns an object containing only the keys that were present
67 > * with a valid value.
68 > */
69 > export function readAgentModelPricingMeta(model: IAgentModelInfo | SessionModelInfo): IAgentModelPricingMeta {
70 const meta = model._meta;
71 if (!meta) {
72 return {};
73 }
74 const result: { -readonly [K in keyof IAgentModelPricingMeta]: IAgentModelPricingMeta[K] } = {};
75 for (const key of NUMBER_KEYS) {
76 const value = meta[key];
77 if (typeof value === 'number') {
78 result[key] = value;
79 }
80 }
81 if (typeof meta.priceCategory === 'string') {
82 result.priceCategory = meta.priceCategory;
83 }
84 if (typeof meta.category === 'string') {
85 result.category = meta.category;
86 }
87 const rawPromo = meta.promo;
88 if (rawPromo && typeof rawPromo === 'object' && !Array.isArray(rawPromo)) {
89 const p = rawPromo as Record<string, unknown>;
90 if (typeof p.id === 'string' && typeof p.discountPercent === 'number' && typeof p.endsAt === 'string' && typeof p.message === 'string') {
91 result.promo = { id: p.id, discountPercent: p.discountPercent, endsAt: p.endsAt, message: p.message };
92 }
93 }
94 return result;
95 }
97 > /**
98 > * Builds a `_meta` payload from {@link IAgentModelPricingMeta}, dropping `undefined` entries. Returns `undefined` when
99 > * no model picker fields are known so callers can avoid attaching an empty `_meta` object.
100 > */
101 > export function createAgentModelPricingMeta(pricing: IAgentModelPricingMeta): Record<string, unknown> | undefined {
102 > const entries = Object.entries(pricing).filter(([, value]) => value !== undefined); agentModelPricing.ts ×7
103 > return entries.length > 0 ? Object.fromEntries(entries) : undefined;
104 > }
106 > /**
107 > * Normalizes a raw CAPI or Copilot SDK billing payload into the camelCase
108 > * {@link ICAPIModelBilling} shape that {@link createPricingMetaFromBilling} expects.
109 > * Prices are converted from the payload's billing batch to credits per million tokens.
110 > */
111 > export function normalizeCAPIBilling(raw: unknown): ICAPIModelBilling | undefined {
112 > if (!raw || typeof raw !== 'object') { agentModelPricing.ts ×7
113 > return undefined; agentModelPricing.ts ×1
114 > }
115 > const billing = raw as Record<string, unknown>; agentModelPricing.ts ×6
116 > const multiplier = typeof billing.multiplier === 'number' ? billing.multiplier : undefined; agentModelPricing.ts ×7
117 > const priceCategory = typeof billing.priceCategory === 'string' ? billing.priceCategory
118 > : typeof (billing as Record<string, unknown>).price_category === 'string' ? (billing as Record<string, unknown>).price_category as string agentModelPricing.ts ×1
119 > : undefined;
120 > const discountPercent = typeof billing.discountPercent === 'number' ? billing.discountPercent agentModelPricing.ts ×7
121 > : typeof (billing as Record<string, unknown>).discount_percent === 'number' ? (billing as Record<string, unknown>).discount_percent as number agentModelPricing.ts ×6
122 > : undefined;
124 > // Resolve token prices: prefer camelCase `tokenPrices`, fall back to snake_case `token_prices`.
125 > const rawTokenPrices = (billing.tokenPrices ?? billing.token_prices) as Record<string, unknown> | undefined;
126 > let tokenPrices: ICAPIModelBilling['tokenPrices'] = undefined;
127 > if (rawTokenPrices && typeof rawTokenPrices === 'object') {
128 > // The CAPI snake_case format nests prices under `default` / `long_context` tiers; agentModelPricing.ts ×3
129 > // the camelCase format flattens them at the top level of `tokenPrices`.
130 > const defaultTier = rawTokenPrices.default as Record<string, unknown> | undefined;
131 > const hasDefault = defaultTier && typeof defaultTier === 'object';
132 > const batchSize = asNumber(rawTokenPrices.batchSize) ?? asNumber(rawTokenPrices.batch_size) ?? 1_000_000;
133 > const scale = batchSize > 0 ? 1_000_000 / batchSize : 1;
134 > const price = (...values: unknown[]): number | undefined => {
135 > const value = values.map(asNumber).find(candidate => candidate !== undefined);
136 > return value === undefined ? undefined : value * scale;
137 > };
138 >
139 > const inputPrice = price(rawTokenPrices.inputPrice, hasDefault ? defaultTier.input_price : undefined);
140 > const cachePrice = price(rawTokenPrices.cacheReadPrice, rawTokenPrices.cachePrice, hasDefault ? defaultTier.cache_read_price : undefined, hasDefault ? defaultTier.cache_price : undefined);
141 > const cacheWritePrice = price(rawTokenPrices.cacheWritePrice, hasDefault ? defaultTier.cache_write_price : undefined);
142 > const outputPrice = price(rawTokenPrices.outputPrice, hasDefault ? defaultTier.output_price : undefined);
143 > const contextMax = asNumber(rawTokenPrices.maxPromptTokens) ?? asNumber(rawTokenPrices.contextMax) ?? asNumber(hasDefault ? defaultTier.max_prompt_tokens : undefined) ?? asNumber(hasDefault ? defaultTier.context_max : undefined);
144 >
145 > const rawLong = (rawTokenPrices.longContext ?? rawTokenPrices.long_context) as Record<string, unknown> | undefined;
146 > let longContext: { readonly contextMax?: number; readonly inputPrice?: number; readonly cachePrice?: number; readonly cacheWritePrice?: number; readonly outputPrice?: number } | undefined;
147 > if (rawLong && typeof rawLong === 'object') {
148 > longContext = {
149 > inputPrice: price(rawLong.inputPrice, rawLong.input_price),
150 > cachePrice: price(rawLong.cacheReadPrice, rawLong.cachePrice, rawLong.cache_read_price, rawLong.cache_price),
151 > cacheWritePrice: price(rawLong.cacheWritePrice, rawLong.cache_write_price),
152 > outputPrice: price(rawLong.outputPrice, rawLong.output_price),
153 > contextMax: asNumber(rawLong.maxPromptTokens) ?? asNumber(rawLong.contextMax) ?? asNumber(rawLong.max_prompt_tokens) ?? asNumber(rawLong.context_max),
154 > };
155 > }
156 >
157 > tokenPrices = { inputPrice, cachePrice, cacheWritePrice, outputPrice, contextMax, longContext };
158 > }
160 > return { multiplier, priceCategory, discountPercent, promo: normalizePromo(billing), tokenPrices };
161 > }
163 > function asNumber(v: unknown): number | undefined { agentModelPricing.ts ×3
164 > return typeof v === 'number' ? v : undefined;
165 > }
167 > function normalizePromo(billing: Record<string, unknown>): ICAPIModelBilling['promo'] { agentModelPricing.ts ×6
168 > const raw = billing.promo as Record<string, unknown> | undefined;
169 > if (!raw || typeof raw !== 'object') {
170 > return undefined; agentModelPricing.ts ×1
171 > }
172 > const id = typeof raw.id === 'string' ? raw.id : undefined; agentModelPricing.ts ×6
173 > const discountPercent = asNumber(raw.discountPercent) ?? asNumber(raw.discount_percent);
174 > const endsAt = typeof raw.endsAt === 'string' ? raw.endsAt
175 : typeof raw.ends_at === 'string' ? raw.ends_at
176 : undefined;
177 > const message = typeof raw.message === 'string' ? raw.message : undefined; agentModelPricing.ts ×6
178 > if (id && typeof discountPercent === 'number' && endsAt && message) {
179 > return { id, discountPercent, endsAt, message }; agentModelPricing.ts ×1
180 > }
181 return undefined;
182 }
184 > /**
185 > * Normalized model billing shape shared by CAPI-backed agents and the Copilot SDK model list.
186 > * Raw snake_case and current SDK fields are converted at the read boundary by {@link normalizeCAPIBilling}.
187 > */
188 > export interface ICAPIModelBilling {
189 > readonly multiplier?: number;
190 > /** Coarse price bucket surfaced as a tag in the model picker hover. */
191 > readonly priceCategory?: string;
192 > /** Whole-number percentage discount (0-100) for the synthetic `auto` model; rendered as a "{n}% discount" detail. */
193 > readonly discountPercent?: number;
194 > /** Promotional info when the model is experiencing a promotional discount. */
195 > readonly promo?: {
196 > readonly id: string;
197 > readonly discountPercent: number;
198 > readonly endsAt: string;
199 > readonly message: string;
200 > };
201 > readonly tokenPrices?: {
202 > readonly contextMax?: number;
203 > readonly inputPrice?: number;
204 > readonly cachePrice?: number;
205 > readonly cacheWritePrice?: number;
206 > readonly outputPrice?: number;
207 > readonly longContext?: {
208 > readonly contextMax?: number;
209 > readonly inputPrice?: number;
210 > readonly cachePrice?: number;
211 > readonly cacheWritePrice?: number;
212 > readonly outputPrice?: number;
213 > };
214 > };
215 > }
216 >
217 > /**
218 > * Converts a CAPI model's billing payload into an {@link IAgentModelPricingMeta} `_meta` bag. Long-context costs are
219 > * only emitted when there is an actual surcharge (at least one long-context price differs from the default tier).
220 > * When emitting, any missing long-context field falls back to the default-tier value so the hover table renders
221 > * complete rows. See {@link hasLongContextSurcharge} for the surcharge detection logic.
222 > *
223 > * @param billing - The model's billing info, narrowed through {@link ICAPIModelBilling}.
224 > * @param priceCategory - An optional override for the price category (e.g. from `modelPickerPriceCategory` on the
225 > * model object itself). Falls back to `billing.priceCategory` when not provided.
226 > * @param category - The model's capability category from its top-level `modelPickerCategory` field.
227 > */
228 > export function createPricingMetaFromBilling(billing: ICAPIModelBilling | undefined, priceCategory?: string, category?: string): Record<string, unknown> | undefined {
229 > const tokenPrices = billing?.tokenPrices; agentModelPricing.ts ×7
230 > const longContext = tokenPrices?.longContext;
231 >
232 > // Only emit long-context costs when there is an actual surcharge (at least
233 > // one price differs from default). When emitting, fall back to the default-
234 > // tier value for any field the long-context tier does not specify so the
235 > // hover table renders complete rows without gaps.
236 > const showLongContext = longContext !== undefined && (
237 > (longContext.inputPrice !== undefined && longContext.inputPrice !== tokenPrices?.inputPrice) || agentModelPricing.ts ×3
238 > (longContext.outputPrice !== undefined && longContext.outputPrice !== tokenPrices?.outputPrice) || agentModelPricing.ts ×1
239 > (longContext.cachePrice !== undefined && longContext.cachePrice !== tokenPrices?.cachePrice) ||
240 > (longContext.cacheWritePrice !== undefined && longContext.cacheWritePrice !== tokenPrices?.cacheWritePrice)
242 >
243 > return createAgentModelPricingMeta({
244 > multiplierNumeric: typeof billing?.multiplier === 'number' ? billing.multiplier : undefined,
245 > inputCost: tokenPrices?.inputPrice,
246 > cacheCost: tokenPrices?.cachePrice,
247 > cacheWriteCost: tokenPrices?.cacheWritePrice,
248 > outputCost: tokenPrices?.outputPrice,
249 > longContextInputCost: showLongContext ? (longContext.inputPrice ?? tokenPrices?.inputPrice) : undefined,
250 > longContextCacheCost: showLongContext ? (longContext.cachePrice ?? tokenPrices?.cachePrice) : undefined,
251 > longContextCacheWriteCost: showLongContext ? (longContext.cacheWritePrice ?? tokenPrices?.cacheWritePrice) : undefined,
252 > longContextOutputCost: showLongContext ? (longContext.outputPrice ?? tokenPrices?.outputPrice) : undefined,
253 > priceCategory: priceCategory ?? (typeof billing?.priceCategory === 'string' ? billing.priceCategory : undefined),
254 > category,
255 > discountPercent: typeof billing?.discountPercent === 'number' ? billing.discountPercent : undefined,
256 > promo: billing?.promo,
257 > });
258 > }
260 > /**
261 > * Whether the model's long-context tier has any cost that differs from its default tier.
262 > * Used to decide whether to show a context-size picker (surcharge → user opts in) or to
263 > * silently use the full context window for free.
264 > */
265 > export function hasLongContextSurcharge(billing: ICAPIModelBilling | undefined): boolean {
266 > const tokenPrices = billing?.tokenPrices; agentModelPricing.ts ×2
267 > const longContext = tokenPrices?.longContext;
268 > if (!longContext) {
269 return false;
270 }
271 > return (longContext.inputPrice !== undefined && longContext.inputPrice !== tokenPrices?.inputPrice) agentModelPricing.ts ×2
272 > || (longContext.outputPrice !== undefined && longContext.outputPrice !== tokenPrices?.outputPrice)
273 > || (longContext.cachePrice !== undefined && longContext.cachePrice !== tokenPrices?.cachePrice)
274 > || (longContext.cacheWritePrice !== undefined && longContext.cacheWritePrice !== tokenPrices?.cacheWritePrice);
275 > }