@jupyterlite/ai 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/lib/agent.d.ts +5 -1
  2. package/lib/agent.js +53 -7
  3. package/lib/chat-model.js +8 -2
  4. package/lib/components/index.d.ts +1 -1
  5. package/lib/components/index.js +1 -1
  6. package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
  7. package/lib/components/usage-display.js +109 -0
  8. package/lib/index.js +5 -5
  9. package/lib/models/settings-model.js +1 -0
  10. package/lib/providers/built-in-providers.js +5 -0
  11. package/lib/providers/generated-context-windows.d.ts +8 -0
  12. package/lib/providers/generated-context-windows.js +96 -0
  13. package/lib/providers/model-info.d.ts +3 -0
  14. package/lib/providers/model-info.js +58 -0
  15. package/lib/tokens.d.ts +21 -0
  16. package/lib/tokens.js +7 -7
  17. package/lib/widgets/ai-settings.js +9 -0
  18. package/lib/widgets/main-area-chat.js +3 -3
  19. package/lib/widgets/provider-config-dialog.js +18 -5
  20. package/package.json +3 -2
  21. package/schema/settings-model.json +11 -0
  22. package/src/agent.ts +79 -7
  23. package/src/chat-model.ts +7 -4
  24. package/src/components/index.ts +1 -1
  25. package/src/components/usage-display.tsx +208 -0
  26. package/src/index.ts +5 -9
  27. package/src/models/settings-model.ts +1 -0
  28. package/src/providers/built-in-providers.ts +5 -0
  29. package/src/providers/generated-context-windows.ts +102 -0
  30. package/src/providers/model-info.ts +88 -0
  31. package/src/tokens.ts +33 -7
  32. package/src/widgets/ai-settings.tsx +42 -0
  33. package/src/widgets/main-area-chat.ts +3 -3
  34. package/src/widgets/provider-config-dialog.tsx +45 -5
  35. package/lib/components/token-usage-display.js +0 -72
  36. package/src/components/token-usage-display.tsx +0 -137
