@jupyterlite/ai 0.15.0 → 0.17.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 (45) hide show
  1. package/lib/agent.d.ts +12 -2
  2. package/lib/agent.js +112 -17
  3. package/lib/chat-commands/clear.js +1 -1
  4. package/lib/chat-model-handler.js +4 -1
  5. package/lib/chat-model.d.ts +25 -24
  6. package/lib/chat-model.js +262 -132
  7. package/lib/components/clear-button.d.ts +1 -1
  8. package/lib/components/clear-button.js +1 -1
  9. package/lib/components/index.d.ts +1 -1
  10. package/lib/components/index.js +1 -1
  11. package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
  12. package/lib/components/usage-display.js +109 -0
  13. package/lib/index.js +205 -20
  14. package/lib/models/settings-model.js +1 -0
  15. package/lib/providers/built-in-providers.js +5 -0
  16. package/lib/providers/generated-context-windows.d.ts +8 -0
  17. package/lib/providers/generated-context-windows.js +96 -0
  18. package/lib/providers/model-info.d.ts +3 -0
  19. package/lib/providers/model-info.js +58 -0
  20. package/lib/tokens.d.ts +34 -3
  21. package/lib/tokens.js +8 -7
  22. package/lib/widgets/ai-settings.js +9 -0
  23. package/lib/widgets/main-area-chat.d.ts +1 -0
  24. package/lib/widgets/main-area-chat.js +10 -4
  25. package/lib/widgets/provider-config-dialog.js +18 -5
  26. package/package.json +3 -2
  27. package/schema/settings-model.json +11 -0
  28. package/src/agent.ts +151 -21
  29. package/src/chat-commands/clear.ts +1 -1
  30. package/src/chat-model-handler.ts +6 -1
  31. package/src/chat-model.ts +350 -175
  32. package/src/components/clear-button.tsx +3 -3
  33. package/src/components/index.ts +1 -1
  34. package/src/components/usage-display.tsx +208 -0
  35. package/src/index.ts +250 -26
  36. package/src/models/settings-model.ts +1 -0
  37. package/src/providers/built-in-providers.ts +5 -0
  38. package/src/providers/generated-context-windows.ts +102 -0
  39. package/src/providers/model-info.ts +88 -0
  40. package/src/tokens.ts +46 -10
  41. package/src/widgets/ai-settings.tsx +42 -0
  42. package/src/widgets/main-area-chat.ts +12 -4
  43. package/src/widgets/provider-config-dialog.tsx +45 -5
  44. package/lib/components/token-usage-display.js +0 -72
  45. package/src/components/token-usage-display.tsx +0 -137
@@ -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';
@@ -213,6 +213,22 @@ const chatModelHandler = {
213
213
  });
214
214
  }
215
215
  };
216
+ /**
217
+ * The active cell manager plugin, to allow copying code from chat to notebook.
218
+ */
219
+ const activeCellManager = {
220
+ id: '@jupyterlite/ai:activeCellManager',
221
+ description: 'Add the active cell manager to the model handler',
222
+ autoStart: true,
223
+ requires: [IChatModelHandler, INotebookTracker],
224
+ activate: (app, modelHandler, notebookTracker) => {
225
+ const activeCellManager = new ActiveCellManager({
226
+ tracker: notebookTracker,
227
+ shell: app.shell
228
+ });
229
+ modelHandler.activeCellManager = activeCellManager;
230
+ }
231
+ };
216
232
  /**
217
233
  * Initialization data for the extension.
218
234
  */
@@ -232,13 +248,12 @@ const plugin = {
232
248
  IThemeManager,
233
249
  ILayoutRestorer,
234
250
  ILabShell,
235
- INotebookTracker,
236
251
  ITranslator,
237
252
  IComponentsRendererFactory,
238
253
  ICommandPalette,
239
254
  IDocumentManager
240
255
  ],
241
- activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator, chatComponentsFactory, palette, documentManager) => {
256
+ activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, translator, chatComponentsFactory, palette, documentManager) => {
242
257
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
243
258
  // Create attachment opener registry to handle file attachments
244
259
  const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
@@ -253,16 +268,6 @@ const plugin = {
253
268
  void app.commands.execute(CommandIds.openSettings);
254
269
  }
255
270
  };
256
- // Create ActiveCellManager if notebook tracker is available, and add it to the
257
- // model registry.
258
- let activeCellManager;
259
- if (notebookTracker) {
260
- activeCellManager = new ActiveCellManager({
261
- tracker: notebookTracker,
262
- shell: app.shell
263
- });
264
- }
265
- modelHandler.activeCellManager = activeCellManager;
266
271
  // Creating the tracker for the chat widgets
267
272
  const namespace = 'ai-chat';
268
273
  const tracker = new WidgetTracker({ namespace });
@@ -338,7 +343,7 @@ const plugin = {
338
343
  app.commands.commandChanged.connect(onCommandChanged);
339
344
  chatPanel.disposed.connect(disconnectSettingsButtonListener);
340
345
  }
