sidekick-shared 0.24.1 → 0.24.2

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,14 @@ 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': 1_050_000,
67
+ 'gpt-5.5': 1_050_000,
34
68
  'gpt-5.4': 1_050_000,
35
69
  'gpt-5.3-codex-spark': 128_000,
36
70
  'gpt-5.3-codex': 400_000,
@@ -49,20 +83,100 @@ const MODEL_CONTEXT_SIZES = {
49
83
  deepseek: 128_000,
50
84
  };
51
85
  /** 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);
86
+ const SORTED_KEYS = sortKeysLongestFirst(MODEL_CONTEXT_SIZES);
53
87
  /** Default context window size when model is unknown. */
54
88
  exports.DEFAULT_CONTEXT_WINDOW = 200_000;
89
+ // ── Override Tables (populated at runtime by Node-only modules) ──
90
+ let observedTable = {};
91
+ let observedSortedKeys = [];
92
+ let catalogTable = {};
93
+ let catalogSortedKeys = [];
94
+ function sortKeysLongestFirst(table) {
95
+ return Object.keys(table).sort((a, b) => b.length - a.length);
96
+ }
97
+ /**
98
+ * Lowercase keys so lookups match regardless of how the source spelled them.
99
+ * The LiteLLM catalog publishes some IDs mixed-case; the static table and the
100
+ * lookup path are both lowercase. Drops non-positive and non-finite values.
101
+ */
102
+ function normalizeOverrides(overrides) {
103
+ const out = {};
104
+ for (const [key, value] of Object.entries(overrides)) {
105
+ if (!Number.isFinite(value) || value <= 0)
106
+ continue;
107
+ out[key.trim().toLowerCase()] = value;
108
+ }
109
+ return out;
110
+ }
111
+ /**
112
+ * Internal: replace the observed-context-window map. Called by
113
+ * `observedContextWindows.ts` on load and on each newly observed value.
114
+ * Not part of the stable public API.
115
+ */
116
+ function _setObservedContextWindows(overrides) {
117
+ observedTable = normalizeOverrides(overrides);
118
+ observedSortedKeys = sortKeysLongestFirst(observedTable);
119
+ }
120
+ /** Internal: snapshot observed overrides (test + diagnostics). */
121
+ function _getObservedContextWindows() {
122
+ return { ...observedTable };
123
+ }
124
+ /** Internal: clear observed overrides (used by tests). */
125
+ function _clearObservedContextWindows() {
126
+ observedTable = {};
127
+ observedSortedKeys = [];
128
+ }
129
+ /**
130
+ * Internal: replace the catalog-context-window map. Called by
131
+ * `pricingCatalog.ts` after a successful LiteLLM hydration.
132
+ * Not part of the stable public API.
133
+ */
134
+ function _setCatalogContextWindows(overrides) {
135
+ catalogTable = normalizeOverrides(overrides);
136
+ catalogSortedKeys = sortKeysLongestFirst(catalogTable);
137
+ }
138
+ /** Internal: snapshot catalog overrides (test + diagnostics). */
139
+ function _getCatalogContextWindows() {
140
+ return { ...catalogTable };
141
+ }
142
+ /** Internal: clear catalog overrides (used by tests and when disabling hydration). */
143
+ function _clearCatalogContextWindows() {
144
+ catalogTable = {};
145
+ catalogSortedKeys = [];
146
+ }
147
+ // ── Lookup ──
148
+ /** Exact match against one table. */
149
+ function exactWindow(table, modelId) {
150
+ const hit = table[modelId];
151
+ return hit === undefined ? null : hit;
152
+ }
153
+ /** Longest-prefix match against one table. */
154
+ function prefixWindow(table, sortedKeys, modelId) {
155
+ for (const key of sortedKeys) {
156
+ if (modelId.startsWith(key))
157
+ return table[key];
158
+ }
159
+ return null;
160
+ }
55
161
  /**
56
162
  * Returns the context window size for a model ID.
57
163
  *
58
164
  * 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.
165
+ * IDs (e.g. "Claude-Opus-5 ") resolve without caller-side normalization.
60
166
  *
61
167
  * Lookup order:
62
168
  * 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")
169
+ * 2. Exact match, most-trusted layer first: observed → catalog → static
170
+ * 3. Longest-prefix match, same layer order
171
+ * (e.g. "claude-opus-4-6-20250414" → "claude-opus-4-6")
65
172
  * 4. DEFAULT_CONTEXT_WINDOW
173
+ *
174
+ * Match quality outranks layer trust: an exact hit in *any* layer beats a
175
+ * prefix guess in *every* layer. Resolving each layer fully before moving on
176
+ * would let a catalog key that merely happens to be a prefix shadow a curated
177
+ * static entry — e.g. the catalog has no "claude-sonnet-4-7", and prefix-first
178
+ * would hand it "claude-sonnet-4" (128K, a GitHub Copilot deployment) instead
179
+ * of the static table's exact 1M.
66
180
  */