package/lib/agent.d.ts CHANGED
@@ -160,9 +160,13 @@ export declare class AgentManager implements IAgentManager {
160
160
  */
161
161
  generateResponse(message: string): Promise<void>;
162
162
  /**
163
- * Updates token usage statistics.
163
+ * Updates cumulative token usage statistics from a completed model step.
164
164
  */
165
165
  private _updateTokenUsage;
166
+ /**
167
+ * Gets the configured context window for the active provider.
168
+ */
169
+ private _getActiveContextWindow;
166
170
  /**
167
171
  * Initializes the AI agent with current settings and tools.
168
172
  * Sets up the agent with model configuration, tools, and MCP tools.
package/lib/agent.js CHANGED
@@ -3,6 +3,7 @@ import { PromiseDelegate } from '@lumino/coreutils';
3
3
  import { Signal } from '@lumino/signaling';
4
4
  import { ToolLoopAgent, stepCountIs } from 'ai';
5
5
  import { createModel } from './providers/models';
6
+ import { getEffectiveContextWindow } from './providers/model-info';
6
7
  import { createProviderTools } from './providers/provider-tools';
7
8
  import { SECRETS_NAMESPACE } from './tokens';
8
9
  /**
@@ -256,7 +257,14 @@ export class AgentManager {
256
257
  return this._activeProvider;
257
258
  }
258
259
  set activeProvider(value) {
260
+ const previousProvider = this._activeProvider;
259
261
  this._activeProvider = value;
262
+ // Reset request-level context estimate only when switching between providers.
263
+ if (previousProvider && previousProvider !== value) {
264
+ this._tokenUsage.lastRequestInputTokens = undefined;
265
+ }
266
+ this._tokenUsage.contextWindow = this._getActiveContextWindow();
267
+ this._tokenUsageChanged.emit(this._tokenUsage);
260
268
  this.initializeAgent();
261
269
  this._activeProviderChanged.emit(this._activeProvider);
262
270
  }
@@ -315,7 +323,11 @@ export class AgentManager {
315
323
  await this._streaming.promise;
316
324
  // Clear history and token usage
317
325
  this._history = [];
318
- this._tokenUsage = { inputTokens: 0, outputTokens: 0 };
326
+ this._tokenUsage = {
327
+ inputTokens: 0,
328
+ outputTokens: 0,
329
+ contextWindow: this._getActiveContextWindow()
330
+ };
319
331
  this._tokenUsageChanged.emit(this._tokenUsage);
320
332
  }
321
333
  /**
@@ -420,9 +432,20 @@ export class AgentManager {
420
432
  abortSignal: this._controller.signal
421
433
  });
422
434
  const streamResult = await this._processStreamResult(result);
423
- // Get response messages and update token usage
435
+ if (streamResult.aborted) {
436
+ try {
437
+ const responseMessages = await result.response;
438
+ if (responseMessages.messages?.length) {
439
+ this._history.push(...Private.sanitizeModelMessages(responseMessages.messages));
440
+ }
441
+ }
442
+ catch {
443
+ // Aborting before a step finishes leaves no completed response to persist.
444
+ }
445
+ break;
446
+ }
447
+ // Get response messages for completed steps.
424
448
  const responseMessages = await result.response;
425
- this._updateTokenUsage(await result.usage);
426
449
  // Add response messages to history
427
450
  if (responseMessages.messages?.length) {
428
451
  responseHistory.push(...responseMessages.messages);
@@ -462,14 +485,25 @@ export class AgentManager {
462
485
  }
463
486
  }
464
487
  /**
465
- * Updates token usage statistics.
488
+ * Updates cumulative token usage statistics from a completed model step.
466
489
  */
467
- _updateTokenUsage(usage) {
490
+ _updateTokenUsage(usage, lastRequestInputTokens) {
491
+ const contextWindow = this._getActiveContextWindow();
492
+ const estimatedRequestInputTokens = lastRequestInputTokens ?? usage?.inputTokens;
468
493
  if (usage) {
469
494
  this._tokenUsage.inputTokens += usage.inputTokens ?? 0;
470
495
  this._tokenUsage.outputTokens += usage.outputTokens ?? 0;
471
- this._tokenUsageChanged.emit(this._tokenUsage);
472
496
  }
497
+ this._tokenUsage.lastRequestInputTokens = estimatedRequestInputTokens;
498
+ this._tokenUsage.contextWindow = contextWindow;
499
+ this._tokenUsageChanged.emit(this._tokenUsage);
500
+ }
501
+ /**
502
+ * Gets the configured context window for the active provider.
503
+ */
504
+ _getActiveContextWindow() {
505
+ const activeProviderConfig = this._settingsModel.getProvider(this._activeProvider);
506
+ return getEffectiveContextWindow(activeProviderConfig, this._providerRegistry);
473
507
  }
474
508
  /**
475
509
  * Initializes the AI agent with current settings and tools.
@@ -521,6 +555,9 @@ export class AgentManager {
521
555
  const activeProviderInfo = activeProviderConfig && this._providerRegistry
522
556
  ? this._providerRegistry.getProviderInfo(activeProviderConfig.provider)
523
557
  : null;
558
+ const contextWindow = getEffectiveContextWindow(activeProviderConfig, this._providerRegistry);
559
+ this._tokenUsage.contextWindow = contextWindow;
560
+ this._tokenUsageChanged.emit(this._tokenUsage);
524
561
  const temperature = activeProviderConfig?.parameters?.temperature ?? DEFAULT_TEMPERATURE;
525
562
  const maxTokens = activeProviderConfig?.parameters?.maxOutputTokens;
526
563
  const maxTurns = activeProviderConfig?.parameters?.maxTurns ?? DEFAULT_MAX_TURNS;
@@ -599,7 +636,10 @@ ${richOutputWorkflowInstruction}`;
599
636
  async _processStreamResult(result) {
600
637
  let fullResponse = '';
601
638
  let currentMessageId = null;
602
- const processResult = { approvalProcessed: false };
639
+ const processResult = {
640
+ approvalProcessed: false,
641
+ aborted: false
642
+ };
603
643
  for await (const part of result.fullStream) {
604
644
  switch (part.type) {
605
645
  case 'text-delta':
@@ -654,6 +694,12 @@ ${richOutputWorkflowInstruction}`;
654
694
  }
655
695
  await this._handleApprovalRequest(part, processResult);
656
696
  break;
697
+ case 'finish-step':
698
+ this._updateTokenUsage(part.usage, part.usage.inputTokens);
699
+ break;
700
+ case 'abort':
701
+ processResult.aborted = true;
702
+ break;
657
703
  // Ignore: text-start, text-end, finish, error, and others
658
704
  default:
659
705
  break;
package/lib/chat-model.js CHANGED
@@ -258,7 +258,7 @@ export class AIChatModel extends AbstractChatModel {
258
258
  return false;
259
259
  }
260
260
  const contentModel = await this._contentsManager
261
- .get(filepath, { content: true })
261
+ .get(filepath, { content: true, type: 'file', format: 'text' })
262
262
  .catch(() => {
263
263
  if (!silent) {
264
264
  console.log(`There is no backup for chat '${this.name}'`);
@@ -268,7 +268,13 @@ export class AIChatModel extends AbstractChatModel {
268
268
  if (!contentModel) {
269
269
  return false;
270
270
  }
271
- const content = JSON.parse(contentModel.content);
271
+ let content;
272
+ try {
273
+ content = JSON.parse(contentModel.content);
274
+ }
275
+ catch (e) {
276
+ throw `Error when parsing the chat ${filepath}\n${e}`;
277
+ }
272
278
  if (content.metadata?.provider) {
273
279
  if (this._settingsModel.getProvider(content.metadata.provider)) {
274
280
  this._agentManager.activeProvider = content.metadata.provider;
@@ -2,5 +2,5 @@ export * from './clear-button';
2
2
  export * from './completion-status';
3
3
  export * from './model-select';
4
4
  export * from './stop-button';
5
- export * from './token-usage-display';
5
+ export * from './usage-display';
6
6
  export * from './tool-select';
@@ -2,5 +2,5 @@ export * from './clear-button';
2
2
  export * from './completion-status';
3
3
  export * from './model-select';
4
4
  export * from './stop-button';
5
- export * from './token-usage-display';
5
+ export * from './usage-display';
6
6
  export * from './tool-select';
@@ -4,9 +4,9 @@ import React from 'react';
4
4
  import { ISignal } from '@lumino/signaling';
5
5
  import type { IAISettingsModel, ITokenUsage } from '../tokens';
6
6
  /**
7
- * Props for the TokenUsageDisplay component.
7
+ * Props for the UsageDisplay component.
8
8
  */
9
- export interface ITokenUsageDisplayProps {
9
+ export interface IUsageDisplayProps {
10
10
  /**
11
11
  * The token usage changed signal
12
12
  */
@@ -25,24 +25,24 @@ export interface ITokenUsageDisplayProps {
25
25
  translator: TranslationBundle;
26
26
  }
27
27
  /**
28
- * React component that displays token usage information.
29
- * Shows input/output token counts with up/down arrows.
30
- * Only renders when token usage display is enabled in settings.
28
+ * React component that displays usage information.
29
+ * Shows input/output token counts and optional estimated context usage.
30
+ * Only renders when token or context usage display is enabled in settings.
31
31
  */
32
- export declare const TokenUsageDisplay: React.FC<ITokenUsageDisplayProps>;
32
+ export declare const UsageDisplay: React.FC<IUsageDisplayProps>;
33
33
  /**
34
- * JupyterLab widget wrapper for the TokenUsageDisplay component.
34
+ * JupyterLab widget wrapper for the UsageDisplay component.
35
35
  * Extends ReactWidget to integrate with the JupyterLab widget system.
36
36
  */
37
- export declare class TokenUsageWidget extends ReactWidget {
37
+ export declare class UsageWidget extends ReactWidget {
38
38
  /**
39
- * Creates a new TokenUsageWidget instance.
39
+ * Creates a new UsageWidget instance.
40
40
  * @param options - Configuration options containing required models
41
41
  */
42
- constructor(options: ITokenUsageDisplayProps);
42
+ constructor(options: IUsageDisplayProps);
43
43
  /**
44
44
  * Renders the React component within the widget.
45
- * @returns The TokenUsageDisplay React element
45
+ * @returns The UsageDisplay React element
46
46
  */
47
47
  protected render(): React.ReactElement;
48
48
  private _options;
@@ -0,0 +1,109 @@
1
+ import { ReactWidget, UseSignal } from '@jupyterlab/ui-components';
2
+ import React from 'react';
3
+ /**
4
+ * React component that displays usage information.
5
+ * Shows input/output token counts and optional estimated context usage.
6
+ * Only renders when token or context usage display is enabled in settings.
7
+ */
8
+ export const UsageDisplay = ({ tokenUsageChanged, settingsModel, initialTokenUsage, translator: trans }) => {
9
+ const formatContextPercent = (value) => {
10
+ return Math.round(value).toLocaleString();
11
+ };
12
+ const badgeStyle = {
13
+ display: 'flex',
14
+ alignItems: 'center',
15
+ gap: '6px',
16
+ fontSize: '12px',
17
+ color: 'var(--jp-ui-font-color2)',
18
+ padding: '4px 8px',
19
+ backgroundColor: 'var(--jp-layout-color1)',
20
+ border: '1px solid var(--jp-border-color1)',
21
+ borderRadius: '4px',
22
+ whiteSpace: 'nowrap'
23
+ };
24
+ return (React.createElement(UseSignal, { signal: settingsModel.stateChanged, initialArgs: undefined }, () => {
25
+ const config = settingsModel.config;
26
+ const showTokenUsage = config.showTokenUsage;
27
+ const showContextUsage = config.showContextUsage;
28
+ if (!showTokenUsage && !showContextUsage) {
29
+ return null;
30
+ }
31
+ return (React.createElement(UseSignal, { signal: tokenUsageChanged, initialArgs: initialTokenUsage }, (_, tokenUsage) => {
32
+ if (!tokenUsage) {
33
+ return null;
34
+ }
35
+ const total = tokenUsage.inputTokens + tokenUsage.outputTokens;
36
+ const hasKnownContextWindow = showContextUsage && tokenUsage.contextWindow !== undefined;
37
+ const contextUsagePercent = tokenUsage.lastRequestInputTokens !== undefined &&
38
+ tokenUsage.contextWindow !== undefined &&
39
+ tokenUsage.contextWindow > 0
40
+ ? Math.max(0, Math.min(100, (tokenUsage.lastRequestInputTokens /
41
+ tokenUsage.contextWindow) *
42
+ 100))
43
+ : undefined;
44
+ const hasContextEstimate = hasKnownContextWindow &&
45
+ contextUsagePercent !== undefined &&
46
+ tokenUsage.lastRequestInputTokens !== undefined;
47
+ const contextLabel = hasContextEstimate
48
+ ? `${formatContextPercent(contextUsagePercent)}%`
49
+ : hasKnownContextWindow
50
+ ? '0%'
51
+ : '?';
52
+ const contextTitle = hasContextEstimate
53
+ ? trans.__('Context Usage (estimated): %1% (%2 / %3 tokens)', formatContextPercent(contextUsagePercent), tokenUsage.lastRequestInputTokens.toLocaleString(), tokenUsage.contextWindow.toLocaleString())
54
+ : hasKnownContextWindow
55
+ ? trans.__('Context usage estimate will appear after the next request. Showing 0% until then. Context window: %1 tokens', tokenUsage.contextWindow.toLocaleString())
56
+ : trans.__('Context Usage unavailable. Configure a context window for the active provider/model to enable estimation.');
57
+ return (React.createElement("div", { style: {
58
+ display: 'flex',
59
+ alignItems: 'center',
60
+ gap: '6px'
61
+ } },
62
+ showTokenUsage && (React.createElement("span", { style: badgeStyle, title: trans.__('Token Usage - Sent: %1, Received: %2, Total: %3', tokenUsage.inputTokens.toLocaleString(), tokenUsage.outputTokens.toLocaleString(), total.toLocaleString()) },
63
+ React.createElement("span", { style: {
64
+ display: 'flex',
65
+ alignItems: 'center',
66
+ gap: '2px'
67
+ } },
68
+ React.createElement("span", null, "\u2191"),
69
+ React.createElement("span", null, tokenUsage.inputTokens.toLocaleString())),
70
+ React.createElement("span", { style: {
71
+ display: 'flex',
72
+ alignItems: 'center',
73
+ gap: '2px'
74
+ } },
75
+ React.createElement("span", null, "\u2193"),
76
+ React.createElement("span", null, tokenUsage.outputTokens.toLocaleString())))),
77
+ showContextUsage && (React.createElement("span", { style: badgeStyle, title: contextTitle },
78
+ React.createElement("span", { style: {
79
+ display: 'flex',
80
+ alignItems: 'center',
81
+ gap: '2px'
82
+ } },
83
+ React.createElement("span", null, "ctx"),
84
+ React.createElement("span", null, contextLabel))))));
85
+ }));
86
+ }));
87
+ };
88
+ /**
89
+ * JupyterLab widget wrapper for the UsageDisplay component.
90
+ * Extends ReactWidget to integrate with the JupyterLab widget system.
91
+ */
92
+ export class UsageWidget extends ReactWidget {
93
+ /**
94
+ * Creates a new UsageWidget instance.
95
+ * @param options - Configuration options containing required models
96
+ */
97
+ constructor(options) {
98
+ super();
99
+ this._options = options;
100
+ }
101
+ /**
102
+ * Renders the React component within the widget.
103
+ * @returns The UsageDisplay React element
104
+ */
105
+ render() {
106
+ return React.createElement(UsageDisplay, { ...this._options });
107
+ }
108
+ _options;
109
+ }
package/lib/index.js CHANGED
@@ -25,7 +25,7 @@ import { ChatModelHandler } from './chat-model-handler';
25
25
  import { CommandIds, IAgentManagerFactory, IAISettingsModel, IChatModelHandler, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE } from './tokens';
26
26
  import { anthropicProvider, googleProvider, mistralProvider, openaiProvider, genericProvider } from './providers/built-in-providers';
27
27
  import { AICompletionProvider } from './completion';
28
- import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, TokenUsageWidget } from './components';
28
+ import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, UsageWidget } from './components';
29
29
  import { AISettingsModel } from './models/settings-model';
30
30
  import { loadSkillsFromPaths, SkillRegistry } from './skills';
31
31
  import { DiffManager } from './diff-manager';
@@ -338,7 +338,7 @@ const plugin = {
338
338
  app.commands.commandChanged.connect(onCommandChanged);
339
339
  chatPanel.disposed.connect(disconnectSettingsButtonListener);
340
340
  }
341
- let tokenUsageWidget = null;
341
+ let usageWidget = null;
342
342
  chatPanel.chatOpened.connect((_, widget) => {
343
343
  const model = widget.model;
344
344
  // Add the widget to the tracker.
@@ -351,14 +351,14 @@ const plugin = {
351
351
  // Update the tracker if the active provider changed.
352
352
  model.agentManager.activeProviderChanged.connect(saveTracker);
353
353
  // Update the token usage widget.
354
- tokenUsageWidget?.dispose();
355
- tokenUsageWidget = new TokenUsageWidget({
354
+ usageWidget?.dispose();
355
+ usageWidget = new UsageWidget({
356
356
  tokenUsageChanged: model.tokenUsageChanged,
357
357
  settingsModel,
358
358
  initialTokenUsage: model.agentManager.tokenUsage,
359
359
  translator: trans
360
360
  });
361
- chatPanel.current?.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
361
+ chatPanel.current?.toolbar.insertBefore('markRead', 'usage', usageWidget);
362
362
  if (model.saveAvailable) {
363
363
  const saveChatButton = new SaveComponentWidget({
364
364
  model,
@@ -13,6 +13,7 @@ export class AISettingsModel extends VDomModel {
13
13
  toolsEnabled: true,
14
14
  sendWithShiftEnter: false,
15
15
  showTokenUsage: false,
16
+ showContextUsage: false,
16
17
  showCellDiff: true,
17
18
  showFileDiff: true,
18
19
  diffDisplayMode: 'split',
@@ -3,6 +3,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google';
3
3
  import { createMistral } from '@ai-sdk/mistral';
4
4
  import { createOpenAI } from '@ai-sdk/openai';
5
5
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
6
+ import { BUILT_IN_PROVIDER_MODEL_INFO } from './generated-context-windows';
6
7
  /**
7
8
  * Anthropic provider
8
9
  */
@@ -26,6 +27,7 @@ export const anthropicProvider = {
26
27
  'claude-sonnet-4-0',
27
28
  'claude-sonnet-4-20250514'
28
29
  ],
30
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.anthropic,
29
31
  supportsBaseURL: true,
30
32
  supportsHeaders: true,
31
33
  providerToolCapabilities: {
@@ -72,6 +74,7 @@ export const googleProvider = {
72
74
  'gemini-flash-latest',
73
75
  'gemini-flash-lite-latest'
74
76
  ],
77
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.google,
75
78
  supportsBaseURL: true,
76
79
  factory: (options) => {
77
80
  if (!options.apiKey) {
@@ -107,6 +110,7 @@ export const mistralProvider = {
107
110
  'codestral-latest',
108
111
  'devstral-latest'
109
112
  ],
113
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.mistral,
110
114
  supportsBaseURL: true,
111
115
  factory: (options) => {
112
116
  if (!options.apiKey) {
@@ -175,6 +179,7 @@ export const openaiProvider = {
175
179
  'gpt-3.5-turbo',
176
180
  'gpt-3.5-turbo-0125'
177
181
  ],
182
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.openai,
178
183
  supportsBaseURL: true,
179
184
  supportsHeaders: true,
180
185
  providerToolCapabilities: {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This file is generated by `jlpm sync:model-context-windows`.
3
+ * Source: https://models.dev/api.json
4
+ * Backed by: https://github.com/anomalyco/models.dev
5
+ * Generated: 2026-04-08T16:23:34.080Z
6
+ */
7
+ import type { IProviderModelInfo } from '../tokens';
8
+ export declare const BUILT_IN_PROVIDER_MODEL_INFO: Record<string, Record<string, IProviderModelInfo>>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * This file is generated by `jlpm sync:model-context-windows`.
3
+ * Source: https://models.dev/api.json
4
+ * Backed by: https://github.com/anomalyco/models.dev
5
+ * Generated: 2026-04-08T16:23:34.080Z
6
+ */
7
+ export const BUILT_IN_PROVIDER_MODEL_INFO = {
8
+ anthropic: {
9
+ 'claude-opus-4-6': { contextWindow: 1000000 },
10
+ 'claude-sonnet-4-6': { contextWindow: 1000000 },
11
+ 'claude-opus-4-5': { contextWindow: 200000 },
12
+ 'claude-opus-4-5-20251101': { contextWindow: 200000 },
13
+ 'claude-sonnet-4-5': { contextWindow: 200000 },
14
+ 'claude-sonnet-4-5-20250929': { contextWindow: 200000 },
15
+ 'claude-haiku-4-5': { contextWindow: 200000 },
16
+ 'claude-haiku-4-5-20251001': { contextWindow: 200000 },
17
+ 'claude-opus-4-1': { contextWindow: 200000 },
18
+ 'claude-opus-4-1-20250805': { contextWindow: 200000 },
19
+ 'claude-opus-4-0': { contextWindow: 200000 },
20
+ 'claude-opus-4-20250514': { contextWindow: 200000 },
21
+ 'claude-sonnet-4-0': { contextWindow: 200000 },
22
+ 'claude-sonnet-4-20250514': { contextWindow: 200000 }
23
+ },
24
+ google: {
25
+ 'gemini-3.1-pro-preview': { contextWindow: 1048576 },
26
+ 'gemini-3.1-pro-preview-customtools': { contextWindow: 1048576 },
27
+ 'gemini-3.1-flash-image-preview': { contextWindow: 131072 },
28
+ 'gemini-3.1-flash-lite-preview': { contextWindow: 1048576 },
29
+ 'gemini-3-flash-preview': { contextWindow: 1048576 },
30
+ 'gemini-2.5-pro': { contextWindow: 1048576 },
31
+ 'gemini-2.5-flash': { contextWindow: 1048576 },
32
+ 'gemini-2.5-flash-image': { contextWindow: 32768 },
33
+ 'gemini-2.5-flash-lite': { contextWindow: 1048576 },
34
+ 'gemini-flash-latest': { contextWindow: 1048576 },
35
+ 'gemini-flash-lite-latest': { contextWindow: 1048576 }
36
+ },
37
+ mistral: {
38
+ 'mistral-large-latest': { contextWindow: 262144 },
39
+ 'mistral-medium-latest': { contextWindow: 128000 },
40
+ 'mistral-medium-2508': { contextWindow: 262144 },
41
+ 'mistral-small-latest': { contextWindow: 256000 },
42
+ 'mistral-small-2506': { contextWindow: 128000 },
43
+ 'ministral-3b-latest': { contextWindow: 128000 },
44
+ 'ministral-8b-latest': { contextWindow: 128000 },
45
+ 'magistral-small-latest': { contextWindow: 128000 },
46
+ 'magistral-medium-latest': { contextWindow: 128000 },
47
+ 'pixtral-large-latest': { contextWindow: 128000 },
48
+ 'codestral-latest': { contextWindow: 256000 },
49
+ 'devstral-latest': { contextWindow: 262144 },
50
+ 'devstral-2512': { contextWindow: 262144 }
51
+ },
52
+ openai: {
53
+ 'gpt-5.4': { contextWindow: 1050000 },
54
+ 'gpt-5.4-mini': { contextWindow: 400000 },
55
+ 'gpt-5.4-nano': { contextWindow: 400000 },
56
+ 'gpt-5.2': { contextWindow: 400000 },
57
+ 'gpt-5.2-2025-12-11': { contextWindow: 400000 },
58
+ 'gpt-5.2-chat-latest': { contextWindow: 128000 },
59
+ 'gpt-5.2-pro': { contextWindow: 400000 },
60
+ 'gpt-5.2-pro-2025-12-11': { contextWindow: 400000 },
61
+ 'gpt-5.2-codex': { contextWindow: 400000 },
62
+ 'gpt-5.1': { contextWindow: 400000 },
63
+ 'gpt-5.1-2025-11-13': { contextWindow: 400000 },
64
+ 'gpt-5.1-chat-latest': { contextWindow: 128000 },
65
+ 'gpt-5': { contextWindow: 400000 },
66
+ 'gpt-5-2025-08-07': { contextWindow: 400000 },
67
+ 'gpt-5-chat-latest': { contextWindow: 400000 },
68
+ 'gpt-5-mini': { contextWindow: 400000 },
69
+ 'gpt-5-mini-2025-08-07': { contextWindow: 400000 },
70
+ 'gpt-5-nano': { contextWindow: 400000 },
71
+ 'gpt-5-nano-2025-08-07': { contextWindow: 400000 },
72
+ 'o4-mini': { contextWindow: 200000 },
73
+ 'o4-mini-2025-04-16': { contextWindow: 200000 },
74
+ 'o3-pro': { contextWindow: 200000 },
75
+ o3: { contextWindow: 200000 },
76
+ 'o3-2025-04-16': { contextWindow: 200000 },
77
+ 'o3-mini': { contextWindow: 200000 },
78
+ 'o3-mini-2025-01-31': { contextWindow: 200000 },
79
+ o1: { contextWindow: 200000 },
80
+ 'o1-2024-12-17': { contextWindow: 200000 },
81
+ 'gpt-4.1': { contextWindow: 1047576 },
82
+ 'gpt-4.1-2025-04-14': { contextWindow: 1047576 },
83
+ 'gpt-4.1-mini': { contextWindow: 1047576 },
84
+ 'gpt-4.1-mini-2025-04-14': { contextWindow: 1047576 },
85
+ 'gpt-4.1-nano': { contextWindow: 1047576 },
86
+ 'gpt-4.1-nano-2025-04-14': { contextWindow: 1047576 },
87
+ 'gpt-4o': { contextWindow: 128000 },
88
+ 'gpt-4o-2024-05-13': { contextWindow: 128000 },
89
+ 'gpt-4o-2024-08-06': { contextWindow: 128000 },
90
+ 'gpt-4o-2024-11-20': { contextWindow: 128000 },
91
+ 'gpt-4o-mini': { contextWindow: 128000 },
92
+ 'gpt-4o-mini-2024-07-18': { contextWindow: 128000 },
93
+ 'gpt-3.5-turbo': { contextWindow: 16385 },
94
+ 'gpt-3.5-turbo-0125': { contextWindow: 16385 }
95
+ }
96
+ };
@@ -0,0 +1,3 @@
1
+ import type { IProviderConfig, IProviderInfo, IProviderModelInfo, IProviderRegistry } from '../tokens';
2
+ export declare function getProviderModelInfo(providerInfo: IProviderInfo | null | undefined, model: string | undefined): IProviderModelInfo | undefined;
3
+ export declare function getEffectiveContextWindow(providerConfig: IProviderConfig | undefined, providerRegistry?: IProviderRegistry): number | undefined;
@@ -0,0 +1,58 @@
1
+ const DATE_SUFFIX = /^(.*)-\d{4}-\d{2}-\d{2}$/;
2
+ const SHORT_VERSION_SUFFIX = /^(.*)-\d{4}$/;
3
+ // Treat rolling aliases and dated releases as the same model family so they
4
+ // can share provider metadata such as context windows.
5
+ function normalizeModelId(modelId) {
6
+ if (modelId.endsWith('-latest')) {
7
+ return modelId.slice(0, -7);
8
+ }
9
+ const dateSuffixMatch = modelId.match(DATE_SUFFIX);
10
+ if (dateSuffixMatch) {
11
+ return dateSuffixMatch[1];
12
+ }
13
+ const shortVersionSuffixMatch = modelId.match(SHORT_VERSION_SUFFIX);
14
+ if (shortVersionSuffixMatch) {
15
+ return shortVersionSuffixMatch[1];
16
+ }
17
+ return modelId;
18
+ }
19
+ function getCandidateModelIds(modelId) {
20
+ const candidates = [modelId];
21
+ const normalizedModelId = normalizeModelId(modelId);
22
+ candidates.push(normalizedModelId);
23
+ if (normalizedModelId !== modelId) {
24
+ candidates.push(`${normalizedModelId}-latest`);
25
+ }
26
+ return [...new Set(candidates)];
27
+ }
28
+ export function getProviderModelInfo(providerInfo, model) {
29
+ if (!providerInfo || !model) {
30
+ return undefined;
31
+ }
32
+ const modelInfo = providerInfo.modelInfo;
33
+ if (!modelInfo) {
34
+ return undefined;
35
+ }
36
+ for (const candidateId of getCandidateModelIds(model)) {
37
+ if (modelInfo[candidateId]) {
38
+ return modelInfo[candidateId];
39
+ }
40
+ }
41
+ const normalizedModelId = normalizeModelId(model);
42
+ // As a last resort, match any known model entry that normalizes to the same
43
+ // base ID, even if the exact alias/version string differs.
44
+ return Object.entries(modelInfo).find(([candidateId]) => {
45
+ return normalizeModelId(candidateId) === normalizedModelId;
46
+ })?.[1];
47
+ }
48
+ export function getEffectiveContextWindow(providerConfig, providerRegistry) {
49
+ if (!providerConfig) {
50
+ return undefined;
51
+ }
52
+ if (providerConfig.parameters?.contextWindow !== undefined) {
53
+ return providerConfig.parameters.contextWindow;
54
+ }
55
+ const providerInfo = providerRegistry?.getProviderInfo(providerConfig.provider);
56
+ return getProviderModelInfo(providerInfo, providerConfig.model)
57
+ ?.contextWindow;
58
+ }
package/lib/tokens.d.ts CHANGED
@@ -153,6 +153,12 @@ export interface IProviderToolCapabilities {
153
153
  /**
154
154
  * Provider information
155
155
  */
156
+ export interface IProviderModelInfo {
157
+ /**
158
+ * Default context window for the model in tokens.
159
+ */
160
+ contextWindow?: number;
161
+ }
156
162
  export interface IProviderInfo {
157
163
  /**
158
164
  * Unique identifier for the provider
@@ -173,6 +179,10 @@ export interface IProviderInfo {
173
179
  * Default model names for this provider
174
180
  */
175
181
  defaultModels: string[];
182
+ /**
183
+ * Optional per-model metadata keyed by model ID.
184
+ */
185
+ modelInfo?: Record<string, IProviderModelInfo>;
176
186
  /**
177
187
  * Whether this provider supports custom base URLs
178
188
  */
@@ -246,6 +256,7 @@ export interface IProviderParameters {
246
256
  temperature?: number;
247
257
  maxOutputTokens?: number;
248
258
  maxTurns?: number;
259
+ contextWindow?: number;
249
260
  supportsFillInMiddle?: boolean;
250
261
  useFilterText?: boolean;
251
262
  }
@@ -282,6 +293,7 @@ export interface IAIConfig {
282
293
  toolsEnabled: boolean;
283
294
  sendWithShiftEnter: boolean;
284
295
  showTokenUsage: boolean;
296
+ showContextUsage: boolean;
285
297
  commandsRequiringApproval: string[];
286
298
  commandsAutoRenderMimeBundles: string[];
287
299
  trustedMimeTypesForAutoRender: string[];
@@ -633,6 +645,15 @@ export interface ITokenUsage {
633
645
  * Number of output tokens generated (completion tokens)
634
646
  */
635
647
  outputTokens: number;
648
+ /**
649
+ * Estimated prompt tokens used by the most recent model request.
650
+ * This is based on the final step of the latest request.
651
+ */
652
+ lastRequestInputTokens?: number;
653
+ /**
654
+ * Configured context window size for the active provider/model.
655
+ */
656
+ contextWindow?: number;
636
657
  }
637
658
  /**
638
659
  * The string that replaces a secret key in settings.