sidekick-shared 0.24.1 → 0.24.3

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.
package/README.md CHANGED
@@ -69,17 +69,17 @@ npm install sidekick-shared
69
69
 
70
70
  `sidekick-shared` ships three public entry points plus a few convenience subpaths. Pick the one that matches your runtime.
71
71
 
72
- | Path | Runtime | What it exposes |
73
- | ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
74
- | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
75
- | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math. |
76
- | `sidekick-shared/node` | Node only | LiteLLM pricing hydration, recent-session/transcript reads, and resilient observed-session collection (`fs` + providers). |
77
- | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
78
- | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
79
- | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
80
- | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
81
- | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, quota history) — fs-free, no Node builtins. |
82
- | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
72
+ | Path | Runtime | What it exposes |
73
+ | ------------------------------ | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
74
+ | `sidekick-shared` | Node (CLI, extension host) | Full public API (readers, providers, parsers, pricing, …). |
75
+ | `sidekick-shared/browser` | **Browser / webview** | Pure helpers: usage normalization/pricing, token estimation, transcript and assistant-turn projection, model/context math. |
76
+ | `sidekick-shared/node` | Node only | LiteLLM pricing and context-window hydration, observed context-window persistence, recent-session/transcript reads, and resilient observed-session collection (`fs` + providers). |
77
+ | `sidekick-shared/phrases` | Any runtime | Phrase arrays + `getRandomPhrase()`. |
78
+ | `sidekick-shared/modelContext` | Any runtime | Direct access to the context-window module. |
79
+ | `sidekick-shared/modelInfo` | Any runtime | Direct access to model parsing and cost math. |
80
+ | `sidekick-shared/formatting` | Any runtime | Direct access to pure token and duration display helpers. |
81
+ | `sidekick-shared/schemas` | Any runtime | Pure Zod boundary schemas (session events, assistant turns, observed-session V1, quota, account status, quota history) — fs-free, no Node builtins. |
82
+ | `sidekick-shared/statusline` | Node only | Cache-only statusline surface: account status + quota snapshot readers, `formatStatusline()`, and burn-rate estimates. |
83
83
 
84
84
  ### Browser / webview runtimes
85
85
 