67
181
  function getModelContextWindowSize(modelId) {
68
182
  if (!modelId)
@@ -72,21 +186,24 @@ function getModelContextWindowSize(modelId) {
72
186
  if (/\[1m\]/i.test(modelId))
73
187
  return 1_000_000;
74
188
  // 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.
189
+ // a caller passes e.g. "claude-opus-5[1m]" and we've already handled it.
76
190
  // Trim/lowercase so padded or mixed-case IDs match the lowercase table keys.
77
191
  const normalized = modelId
78
192
  .replace(/\[1m\]/gi, '')
79
193
  .trim()
80
194
  .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
- }
195
+ // Pass 1 — exact matches, most-trusted layer first.
196
+ const exact = exactWindow(observedTable, normalized) ??
197
+ exactWindow(catalogTable, normalized) ??
198
+ exactWindow(MODEL_CONTEXT_SIZES, normalized);
199
+ if (exact !== null)
200
+ return exact;
201
+ // Pass 2 — longest-prefix matches, same layer order. Resolves versioned IDs
202
+ // that no layer spells out, e.g. "claude-opus-4-6-20250414".
203
+ const prefix = prefixWindow(observedTable, observedSortedKeys, normalized) ??
204
+ prefixWindow(catalogTable, catalogSortedKeys, normalized) ??
205
+ prefixWindow(MODEL_CONTEXT_SIZES, SORTED_KEYS, normalized);
206
+ if (prefix !== null)
207
+ return prefix;
91
208
  return exports.DEFAULT_CONTEXT_WINDOW;
92
209
  }
package/dist/modelInfo.js CHANGED
@@ -76,6 +76,14 @@ 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
+ 'claude-sonnet-5': {
82
+ inputCostPerMillion: 3.0,
83
+ outputCostPerMillion: 15.0,
84
+ cacheWriteCostPerMillion: 3.75,
85
+ cacheReadCostPerMillion: 0.3,
86
+ },
79
87
  'claude-sonnet-4-6': {
80
88
  inputCostPerMillion: 3.0,
81
89
  outputCostPerMillion: 15.0,
@@ -106,6 +114,12 @@ const PRICING_TABLE = {
106
114
  cacheWriteCostPerMillion: 3.75,
107
115
  cacheReadCostPerMillion: 0.3,
108
116
  },
117
+ 'claude-opus-5': {
118
+ inputCostPerMillion: 5.0,
119
+ outputCostPerMillion: 25.0,
120
+ cacheWriteCostPerMillion: 6.25,
121
+ cacheReadCostPerMillion: 0.5,
122
+ },
109
123
  'claude-opus-4-8': {
110
124
  inputCostPerMillion: 5.0,
111
125
  outputCostPerMillion: 25.0,
@@ -186,14 +200,45 @@ const PRICING_TABLE = {
186
200
  cacheWriteCostPerMillion: 0,
187
201
  cacheReadCostPerMillion: 0,
188
202
  },
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.
203
+ // ── OpenAI: GPT-5 family ──
204
+ // Published rates, verified against the LiteLLM catalog. Runtime hydration
205
+ // still supersedes these; they are the offline baseline. Entries below
206
+ // `gpt-5.4` predate the published-rate pass and remain rough estimates.
207
+ 'gpt-5.6-sol': {
208
+ inputCostPerMillion: 5.0,
209
+ outputCostPerMillion: 30.0,
210
+ cacheWriteCostPerMillion: 6.25,
211
+ cacheReadCostPerMillion: 0.5,
212
+ },
213
+ 'gpt-5.6-terra': {
214
+ inputCostPerMillion: 2.5,
215
+ outputCostPerMillion: 15.0,
216
+ cacheWriteCostPerMillion: 3.13,
217
+ cacheReadCostPerMillion: 0.25,
218
+ },
219
+ 'gpt-5.6': {
220
+ inputCostPerMillion: 5.0,
221
+ outputCostPerMillion: 30.0,
222
+ cacheWriteCostPerMillion: 6.25,
223
+ cacheReadCostPerMillion: 0.5,
224
+ },
225
+ 'gpt-5.5': {
226
+ inputCostPerMillion: 5.0,
227
+ outputCostPerMillion: 30.0,
228
+ cacheWriteCostPerMillion: 0,
229
+ cacheReadCostPerMillion: 0.5,
230
+ },
231
+ 'gpt-5.4-mini': {
232
+ inputCostPerMillion: 0.75,
233
+ outputCostPerMillion: 4.5,
234
+ cacheWriteCostPerMillion: 0,
235
+ cacheReadCostPerMillion: 0.07,
236
+ },
192
237
  'gpt-5.4': {
193
- inputCostPerMillion: 1.25,
194
- outputCostPerMillion: 10.0,
238
+ inputCostPerMillion: 2.5,
239
+ outputCostPerMillion: 15.0,
195
240
  cacheWriteCostPerMillion: 0,
196
- cacheReadCostPerMillion: 0.125,
241
+ cacheReadCostPerMillion: 0.25,
197
242
  },
198
243
  'gpt-5.3-codex': {
199
244
  inputCostPerMillion: 1.25,
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.2",
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",