341
- let tokenUsageWidget = null;
346
+ let usageWidget = null;
342
347
  chatPanel.chatOpened.connect((_, widget) => {
343
348
  const model = widget.model;
344
349
  // Add the widget to the tracker.
@@ -346,19 +351,29 @@ const plugin = {
346
351
  function saveTracker() {
347
352
  tracker.save(widget);
348
353
  }
354
+ function updateToolbarTitleOverlay() {
355
+ const titleNode = chatPanel.current?.toolbar.node
356
+ .getElementsByClassName('jp-chat-sidepanel-widget-title')
357
+ .item(0);
358
+ if (titleNode) {
359
+ titleNode.setAttribute('title', model.title ?? model.name);
360
+ }
361
+ }
362
+ model.titleChanged.connect(updateToolbarTitleOverlay);
363
+ updateToolbarTitleOverlay();
349
364
  // Update the tracker if the model name changed.
350
365
  model.nameChanged.connect(saveTracker);
351
366
  // Update the tracker if the active provider changed.
352
367
  model.agentManager.activeProviderChanged.connect(saveTracker);
353
368
  // Update the token usage widget.
354
- tokenUsageWidget?.dispose();
355
- tokenUsageWidget = new TokenUsageWidget({
369
+ usageWidget?.dispose();
370
+ usageWidget = new UsageWidget({
356
371
  tokenUsageChanged: model.tokenUsageChanged,
357
372
  settingsModel,
358
373
  initialTokenUsage: model.agentManager.tokenUsage,
359
374
  translator: trans
360
375
  });
361
- chatPanel.current?.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
376
+ chatPanel.current?.toolbar.insertBefore('markRead', 'usage', usageWidget);
362
377
  if (model.saveAvailable) {
363
378
  const saveChatButton = new SaveComponentWidget({
364
379
  model,
@@ -386,6 +401,7 @@ const plugin = {
386
401
  chatPanel: widget
387
402
  });
388
403
  widget.disposed.connect(() => {
404
+ model.titleChanged.disconnect(updateToolbarTitleOverlay);
389
405
  model.nameChanged.disconnect(saveTracker);
390
406
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
391
407
  model.writersChanged?.disconnect(writersChanged);
@@ -514,6 +530,49 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
514
530
  model.nameChanged.disconnect(saveTracker);
515
531
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
516
532
  });
533
+ return widget;
534
+ };
535
+ const focusOnChat = (area, widget) => {
536
+ if (area === 'main' && widget) {
537
+ app.shell.activateById(widget.id);
538
+ }
539
+ else {
540
+ app.shell.activateById(chatPanel.id);
541
+ }
542
+ };
543
+ const applyInputArgs = (model, args) => {
544
+ const input = typeof args.input === 'string' ? args.input : undefined;
545
+ const autoSend = args.autoSend === true;
546
+ const shouldFocus = args.focus !== false;
547
+ if (input !== undefined) {
548
+ model.input.value = input;
549
+ }
550
+ if (autoSend && input !== undefined) {
551
+ model.input.send(model.input.value);
552
+ }
553
+ if (shouldFocus) {
554
+ model.input.focus();
555
+ }
556
+ };
557
+ const findChatWidget = (name, provider) => {
558
+ if (!name && !provider) {
559
+ return;
560
+ }
561
+ return tracker.find(widget => {
562
+ const model = widget.model;
563
+ return ((!name || widget.model.name === name) &&
564
+ (!provider || model.agentManager.activeProvider === provider));
565
+ });
566
+ };
567
+ const disposeSideChatModel = (model) => {
568
+ const loadedName = chatPanel
569
+ .getLoadedModelNames()
570
+ .find(name => chatPanel.getLoadedModel(name) === model);
571
+ if (!loadedName) {
572
+ return false;
573
+ }
574
+ chatPanel.disposeLoadedModel(loadedName);
575
+ return true;
517
576
  };
518
577
  commands.addCommand(CommandIds.openChat, {
519
578
  label: trans.__('Open a chat'),
@@ -543,12 +602,18 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
543
602
  if (!model) {
544
603
  return false;
545
604
  }
605
+ const shouldFocus = args.focus === true;
606
+ let widget;
546
607
  if (area === 'main') {
547
- openInMain(model);
608
+ widget = openInMain(model);
548
609
  }
549
610
  else {
550
- chatPanel.open({ model });
611
+ widget = chatPanel.open({ model });
612
+ }
613
+ if (shouldFocus) {
614
+ focusOnChat(area, widget);
551
615
  }
616
+ applyInputArgs(model, { ...args, focus: shouldFocus });
552
617
  return true;
553
618
  },
554
619
  describedBy: {
@@ -567,6 +632,116 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
567
632
  provider: {
568
633
  type: 'string',
569
634
  description: trans.__('The provider/model to use with this chat')
635
+ },
636
+ input: {
637
+ type: 'string',
638
+ description: trans.__('The input text to prefill in the chat')
639
+ },
640
+ focus: {
641
+ type: 'boolean',
642
+ description: trans.__('Whether to focus the chat input after opening it')
643
+ },
644
+ autoSend: {
645
+ type: 'boolean',
646
+ description: trans.__('Whether to auto-send the provided input after opening the chat')
647
+ }
648
+ }
649
+ }
650
+ }
651
+ });
652
+ commands.addCommand(CommandIds.openOrRevealChat, {
653
+ label: trans.__('Open or reveal the chat panel'),
654
+ execute: async (args) => {
655
+ const area = args.area === 'main' ? 'main' : 'side';
656
+ const provider = args.provider ?? undefined;
657
+ const name = args.name ?? undefined;
658
+ const shouldFocus = args.focus === true;
659
+ let existingWidget = findChatWidget(name, provider);
660
+ if (!existingWidget && !name) {
661
+ const providerConfig = provider
662
+ ? settingsModel.getProvider(provider)
663
+ : settingsModel.getDefaultProvider();
664
+ existingWidget = findChatWidget(undefined, providerConfig?.id);
665
+ }
666
+ // If the side chat model is loaded but not currently displayed, reveal it first.
667
+ if (!existingWidget && name) {
668
+ const loadedModel = chatPanel.getLoadedModel(name);
669
+ if (loadedModel) {
670
+ existingWidget = chatPanel.open({ model: loadedModel });
671
+ }
672
+ }
673
+ if (!existingWidget) {
674
+ return commands.execute(CommandIds.openChat, {
675
+ ...args,
676
+ focus: shouldFocus
677
+ });
678
+ }
679
+ const currentArea = existingWidget instanceof MainAreaChat ? 'main' : 'side';
680
+ if (currentArea !== area) {
681
+ const targetName = existingWidget.model.name;
682
+ const moved = (await commands.execute(CommandIds.moveChat, {
683
+ name: targetName,
684
+ area
685
+ }));
686
+ if (!moved) {
687
+ return false;
688
+ }
689
+ const movedWidget = findChatWidget(targetName);
690
+ if (!movedWidget) {
691
+ return false;
692
+ }
693
+ if (area === 'side') {
694
+ chatPanel.open({ model: movedWidget.model });
695
+ }
696
+ if (shouldFocus) {
697
+ focusOnChat(area, movedWidget);
698
+ }
699
+ applyInputArgs(movedWidget.model, {
700
+ ...args,
701
+ focus: shouldFocus
702
+ });
703
+ return true;
704
+ }
705
+ if (area === 'side') {
706
+ chatPanel.open({ model: existingWidget.model });
707
+ }
708
+ if (shouldFocus) {
709
+ focusOnChat(area, existingWidget);
710
+ }
711
+ applyInputArgs(existingWidget.model, {
712
+ ...args,
713
+ focus: shouldFocus
714
+ });
715
+ return true;
716
+ },
717
+ describedBy: {
718
+ args: {
719
+ type: 'object',
720
+ properties: {
721
+ area: {
722
+ type: 'string',
723
+ enum: ['main', 'side'],
724
+ description: trans.__('The name of the area to open or reveal the chat in')
725
+ },
726
+ name: {
727
+ type: 'string',
728
+ description: trans.__('The name of the chat')
729
+ },
730
+ provider: {
731
+ type: 'string',
732
+ description: trans.__('The provider/model to use with this chat')
733
+ },
734
+ input: {
735
+ type: 'string',
736
+ description: trans.__('The input text to prefill in the chat')
737
+ },
738
+ focus: {
739
+ type: 'boolean',
740
+ description: trans.__('Whether to focus the chat input after opening it')
741
+ },
742
+ autoSend: {
743
+ type: 'boolean',
744
+ description: trans.__('Whether to auto-send the provided input after opening the chat')
570
745
  }
571
746
  }
572
747
  }
@@ -615,7 +790,8 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
615
790
  activeProvider: previousModel.agentManager.activeProvider,
616
791
  tokenUsage: previousModel.agentManager.tokenUsage,
617
792
  messages: previousModel.messages,
618
- autosave: previousModel.autosave
793
+ autosave: previousModel.autosave,
794
+ title: previousModel.title
619
795
  });
620
796
  // Wait (with timeout) for the tracker to have updated the previous widget.
621
797
  const status = await Promise.any([
@@ -630,6 +806,14 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
630
806
  }
631
807
  if (area === 'main') {
632
808
  openInMain(model);
809
+ if (previousWidget instanceof ChatWidget) {
810
+ // Clean up the side-panel model entry before disposing the previous
811
+ // widget/model state.
812
+ if (!disposeSideChatModel(previousModel)) {
813
+ previousWidget.dispose();
814
+ previousModel.dispose();
815
+ }
816
+ }
633
817
  }
634
818
  else {
635
819
  previousWidget?.dispose();
@@ -1118,6 +1302,7 @@ export default [
1118
1302
  skillRegistryPlugin,
1119
1303
  skillsCommandPlugin,
1120
1304
  chatModelHandler,
1305
+ activeCellManager,
1121
1306
  plugin,
1122
1307
  toolRegistry,
1123
1308
  agentManagerFactory,
@@ -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
+ }