@@ -103,14 +103,23 @@ import {
103
103
 
104
104
  ### Node / CLI / extension host
105
105
 
106
- Hydrate the pricing catalog from the `node` subpath:
106
+ Hydrate the model catalog from the `node` subpath. One fetch covers both prices and context window sizes:
107
107
 
108
108
  ```typescript
109
- import { hydratePricingCatalog } from 'sidekick-shared/node';
109
+ import { hydratePricingCatalog, loadObservedContextWindows } from 'sidekick-shared/node';
110
110
 
111
- await hydratePricingCatalog({ cacheDir: '~/.config/sidekick' });
111
+ const { entries, contextWindowEntries } = await hydratePricingCatalog({
112
+ cacheDir: '~/.config/sidekick',
113
+ });
114
+
115
+ // Context windows a provider reported on an earlier run. These outrank the
116
+ // catalog, which only knows each model's published maximum — Codex reports the
117
+ // window your account tier actually gets. Local read, offline-safe.
118
+ await loadObservedContextWindows();
112
119
  ```
113
120
 
121
+ Both calls always resolve; a network failure, missing cache, or malformed store leaves lookups on the static tables. To record a window a provider reports at runtime, call `recordObservedContextWindow(modelId, window)` — it persists only when the value changes.
122
+
114
123
  ### Normalize and price provider usage
115
124
 
116
125
  ```typescript
package/dist/index.d.ts CHANGED
@@ -164,8 +164,10 @@ export type { StatuslineInput, StatuslineSelection } from './statusline/formatte
164
164
  export { getModelContextWindowSize, DEFAULT_CONTEXT_WINDOW } from './modelContext';
165
165
  export { parseModelId, getModelPricing, getModelInfo, calculateCost, calculateCostWithPricing, calculateCostWithProvenance, mergeCostSources, shortModelName, getModelDisplayInfo, compareModelIds, sortModelIds, formatCost, } from './modelInfo';
166
166
  export type { ModelPricing, CostTokenUsage, CostSource, CostProvenanceInput, CostWithProvenance, ModelInfo, ModelProvider, ParsedModelId, ModelDisplayInfo, } from './modelInfo';
167
- export { hydratePricingCatalog, normalizeLiteLlmCatalog, LITELLM_CATALOG_URL, } from './pricingCatalog';
167
+ export { hydratePricingCatalog, normalizeLiteLlmCatalog, normalizeLiteLlmContextWindows, LITELLM_CATALOG_URL, } from './pricingCatalog';
168
168
  export type { HydrateOptions, HydrateResult } from './pricingCatalog';
169
+ export { loadObservedContextWindows, recordObservedContextWindow, getObservedContextWindowPath, } from './observedContextWindows';
170
+ export type { ObservedContextWindowOptions, ObservedContextWindowStore, } from './observedContextWindows';
169
171
  export { extractTokenUsage } from './extractors/tokenUsage';
170
172
  export { extractUrls, extractFilePaths, extractCommands } from './extractors/sessionAssets';
171
173
  export type { AssetAgent, ExtractedAsset, ExtractedAssetProvenance, ExtractedAssetType, ExtractedAssets, SourceAssets, } from './extractors/sessionAssets';
package/dist/index.js CHANGED
@@ -8,8 +8,8 @@ exports.createSessionContextProjector = exports.calculateSessionContextPressure
8
8
  exports.scoreSessionQuality = exports.calculateQualityTrend = exports.PatternExtractor = exports.HeatmapTracker = exports.FrequencyTracker = exports.readErrorHistory = exports.getTopFailingTools = exports.appendErrorHistory = exports.MAX_ERROR_HISTORY_SESSIONS = exports.ERROR_HISTORY_SCHEMA_VERSION = exports.getSnapshotPath = exports.isSnapshotValid = exports.deleteSnapshot = exports.loadSnapshot = exports.saveSnapshot = exports.SessionMonitor = exports.parseTodoDependencies = exports.EventAggregator = exports.getRandomPhrase = exports.PHRASE_CATEGORIES = exports.ALL_PHRASES = exports.HIGHLIGHT_CSS = exports.clearHighlightCache = exports.highlightEvent = exports.formatSessionJson = exports.formatSessionMarkdown = exports.formatSessionText = exports.classifyNoise = exports.shouldMergeWithPrevious = exports.classifyFollowEvent = exports.classifyMessage = exports.getSoftNoiseReason = exports.isHardNoiseFollowEvent = exports.isHardNoise = exports.formatToolSummary = exports.formatTokenCount = exports.formatDurationMs = exports.createJsonlTail = exports.toFollowEvents = exports.createWatcher = exports.parseChangelog = exports.extractProposedPlanShared = exports.parsePlanMarkdownShared = exports.PlanExtractor = exports.segmentAssistantTurn = exports.reasoningSummary = exports.isAssistantTurnSubagentTool = exports.extractTurnSubagents = exports.assistantTurnEventsFromSessionEvents = exports.readSessionContextSnapshot = void 0;
9
9
  exports.getCodexProfileHome = exports.getCodexProfilesDir = exports.readClaudeProfileIdentity = exports.isClaudeProfileAuthenticated = exports.claudeKeychainService = exports.claudeKeychainSuffix = exports.getClaudeProfileHome = exports.getClaudeProfilesDir = exports.getActiveAccountStatus = exports.removeSavedAccountProfile = exports.replaceSavedAccountProfiles = exports.setActiveSavedAccount = exports.upsertSavedAccountProfile = exports.getActiveSavedAccount = exports.listSavedAccountProfiles = exports.writeSavedAccountRegistry = exports.readSavedAccountRegistry = exports.getAccountsDir = exports.resolveClaudeLoginCommand = exports.listAllAccounts = exports.switchAccount = exports.spawnAccountLogin = exports.finalizeAccountLogin = exports.getAccountLoginStatus = exports.beginAccountLogin = exports.isMultiAccountEnabled = exports.resolveActiveClaudeAccount = exports.getActiveAccount = exports.listAccounts = exports.removeAccount = exports.reconcileClaudeAuthState = exports.applyActiveClaudeToLiveHome = exports.resolveActiveClaudeHome = exports.switchToAccount = exports.addCurrentAccount = exports.readActiveClaudeAccount = exports.writeAccountRegistry = exports.readAccountRegistry = exports.ensureDefaultAccounts = exports.readClaudeMaxAccessTokenSync = exports.readClaudeMaxCredentials = exports.writeActiveCredentials = exports.readActiveCredentials = exports.openInBrowser = exports.parseTranscriptFromEvents = exports.parseTranscript = exports.generateHtmlReport = exports.formatCompactionLedger = exports.calculateCompactionLedger = exports.calculateCodeImpact = void 0;
10
10
  exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.MultiProviderQuotaService = exports.resolveZaiQuota = exports.readZaiCredentials = exports.quotaStateFromZaiQuotaLimitPayload = exports.fetchZaiQuotaFromApi = exports.CodexQuotaWatcher = exports.resolveCodexQuotaFromLocalSources = exports.resolveCodexQuota = exports.readLatestCodexQuotaFromRollouts = exports.quotaFromCodexRateLimits = exports.fetchCodexQuotaFromApi = exports.fetchCodexResetCreditsFromApi = exports.getWorkspaceIdFromPath = exports.pruneQuotaHistory = exports.readQuotaHistoryDailyBuckets = exports.readQuotaHistoryRange = exports.appendQuotaHistorySample = exports.writeQuotaSnapshot = exports.readQuotaSnapshot = exports.QuotaPoller = exports.describeQuotaFailure = exports.withQuotaProjections = exports.projectQuotaWindow = exports.fetchQuota = exports.SEVEN_DAY_WINDOW_MS = exports.FIVE_HOUR_WINDOW_MS = exports.AutoSwitchController = exports.decideAutoSwitch = exports.DEFAULT_AUTO_SWITCH_CONFIG = exports.removeLauncher = exports.writeLauncher = exports.isShellHookInstalled = exports.uninstallShellHook = exports.installShellHook = exports.setTerminalActiveProfile = exports.removeCodexAccount = exports.switchToCodexAccount = exports.finalizeCodexAccount = exports.prepareCodexAccount = exports.getCodexExecutionEnv = exports.resolveSidekickCodexHome = exports.resolveActiveCodexAccount = exports.getActiveCodexAccount = exports.listCodexAccounts = exports.getSystemCodexHome = exports.getCodexMonitoringHomes = void 0;
11
- exports.zaiProviderQuotaStateSchema = exports.codexProviderQuotaStateSchema = exports.claudeProviderQuotaStateSchema = exports.providerQuotaStateSchema = exports.runtimeQuotaProviderSchema = exports.quotaFailureDescriptorSchema = exports.peakHoursStateSchema = exports.codexResetCreditsSnapshotSchema = exports.codexResetCreditSchema = exports.quotaSourceSchema = exports.quotaProviderIdSchema = exports.quotaFailureKindSchema = exports.quotaStateSchema = exports.quotaWindowSchema = exports.extractSessionEvents = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.ToolCallTracker = exports.extractToolCalls = exports.extractToolCall = exports.extractErrorMessage = exports.categorizeError = exports.gatherAssetsForCwd = exports.codexSessions = exports.readCodexAssets = exports.claudeSessions = exports.readClaudeAssets = exports.extractCommands = exports.extractFilePaths = exports.extractUrls = exports.extractTokenUsage = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
12
- exports.scopePeakHoursToSessionProvider = exports.isClaudeCodeSessionProvider = exports.fetchPeakHoursStatus = exports.createPeakHoursNotApplicableState = exports.runDoctor = exports.getSessionDiagnostics = exports.formatHealthReport = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.assistantTurnToolRefSchema = exports.assistantTurnToolGroupStepSchema = exports.assistantTurnTimelineItemSchema = exports.assistantTurnSubagentStatusSchema = exports.assistantTurnSubagentSchema = exports.assistantTurnReasoningTimelineItemSchema = exports.assistantTurnProjectionSchema = exports.assistantTurnProcessStepSchema = exports.assistantTurnProcessSchema = exports.assistantTurnNarrationStepSchema = exports.assistantTurnEventTypeSchema = exports.assistantTurnEventSchema = exports.listAllAccountsResultSchema = exports.savedAccountProfileSchema = exports.accountEntrySchema = exports.accountLoginStatusSchema = exports.beginAccountLoginResultSchema = exports.accountManagerResultSchema = exports.accountProviderIdSchema = exports.activeAccountStatusSchema = exports.activeProviderAccountStatusSchema = exports.quotaHistoryDailyBucketSchema = exports.quotaHistorySampleSchema = exports.quotaHistoryRuntimeProviderSchema = exports.providerQuotaMapSchema = void 0;
11
+ exports.runtimeQuotaProviderSchema = exports.quotaFailureDescriptorSchema = exports.peakHoursStateSchema = exports.codexResetCreditsSnapshotSchema = exports.codexResetCreditSchema = exports.quotaSourceSchema = exports.quotaProviderIdSchema = exports.quotaFailureKindSchema = exports.quotaStateSchema = exports.quotaWindowSchema = exports.extractSessionEvents = exports.permissionModeSchema = exports.sessionEventSchema = exports.sessionMessageSchema = exports.messageUsageSchema = exports.ToolCallTracker = exports.extractToolCalls = exports.extractToolCall = exports.extractErrorMessage = exports.categorizeError = exports.gatherAssetsForCwd = exports.codexSessions = exports.readCodexAssets = exports.claudeSessions = exports.readClaudeAssets = exports.extractCommands = exports.extractFilePaths = exports.extractUrls = exports.extractTokenUsage = exports.getObservedContextWindowPath = exports.recordObservedContextWindow = exports.loadObservedContextWindows = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmContextWindows = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = exports.formatCost = exports.sortModelIds = exports.compareModelIds = exports.getModelDisplayInfo = exports.shortModelName = exports.mergeCostSources = exports.calculateCostWithProvenance = exports.calculateCostWithPricing = exports.calculateCost = exports.getModelInfo = exports.getModelPricing = exports.parseModelId = exports.DEFAULT_CONTEXT_WINDOW = exports.getModelContextWindowSize = void 0;
12
+ exports.scopePeakHoursToSessionProvider = exports.isClaudeCodeSessionProvider = exports.fetchPeakHoursStatus = exports.createPeakHoursNotApplicableState = exports.runDoctor = exports.getSessionDiagnostics = exports.formatHealthReport = exports.fetchOpenAIStatus = exports.fetchProviderStatus = exports.assistantTurnToolRefSchema = exports.assistantTurnToolGroupStepSchema = exports.assistantTurnTimelineItemSchema = exports.assistantTurnSubagentStatusSchema = exports.assistantTurnSubagentSchema = exports.assistantTurnReasoningTimelineItemSchema = exports.assistantTurnProjectionSchema = exports.assistantTurnProcessStepSchema = exports.assistantTurnProcessSchema = exports.assistantTurnNarrationStepSchema = exports.assistantTurnEventTypeSchema = exports.assistantTurnEventSchema = exports.listAllAccountsResultSchema = exports.savedAccountProfileSchema = exports.accountEntrySchema = exports.accountLoginStatusSchema = exports.beginAccountLoginResultSchema = exports.accountManagerResultSchema = exports.accountProviderIdSchema = exports.activeAccountStatusSchema = exports.activeProviderAccountStatusSchema = exports.quotaHistoryDailyBucketSchema = exports.quotaHistorySampleSchema = exports.quotaHistoryRuntimeProviderSchema = exports.providerQuotaMapSchema = exports.zaiProviderQuotaStateSchema = exports.codexProviderQuotaStateSchema = exports.claudeProviderQuotaStateSchema = exports.providerQuotaStateSchema = void 0;
13
13
  var usageNormalization_1 = require("./usageNormalization");
14
14
  Object.defineProperty(exports, "normalizeProviderUsage", { enumerable: true, get: function () { return usageNormalization_1.normalizeProviderUsage; } });
15
15
  Object.defineProperty(exports, "extractNormalizedUsage", { enumerable: true, get: function () { return usageNormalization_1.extractNormalizedUsage; } });
@@ -397,7 +397,13 @@ Object.defineProperty(exports, "formatCost", { enumerable: true, get: function (
397
397
  var pricingCatalog_1 = require("./pricingCatalog");
398
398
  Object.defineProperty(exports, "hydratePricingCatalog", { enumerable: true, get: function () { return pricingCatalog_1.hydratePricingCatalog; } });
399
399
  Object.defineProperty(exports, "normalizeLiteLlmCatalog", { enumerable: true, get: function () { return pricingCatalog_1.normalizeLiteLlmCatalog; } });
400
+ Object.defineProperty(exports, "normalizeLiteLlmContextWindows", { enumerable: true, get: function () { return pricingCatalog_1.normalizeLiteLlmContextWindows; } });
400
401
  Object.defineProperty(exports, "LITELLM_CATALOG_URL", { enumerable: true, get: function () { return pricingCatalog_1.LITELLM_CATALOG_URL; } });
402
+ // Observed context windows (provider-reported, persisted) — Node-only.
403
+ var observedContextWindows_1 = require("./observedContextWindows");
404
+ Object.defineProperty(exports, "loadObservedContextWindows", { enumerable: true, get: function () { return observedContextWindows_1.loadObservedContextWindows; } });
405
+ Object.defineProperty(exports, "recordObservedContextWindow", { enumerable: true, get: function () { return observedContextWindows_1.recordObservedContextWindow; } });
406
+ Object.defineProperty(exports, "getObservedContextWindowPath", { enumerable: true, get: function () { return observedContextWindows_1.getObservedContextWindowPath; } });
401
407
  // Extractors — per-event token usage and tool call extraction
402
408
  var tokenUsage_1 = require("./extractors/tokenUsage");
403
409
  Object.defineProperty(exports, "extractTokenUsage", { enumerable: true, get: function () { return tokenUsage_1.extractTokenUsage; } });
@@ -1,19 +1,66 @@
1
1
  /**
2
2
  * Centralized model ID → context window size lookup.
3
3
  * Single source of truth used by all providers and the CLI dashboard.
4
+ *
5
+ * Resolution is layered, most-trusted first:
6
+ * 1. Runtime-reported — providers that self-report their window (Codex) apply
7
+ * it in `getContextWindowLimit`, ahead of this module.
8
+ * 2. Observed overrides — the last window a provider actually reported for a
9
+ * model, persisted across runs by `observedContextWindows.ts`. Reflects the
10
+ * effective window for this account/tier, which can be well below the
11
+ * model's published maximum.
12
+ * 3. Catalog overrides — `max_input_tokens` from the LiteLLM catalog, hydrated
13
+ * by `pricingCatalog.ts`. Keeps new models correct without a code change.
14
+ * 4. The static table below — offline baseline of last resort.
15
+ *
16
+ * Within that order, match quality comes first: every layer is tried for an
17
+ * exact hit before any layer is tried for a prefix hit. See
18
+ * `getModelContextWindowSize`.
19
+ *
20
+ * Layers 2 and 3 are pushed in by Node-only modules through the `_set*` hooks,
21
+ * so this file stays browser-safe (no node:fs, no fetch) and can be bundled for
22
+ * webviews. Same pattern as `_setPricingOverrides` in `modelInfo.ts`.
4
23
  */
5
24
  /** Default context window size when model is unknown. */
6
25
  export declare const DEFAULT_CONTEXT_WINDOW = 200000;
26
+ /**
27
+ * Internal: replace the observed-context-window map. Called by
28
+ * `observedContextWindows.ts` on load and on each newly observed value.
29
+ * Not part of the stable public API.
30
+ */
31
+ export declare function _setObservedContextWindows(overrides: Record<string, number>): void;
32
+ /** Internal: snapshot observed overrides (test + diagnostics). */
33
+ export declare function _getObservedContextWindows(): Record<string, number>;
34
+ /** Internal: clear observed overrides (used by tests). */
35
+ export declare function _clearObservedContextWindows(): void;
36
+ /**
37
+ * Internal: replace the catalog-context-window map. Called by
38
+ * `pricingCatalog.ts` after a successful LiteLLM hydration.
39
+ * Not part of the stable public API.
40
+ */
41
+ export declare function _setCatalogContextWindows(overrides: Record<string, number>): void;
42
+ /** Internal: snapshot catalog overrides (test + diagnostics). */
43
+ export declare function _getCatalogContextWindows(): Record<string, number>;
44
+ /** Internal: clear catalog overrides (used by tests and when disabling hydration). */
45
+ export declare function _clearCatalogContextWindows(): void;
7
46
  /**
8
47
  * Returns the context window size for a model ID.
9
48
  *
10
49
  * Input is trimmed and lowercased before lookup, so padded or mixed-case
11
- * IDs (e.g. "Claude-Opus-4-8 ") resolve without caller-side normalization.
50
+ * IDs (e.g. "Claude-Opus-5 ") resolve without caller-side normalization.
12
51
  *
13
52
  * Lookup order:
14
53
  * 1. Explicit "[1m]" suffix (Claude Code's 1M-variant marker) → 1_000_000
15
- * 2. Exact match against MODEL_CONTEXT_SIZES
16
- * 3. Longest-prefix match (e.g. "claude-opus-4-6-20250414" → "claude-opus-4-6")
54
+ * 2. Exact match, most-trusted layer first: observed → catalog → static
55
+ * 3. Longest-prefix match, same layer order
56
+ * (e.g. "claude-opus-4-6-20250414" → "claude-opus-4-6")
17
57
  * 4. DEFAULT_CONTEXT_WINDOW
58
+ *
59
+ * Match quality outranks layer trust: an exact hit in *any* layer beats a
60
+ * prefix guess in *every* layer. Resolving each layer fully before moving on
61
+ * would let a catalog key that merely happens to be a prefix shadow a curated
62
+ * static entry — e.g. the catalog has no "claude-sonnet-4-7", and prefix-first
63
+ * would hand it "claude-sonnet-4" (128K, a GitHub Copilot deployment) instead
64
+ * of the static table's exact 1M.
18
65
  */
19
66
  export declare function getModelContextWindowSize(modelId?: string): number;
@@ -2,17 +2,44 @@
2
2
  /**
3
3
  * Centralized model ID → context window size lookup.
4
4
  * Single source of truth used by all providers and the CLI dashboard.
5
+ *
6
+ * Resolution is layered, most-trusted first:
7
+ * 1. Runtime-reported — providers that self-report their window (Codex) apply
8
+ * it in `getContextWindowLimit`, ahead of this module.
9
+ * 2. Observed overrides — the last window a provider actually reported for a
10
+ * model, persisted across runs by `observedContextWindows.ts`. Reflects the
11
+ * effective window for this account/tier, which can be well below the
12
+ * model's published maximum.
13
+ * 3. Catalog overrides — `max_input_tokens` from the LiteLLM catalog, hydrated
14
+ * by `pricingCatalog.ts`. Keeps new models correct without a code change.
15
+ * 4. The static table below — offline baseline of last resort.
16
+ *
17
+ * Within that order, match quality comes first: every layer is tried for an
18
+ * exact hit before any layer is tried for a prefix hit. See
19
+ * `getModelContextWindowSize`.
20
+ *
21
+ * Layers 2 and 3 are pushed in by Node-only modules through the `_set*` hooks,
22
+ * so this file stays browser-safe (no node:fs, no fetch) and can be bundled for
23
+ * webviews. Same pattern as `_setPricingOverrides` in `modelInfo.ts`.
5
24
  */
6
25
  Object.defineProperty(exports, "__esModule", { value: true });
7
26
  exports.DEFAULT_CONTEXT_WINDOW = void 0;
27
+ exports._setObservedContextWindows = _setObservedContextWindows;
28
+ exports._getObservedContextWindows = _getObservedContextWindows;
29
+ exports._clearObservedContextWindows = _clearObservedContextWindows;
30
+ exports._setCatalogContextWindows = _setCatalogContextWindows;
31
+ exports._getCatalogContextWindows = _getCatalogContextWindows;
32
+ exports._clearCatalogContextWindows = _clearCatalogContextWindows;
8
33
  exports.getModelContextWindowSize = getModelContextWindowSize;
9
34
  /** Known model context window sizes (in tokens). */
10
35
  const MODEL_CONTEXT_SIZES = {
11
36
  // Claude — native 1M context (Fable 5, Opus 4.6+, Sonnet 4.6+)
12
37
  'claude-fable-5': 1_000_000,
38
+ 'claude-opus-5': 1_000_000,
13
39
  'claude-opus-4-8': 1_000_000,
14
40
  'claude-opus-4-7': 1_000_000,
15
41
  'claude-opus-4-6': 1_000_000,
42
+ 'claude-sonnet-5': 1_000_000,
16
43
  'claude-sonnet-4-7': 1_000_000,
17
44
  'claude-sonnet-4-6': 1_000_000,
18
45
  // Claude 4 family
@@ -30,7 +57,15 @@ const MODEL_CONTEXT_SIZES = {
30
57
  // OpenAI GPT-4.1 series (1M context)
31
58
  'gpt-4.1': 1_048_576,
32
59
  // OpenAI GPT-5 series (keys sorted longest-first below; explicit entries
33
- // for every variant so prefix matching can't misclassify a new one)
60
+ // for every variant so prefix matching can't misclassify a new one).
61
+ // These are published model maxima. Codex reports a smaller *effective*
62
+ // window per account tier via token_count; that value wins through the
63
+ // observed-override layer.
64
+ 'gpt-5.6-sol': 1_050_000,
65
+ 'gpt-5.6-terra': 1_050_000,
66
+ 'gpt-5.6-luna': 1_050_000,
67
+ 'gpt-5.6': 1_050_000,
68
+ 'gpt-5.5': 1_050_000,
34
69
  'gpt-5.4': 1_050_000,
35
70
  'gpt-5.3-codex-spark': 128_000,
36
71
  'gpt-5.3-codex': 400_000,
@@ -49,20 +84,100 @@ const MODEL_CONTEXT_SIZES = {
49
84
  deepseek: 128_000,
50
85
  };
51
86
  /** Prefix keys sorted longest-first for correct prefix matching. */
52
- const SORTED_KEYS = Object.keys(MODEL_CONTEXT_SIZES).sort((a, b) => b.length - a.length);
87
+ const SORTED_KEYS = sortKeysLongestFirst(MODEL_CONTEXT_SIZES);
53
88
  /** Default context window size when model is unknown. */
54
89
  exports.DEFAULT_CONTEXT_WINDOW = 200_000;
90
+ // ── Override Tables (populated at runtime by Node-only modules) ──
91
+ let observedTable = {};
92
+ let observedSortedKeys = [];
93
+ let catalogTable = {};
94
+ let catalogSortedKeys = [];
95
+ function sortKeysLongestFirst(table) {
96
+ return Object.keys(table).sort((a, b) => b.length - a.length);
97
+ }
98
+ /**
99
+ * Lowercase keys so lookups match regardless of how the source spelled them.
100
+ * The LiteLLM catalog publishes some IDs mixed-case; the static table and the
101
+ * lookup path are both lowercase. Drops non-positive and non-finite values.
102
+ */
103
+ function normalizeOverrides(overrides) {
104
+ const out = {};
105
+ for (const [key, value] of Object.entries(overrides)) {
106
+ if (!Number.isFinite(value) || value <= 0)
107
+ continue;
108
+ out[key.trim().toLowerCase()] = value;
109
+ }
110
+ return out;
111
+ }
112
+ /**
113
+ * Internal: replace the observed-context-window map. Called by
114
+ * `observedContextWindows.ts` on load and on each newly observed value.
115
+ * Not part of the stable public API.
116
+ */
117
+ function _setObservedContextWindows(overrides) {
118
+ observedTable = normalizeOverrides(overrides);
119
+ observedSortedKeys = sortKeysLongestFirst(observedTable);
120
+ }
121
+ /** Internal: snapshot observed overrides (test + diagnostics). */
122
+ function _getObservedContextWindows() {
123
+ return { ...observedTable };
124
+ }
125
+ /** Internal: clear observed overrides (used by tests). */
126
+ function _clearObservedContextWindows() {
127
+ observedTable = {};
128
+ observedSortedKeys = [];
129
+ }
130
+ /**
131
+ * Internal: replace the catalog-context-window map. Called by
132
+ * `pricingCatalog.ts` after a successful LiteLLM hydration.
133
+ * Not part of the stable public API.
134
+ */
135
+ function _setCatalogContextWindows(overrides) {
136
+ catalogTable = normalizeOverrides(overrides);
137
+ catalogSortedKeys = sortKeysLongestFirst(catalogTable);
138
+ }
139
+ /** Internal: snapshot catalog overrides (test + diagnostics). */
140
+ function _getCatalogContextWindows() {
141
+ return { ...catalogTable };
142
+ }
143
+ /** Internal: clear catalog overrides (used by tests and when disabling hydration). */
144
+ function _clearCatalogContextWindows() {
145
+ catalogTable = {};
146
+ catalogSortedKeys = [];
147
+ }
148
+ // ── Lookup ──
149
+ /** Exact match against one table. */
150
+ function exactWindow(table, modelId) {
151
+ const hit = table[modelId];
152
+ return hit === undefined ? null : hit;
153
+ }
154
+ /** Longest-prefix match against one table. */
155
+ function prefixWindow(table, sortedKeys, modelId) {
156
+ for (const key of sortedKeys) {
157
+ if (modelId.startsWith(key))
158
+ return table[key];
159
+ }
160
+ return null;
161
+ }
55
162
  /**
56
163
  * Returns the context window size for a model ID.
57
164
  *
58
165
  * Input is trimmed and lowercased before lookup, so padded or mixed-case
59
- * IDs (e.g. "Claude-Opus-4-8 ") resolve without caller-side normalization.
166
+ * IDs (e.g. "Claude-Opus-5 ") resolve without caller-side normalization.
60
167
  *
61
168
  * Lookup order:
62
169
  * 1. Explicit "[1m]" suffix (Claude Code's 1M-variant marker) → 1_000_000
63
- * 2. Exact match against MODEL_CONTEXT_SIZES
64
- * 3. Longest-prefix match (e.g. "claude-opus-4-6-20250414" → "claude-opus-4-6")
170
+ * 2. Exact match, most-trusted layer first: observed → catalog → static
171
+ * 3. Longest-prefix match, same layer order
172
+ * (e.g. "claude-opus-4-6-20250414" → "claude-opus-4-6")
65
173
  * 4. DEFAULT_CONTEXT_WINDOW
174
+ *
175
+ * Match quality outranks layer trust: an exact hit in *any* layer beats a
176
+ * prefix guess in *every* layer. Resolving each layer fully before moving on
177
+ * would let a catalog key that merely happens to be a prefix shadow a curated
178
+ * static entry — e.g. the catalog has no "claude-sonnet-4-7", and prefix-first
179
+ * would hand it "claude-sonnet-4" (128K, a GitHub Copilot deployment) instead
180
+ * of the static table's exact 1M.
66
181
  */
67
182
  function getModelContextWindowSize(modelId) {
68
183
  if (!modelId)
@@ -72,21 +187,24 @@ function getModelContextWindowSize(modelId) {
72
187
  if (/\[1m\]/i.test(modelId))
73
188
  return 1_000_000;
74
189
  // Strip the suffix if present, so the normal lookup still succeeds when
75
- // a caller passes e.g. "claude-opus-4-7[1m]" and we've already handled it.
190
+ // a caller passes e.g. "claude-opus-5[1m]" and we've already handled it.
76
191
  // Trim/lowercase so padded or mixed-case IDs match the lowercase table keys.
77
192
  const normalized = modelId
78
193
  .replace(/\[1m\]/gi, '')
79
194
  .trim()
80
195
  .toLowerCase();
81
- // Exact match
82
- if (MODEL_CONTEXT_SIZES[normalized] !== undefined) {
83
- return MODEL_CONTEXT_SIZES[normalized];
84
- }
85
- // Longest-prefix match
86
- for (const key of SORTED_KEYS) {
87
- if (normalized.startsWith(key)) {
88
- return MODEL_CONTEXT_SIZES[key];
89
- }
90
- }
196
+ // Pass 1 — exact matches, most-trusted layer first.
197
+ const exact = exactWindow(observedTable, normalized) ??
198
+ exactWindow(catalogTable, normalized) ??
199
+ exactWindow(MODEL_CONTEXT_SIZES, normalized);
200
+ if (exact !== null)
201
+ return exact;
202
+ // Pass 2 — longest-prefix matches, same layer order. Resolves versioned IDs
203
+ // that no layer spells out, e.g. "claude-opus-4-6-20250414".
204
+ const prefix = prefixWindow(observedTable, observedSortedKeys, normalized) ??
205
+ prefixWindow(catalogTable, catalogSortedKeys, normalized) ??
206
+ prefixWindow(MODEL_CONTEXT_SIZES, SORTED_KEYS, normalized);
207
+ if (prefix !== null)
208
+ return prefix;
91
209
  return exports.DEFAULT_CONTEXT_WINDOW;
92
210
  }
package/dist/modelInfo.js CHANGED
@@ -76,6 +76,17 @@ const PRICING_TABLE = {
76
76
  cacheWriteCostPerMillion: 1.0,
77
77
  cacheReadCostPerMillion: 0.08,
78
78
  },
79
+ // Sonnet 5 carries the standard rate here. The introductory $2/$10 (through
80
+ // 2026-08-31) arrives via catalog hydration, which supersedes this baseline.
81
+ // Sonnet 5 is $2/$10, not the $3/$15 every earlier Sonnet charged, and it
82
+ // carries no >200K surcharge the way Sonnet 4.5 does. The catalog agrees
83
+ // across all eleven of its Sonnet 5 entries.
84
+ 'claude-sonnet-5': {
85
+ inputCostPerMillion: 2.0,
86
+ outputCostPerMillion: 10.0,
87
+ cacheWriteCostPerMillion: 2.5,
88
+ cacheReadCostPerMillion: 0.2,
89
+ },
79
90
  'claude-sonnet-4-6': {
80
91
  inputCostPerMillion: 3.0,
81
92
  outputCostPerMillion: 15.0,
@@ -106,6 +117,12 @@ const PRICING_TABLE = {
106
117
  cacheWriteCostPerMillion: 3.75,
107
118
  cacheReadCostPerMillion: 0.3,
108
119
  },
120
+ 'claude-opus-5': {
121
+ inputCostPerMillion: 5.0,
122
+ outputCostPerMillion: 25.0,
123
+ cacheWriteCostPerMillion: 6.25,
124
+ cacheReadCostPerMillion: 0.5,
125
+ },
109
126
  'claude-opus-4-8': {
110
127
  inputCostPerMillion: 5.0,
111
128
  outputCostPerMillion: 25.0,
@@ -162,6 +179,21 @@ const PRICING_TABLE = {
162
179
  cacheReadCostPerMillion: 1.5,
163
180
  },
164
181
  // ── OpenAI: GPT-4.x family ──
182
+ // The `-tts`, `-transcribe`, and `-realtime-preview` variants also bill above
183
+ // the key they inherit, but no coding agent routes to them, so they are left
184
+ // out rather than carried here. Catalog hydration still prices them correctly.
185
+ 'gpt-4.1-mini': {
186
+ inputCostPerMillion: 0.4,
187
+ outputCostPerMillion: 1.6,
188
+ cacheWriteCostPerMillion: 0,
189
+ cacheReadCostPerMillion: 0.1,
190
+ },
191
+ 'gpt-4.1-nano': {
192
+ inputCostPerMillion: 0.1,
193
+ outputCostPerMillion: 0.4,
194
+ cacheWriteCostPerMillion: 0,
195
+ cacheReadCostPerMillion: 0.025,
196
+ },
165
197
  'gpt-4.1': {
166
198
  inputCostPerMillion: 2.0,
167
199
  outputCostPerMillion: 8.0,
@@ -174,6 +206,14 @@ const PRICING_TABLE = {
174
206
  cacheWriteCostPerMillion: 0,
175
207
  cacheReadCostPerMillion: 0.075,
176
208
  },
209
+ // The launch snapshot kept its original $5/$15 when gpt-4o was cut to
210
+ // $2.50/$10, so a pinned id must not read the current rate.
211
+ 'gpt-4o-2024-05-13': {
212
+ inputCostPerMillion: 5.0,
213
+ outputCostPerMillion: 15.0,
214
+ cacheWriteCostPerMillion: 0,
215
+ cacheReadCostPerMillion: 0,
216
+ },
177
217
  'gpt-4o': {
178
218
  inputCostPerMillion: 2.5,
179
219
  outputCostPerMillion: 10.0,
@@ -186,27 +226,132 @@ const PRICING_TABLE = {
186
226
  cacheWriteCostPerMillion: 0,
187
227
  cacheReadCostPerMillion: 0,
188
228
  },
189
- // ── OpenAI: GPT-5 family (baseline estimate; LiteLLM catalog overrides) ──
190
- // Codex emits `gpt-5.4`, `gpt-5.3-codex`, `gpt-5`. Anchoring the static
191
- // estimate to gpt-4o-tier rates so offline users see a reasonable ballpark.
229
+ // ── OpenAI: GPT-5 family ──
230
+ // Published rates, verified against the LiteLLM catalog. Runtime hydration
231
+ // still supersedes these; they are the offline baseline.
232
+ //
233
+ // Every tier that ships at a rate of its own needs an entry of its own. Keys
234
+ // are matched longest-first, so a `-mini`/`-nano`/`-pro` variant with no entry
235
+ // silently inherits its base model's rate and reports a wrong number rather
236
+ // than none — `gpt-5-nano` read 25x its true cost this way.
237
+ 'gpt-5.6-sol': {
238
+ inputCostPerMillion: 5.0,
239
+ outputCostPerMillion: 30.0,
240
+ cacheWriteCostPerMillion: 6.25,
241
+ cacheReadCostPerMillion: 0.5,
242
+ },
243
+ 'gpt-5.6-terra': {
244
+ inputCostPerMillion: 2.5,
245
+ outputCostPerMillion: 15.0,
246
+ cacheWriteCostPerMillion: 3.13,
247
+ cacheReadCostPerMillion: 0.25,
248
+ },
249
+ 'gpt-5.6-luna': {
250
+ inputCostPerMillion: 1.0,
251
+ outputCostPerMillion: 6.0,
252
+ cacheWriteCostPerMillion: 1.25,
253
+ cacheReadCostPerMillion: 0.1,
254
+ },
255
+ 'gpt-5.6': {
256
+ inputCostPerMillion: 5.0,
257
+ outputCostPerMillion: 30.0,
258
+ cacheWriteCostPerMillion: 6.25,
259
+ cacheReadCostPerMillion: 0.5,
260
+ },
261
+ 'gpt-5.5-pro': {
262
+ inputCostPerMillion: 30.0,
263
+ outputCostPerMillion: 180.0,
264
+ cacheWriteCostPerMillion: 0,
265
+ cacheReadCostPerMillion: 3.0,
266
+ },
267
+ 'gpt-5.5': {
268
+ inputCostPerMillion: 5.0,
269
+ outputCostPerMillion: 30.0,
270
+ cacheWriteCostPerMillion: 0,
271
+ cacheReadCostPerMillion: 0.5,
272
+ },
273
+ 'gpt-5.4-mini': {
274
+ inputCostPerMillion: 0.75,
275
+ outputCostPerMillion: 4.5,
276
+ cacheWriteCostPerMillion: 0,
277
+ cacheReadCostPerMillion: 0.07,
278
+ },
279
+ 'gpt-5.4-nano': {
280
+ inputCostPerMillion: 0.2,
281
+ outputCostPerMillion: 1.25,
282
+ cacheWriteCostPerMillion: 0,
283
+ cacheReadCostPerMillion: 0.02,
284
+ },
285
+ 'gpt-5.4-pro': {
286
+ inputCostPerMillion: 30.0,
287
+ outputCostPerMillion: 180.0,
288
+ cacheWriteCostPerMillion: 0,
289
+ cacheReadCostPerMillion: 3.0,
290
+ },
192
291
  'gpt-5.4': {
193
- inputCostPerMillion: 1.25,
194
- outputCostPerMillion: 10.0,
292
+ inputCostPerMillion: 2.5,
293
+ outputCostPerMillion: 15.0,
195
294
  cacheWriteCostPerMillion: 0,
196
- cacheReadCostPerMillion: 0.125,
295
+ cacheReadCostPerMillion: 0.25,
197
296
  },
198
297
  'gpt-5.3-codex': {
199
- inputCostPerMillion: 1.25,
200
- outputCostPerMillion: 10.0,
298
+ inputCostPerMillion: 1.75,
299
+ outputCostPerMillion: 14.0,
201
300
  cacheWriteCostPerMillion: 0,
202
- cacheReadCostPerMillion: 0.125,
301
+ cacheReadCostPerMillion: 0.175,
302
+ },
303
+ 'gpt-5.3-chat-latest': {
304
+ inputCostPerMillion: 1.75,
305
+ outputCostPerMillion: 14.0,
306
+ cacheWriteCostPerMillion: 0,
307
+ cacheReadCostPerMillion: 0.175,
203
308
  },
309
+ // No bare `gpt-5.3` ships in the catalog; both real variants above are
310
+ // $1.75/$14. This entry stays an estimate and only answers ids that match
311
+ // neither variant.
204
312
  'gpt-5.3': {
205
313
  inputCostPerMillion: 1.25,
206
314
  outputCostPerMillion: 10.0,
207
315
  cacheWriteCostPerMillion: 0,
208
316
  cacheReadCostPerMillion: 0.125,
209
317
  },
318
+ 'gpt-5.2-pro': {
319
+ inputCostPerMillion: 21.0,
320
+ outputCostPerMillion: 168.0,
321
+ cacheWriteCostPerMillion: 0,
322
+ cacheReadCostPerMillion: 0,
323
+ },
324
+ // Also answers `gpt-5.2-codex` and `gpt-5.2-chat*`, which bill at this rate.
325
+ 'gpt-5.2': {
326
+ inputCostPerMillion: 1.75,
327
+ outputCostPerMillion: 14.0,
328
+ cacheWriteCostPerMillion: 0,
329
+ cacheReadCostPerMillion: 0.175,
330
+ },
331
+ 'gpt-5.1-codex-mini': {
332
+ inputCostPerMillion: 0.25,
333
+ outputCostPerMillion: 2.0,
334
+ cacheWriteCostPerMillion: 0,
335
+ cacheReadCostPerMillion: 0.025,
336
+ },
337
+ 'gpt-5-pro': {
338
+ inputCostPerMillion: 15.0,
339
+ outputCostPerMillion: 120.0,
340
+ cacheWriteCostPerMillion: 0,
341
+ cacheReadCostPerMillion: 0,
342
+ },
343
+ 'gpt-5-mini': {
344
+ inputCostPerMillion: 0.25,
345
+ outputCostPerMillion: 2.0,
346
+ cacheWriteCostPerMillion: 0,
347
+ cacheReadCostPerMillion: 0.025,
348
+ },
349
+ 'gpt-5-nano': {
350
+ inputCostPerMillion: 0.05,
351
+ outputCostPerMillion: 0.4,
352
+ cacheWriteCostPerMillion: 0,
353
+ cacheReadCostPerMillion: 0.005,
354
+ },
210
355
  'gpt-5': {
211
356
  inputCostPerMillion: 1.25,
212
357
  outputCostPerMillion: 10.0,
@@ -214,23 +359,46 @@ const PRICING_TABLE = {
214
359
  cacheReadCostPerMillion: 0.125,
215
360
  },
216
361
  // ── OpenAI: o-series (reasoning models) ──
362
+ // The `-pro` and deep-research tiers cost an order of magnitude more than the
363
+ // base model whose key they would otherwise match.
364
+ 'o3-deep-research': {
365
+ inputCostPerMillion: 10.0,
366
+ outputCostPerMillion: 40.0,
367
+ cacheWriteCostPerMillion: 0,
368
+ cacheReadCostPerMillion: 2.5,
369
+ },
217
370
  'o3-mini': {
218
371
  inputCostPerMillion: 1.1,
219
372
  outputCostPerMillion: 4.4,
220
373
  cacheWriteCostPerMillion: 0,
221
374
  cacheReadCostPerMillion: 0.55,
222
375
  },
376
+ 'o3-pro': {
377
+ inputCostPerMillion: 20.0,
378
+ outputCostPerMillion: 80.0,
379
+ cacheWriteCostPerMillion: 0,
380
+ cacheReadCostPerMillion: 0,
381
+ },
223
382
  o3: {
224
383
  inputCostPerMillion: 2.0,
225
384
  outputCostPerMillion: 8.0,
226
385
  cacheWriteCostPerMillion: 0,
227
386
  cacheReadCostPerMillion: 0.5,
228
387
  },
388
+ // OpenAI cut o1-mini to $1.10/$4.40 after launch; $3/$12 was the launch rate.
389
+ // No bare catalog key settles it — Azure's $1.21/$4.84 is its usual 1.1x
390
+ // uplift on exactly this number, and Replicate's OpenAI passthrough agrees.
229
391
  'o1-mini': {
230
- inputCostPerMillion: 3.0,
231
- outputCostPerMillion: 12.0,
392
+ inputCostPerMillion: 1.1,
393
+ outputCostPerMillion: 4.4,
232
394
  cacheWriteCostPerMillion: 0,
233
- cacheReadCostPerMillion: 1.5,
395
+ cacheReadCostPerMillion: 0.55,
396
+ },
397
+ 'o1-pro': {
398
+ inputCostPerMillion: 150.0,
399
+ outputCostPerMillion: 600.0,
400
+ cacheWriteCostPerMillion: 0,
401
+ cacheReadCostPerMillion: 0,
234
402
  },
235
403
  o1: {
236
404
  inputCostPerMillion: 15.0,
package/dist/node.d.ts CHANGED
@@ -5,8 +5,10 @@
5
5
  * and `node:path`; safe for extension-host and CLI consumers. Not safe
6
6
  * for browser bundles — use `sidekick-shared/browser` there.
7
7
  */
8
- export { hydratePricingCatalog, normalizeLiteLlmCatalog, LITELLM_CATALOG_URL, } from './pricingCatalog';
8
+ export { hydratePricingCatalog, normalizeLiteLlmCatalog, normalizeLiteLlmContextWindows, LITELLM_CATALOG_URL, } from './pricingCatalog';
9
9
  export type { HydrateOptions, HydrateResult } from './pricingCatalog';
10
+ export { loadObservedContextWindows, recordObservedContextWindow, getObservedContextWindowPath, } from './observedContextWindows';
11
+ export type { ObservedContextWindowOptions, ObservedContextWindowStore, } from './observedContextWindows';
10
12
  export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
11
13
  export type { ProviderSessionIndex, ListRecentSessionsOptions } from './sessionTranscripts';
12
14
  export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, } from './observedSessionCollector';
package/dist/node.js CHANGED
@@ -7,11 +7,16 @@
7
7
  * for browser bundles — use `sidekick-shared/browser` there.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.fileFingerprint = exports.observedSessionSourceFromProvider = exports.ObservedSessionCollector = exports.readSessionTranscript = exports.listRecentSessions = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = void 0;
10
+ exports.fileFingerprint = exports.observedSessionSourceFromProvider = exports.ObservedSessionCollector = exports.readSessionTranscript = exports.listRecentSessions = exports.getObservedContextWindowPath = exports.recordObservedContextWindow = exports.loadObservedContextWindows = exports.LITELLM_CATALOG_URL = exports.normalizeLiteLlmContextWindows = exports.normalizeLiteLlmCatalog = exports.hydratePricingCatalog = void 0;
11
11
  var pricingCatalog_1 = require("./pricingCatalog");
12
12
  Object.defineProperty(exports, "hydratePricingCatalog", { enumerable: true, get: function () { return pricingCatalog_1.hydratePricingCatalog; } });
13
13
  Object.defineProperty(exports, "normalizeLiteLlmCatalog", { enumerable: true, get: function () { return pricingCatalog_1.normalizeLiteLlmCatalog; } });
14
+ Object.defineProperty(exports, "normalizeLiteLlmContextWindows", { enumerable: true, get: function () { return pricingCatalog_1.normalizeLiteLlmContextWindows; } });
14
15
  Object.defineProperty(exports, "LITELLM_CATALOG_URL", { enumerable: true, get: function () { return pricingCatalog_1.LITELLM_CATALOG_URL; } });
16
+ var observedContextWindows_1 = require("./observedContextWindows");
17
+ Object.defineProperty(exports, "loadObservedContextWindows", { enumerable: true, get: function () { return observedContextWindows_1.loadObservedContextWindows; } });
18
+ Object.defineProperty(exports, "recordObservedContextWindow", { enumerable: true, get: function () { return observedContextWindows_1.recordObservedContextWindow; } });
19
+ Object.defineProperty(exports, "getObservedContextWindowPath", { enumerable: true, get: function () { return observedContextWindows_1.getObservedContextWindowPath; } });
15
20
  var sessionTranscripts_1 = require("./sessionTranscripts");
16
21
  Object.defineProperty(exports, "listRecentSessions", { enumerable: true, get: function () { return sessionTranscripts_1.listRecentSessions; } });
17
22
  Object.defineProperty(exports, "readSessionTranscript", { enumerable: true, get: function () { return sessionTranscripts_1.readSessionTranscript; } });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Persisted record of context windows providers actually reported (Node-only).
3
+ *
4
+ * Some agents report the real context window for the running session — Codex
5
+ * emits `model_context_window` on every `token_count` event. That number is the
6
+ * *effective* window for the account and tier, which can sit well below the
7
+ * model's published maximum (e.g. 258,400 observed for `gpt-5.6-sol` against a
8
+ * catalog maximum of 1,050,000).
9
+ *
10
+ * A live session uses that value directly. This module keeps it across runs, so
11
+ * historical and static views don't fall back to a published maximum that would
12
+ * make the context gauge read far too empty.
13
+ *
14
+ * Precedence, most-trusted first: runtime → observed (here) → LiteLLM catalog →
15
+ * static table. See `modelContext.ts`.
16
+ *
17
+ * @module observedContextWindows
18
+ */
19
+ export interface ObservedContextWindowOptions {
20
+ /** Directory for the store (defaults to the Sidekick config dir). */
21
+ cacheDir?: string;
22
+ /** Optional logger (useful for diagnostics without pulling a logger dep). */
23
+ logger?: (msg: string) => void;
24
+ }
25
+ export interface ObservedContextWindowEntry {
26
+ contextWindow: number;
27
+ /** ISO timestamp of the most recent observation. */
28
+ observedAt: string;
29
+ }
30
+ export interface ObservedContextWindowStore {
31
+ version: 1;
32
+ models: Record<string, ObservedContextWindowEntry>;
33
+ }
34
+ /** Resolve the on-disk store path. */
35
+ export declare function getObservedContextWindowPath(cacheDir?: string): string;
36
+ /**
37
+ * Load persisted observations and apply them as context-window overrides.
38
+ *
39
+ * Always resolves — never throws. A missing, unreadable, or malformed store
40
+ * leaves the overrides empty and lookups fall through to catalog/static.
41
+ */
42
+ export declare function loadObservedContextWindows(options?: ObservedContextWindowOptions): Promise<Record<string, number>>;
43
+ /**
44
+ * Record a provider-reported context window for a model.
45
+ *
46
+ * Applies in memory immediately, then persists — but only when the value
47
+ * actually changed. Codex reports a window on every `token_count` event, so an
48
+ * unconditional write would mean disk I/O many times per session for a value
49
+ * that almost never moves.
50
+ *
51
+ * Always resolves — never throws. Persistence failures are logged and dropped;
52
+ * the in-memory override still stands for this process.
53
+ */
54
+ export declare function recordObservedContextWindow(modelId: string, contextWindow: number, options?: ObservedContextWindowOptions): Promise<void>;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ /**
3
+ * Persisted record of context windows providers actually reported (Node-only).
4
+ *
5
+ * Some agents report the real context window for the running session — Codex
6
+ * emits `model_context_window` on every `token_count` event. That number is the
7
+ * *effective* window for the account and tier, which can sit well below the
8
+ * model's published maximum (e.g. 258,400 observed for `gpt-5.6-sol` against a
9
+ * catalog maximum of 1,050,000).
10
+ *
11
+ * A live session uses that value directly. This module keeps it across runs, so
12
+ * historical and static views don't fall back to a published maximum that would
13
+ * make the context gauge read far too empty.
14
+ *
15
+ * Precedence, most-trusted first: runtime → observed (here) → LiteLLM catalog →
16
+ * static table. See `modelContext.ts`.
17
+ *
18
+ * @module observedContextWindows
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ exports.getObservedContextWindowPath = getObservedContextWindowPath;
55
+ exports.loadObservedContextWindows = loadObservedContextWindows;
56
+ exports.recordObservedContextWindow = recordObservedContextWindow;
57
+ const node_fs_1 = require("node:fs");
58
+ const path = __importStar(require("node:path"));
59
+ const modelContext_1 = require("./modelContext");
60
+ const paths_1 = require("./paths");
61
+ const atomic_1 = require("./writers/atomic");
62
+ // ── Constants ──
63
+ const STORE_FILE_NAME = 'observed-context-windows.json';
64
+ const STORE_VERSION = 1;
65
+ /** Resolve the on-disk store path. */
66
+ function getObservedContextWindowPath(cacheDir) {
67
+ return path.join(cacheDir ?? (0, paths_1.getConfigDir)(), STORE_FILE_NAME);
68
+ }
69
+ function emptyStore() {
70
+ return { version: STORE_VERSION, models: {} };
71
+ }
72
+ /** Keep only well-formed entries; a hand-edited or partly-corrupt file shouldn't poison lookups. */
73
+ function toOverrideMap(store) {
74
+ const out = {};
75
+ for (const [modelId, entry] of Object.entries(store.models ?? {})) {
76
+ const window = entry?.contextWindow;
77
+ if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
78
+ continue;
79
+ out[modelId] = window;
80
+ }
81
+ return out;
82
+ }
83
+ // ── Public API ──
84
+ /**
85
+ * Load persisted observations and apply them as context-window overrides.
86
+ *
87
+ * Always resolves — never throws. A missing, unreadable, or malformed store
88
+ * leaves the overrides empty and lookups fall through to catalog/static.
89
+ */
90
+ async function loadObservedContextWindows(options = {}) {
91
+ const storePath = getObservedContextWindowPath(options.cacheDir);
92
+ try {
93
+ const raw = await node_fs_1.promises.readFile(storePath, 'utf8');
94
+ const parsed = JSON.parse(raw);
95
+ if (!parsed || typeof parsed !== 'object' || !parsed.models)
96
+ return {};
97
+ const overrides = toOverrideMap(parsed);
98
+ (0, modelContext_1._setObservedContextWindows)(overrides);
99
+ return overrides;
100
+ }
101
+ catch (err) {
102
+ const code = err.code;
103
+ if (code !== 'ENOENT') {
104
+ options.logger?.(`observedContextWindows: failed to load ${storePath}: ${String(err)}`);
105
+ }
106
+ return {};
107
+ }
108
+ }
109
+ /**
110
+ * Record a provider-reported context window for a model.
111
+ *
112
+ * Applies in memory immediately, then persists — but only when the value
113
+ * actually changed. Codex reports a window on every `token_count` event, so an
114
+ * unconditional write would mean disk I/O many times per session for a value
115
+ * that almost never moves.
116
+ *
117
+ * Always resolves — never throws. Persistence failures are logged and dropped;
118
+ * the in-memory override still stands for this process.
119
+ */
120
+ async function recordObservedContextWindow(modelId, contextWindow, options = {}) {
121
+ const normalizedId = modelId?.trim().toLowerCase();
122
+ if (!normalizedId)
123
+ return;
124
+ if (!Number.isFinite(contextWindow) || contextWindow <= 0)
125
+ return;
126
+ const current = (0, modelContext_1._getObservedContextWindows)();
127
+ if (current[normalizedId] === contextWindow)
128
+ return;
129
+ (0, modelContext_1._setObservedContextWindows)({ ...current, [normalizedId]: contextWindow });
130
+ const storePath = getObservedContextWindowPath(options.cacheDir);
131
+ try {
132
+ // Lock-guarded read-modify-write: the CLI and the extension host can both
133
+ // be observing sessions at once, and each should merge rather than clobber.
134
+ await (0, atomic_1.updateJsonStoreAtomic)(storePath, emptyStore, (latest) => {
135
+ const models = latest?.models ?? {};
136
+ if (models[normalizedId]?.contextWindow === contextWindow)
137
+ return latest;
138
+ return {
139
+ version: STORE_VERSION,
140
+ models: {
141
+ ...models,
142
+ [normalizedId]: { contextWindow, observedAt: new Date().toISOString() },
143
+ },
144
+ };
145
+ });
146
+ }
147
+ catch (err) {
148
+ options.logger?.(`observedContextWindows: failed to persist ${storePath}: ${String(err)}`);
149
+ }
150
+ }
@@ -36,6 +36,8 @@ export interface HydrateResult {
36
36
  source: 'cache' | 'network' | 'offline';
37
37
  /** Number of pricing entries applied to the override map. */
38
38
  entries: number;
39
+ /** Number of context-window entries applied to the override map. */
40
+ contextWindowEntries: number;
39
41
  /** ISO timestamp of the fetch that produced the cache. */
40
42
  fetchedAt: string;
41
43
  }
@@ -56,3 +58,13 @@ export declare function hydratePricingCatalog(options: HydrateOptions): Promise<
56
58
  * Exported for tests; rarely called directly.
57
59
  */
58
60
  export declare function normalizeLiteLlmCatalog(raw: unknown): Record<string, ModelPricing>;
61
+ /**
62
+ * Convert a LiteLLM catalog payload to a model → context-window override map.
63
+ *
64
+ * Kept separate from `normalizeLiteLlmCatalog` rather than folded into its
65
+ * return type: that function is public API on a published package, and pricing
66
+ * and context coverage differ (an entry can have one without the other).
67
+ *
68
+ * Exported for tests; rarely called directly.
69
+ */
70
+ export declare function normalizeLiteLlmContextWindows(raw: unknown): Record<string, number>;
@@ -54,8 +54,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
54
54
  exports.LITELLM_CATALOG_URL = void 0;
55
55
  exports.hydratePricingCatalog = hydratePricingCatalog;
56
56
  exports.normalizeLiteLlmCatalog = normalizeLiteLlmCatalog;
57
+ exports.normalizeLiteLlmContextWindows = normalizeLiteLlmContextWindows;
57
58
  const node_fs_1 = require("node:fs");
58
59
  const path = __importStar(require("node:path"));
60
+ const modelContext_1 = require("./modelContext");
59
61
  const modelInfo_1 = require("./modelInfo");
60
62
  // ── Constants ──
61
63
  exports.LITELLM_CATALOG_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json';
@@ -80,43 +82,51 @@ async function hydratePricingCatalog(options) {
80
82
  // 1. Fresh cache?
81
83
  const cached = await readCache(cachePath);
82
84
  if (cached && now - Date.parse(cached.fetchedAt) < ttlMs) {
83
- (0, modelInfo_1._setPricingOverrides)(cached.overrides);
84
- return {
85
- source: 'cache',
86
- entries: Object.keys(cached.overrides).length,
87
- fetchedAt: cached.fetchedAt,
88
- };
85
+ return applyCacheFile(cached, 'cache');
89
86
  }
90
87
  // 2. Try network.
91
88
  if (fetchImpl) {
92
89
  const fetched = await fetchCatalog(url, fetchImpl, timeoutMs, logger);
93
90
  if (fetched) {
94
- const overrides = normalizeLiteLlmCatalog(fetched);
95
91
  const payload = {
96
92
  fetchedAt: new Date(now).toISOString(),
97
93
  url,
98
- overrides,
94
+ overrides: normalizeLiteLlmCatalog(fetched),
95
+ contextWindows: normalizeLiteLlmContextWindows(fetched),
99
96
  };
100
97
  await writeCache(cachePath, payload, logger);
101
- (0, modelInfo_1._setPricingOverrides)(overrides);
102
- return {
103
- source: 'network',
104
- entries: Object.keys(overrides).length,
105
- fetchedAt: payload.fetchedAt,
106
- };
98
+ return applyCacheFile(payload, 'network');
107
99
  }
108
100
  }
109
101
  // 3. Fall back to stale cache if present.
110
102
  if (cached) {
111
- (0, modelInfo_1._setPricingOverrides)(cached.overrides);
112
- return {
113
- source: 'cache',
114
- entries: Object.keys(cached.overrides).length,
115
- fetchedAt: cached.fetchedAt,
116
- };
103
+ return applyCacheFile(cached, 'cache');
117
104
  }
118
- // 4. Nothing we can do — stay on static table.
119
- return { source: 'offline', entries: 0, fetchedAt: new Date(now).toISOString() };
105
+ // 4. Nothing we can do — stay on static tables.
106
+ return {
107
+ source: 'offline',
108
+ entries: 0,
109
+ contextWindowEntries: 0,
110
+ fetchedAt: new Date(now).toISOString(),
111
+ };
112
+ }
113
+ /**
114
+ * Push a cache payload into both override maps and describe what was applied.
115
+ * `contextWindows` is absent on caches written before context hydration; those
116
+ * contribute no overrides rather than clearing whatever is already loaded.
117
+ */
118
+ function applyCacheFile(cache, source) {
119
+ const contextWindows = cache.contextWindows ?? {};
120
+ (0, modelInfo_1._setPricingOverrides)(cache.overrides);
121
+ if (Object.keys(contextWindows).length > 0) {
122
+ (0, modelContext_1._setCatalogContextWindows)(contextWindows);
123
+ }
124
+ return {
125
+ source,
126
+ entries: Object.keys(cache.overrides).length,
127
+ contextWindowEntries: Object.keys(contextWindows).length,
128
+ fetchedAt: cache.fetchedAt,
129
+ };
120
130
  }
121
131
  /**
122
132
  * Convert a LiteLLM catalog payload to our `ModelPricing` override map.
@@ -145,6 +155,57 @@ function normalizeLiteLlmCatalog(raw) {
145
155
  }
146
156
  return out;
147
157
  }
158
+ /**
159
+ * Convert a LiteLLM catalog payload to a model → context-window override map.
160
+ *
161
+ * Kept separate from `normalizeLiteLlmCatalog` rather than folded into its
162
+ * return type: that function is public API on a published package, and pricing
163
+ * and context coverage differ (an entry can have one without the other).
164
+ *
165
+ * Exported for tests; rarely called directly.
166
+ */
167
+ function normalizeLiteLlmContextWindows(raw) {
168
+ const out = {};
169
+ if (!raw || typeof raw !== 'object')
170
+ return out;
171
+ // Bare-model aliases are gathered separately rather than written as we go. A
172
+ // `provider/model` entry describes that *provider's deployment* window, which
173
+ // is often a truncation of the model's own — the live catalog carries five
174
+ // different windows for `claude-sonnet-4`, from GitHub Copilot's 128K to
175
+ // Vertex's 1M. Taking whichever appeared first in the JSON is a coin flip, so
176
+ // an alias is only promoted when every provider mentioning it agrees.
177
+ const aliasWindows = new Map();
178
+ for (const [key, entry] of Object.entries(raw)) {
179
+ // LiteLLM ships a `sample_spec` header entry. Skip it.
180
+ if (key === 'sample_spec')
181
+ continue;
182
+ if (!entry || typeof entry !== 'object')
183
+ continue;
184
+ const window = entry.max_input_tokens;
185
+ if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
186
+ continue;
187
+ out[key] = window;
188
+ const bare = stripProviderPrefix(key);
189
+ if (bare && bare !== key) {
190
+ const seen = aliasWindows.get(bare) ?? new Set();
191
+ seen.add(window);
192
+ aliasWindows.set(bare, seen);
193
+ }
194
+ }
195
+ for (const [bare, windows] of aliasWindows) {
196
+ // A top-level entry for the same ID is authoritative — never shadow it.
197
+ if (out[bare] !== undefined)
198
+ continue;
199
+ // Providers disagree, so none of their numbers can be attributed to the
200
+ // bare model. Record nothing and let a more trusted layer answer.
201
+ if (windows.size !== 1)
202
+ continue;
203
+ const [only] = windows;
204
+ if (typeof only === 'number')
205
+ out[bare] = only;
206
+ }
207
+ return out;
208
+ }
148
209
  // ── Internals ──
149
210
  function liteLlmEntryToPricing(entry) {
150
211
  const input = entry.input_cost_per_token;
@@ -53,6 +53,7 @@ const codexParser_1 = require("../parsers/codexParser");
53
53
  const codexDatabase_1 = require("./codexDatabase");
54
54
  const codexProfiles_1 = require("../codexProfiles");
55
55
  const modelContext_1 = require("../modelContext");
56
+ const observedContextWindows_1 = require("../observedContextWindows");
56
57
  const usageNormalization_1 = require("../usageNormalization");
57
58
  // ---------------------------------------------------------------------------
58
59
  // Helper functions
@@ -510,10 +511,12 @@ class CodexReader {
510
511
  // Skip malformed JSON lines
511
512
  }
512
513
  }
513
- // Propagate model_context_window from token_count events to the provider
514
+ // Propagate model_context_window from token_count events to the provider.
515
+ // The model comes along so the value can be persisted per model — the
516
+ // window Codex reports is tier-specific, not the published maximum.
514
517
  const mcw = this.parser.getModelContextWindow();
515
518
  if (mcw && this.onContextWindowLimit) {
516
- this.onContextWindowLimit(mcw);
519
+ this.onContextWindowLimit(mcw, this.parser.getCurrentModel() ?? undefined);
517
520
  }
518
521
  // Propagate rate_limits from token_count events to the provider
519
522
  const rateLimits = this.parser.getLastRateLimits();
@@ -791,8 +794,15 @@ class CodexProvider {
791
794
  }
792
795
  // --- Data reading ---
793
796
  createReader(sessionPath) {
794
- return new CodexReader(sessionPath, (limit) => {
797
+ return new CodexReader(sessionPath, (limit, modelId) => {
795
798
  this.dynamicContextWindowLimit = limit;
799
+ if (modelId) {
800
+ // Fire-and-forget: persist so historical/static views keep this
801
+ // tier-specific window instead of falling back to the catalog maximum.
802
+ void (0, observedContextWindows_1.recordObservedContextWindow)(modelId, limit).catch(() => {
803
+ /* non-fatal; the in-memory limit above still applies */
804
+ });
805
+ }
796
806
  }, (limits) => {
797
807
  this.lastRateLimitsData = limits;
798
808
  }, (usage) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.24.1",
3
+ "version": "0.24.3",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",