@bastani/atomic 0.8.31-alpha.1 → 0.8.31-alpha.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.
Files changed (148) hide show
  1. package/CHANGELOG.md +17 -5
  2. package/README.md +12 -10
  3. package/dist/builtin/cursor/CHANGELOG.md +1 -1
  4. package/dist/builtin/cursor/package.json +2 -2
  5. package/dist/builtin/intercom/CHANGELOG.md +1 -1
  6. package/dist/builtin/intercom/package.json +2 -2
  7. package/dist/builtin/mcp/CHANGELOG.md +1 -1
  8. package/dist/builtin/mcp/package.json +3 -3
  9. package/dist/builtin/subagents/CHANGELOG.md +10 -1
  10. package/dist/builtin/subagents/agents/codebase-online-researcher.md +8 -8
  11. package/dist/builtin/subagents/agents/debugger.md +6 -6
  12. package/dist/builtin/subagents/package.json +4 -4
  13. package/dist/builtin/subagents/skills/effective-liteparse/SKILL.md +118 -0
  14. package/dist/builtin/subagents/skills/effective-liteparse/scripts/search.py +128 -0
  15. package/dist/builtin/subagents/skills/playwright-cli/SKILL.md +404 -0
  16. package/dist/builtin/subagents/skills/playwright-cli/references/element-attributes.md +23 -0
  17. package/dist/builtin/subagents/skills/playwright-cli/references/playwright-tests.md +39 -0
  18. package/dist/builtin/subagents/skills/playwright-cli/references/request-mocking.md +87 -0
  19. package/dist/builtin/subagents/skills/playwright-cli/references/running-code.md +241 -0
  20. package/dist/builtin/subagents/skills/playwright-cli/references/session-management.md +225 -0
  21. package/dist/builtin/subagents/skills/playwright-cli/references/spec-driven-testing.md +305 -0
  22. package/dist/builtin/subagents/skills/playwright-cli/references/storage-state.md +275 -0
  23. package/dist/builtin/subagents/skills/playwright-cli/references/test-generation.md +134 -0
  24. package/dist/builtin/subagents/skills/playwright-cli/references/tracing.md +139 -0
  25. package/dist/builtin/subagents/skills/playwright-cli/references/video-recording.md +143 -0
  26. package/dist/builtin/web-access/CHANGELOG.md +1 -1
  27. package/dist/builtin/web-access/package.json +2 -2
  28. package/dist/builtin/workflows/CHANGELOG.md +7 -1
  29. package/dist/builtin/workflows/README.md +4 -4
  30. package/dist/builtin/workflows/builtin/open-claude-design.ts +59 -56
  31. package/dist/builtin/workflows/builtin/ralph.ts +56 -3
  32. package/dist/builtin/workflows/builtin/shared-prompts.ts +1 -1
  33. package/dist/builtin/workflows/package.json +2 -2
  34. package/dist/builtin/workflows/skills/research-codebase/SKILL.md +1 -1
  35. package/dist/cli/args.d.ts.map +1 -1
  36. package/dist/cli/args.js +1 -1
  37. package/dist/cli/args.js.map +1 -1
  38. package/dist/core/agent-session.d.ts +1 -0
  39. package/dist/core/agent-session.d.ts.map +1 -1
  40. package/dist/core/agent-session.js +49 -21
  41. package/dist/core/agent-session.js.map +1 -1
  42. package/dist/core/context-window.d.ts +26 -1
  43. package/dist/core/context-window.d.ts.map +1 -1
  44. package/dist/core/context-window.js +30 -6
  45. package/dist/core/context-window.js.map +1 -1
  46. package/dist/core/copilot-model-catalog.d.ts +39 -21
  47. package/dist/core/copilot-model-catalog.d.ts.map +1 -1
  48. package/dist/core/copilot-model-catalog.js +44 -16
  49. package/dist/core/copilot-model-catalog.js.map +1 -1
  50. package/dist/core/model-registry.d.ts.map +1 -1
  51. package/dist/core/model-registry.js +6 -4
  52. package/dist/core/model-registry.js.map +1 -1
  53. package/dist/core/project-trust.d.ts.map +1 -1
  54. package/dist/core/project-trust.js +2 -1
  55. package/dist/core/project-trust.js.map +1 -1
  56. package/dist/core/sdk.d.ts.map +1 -1
  57. package/dist/core/sdk.js +18 -7
  58. package/dist/core/sdk.js.map +1 -1
  59. package/dist/core/settings-manager.d.ts +11 -2
  60. package/dist/core/settings-manager.d.ts.map +1 -1
  61. package/dist/core/settings-manager.js +62 -8
  62. package/dist/core/settings-manager.js.map +1 -1
  63. package/dist/core/system-prompt.d.ts.map +1 -1
  64. package/dist/core/system-prompt.js +1 -0
  65. package/dist/core/system-prompt.js.map +1 -1
  66. package/dist/core/tools/edit-diff.d.ts +1 -2
  67. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  68. package/dist/core/tools/edit-diff.js +1 -2
  69. package/dist/core/tools/edit-diff.js.map +1 -1
  70. package/dist/index.d.ts +2 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  75. package/dist/modes/interactive/components/config-selector.js +5 -7
  76. package/dist/modes/interactive/components/config-selector.js.map +1 -1
  77. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  78. package/dist/modes/interactive/components/model-selector.js +2 -1
  79. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  80. package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
  81. package/dist/modes/interactive/components/scoped-models-selector.js +4 -1
  82. package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
  83. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  84. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  85. package/dist/modes/interactive/components/settings-selector.js +165 -15
  86. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  87. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  88. package/dist/modes/interactive/components/tree-selector.js +44 -4
  89. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  90. package/dist/modes/interactive/interactive-mode.d.ts +1 -1
  91. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  92. package/dist/modes/interactive/interactive-mode.js +24 -54
  93. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  94. package/dist/modes/interactive/model-search.d.ts +7 -0
  95. package/dist/modes/interactive/model-search.d.ts.map +1 -0
  96. package/dist/modes/interactive/model-search.js +6 -0
  97. package/dist/modes/interactive/model-search.js.map +1 -0
  98. package/dist/modes/interactive/theme/theme-controller.d.ts +30 -0
  99. package/dist/modes/interactive/theme/theme-controller.d.ts.map +1 -0
  100. package/dist/modes/interactive/theme/theme-controller.js +108 -0
  101. package/dist/modes/interactive/theme/theme-controller.js.map +1 -0
  102. package/dist/modes/interactive/theme/theme-schema.json +2 -1
  103. package/dist/modes/interactive/theme/theme.d.ts +5 -0
  104. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  105. package/dist/modes/interactive/theme/theme.js +70 -29
  106. package/dist/modes/interactive/theme/theme.js.map +1 -1
  107. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  108. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  109. package/dist/modes/rpc/rpc-client.js +1 -1
  110. package/dist/modes/rpc/rpc-client.js.map +1 -1
  111. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  112. package/dist/modes/rpc/rpc-mode.js +1 -1
  113. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  114. package/dist/package-manager-cli.d.ts.map +1 -1
  115. package/dist/package-manager-cli.js +39 -9
  116. package/dist/package-manager-cli.js.map +1 -1
  117. package/docs/extensions.md +21 -0
  118. package/docs/models.md +3 -3
  119. package/docs/packages.md +13 -9
  120. package/docs/providers.md +3 -3
  121. package/docs/quickstart.md +14 -0
  122. package/docs/rpc.md +3 -3
  123. package/docs/sdk.md +15 -11
  124. package/docs/session-format.md +1 -1
  125. package/docs/settings.md +8 -3
  126. package/docs/themes.md +3 -1
  127. package/docs/tui.md +1 -1
  128. package/docs/usage.md +12 -9
  129. package/docs/workflows.md +9 -7
  130. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  131. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  132. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  133. package/examples/extensions/gondolin/package-lock.json +2 -2
  134. package/examples/extensions/gondolin/package.json +1 -1
  135. package/examples/extensions/preset.ts +10 -4
  136. package/examples/extensions/provider-payload.ts +5 -5
  137. package/examples/extensions/sandbox/index.ts +2 -2
  138. package/examples/extensions/sandbox/package-lock.json +3 -3
  139. package/examples/extensions/sandbox/package.json +2 -2
  140. package/examples/extensions/subagent/agents.ts +2 -2
  141. package/examples/extensions/subagent/index.ts +4 -2
  142. package/examples/extensions/with-deps/package-lock.json +2 -2
  143. package/examples/extensions/with-deps/package.json +1 -1
  144. package/package.json +5 -5
  145. package/dist/builtin/subagents/skills/browser/EXAMPLES.md +0 -151
  146. package/dist/builtin/subagents/skills/browser/LICENSE.txt +0 -21
  147. package/dist/builtin/subagents/skills/browser/REFERENCE.md +0 -451
  148. package/dist/builtin/subagents/skills/browser/SKILL.md +0 -170
@@ -5,6 +5,13 @@ declare module "@earendil-works/pi-ai" {
5
5
  contextWindowOptions?: readonly number[];
6
6
  /** Original/default scalar context window, preserved when contextWindow is overridden for a session. */
7
7
  defaultContextWindow?: number;
8
+ /**
9
+ * Hard prompt/input cap for providers (e.g. GitHub Copilot `max_prompt_tokens`) that enforce an
10
+ * input budget below the displayed context window. When set and below `contextWindow`, it is the
11
+ * effective input budget for compaction thresholds and overflow recovery; `contextWindow` remains
12
+ * the displayed/branded window.
13
+ */
14
+ maxInputTokens?: number;
8
15
  }
9
16
  }
10
17
  export interface ContextWindowParseResult {
@@ -18,12 +25,30 @@ export interface ContextWindowSelection<TApi extends Api = Api> {
18
25
  export interface ContextWindowSelectionError {
19
26
  error: string;
20
27
  }
28
+ export interface ContextWindowSelectionOptions {
29
+ /**
30
+ * GitHub Copilot advertises some long-context tiers below their branded 1M size
31
+ * (for example 936k or 922k input tokens). When enabled, a request above an
32
+ * advertised long tier selects the largest supported Copilot window not
33
+ * exceeding the request, but never silently falls back to the model's base
34
+ * window.
35
+ */
36
+ allowCopilotLongContextFallback?: boolean;
37
+ }
21
38
  export declare function validateContextWindowValue(value: number): string | undefined;
22
39
  export declare function parseContextWindowValue(input: string): ContextWindowParseResult;
23
40
  export declare function formatContextWindow(value: number): string;
24
41
  export declare function normalizeContextWindowOptions(values: readonly number[] | undefined): number[];
25
42
  export declare function getModelDefaultContextWindow(model: Model<Api>): number;
43
+ /**
44
+ * Effective input-token budget for compaction/overflow decisions. Equals the displayed
45
+ * `contextWindow` unless a smaller hard input cap (`maxInputTokens`, e.g. GitHub Copilot's
46
+ * `max_prompt_tokens`) is advertised, in which case the lower of the two is used. This lets a model
47
+ * display its full/branded window while compaction and overflow recovery respect the real,
48
+ * server-enforced input limit.
49
+ */
50
+ export declare function getEffectiveInputBudget(model: Model<Api>): number;
26
51
  export declare function getSupportedContextWindows(model: Model<Api>): number[];
27
52
  export declare function withContextWindowOptions<TApi extends Api>(model: Model<TApi>, contextWindowOptions: readonly number[]): Model<TApi>;
28
- export declare function selectContextWindow<TApi extends Api>(model: Model<TApi>, contextWindow: number): ContextWindowSelection<TApi> | ContextWindowSelectionError;
53
+ export declare function selectContextWindow<TApi extends Api>(model: Model<TApi>, contextWindow: number, options?: ContextWindowSelectionOptions): ContextWindowSelection<TApi> | ContextWindowSelectionError;
29
54
  //# sourceMappingURL=context-window.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-window.d.ts","sourceRoot":"","sources":["../../src/core/context-window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,QAAQ,uBAAuB,CAAC,CAAC;IACvC,UAAU,KAAK,CAAC,IAAI,SAAS,GAAG;QAC/B,oHAAoH;QACpH,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACzC,wGAAwG;QACxG,oBAAoB,CAAC,EAAE,MAAM,CAAC;KAC9B;CACD;AAED,MAAM,WAAW,wBAAwB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB,CAAC,IAAI,SAAS,GAAG,GAAG,GAAG;IAC7D,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC3C,KAAK,EAAE,MAAM,CAAC;CACd;AAWD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE5E;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,wBAAwB,CAqB/E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUzD;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAS7F;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAEtE;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAEtE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,SAAS,GAAG,EACxD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACrC,KAAK,CAAC,IAAI,CAAC,CAMb;AAED,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,GAAG,EACnD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,aAAa,EAAE,MAAM,GACnB,sBAAsB,CAAC,IAAI,CAAC,GAAG,2BAA2B,CAsB5D","sourcesContent":["import type { Api, Model } from \"@earendil-works/pi-ai\";\n\ndeclare module \"@earendil-works/pi-ai\" {\n\tinterface Model<TApi extends Api> {\n\t\t/** Selectable context-window sizes for this model. The scalar contextWindow remains the default/effective value. */\n\t\tcontextWindowOptions?: readonly number[];\n\t\t/** Original/default scalar context window, preserved when contextWindow is overridden for a session. */\n\t\tdefaultContextWindow?: number;\n\t}\n}\n\nexport interface ContextWindowParseResult {\n\tvalue?: number;\n\terror?: string;\n}\n\nexport interface ContextWindowSelection<TApi extends Api = Api> {\n\tmodel: Model<TApi>;\n\tcontextWindow: number;\n}\n\nexport interface ContextWindowSelectionError {\n\terror: string;\n}\n\nconst CONTEXT_WINDOW_UNITS: Record<string, number> = {\n\tk: 1_000,\n\tm: 1_000_000,\n};\n\nfunction isPositiveInteger(value: number): boolean {\n\treturn Number.isFinite(value) && Number.isInteger(value) && value > 0;\n}\n\nexport function validateContextWindowValue(value: number): string | undefined {\n\treturn isPositiveInteger(value) ? undefined : \"Context window must be a positive integer token count\";\n}\n\nexport function parseContextWindowValue(input: string): ContextWindowParseResult {\n\tconst trimmed = input.trim();\n\tif (!trimmed) {\n\t\treturn { error: \"Context window requires a value\" };\n\t}\n\n\tconst match = /^(\\d+(?:\\.\\d+)?)([kKmM])?$/.exec(trimmed);\n\tif (!match) {\n\t\treturn { error: `Invalid context window \"${input}\". Use a positive number, or a compact value like 400k or 1m.` };\n\t}\n\n\tconst numericValue = Number(match[1]);\n\tconst unit = match[2]?.toLowerCase();\n\tconst multiplier = unit ? CONTEXT_WINDOW_UNITS[unit] : 1;\n\tconst tokens = numericValue * multiplier;\n\tconst validationError = validateContextWindowValue(tokens);\n\tif (validationError) {\n\t\treturn { error: `Invalid context window \"${input}\". ${validationError}.` };\n\t}\n\n\treturn { value: tokens };\n}\n\nexport function formatContextWindow(value: number): string {\n\tif (value >= 1_000_000) {\n\t\tconst millions = value / 1_000_000;\n\t\treturn millions % 1 === 0 ? `${millions}m` : `${millions.toFixed(1)}m`;\n\t}\n\tif (value >= 1_000) {\n\t\tconst thousands = value / 1_000;\n\t\treturn thousands % 1 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;\n\t}\n\treturn String(value);\n}\n\nexport function normalizeContextWindowOptions(values: readonly number[] | undefined): number[] {\n\tconst seen = new Set<number>();\n\tconst normalized: number[] = [];\n\tfor (const value of values ?? []) {\n\t\tif (!isPositiveInteger(value) || seen.has(value)) continue;\n\t\tseen.add(value);\n\t\tnormalized.push(value);\n\t}\n\treturn normalized.sort((a, b) => a - b);\n}\n\nexport function getModelDefaultContextWindow(model: Model<Api>): number {\n\treturn isPositiveInteger(model.defaultContextWindow ?? 0) ? model.defaultContextWindow! : model.contextWindow;\n}\n\nexport function getSupportedContextWindows(model: Model<Api>): number[] {\n\treturn normalizeContextWindowOptions([getModelDefaultContextWindow(model), ...(model.contextWindowOptions ?? [])]);\n}\n\nexport function withContextWindowOptions<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindowOptions: readonly number[],\n): Model<TApi> {\n\treturn {\n\t\t...model,\n\t\tdefaultContextWindow: getModelDefaultContextWindow(model as Model<Api>),\n\t\tcontextWindowOptions: normalizeContextWindowOptions(contextWindowOptions),\n\t};\n}\n\nexport function selectContextWindow<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindow: number,\n): ContextWindowSelection<TApi> | ContextWindowSelectionError {\n\tconst validationError = validateContextWindowValue(contextWindow);\n\tif (validationError) {\n\t\treturn { error: validationError };\n\t}\n\n\tconst supported = getSupportedContextWindows(model as Model<Api>);\n\tif (!supported.includes(contextWindow)) {\n\t\treturn {\n\t\t\terror: `Context window ${formatContextWindow(contextWindow)} is not supported by ${model.provider}/${model.id}. Supported values: ${supported.map(formatContextWindow).join(\", \")}.`,\n\t\t};\n\t}\n\n\treturn {\n\t\tmodel: {\n\t\t\t...model,\n\t\t\tdefaultContextWindow: getModelDefaultContextWindow(model as Model<Api>),\n\t\t\tcontextWindow,\n\t\t\tcontextWindowOptions: supported,\n\t\t},\n\t\tcontextWindow,\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-window.d.ts","sourceRoot":"","sources":["../../src/core/context-window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,QAAQ,uBAAuB,CAAC,CAAC;IACvC,UAAU,KAAK,CAAC,IAAI,SAAS,GAAG;QAC/B,oHAAoH;QACpH,oBAAoB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QACzC,wGAAwG;QACxG,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B;;;;;WAKG;QACH,cAAc,CAAC,EAAE,MAAM,CAAC;KACxB;CACD;AAED,MAAM,WAAW,wBAAwB;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB,CAAC,IAAI,SAAS,GAAG,GAAG,GAAG;IAC7D,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,2BAA2B;IAC3C,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,6BAA6B;IAC7C;;;;;;OAMG;IACH,+BAA+B,CAAC,EAAE,OAAO,CAAC;CAC1C;AAWD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE5E;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,wBAAwB,CAqB/E;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUzD;AAED,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAS7F;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAEtE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAGjE;AAED,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,EAAE,CAEtE;AAED,wBAAgB,wBAAwB,CAAC,IAAI,SAAS,GAAG,EACxD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACrC,KAAK,CAAC,IAAI,CAAC,CAMb;AAuBD,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,GAAG,EACnD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,EAClB,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,6BAAkC,GACzC,sBAAsB,CAAC,IAAI,CAAC,GAAG,2BAA2B,CAwB5D","sourcesContent":["import type { Api, Model } from \"@earendil-works/pi-ai\";\n\ndeclare module \"@earendil-works/pi-ai\" {\n\tinterface Model<TApi extends Api> {\n\t\t/** Selectable context-window sizes for this model. The scalar contextWindow remains the default/effective value. */\n\t\tcontextWindowOptions?: readonly number[];\n\t\t/** Original/default scalar context window, preserved when contextWindow is overridden for a session. */\n\t\tdefaultContextWindow?: number;\n\t\t/**\n\t\t * Hard prompt/input cap for providers (e.g. GitHub Copilot `max_prompt_tokens`) that enforce an\n\t\t * input budget below the displayed context window. When set and below `contextWindow`, it is the\n\t\t * effective input budget for compaction thresholds and overflow recovery; `contextWindow` remains\n\t\t * the displayed/branded window.\n\t\t */\n\t\tmaxInputTokens?: number;\n\t}\n}\n\nexport interface ContextWindowParseResult {\n\tvalue?: number;\n\terror?: string;\n}\n\nexport interface ContextWindowSelection<TApi extends Api = Api> {\n\tmodel: Model<TApi>;\n\tcontextWindow: number;\n}\n\nexport interface ContextWindowSelectionError {\n\terror: string;\n}\n\nexport interface ContextWindowSelectionOptions {\n\t/**\n\t * GitHub Copilot advertises some long-context tiers below their branded 1M size\n\t * (for example 936k or 922k input tokens). When enabled, a request above an\n\t * advertised long tier selects the largest supported Copilot window not\n\t * exceeding the request, but never silently falls back to the model's base\n\t * window.\n\t */\n\tallowCopilotLongContextFallback?: boolean;\n}\n\nconst CONTEXT_WINDOW_UNITS: Record<string, number> = {\n\tk: 1_000,\n\tm: 1_000_000,\n};\n\nfunction isPositiveInteger(value: number): boolean {\n\treturn Number.isFinite(value) && Number.isInteger(value) && value > 0;\n}\n\nexport function validateContextWindowValue(value: number): string | undefined {\n\treturn isPositiveInteger(value) ? undefined : \"Context window must be a positive integer token count\";\n}\n\nexport function parseContextWindowValue(input: string): ContextWindowParseResult {\n\tconst trimmed = input.trim();\n\tif (!trimmed) {\n\t\treturn { error: \"Context window requires a value\" };\n\t}\n\n\tconst match = /^(\\d+(?:\\.\\d+)?)([kKmM])?$/.exec(trimmed);\n\tif (!match) {\n\t\treturn { error: `Invalid context window \"${input}\". Use a positive number, or a compact value like 400k or 1m.` };\n\t}\n\n\tconst numericValue = Number(match[1]);\n\tconst unit = match[2]?.toLowerCase();\n\tconst multiplier = unit ? CONTEXT_WINDOW_UNITS[unit] : 1;\n\tconst tokens = numericValue * multiplier;\n\tconst validationError = validateContextWindowValue(tokens);\n\tif (validationError) {\n\t\treturn { error: `Invalid context window \"${input}\". ${validationError}.` };\n\t}\n\n\treturn { value: tokens };\n}\n\nexport function formatContextWindow(value: number): string {\n\tif (value >= 1_000_000) {\n\t\tconst millions = value / 1_000_000;\n\t\treturn millions % 1 === 0 ? `${millions}m` : `${millions.toFixed(1)}m`;\n\t}\n\tif (value >= 1_000) {\n\t\tconst thousands = value / 1_000;\n\t\treturn thousands % 1 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;\n\t}\n\treturn String(value);\n}\n\nexport function normalizeContextWindowOptions(values: readonly number[] | undefined): number[] {\n\tconst seen = new Set<number>();\n\tconst normalized: number[] = [];\n\tfor (const value of values ?? []) {\n\t\tif (!isPositiveInteger(value) || seen.has(value)) continue;\n\t\tseen.add(value);\n\t\tnormalized.push(value);\n\t}\n\treturn normalized.sort((a, b) => a - b);\n}\n\nexport function getModelDefaultContextWindow(model: Model<Api>): number {\n\treturn isPositiveInteger(model.defaultContextWindow ?? 0) ? model.defaultContextWindow! : model.contextWindow;\n}\n\n/**\n * Effective input-token budget for compaction/overflow decisions. Equals the displayed\n * `contextWindow` unless a smaller hard input cap (`maxInputTokens`, e.g. GitHub Copilot's\n * `max_prompt_tokens`) is advertised, in which case the lower of the two is used. This lets a model\n * display its full/branded window while compaction and overflow recovery respect the real,\n * server-enforced input limit.\n */\nexport function getEffectiveInputBudget(model: Model<Api>): number {\n\tconst cap = model.maxInputTokens;\n\treturn isPositiveInteger(cap ?? 0) ? Math.min(model.contextWindow, cap as number) : model.contextWindow;\n}\n\nexport function getSupportedContextWindows(model: Model<Api>): number[] {\n\treturn normalizeContextWindowOptions([getModelDefaultContextWindow(model), ...(model.contextWindowOptions ?? [])]);\n}\n\nexport function withContextWindowOptions<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindowOptions: readonly number[],\n): Model<TApi> {\n\treturn {\n\t\t...model,\n\t\tdefaultContextWindow: getModelDefaultContextWindow(model as Model<Api>),\n\t\tcontextWindowOptions: normalizeContextWindowOptions(contextWindowOptions),\n\t};\n}\n\nfunction resolveSelectableContextWindow(\n\tmodel: Model<Api>,\n\trequestedContextWindow: number,\n\tsupported: readonly number[],\n\toptions: ContextWindowSelectionOptions,\n): number | undefined {\n\tif (supported.includes(requestedContextWindow)) {\n\t\treturn requestedContextWindow;\n\t}\n\n\tif (options.allowCopilotLongContextFallback !== true || model.provider !== \"github-copilot\") {\n\t\treturn undefined;\n\t}\n\n\tconst defaultContextWindow = getModelDefaultContextWindow(model);\n\tconst candidates = supported.filter(\n\t\t(contextWindow) => contextWindow <= requestedContextWindow && contextWindow > defaultContextWindow,\n\t);\n\treturn candidates.length > 0 ? Math.max(...candidates) : undefined;\n}\n\nexport function selectContextWindow<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindow: number,\n\toptions: ContextWindowSelectionOptions = {},\n): ContextWindowSelection<TApi> | ContextWindowSelectionError {\n\tconst validationError = validateContextWindowValue(contextWindow);\n\tif (validationError) {\n\t\treturn { error: validationError };\n\t}\n\n\tconst apiModel = model as Model<Api>;\n\tconst supported = getSupportedContextWindows(apiModel);\n\tconst selectedContextWindow = resolveSelectableContextWindow(apiModel, contextWindow, supported, options);\n\tif (selectedContextWindow === undefined) {\n\t\treturn {\n\t\t\terror: `Context window ${formatContextWindow(contextWindow)} is not supported by ${model.provider}/${model.id}. Supported values: ${supported.map(formatContextWindow).join(\", \")}.`,\n\t\t};\n\t}\n\n\treturn {\n\t\tmodel: {\n\t\t\t...model,\n\t\t\tdefaultContextWindow: getModelDefaultContextWindow(apiModel),\n\t\t\tcontextWindow: selectedContextWindow,\n\t\t\tcontextWindowOptions: supported,\n\t\t},\n\t\tcontextWindow: selectedContextWindow,\n\t};\n}\n"]}
@@ -52,6 +52,17 @@ export function normalizeContextWindowOptions(values) {
52
52
  export function getModelDefaultContextWindow(model) {
53
53
  return isPositiveInteger(model.defaultContextWindow ?? 0) ? model.defaultContextWindow : model.contextWindow;
54
54
  }
55
+ /**
56
+ * Effective input-token budget for compaction/overflow decisions. Equals the displayed
57
+ * `contextWindow` unless a smaller hard input cap (`maxInputTokens`, e.g. GitHub Copilot's
58
+ * `max_prompt_tokens`) is advertised, in which case the lower of the two is used. This lets a model
59
+ * display its full/branded window while compaction and overflow recovery respect the real,
60
+ * server-enforced input limit.
61
+ */
62
+ export function getEffectiveInputBudget(model) {
63
+ const cap = model.maxInputTokens;
64
+ return isPositiveInteger(cap ?? 0) ? Math.min(model.contextWindow, cap) : model.contextWindow;
65
+ }
55
66
  export function getSupportedContextWindows(model) {
56
67
  return normalizeContextWindowOptions([getModelDefaultContextWindow(model), ...(model.contextWindowOptions ?? [])]);
57
68
  }
@@ -62,13 +73,26 @@ export function withContextWindowOptions(model, contextWindowOptions) {
62
73
  contextWindowOptions: normalizeContextWindowOptions(contextWindowOptions),
63
74
  };
64
75
  }
65
- export function selectContextWindow(model, contextWindow) {
76
+ function resolveSelectableContextWindow(model, requestedContextWindow, supported, options) {
77
+ if (supported.includes(requestedContextWindow)) {
78
+ return requestedContextWindow;
79
+ }
80
+ if (options.allowCopilotLongContextFallback !== true || model.provider !== "github-copilot") {
81
+ return undefined;
82
+ }
83
+ const defaultContextWindow = getModelDefaultContextWindow(model);
84
+ const candidates = supported.filter((contextWindow) => contextWindow <= requestedContextWindow && contextWindow > defaultContextWindow);
85
+ return candidates.length > 0 ? Math.max(...candidates) : undefined;
86
+ }
87
+ export function selectContextWindow(model, contextWindow, options = {}) {
66
88
  const validationError = validateContextWindowValue(contextWindow);
67
89
  if (validationError) {
68
90
  return { error: validationError };
69
91
  }
70
- const supported = getSupportedContextWindows(model);
71
- if (!supported.includes(contextWindow)) {
92
+ const apiModel = model;
93
+ const supported = getSupportedContextWindows(apiModel);
94
+ const selectedContextWindow = resolveSelectableContextWindow(apiModel, contextWindow, supported, options);
95
+ if (selectedContextWindow === undefined) {
72
96
  return {
73
97
  error: `Context window ${formatContextWindow(contextWindow)} is not supported by ${model.provider}/${model.id}. Supported values: ${supported.map(formatContextWindow).join(", ")}.`,
74
98
  };
@@ -76,11 +100,11 @@ export function selectContextWindow(model, contextWindow) {
76
100
  return {
77
101
  model: {
78
102
  ...model,
79
- defaultContextWindow: getModelDefaultContextWindow(model),
80
- contextWindow,
103
+ defaultContextWindow: getModelDefaultContextWindow(apiModel),
104
+ contextWindow: selectedContextWindow,
81
105
  contextWindowOptions: supported,
82
106
  },
83
- contextWindow,
107
+ contextWindow: selectedContextWindow,
84
108
  };
85
109
  }
86
110
  //# sourceMappingURL=context-window.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context-window.js","sourceRoot":"","sources":["../../src/core/context-window.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Api, Model } from \"@earendil-works/pi-ai\";\n\ndeclare module \"@earendil-works/pi-ai\" {\n\tinterface Model<TApi extends Api> {\n\t\t/** Selectable context-window sizes for this model. The scalar contextWindow remains the default/effective value. */\n\t\tcontextWindowOptions?: readonly number[];\n\t\t/** Original/default scalar context window, preserved when contextWindow is overridden for a session. */\n\t\tdefaultContextWindow?: number;\n\t}\n}\n\nexport interface ContextWindowParseResult {\n\tvalue?: number;\n\terror?: string;\n}\n\nexport interface ContextWindowSelection<TApi extends Api = Api> {\n\tmodel: Model<TApi>;\n\tcontextWindow: number;\n}\n\nexport interface ContextWindowSelectionError {\n\terror: string;\n}\n\nconst CONTEXT_WINDOW_UNITS: Record<string, number> = {\n\tk: 1_000,\n\tm: 1_000_000,\n};\n\nfunction isPositiveInteger(value: number): boolean {\n\treturn Number.isFinite(value) && Number.isInteger(value) && value > 0;\n}\n\nexport function validateContextWindowValue(value: number): string | undefined {\n\treturn isPositiveInteger(value) ? undefined : \"Context window must be a positive integer token count\";\n}\n\nexport function parseContextWindowValue(input: string): ContextWindowParseResult {\n\tconst trimmed = input.trim();\n\tif (!trimmed) {\n\t\treturn { error: \"Context window requires a value\" };\n\t}\n\n\tconst match = /^(\\d+(?:\\.\\d+)?)([kKmM])?$/.exec(trimmed);\n\tif (!match) {\n\t\treturn { error: `Invalid context window \"${input}\". Use a positive number, or a compact value like 400k or 1m.` };\n\t}\n\n\tconst numericValue = Number(match[1]);\n\tconst unit = match[2]?.toLowerCase();\n\tconst multiplier = unit ? CONTEXT_WINDOW_UNITS[unit] : 1;\n\tconst tokens = numericValue * multiplier;\n\tconst validationError = validateContextWindowValue(tokens);\n\tif (validationError) {\n\t\treturn { error: `Invalid context window \"${input}\". ${validationError}.` };\n\t}\n\n\treturn { value: tokens };\n}\n\nexport function formatContextWindow(value: number): string {\n\tif (value >= 1_000_000) {\n\t\tconst millions = value / 1_000_000;\n\t\treturn millions % 1 === 0 ? `${millions}m` : `${millions.toFixed(1)}m`;\n\t}\n\tif (value >= 1_000) {\n\t\tconst thousands = value / 1_000;\n\t\treturn thousands % 1 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;\n\t}\n\treturn String(value);\n}\n\nexport function normalizeContextWindowOptions(values: readonly number[] | undefined): number[] {\n\tconst seen = new Set<number>();\n\tconst normalized: number[] = [];\n\tfor (const value of values ?? []) {\n\t\tif (!isPositiveInteger(value) || seen.has(value)) continue;\n\t\tseen.add(value);\n\t\tnormalized.push(value);\n\t}\n\treturn normalized.sort((a, b) => a - b);\n}\n\nexport function getModelDefaultContextWindow(model: Model<Api>): number {\n\treturn isPositiveInteger(model.defaultContextWindow ?? 0) ? model.defaultContextWindow! : model.contextWindow;\n}\n\nexport function getSupportedContextWindows(model: Model<Api>): number[] {\n\treturn normalizeContextWindowOptions([getModelDefaultContextWindow(model), ...(model.contextWindowOptions ?? [])]);\n}\n\nexport function withContextWindowOptions<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindowOptions: readonly number[],\n): Model<TApi> {\n\treturn {\n\t\t...model,\n\t\tdefaultContextWindow: getModelDefaultContextWindow(model as Model<Api>),\n\t\tcontextWindowOptions: normalizeContextWindowOptions(contextWindowOptions),\n\t};\n}\n\nexport function selectContextWindow<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindow: number,\n): ContextWindowSelection<TApi> | ContextWindowSelectionError {\n\tconst validationError = validateContextWindowValue(contextWindow);\n\tif (validationError) {\n\t\treturn { error: validationError };\n\t}\n\n\tconst supported = getSupportedContextWindows(model as Model<Api>);\n\tif (!supported.includes(contextWindow)) {\n\t\treturn {\n\t\t\terror: `Context window ${formatContextWindow(contextWindow)} is not supported by ${model.provider}/${model.id}. Supported values: ${supported.map(formatContextWindow).join(\", \")}.`,\n\t\t};\n\t}\n\n\treturn {\n\t\tmodel: {\n\t\t\t...model,\n\t\t\tdefaultContextWindow: getModelDefaultContextWindow(model as Model<Api>),\n\t\t\tcontextWindow,\n\t\t\tcontextWindowOptions: supported,\n\t\t},\n\t\tcontextWindow,\n\t};\n}\n"]}
1
+ {"version":3,"file":"context-window.js","sourceRoot":"","sources":["../../src/core/context-window.ts"],"names":[],"mappings":"AA2CA,MAAM,oBAAoB,GAA2B;IACpD,CAAC,EAAE,KAAK;IACR,CAAC,EAAE,SAAS;CACZ,CAAC;AAEF,SAAS,iBAAiB,CAAC,KAAa;IACvC,OAAO,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAa;IACvD,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,uDAAuD,CAAC;AACvG,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAAa;IACpD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,EAAE,KAAK,EAAE,iCAAiC,EAAE,CAAC;IACrD,CAAC;IAED,MAAM,KAAK,GAAG,4BAA4B,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,EAAE,KAAK,EAAE,2BAA2B,KAAK,+DAA+D,EAAE,CAAC;IACnH,CAAC;IAED,MAAM,YAAY,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;IACrC,MAAM,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzD,MAAM,MAAM,GAAG,YAAY,GAAG,UAAU,CAAC;IACzC,MAAM,eAAe,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAC;IAC3D,IAAI,eAAe,EAAE,CAAC;QACrB,OAAO,EAAE,KAAK,EAAE,2BAA2B,KAAK,MAAM,eAAe,GAAG,EAAE,CAAC;IAC5E,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAChD,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;QACxB,MAAM,QAAQ,GAAG,KAAK,GAAG,SAAS,CAAC;QACnC,OAAO,QAAQ,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IACxE,CAAC;IACD,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;QACpB,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,CAAC;QAChC,OAAO,SAAS,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3E,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACtB,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,MAAqC;IAClF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,MAAM,IAAI,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;YAAE,SAAS;QAC3D,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAChB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IACD,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,4BAA4B,CAAC,KAAiB;IAC7D,OAAO,iBAAiB,CAAC,KAAK,CAAC,oBAAoB,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAqB,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;AAC/G,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAiB;IACxD,MAAM,GAAG,GAAG,KAAK,CAAC,cAAc,CAAC;IACjC,OAAO,iBAAiB,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,aAAa,EAAE,GAAa,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC;AACzG,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,KAAiB;IAC3D,OAAO,6BAA6B,CAAC,CAAC,4BAA4B,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AACpH,CAAC;AAED,MAAM,UAAU,wBAAwB,CACvC,KAAkB,EAClB,oBAAuC;IAEvC,OAAO;QACN,GAAG,KAAK;QACR,oBAAoB,EAAE,4BAA4B,CAAC,KAAmB,CAAC;QACvE,oBAAoB,EAAE,6BAA6B,CAAC,oBAAoB,CAAC;KACzE,CAAC;AACH,CAAC;AAED,SAAS,8BAA8B,CACtC,KAAiB,EACjB,sBAA8B,EAC9B,SAA4B,EAC5B,OAAsC;IAEtC,IAAI,SAAS,CAAC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,CAAC;QAChD,OAAO,sBAAsB,CAAC;IAC/B,CAAC;IAED,IAAI,OAAO,CAAC,+BAA+B,KAAK,IAAI,IAAI,KAAK,CAAC,QAAQ,KAAK,gBAAgB,EAAE,CAAC;QAC7F,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,oBAAoB,GAAG,4BAA4B,CAAC,KAAK,CAAC,CAAC;IACjE,MAAM,UAAU,GAAG,SAAS,CAAC,MAAM,CAClC,CAAC,aAAa,EAAE,EAAE,CAAC,aAAa,IAAI,sBAAsB,IAAI,aAAa,GAAG,oBAAoB,CAClG,CAAC;IACF,OAAO,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,mBAAmB,CAClC,KAAkB,EAClB,aAAqB,EACrB,OAAO,GAAkC,EAAE;IAE3C,MAAM,eAAe,GAAG,0BAA0B,CAAC,aAAa,CAAC,CAAC;IAClE,IAAI,eAAe,EAAE,CAAC;QACrB,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC;IACnC,CAAC;IAED,MAAM,QAAQ,GAAG,KAAmB,CAAC;IACrC,MAAM,SAAS,GAAG,0BAA0B,CAAC,QAAQ,CAAC,CAAC;IACvD,MAAM,qBAAqB,GAAG,8BAA8B,CAAC,QAAQ,EAAE,aAAa,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAC1G,IAAI,qBAAqB,KAAK,SAAS,EAAE,CAAC;QACzC,OAAO;YACN,KAAK,EAAE,kBAAkB,mBAAmB,CAAC,aAAa,CAAC,wBAAwB,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,EAAE,uBAAuB,SAAS,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;SACpL,CAAC;IACH,CAAC;IAED,OAAO;QACN,KAAK,EAAE;YACN,GAAG,KAAK;YACR,oBAAoB,EAAE,4BAA4B,CAAC,QAAQ,CAAC;YAC5D,aAAa,EAAE,qBAAqB;YACpC,oBAAoB,EAAE,SAAS;SAC/B;QACD,aAAa,EAAE,qBAAqB;KACpC,CAAC;AACH,CAAC","sourcesContent":["import type { Api, Model } from \"@earendil-works/pi-ai\";\n\ndeclare module \"@earendil-works/pi-ai\" {\n\tinterface Model<TApi extends Api> {\n\t\t/** Selectable context-window sizes for this model. The scalar contextWindow remains the default/effective value. */\n\t\tcontextWindowOptions?: readonly number[];\n\t\t/** Original/default scalar context window, preserved when contextWindow is overridden for a session. */\n\t\tdefaultContextWindow?: number;\n\t\t/**\n\t\t * Hard prompt/input cap for providers (e.g. GitHub Copilot `max_prompt_tokens`) that enforce an\n\t\t * input budget below the displayed context window. When set and below `contextWindow`, it is the\n\t\t * effective input budget for compaction thresholds and overflow recovery; `contextWindow` remains\n\t\t * the displayed/branded window.\n\t\t */\n\t\tmaxInputTokens?: number;\n\t}\n}\n\nexport interface ContextWindowParseResult {\n\tvalue?: number;\n\terror?: string;\n}\n\nexport interface ContextWindowSelection<TApi extends Api = Api> {\n\tmodel: Model<TApi>;\n\tcontextWindow: number;\n}\n\nexport interface ContextWindowSelectionError {\n\terror: string;\n}\n\nexport interface ContextWindowSelectionOptions {\n\t/**\n\t * GitHub Copilot advertises some long-context tiers below their branded 1M size\n\t * (for example 936k or 922k input tokens). When enabled, a request above an\n\t * advertised long tier selects the largest supported Copilot window not\n\t * exceeding the request, but never silently falls back to the model's base\n\t * window.\n\t */\n\tallowCopilotLongContextFallback?: boolean;\n}\n\nconst CONTEXT_WINDOW_UNITS: Record<string, number> = {\n\tk: 1_000,\n\tm: 1_000_000,\n};\n\nfunction isPositiveInteger(value: number): boolean {\n\treturn Number.isFinite(value) && Number.isInteger(value) && value > 0;\n}\n\nexport function validateContextWindowValue(value: number): string | undefined {\n\treturn isPositiveInteger(value) ? undefined : \"Context window must be a positive integer token count\";\n}\n\nexport function parseContextWindowValue(input: string): ContextWindowParseResult {\n\tconst trimmed = input.trim();\n\tif (!trimmed) {\n\t\treturn { error: \"Context window requires a value\" };\n\t}\n\n\tconst match = /^(\\d+(?:\\.\\d+)?)([kKmM])?$/.exec(trimmed);\n\tif (!match) {\n\t\treturn { error: `Invalid context window \"${input}\". Use a positive number, or a compact value like 400k or 1m.` };\n\t}\n\n\tconst numericValue = Number(match[1]);\n\tconst unit = match[2]?.toLowerCase();\n\tconst multiplier = unit ? CONTEXT_WINDOW_UNITS[unit] : 1;\n\tconst tokens = numericValue * multiplier;\n\tconst validationError = validateContextWindowValue(tokens);\n\tif (validationError) {\n\t\treturn { error: `Invalid context window \"${input}\". ${validationError}.` };\n\t}\n\n\treturn { value: tokens };\n}\n\nexport function formatContextWindow(value: number): string {\n\tif (value >= 1_000_000) {\n\t\tconst millions = value / 1_000_000;\n\t\treturn millions % 1 === 0 ? `${millions}m` : `${millions.toFixed(1)}m`;\n\t}\n\tif (value >= 1_000) {\n\t\tconst thousands = value / 1_000;\n\t\treturn thousands % 1 === 0 ? `${thousands}k` : `${thousands.toFixed(1)}k`;\n\t}\n\treturn String(value);\n}\n\nexport function normalizeContextWindowOptions(values: readonly number[] | undefined): number[] {\n\tconst seen = new Set<number>();\n\tconst normalized: number[] = [];\n\tfor (const value of values ?? []) {\n\t\tif (!isPositiveInteger(value) || seen.has(value)) continue;\n\t\tseen.add(value);\n\t\tnormalized.push(value);\n\t}\n\treturn normalized.sort((a, b) => a - b);\n}\n\nexport function getModelDefaultContextWindow(model: Model<Api>): number {\n\treturn isPositiveInteger(model.defaultContextWindow ?? 0) ? model.defaultContextWindow! : model.contextWindow;\n}\n\n/**\n * Effective input-token budget for compaction/overflow decisions. Equals the displayed\n * `contextWindow` unless a smaller hard input cap (`maxInputTokens`, e.g. GitHub Copilot's\n * `max_prompt_tokens`) is advertised, in which case the lower of the two is used. This lets a model\n * display its full/branded window while compaction and overflow recovery respect the real,\n * server-enforced input limit.\n */\nexport function getEffectiveInputBudget(model: Model<Api>): number {\n\tconst cap = model.maxInputTokens;\n\treturn isPositiveInteger(cap ?? 0) ? Math.min(model.contextWindow, cap as number) : model.contextWindow;\n}\n\nexport function getSupportedContextWindows(model: Model<Api>): number[] {\n\treturn normalizeContextWindowOptions([getModelDefaultContextWindow(model), ...(model.contextWindowOptions ?? [])]);\n}\n\nexport function withContextWindowOptions<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindowOptions: readonly number[],\n): Model<TApi> {\n\treturn {\n\t\t...model,\n\t\tdefaultContextWindow: getModelDefaultContextWindow(model as Model<Api>),\n\t\tcontextWindowOptions: normalizeContextWindowOptions(contextWindowOptions),\n\t};\n}\n\nfunction resolveSelectableContextWindow(\n\tmodel: Model<Api>,\n\trequestedContextWindow: number,\n\tsupported: readonly number[],\n\toptions: ContextWindowSelectionOptions,\n): number | undefined {\n\tif (supported.includes(requestedContextWindow)) {\n\t\treturn requestedContextWindow;\n\t}\n\n\tif (options.allowCopilotLongContextFallback !== true || model.provider !== \"github-copilot\") {\n\t\treturn undefined;\n\t}\n\n\tconst defaultContextWindow = getModelDefaultContextWindow(model);\n\tconst candidates = supported.filter(\n\t\t(contextWindow) => contextWindow <= requestedContextWindow && contextWindow > defaultContextWindow,\n\t);\n\treturn candidates.length > 0 ? Math.max(...candidates) : undefined;\n}\n\nexport function selectContextWindow<TApi extends Api>(\n\tmodel: Model<TApi>,\n\tcontextWindow: number,\n\toptions: ContextWindowSelectionOptions = {},\n): ContextWindowSelection<TApi> | ContextWindowSelectionError {\n\tconst validationError = validateContextWindowValue(contextWindow);\n\tif (validationError) {\n\t\treturn { error: validationError };\n\t}\n\n\tconst apiModel = model as Model<Api>;\n\tconst supported = getSupportedContextWindows(apiModel);\n\tconst selectedContextWindow = resolveSelectableContextWindow(apiModel, contextWindow, supported, options);\n\tif (selectedContextWindow === undefined) {\n\t\treturn {\n\t\t\terror: `Context window ${formatContextWindow(contextWindow)} is not supported by ${model.provider}/${model.id}. Supported values: ${supported.map(formatContextWindow).join(\", \")}.`,\n\t\t};\n\t}\n\n\treturn {\n\t\tmodel: {\n\t\t\t...model,\n\t\t\tdefaultContextWindow: getModelDefaultContextWindow(apiModel),\n\t\t\tcontextWindow: selectedContextWindow,\n\t\t\tcontextWindowOptions: supported,\n\t\t},\n\t\tcontextWindow: selectedContextWindow,\n\t};\n}\n"]}
@@ -1,17 +1,24 @@
1
1
  /**
2
- * GitHub Copilot model catalog (CAPI) — dynamic, input-token context windows.
2
+ * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.
3
3
  *
4
- * GitHub's Copilot API (CAPI) exposes per-model limits via `GET {baseUrl}/models`. Every window
5
- * Atomic shows for a Copilot model is measured in INPUT (prompt) tokens, exactly like every other
6
- * provider's `contextWindow`, so GitHub models read consistently across the UI:
4
+ * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:
7
5
  *
8
- * - A model's input budget = `capabilities.limits.max_prompt_tokens`
9
- * ?? `capabilities.limits.max_context_window_tokens`
10
- * ?? 128_000 (the two fallbacks are safeties real CAPI entries never hit).
11
- * - Models with tiered pricing expose per-tier input budgets via
12
- * `billing.token_prices.<tier>.context_max`. The `default` tier is the base context window
13
- * (e.g. gpt-5.5 272k, Claude 200k); a `long_context` tier adds a selectable larger input
14
- * window (e.g. gpt-5.5 922k, Claude 936k) the user can switch to via the `/model` picker.
6
+ * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity
7
+ * (prompt + completion reserve).
8
+ * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely
9
+ * fill before the provider must reserve output tokens.
10
+ * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The
11
+ * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a
12
+ * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).
13
+ *
14
+ * Atomic shows the model's full context window for the selectable long tier (the
15
+ * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`
16
+ * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side
17
+ * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as
18
+ * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction
19
+ * thresholds and the overflow-recovery guard, so the branded total can be displayed without
20
+ * overrunning the server limit. The default (short) tier stays at the `default` billing tier's
21
+ * prompt budget.
15
22
  *
16
23
  * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers
17
24
  * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes
@@ -21,15 +28,24 @@
21
28
  /** Resolved input-token context window(s) for a single Copilot model. */
22
29
  export interface CopilotModelContext {
23
30
  /**
24
- * Base context window in INPUT tokens — shown in the footer and used for compaction. The
25
- * default tier's `context_max`, or the model-level `max_prompt_tokens` fallback otherwise.
31
+ * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the
32
+ * model-level `max_prompt_tokens` fallback otherwise.
26
33
  */
27
34
  contextWindow: number;
28
35
  /**
29
- * Selectable input-token windows (`[default, long]`) when the model exposes a `long_context`
30
- * tier larger than its default; absent for single-window models.
36
+ * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than
37
+ * its default; absent for single-window models. The long entry is the model's full
38
+ * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and
39
+ * `anthropic/*`.
31
40
  */
32
41
  contextWindowOptions?: readonly number[];
42
+ /**
43
+ * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used
44
+ * as the effective input budget for compaction thresholds and overflow recovery so the branded
45
+ * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the
46
+ * displayed window already equals the input cap.
47
+ */
48
+ maxInputTokens?: number;
33
49
  }
34
50
  /** Map of model id → resolved input-token context window(s). */
35
51
  export type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;
@@ -46,7 +62,7 @@ export declare const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.gith
46
62
  /** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */
47
63
  export declare const COPILOT_CATALOG_CACHE_TTL_MS: number;
48
64
  /** Current on-disk cache schema version. */
49
- export declare const COPILOT_CATALOG_CACHE_VERSION: 2;
65
+ export declare const COPILOT_CATALOG_CACHE_VERSION: 3;
50
66
  /**
51
67
  * Resolve the Copilot CAPI base URL.
52
68
  *
@@ -56,15 +72,17 @@ export declare const COPILOT_CATALOG_CACHE_VERSION: 2;
56
72
  * stable parsing logic is reimplemented here.)
57
73
  */
58
74
  export declare function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string;
59
- /** Raw input-token limits parsed from a CAPI model entry. */
75
+ /** Raw token limits parsed from a CAPI model entry. */
60
76
  export interface CopilotModelLimits {
61
- /** `capabilities.limits.max_prompt_tokens`. */
77
+ /** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */
62
78
  maxPromptTokens?: number;
63
- /** `capabilities.limits.max_context_window_tokens`. */
79
+ /** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */
64
80
  maxContextWindowTokens?: number;
65
- /** `billing.token_prices.default.context_max`. */
81
+ /** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */
82
+ maxOutputTokens?: number;
83
+ /** `billing.token_prices.default.context_max` — default-tier prompt threshold. */
66
84
  defaultContextMax?: number;
67
- /** `billing.token_prices.long_context.context_max`. */
85
+ /** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */
68
86
  longContextMax?: number;
69
87
  }
70
88
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"copilot-model-catalog.d.ts","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic, input-token context windows.\n *\n * GitHub's Copilot API (CAPI) exposes per-model limits via `GET {baseUrl}/models`. Every window\n * Atomic shows for a Copilot model is measured in INPUT (prompt) tokens, exactly like every other\n * provider's `contextWindow`, so GitHub models read consistently across the UI:\n *\n * - A model's input budget = `capabilities.limits.max_prompt_tokens`\n * ?? `capabilities.limits.max_context_window_tokens`\n * ?? 128_000 (the two fallbacks are safeties real CAPI entries never hit).\n * - Models with tiered pricing expose per-tier input budgets via\n * `billing.token_prices.<tier>.context_max`. The `default` tier is the base context window\n * (e.g. gpt-5.5 272k, Claude 200k); a `long_context` tier adds a selectable larger input\n * window (e.g. gpt-5.5 922k, Claude 936k) the user can switch to via the `/model` picker.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base context window in INPUT tokens — shown in the footer and used for compaction. The\n\t * default tier's `context_max`, or the model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable input-token windows (`[default, long]`) when the model exposes a `long_context`\n\t * tier larger than its default; absent for single-window models.\n\t */\n\tcontextWindowOptions?: readonly number[];\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 2 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai\n * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,\n * stable parsing logic is reimplemented here.)\n */\nexport function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string {\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw input-token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens`. */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens`. */\n\tmaxContextWindowTokens?: number;\n\t/** `billing.token_prices.default.context_max`. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max`. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\treturn { contextWindow: base, contextWindowOptions: [base, limits.longContextMax] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) return { contextWindow, contextWindowOptions: options };\n\t}\n\treturn { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"copilot-model-catalog.d.ts","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.\n *\n * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:\n *\n * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity\n * (prompt + completion reserve).\n * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely\n * fill before the provider must reserve output tokens.\n * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The\n * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a\n * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).\n *\n * Atomic shows the model's full context window for the selectable long tier (the\n * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`\n * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side\n * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as\n * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction\n * thresholds and the overflow-recovery guard, so the branded total can be displayed without\n * overrunning the server limit. The default (short) tier stays at the `default` billing tier's\n * prompt budget.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the\n\t * model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than\n\t * its default; absent for single-window models. The long entry is the model's full\n\t * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and\n\t * `anthropic/*`.\n\t */\n\tcontextWindowOptions?: readonly number[];\n\t/**\n\t * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used\n\t * as the effective input budget for compaction thresholds and overflow recovery so the branded\n\t * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the\n\t * displayed window already equals the input cap.\n\t */\n\tmaxInputTokens?: number;\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 3 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai\n * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,\n * stable parsing logic is reimplemented here.)\n */\nexport function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string {\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */\n\tmaxContextWindowTokens?: number;\n\t/** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */\n\tmaxOutputTokens?: number;\n\t/** `billing.token_prices.default.context_max` — default-tier prompt threshold. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\t// Display the model's full context window as the long tier (matching openai/* and anthropic/*)\n\t\t// when CAPI advertises it; otherwise fall back to the long-context prompt threshold for\n\t\t// older/sparse payloads.\n\t\tconst longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;\n\t\t// The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive\n\t\t// it from total − output reserve, else fall back to the long-context prompt threshold.\n\t\tconst derivedInputCap =\n\t\t\tlimits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined\n\t\t\t\t? limits.maxContextWindowTokens - limits.maxOutputTokens\n\t\t\t\t: undefined;\n\t\tconst inputCap =\n\t\t\tlimits.maxPromptTokens ??\n\t\t\t(derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??\n\t\t\tlimits.longContextMax;\n\t\t// Only carry the cap when the displayed long window actually exceeds it (the branded-total\n\t\t// case); when they coincide there is no gap and the input budget is just the window.\n\t\treturn longWindow > inputCap\n\t\t\t? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }\n\t\t\t: { contextWindow: base, contextWindowOptions: [base, longWindow] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tmaxOutputTokens: toPositiveInt(limits?.max_output_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst maxInputTokens = toPositiveInt(record?.maxInputTokens);\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) {\n\t\t\treturn maxInputTokens !== undefined\n\t\t\t\t? { contextWindow, contextWindowOptions: options, maxInputTokens }\n\t\t\t\t: { contextWindow, contextWindowOptions: options };\n\t\t}\n\t}\n\treturn maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
@@ -1,17 +1,24 @@
1
1
  /**
2
- * GitHub Copilot model catalog (CAPI) — dynamic, input-token context windows.
2
+ * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.
3
3
  *
4
- * GitHub's Copilot API (CAPI) exposes per-model limits via `GET {baseUrl}/models`. Every window
5
- * Atomic shows for a Copilot model is measured in INPUT (prompt) tokens, exactly like every other
6
- * provider's `contextWindow`, so GitHub models read consistently across the UI:
4
+ * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:
7
5
  *
8
- * - A model's input budget = `capabilities.limits.max_prompt_tokens`
9
- * ?? `capabilities.limits.max_context_window_tokens`
10
- * ?? 128_000 (the two fallbacks are safeties real CAPI entries never hit).
11
- * - Models with tiered pricing expose per-tier input budgets via
12
- * `billing.token_prices.<tier>.context_max`. The `default` tier is the base context window
13
- * (e.g. gpt-5.5 272k, Claude 200k); a `long_context` tier adds a selectable larger input
14
- * window (e.g. gpt-5.5 922k, Claude 936k) the user can switch to via the `/model` picker.
6
+ * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity
7
+ * (prompt + completion reserve).
8
+ * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely
9
+ * fill before the provider must reserve output tokens.
10
+ * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The
11
+ * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a
12
+ * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).
13
+ *
14
+ * Atomic shows the model's full context window for the selectable long tier (the
15
+ * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`
16
+ * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side
17
+ * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as
18
+ * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction
19
+ * thresholds and the overflow-recovery guard, so the branded total can be displayed without
20
+ * overrunning the server limit. The default (short) tier stays at the `default` billing tier's
21
+ * prompt budget.
15
22
  *
16
23
  * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers
17
24
  * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes
@@ -39,7 +46,7 @@ export const DEFAULT_COPILOT_API_BASE_URL = "https://api.individual.githubcopilo
39
46
  /** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */
40
47
  export const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;
41
48
  /** Current on-disk cache schema version. */
42
- export const COPILOT_CATALOG_CACHE_VERSION = 2;
49
+ export const COPILOT_CATALOG_CACHE_VERSION = 3;
43
50
  /**
44
51
  * Resolve the Copilot CAPI base URL.
45
52
  *
@@ -86,7 +93,23 @@ export function resolveCopilotModelContext(limits) {
86
93
  const maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;
87
94
  const base = limits.defaultContextMax ?? maxInput;
88
95
  if (limits.longContextMax !== undefined && limits.longContextMax > base) {
89
- return { contextWindow: base, contextWindowOptions: [base, limits.longContextMax] };
96
+ // Display the model's full context window as the long tier (matching openai/* and anthropic/*)
97
+ // when CAPI advertises it; otherwise fall back to the long-context prompt threshold for
98
+ // older/sparse payloads.
99
+ const longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;
100
+ // The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive
101
+ // it from total − output reserve, else fall back to the long-context prompt threshold.
102
+ const derivedInputCap = limits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined
103
+ ? limits.maxContextWindowTokens - limits.maxOutputTokens
104
+ : undefined;
105
+ const inputCap = limits.maxPromptTokens ??
106
+ (derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??
107
+ limits.longContextMax;
108
+ // Only carry the cap when the displayed long window actually exceeds it (the branded-total
109
+ // case); when they coincide there is no gap and the input budget is just the window.
110
+ return longWindow > inputCap
111
+ ? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }
112
+ : { contextWindow: base, contextWindowOptions: [base, longWindow] };
90
113
  }
91
114
  return { contextWindow: base };
92
115
  }
@@ -110,6 +133,7 @@ export function parseCopilotModelCatalog(body) {
110
133
  const context = resolveCopilotModelContext({
111
134
  maxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),
112
135
  maxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),
136
+ maxOutputTokens: toPositiveInt(limits?.max_output_tokens),
113
137
  defaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),
114
138
  longContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),
115
139
  });
@@ -169,13 +193,17 @@ function sanitizeCachedContext(value) {
169
193
  const contextWindow = toPositiveInt(record?.contextWindow);
170
194
  if (contextWindow === undefined)
171
195
  return undefined;
196
+ const maxInputTokens = toPositiveInt(record?.maxInputTokens);
172
197
  const rawOptions = record?.contextWindowOptions;
173
198
  if (Array.isArray(rawOptions)) {
174
199
  const options = rawOptions.map(toPositiveInt).filter((n) => n !== undefined);
175
- if (options.length > 1)
176
- return { contextWindow, contextWindowOptions: options };
200
+ if (options.length > 1) {
201
+ return maxInputTokens !== undefined
202
+ ? { contextWindow, contextWindowOptions: options, maxInputTokens }
203
+ : { contextWindow, contextWindowOptions: options };
204
+ }
177
205
  }
178
- return { contextWindow };
206
+ return maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };
179
207
  }
180
208
  /** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */
181
209
  export function readCopilotCatalogCache(path, options) {
@@ -1 +1 @@
1
- {"version":3,"file":"copilot-model-catalog.js","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic, input-token context windows.\n *\n * GitHub's Copilot API (CAPI) exposes per-model limits via `GET {baseUrl}/models`. Every window\n * Atomic shows for a Copilot model is measured in INPUT (prompt) tokens, exactly like every other\n * provider's `contextWindow`, so GitHub models read consistently across the UI:\n *\n * - A model's input budget = `capabilities.limits.max_prompt_tokens`\n * ?? `capabilities.limits.max_context_window_tokens`\n * ?? 128_000 (the two fallbacks are safeties real CAPI entries never hit).\n * - Models with tiered pricing expose per-tier input budgets via\n * `billing.token_prices.<tier>.context_max`. The `default` tier is the base context window\n * (e.g. gpt-5.5 272k, Claude 200k); a `long_context` tier adds a selectable larger input\n * window (e.g. gpt-5.5 922k, Claude 936k) the user can switch to via the `/model` picker.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base context window in INPUT tokens — shown in the footer and used for compaction. The\n\t * default tier's `context_max`, or the model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable input-token windows (`[default, long]`) when the model exposes a `long_context`\n\t * tier larger than its default; absent for single-window models.\n\t */\n\tcontextWindowOptions?: readonly number[];\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 2 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai\n * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,\n * stable parsing logic is reimplemented here.)\n */\nexport function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string {\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw input-token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens`. */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens`. */\n\tmaxContextWindowTokens?: number;\n\t/** `billing.token_prices.default.context_max`. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max`. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\treturn { contextWindow: base, contextWindowOptions: [base, limits.longContextMax] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) return { contextWindow, contextWindowOptions: options };\n\t}\n\treturn { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"copilot-model-catalog.js","sourceRoot":"","sources":["../../src/core/copilot-model-catalog.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * GitHub Copilot model catalog (CAPI) — dynamic prompt-token budgets.\n *\n * GitHub's Copilot API (CAPI) exposes distinct model limits via `GET {baseUrl}/models`:\n *\n * - `capabilities.limits.max_context_window_tokens` is the model's total context capacity\n * (prompt + completion reserve).\n * - `capabilities.limits.max_prompt_tokens` is the maximum prompt/input budget Atomic can safely\n * fill before the provider must reserve output tokens.\n * - `billing.token_prices.<tier>.context_max` is a prompt-token billing/selection threshold. The\n * `default` tier is the short prompt budget (e.g. gpt-5.5 272k, Claude 200k); a\n * `long_context` tier adds a selectable larger prompt budget (e.g. gpt-5.5 922k, Claude 936k).\n *\n * Atomic shows the model's full context window for the selectable long tier (the\n * `max_context_window_tokens` total, e.g. 1_000_000/1_050_000), matching how the native `openai/*`\n * and `anthropic/*` providers advertise these models. Because GitHub enforces a lower server-side\n * prompt cap (`max_prompt_tokens`, e.g. 936k/922k) below that total, the prompt cap is retained as\n * an internal effective input budget (`CopilotModelContext.maxInputTokens`) that drives compaction\n * thresholds and the overflow-recovery guard, so the branded total can be displayed without\n * overrunning the server limit. The default (short) tier stays at the `default` billing tier's\n * prompt budget.\n *\n * This data is intentionally NOT baked into a static map: GitHub adds/removes models and retiers\n * windows over time (e.g. a model that disappears from the catalog), so a hardcoded snapshot goes\n * stale. Instead the catalog is fetched live (gated on the user actually having the GitHub Copilot\n * provider) and cached on disk for a short TTL, exactly like the Copilot CLI.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** Resolved input-token context window(s) for a single Copilot model. */\nexport interface CopilotModelContext {\n\t/**\n\t * Base/displayed context window — shown in the footer. The default tier's `context_max`, or the\n\t * model-level `max_prompt_tokens` fallback otherwise.\n\t */\n\tcontextWindow: number;\n\t/**\n\t * Selectable windows (`[default, long]`) when the model exposes a `long_context` tier larger than\n\t * its default; absent for single-window models. The long entry is the model's full\n\t * `max_context_window_tokens` (total capacity) when advertised, matching `openai/*` and\n\t * `anthropic/*`.\n\t */\n\tcontextWindowOptions?: readonly number[];\n\t/**\n\t * Hard prompt/input cap (`max_prompt_tokens`) when it sits below the displayed long window. Used\n\t * as the effective input budget for compaction thresholds and overflow recovery so the branded\n\t * total can be shown without overrunning GitHub's server-side prompt limit. Absent when the\n\t * displayed window already equals the input cap.\n\t */\n\tmaxInputTokens?: number;\n}\n\n/** Map of model id → resolved input-token context window(s). */\nexport type CopilotModelCatalog = ReadonlyMap<string, CopilotModelContext>;\n\n/** Safety fallback when a model reports neither `max_prompt_tokens` nor `max_context_window_tokens`. */\nexport const COPILOT_CONTEXT_WINDOW_FALLBACK = 128_000;\n\nexport const COPILOT_CATALOG_API_VERSION = \"2026-06-01\";\n\n/**\n * Headers GitHub's CAPI expects for catalog reads. Mirrors the editor headers pi-ai already sends\n * for Copilot token refresh and model-policy calls, plus the dated API version.\n */\nexport const COPILOT_CATALOG_HEADERS: Readonly<Record<string, string>> = {\n\t\"User-Agent\": \"GitHubCopilotChat/0.35.0\",\n\t\"Editor-Version\": \"vscode/1.107.0\",\n\t\"Editor-Plugin-Version\": \"copilot-chat/0.35.0\",\n\t\"Copilot-Integration-Id\": \"vscode-chat\",\n\t\"X-GitHub-Api-Version\": COPILOT_CATALOG_API_VERSION,\n};\n\n/** Default (non-enterprise) Copilot CAPI base URL when the token has no resolvable `proxy-ep`. */\nexport const DEFAULT_COPILOT_API_BASE_URL = \"https://api.individual.githubcopilot.com\";\n\n/** Disk-cache freshness window, matching the Copilot CLI's list-models cache TTL. */\nexport const COPILOT_CATALOG_CACHE_TTL_MS = 30 * 60 * 1000;\n\n/** Current on-disk cache schema version. */\nexport const COPILOT_CATALOG_CACHE_VERSION = 3 as const;\n\n/**\n * Resolve the Copilot CAPI base URL.\n *\n * Copilot access tokens embed a `proxy-ep=proxy.<host>` segment; the API host is the same host with\n * `proxy.` swapped for `api.`. Falls back to the enterprise host or the individual default. (pi-ai\n * exposes an equivalent helper, but its published `dist` mangles the export name, so the small,\n * stable parsing logic is reimplemented here.)\n */\nexport function copilotApiBaseUrlFromToken(token: string | undefined, enterpriseDomain?: string): string {\n\tif (token) {\n\t\tconst match = token.match(/proxy-ep=([^;]+)/);\n\t\tif (match) {\n\t\t\treturn `https://${match[1].replace(/^proxy\\./, \"api.\")}`;\n\t\t}\n\t}\n\tif (enterpriseDomain) return `https://copilot-api.${enterpriseDomain}`;\n\treturn DEFAULT_COPILOT_API_BASE_URL;\n}\n\nfunction trimTrailingSlash(url: string): string {\n\treturn url.replace(/\\/+$/, \"\");\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n\treturn value && typeof value === \"object\" ? (value as Record<string, unknown>) : undefined;\n}\n\nfunction toPositiveInt(value: unknown): number | undefined {\n\treturn typeof value === \"number\" && Number.isInteger(value) && value > 0 ? value : undefined;\n}\n\n/** Raw token limits parsed from a CAPI model entry. */\nexport interface CopilotModelLimits {\n\t/** `capabilities.limits.max_prompt_tokens` — maximum prompt/input budget (the hard input cap). */\n\tmaxPromptTokens?: number;\n\t/** `capabilities.limits.max_context_window_tokens` — total context capacity (the displayed long tier). */\n\tmaxContextWindowTokens?: number;\n\t/** `capabilities.limits.max_output_tokens` — output reserve; derives the input cap when `max_prompt_tokens` is absent. */\n\tmaxOutputTokens?: number;\n\t/** `billing.token_prices.default.context_max` — default-tier prompt threshold. */\n\tdefaultContextMax?: number;\n\t/** `billing.token_prices.long_context.context_max` — long-context prompt threshold. */\n\tlongContextMax?: number;\n}\n\n/**\n * Resolve a model's input-token context window(s) from its CAPI limits.\n *\n * `contextWindow` is the model's base input budget — the default tier's `context_max` when tiered,\n * otherwise `max_prompt_tokens ?? max_context_window_tokens ?? 128_000`. A `long_context` tier that\n * is larger than the base adds a second selectable window. Returns `undefined` when the entry\n * carries no usable limit signal at all.\n */\nexport function resolveCopilotModelContext(limits: CopilotModelLimits): CopilotModelContext | undefined {\n\tconst hasSignal =\n\t\tlimits.maxPromptTokens !== undefined ||\n\t\tlimits.maxContextWindowTokens !== undefined ||\n\t\tlimits.defaultContextMax !== undefined ||\n\t\tlimits.longContextMax !== undefined;\n\tif (!hasSignal) return undefined;\n\n\tconst maxInput = limits.maxPromptTokens ?? limits.maxContextWindowTokens ?? COPILOT_CONTEXT_WINDOW_FALLBACK;\n\tconst base = limits.defaultContextMax ?? maxInput;\n\tif (limits.longContextMax !== undefined && limits.longContextMax > base) {\n\t\t// Display the model's full context window as the long tier (matching openai/* and anthropic/*)\n\t\t// when CAPI advertises it; otherwise fall back to the long-context prompt threshold for\n\t\t// older/sparse payloads.\n\t\tconst longWindow = limits.maxContextWindowTokens ?? limits.longContextMax;\n\t\t// The hard prompt/input cap GitHub enforces server-side: prefer max_prompt_tokens, else derive\n\t\t// it from total − output reserve, else fall back to the long-context prompt threshold.\n\t\tconst derivedInputCap =\n\t\t\tlimits.maxContextWindowTokens !== undefined && limits.maxOutputTokens !== undefined\n\t\t\t\t? limits.maxContextWindowTokens - limits.maxOutputTokens\n\t\t\t\t: undefined;\n\t\tconst inputCap =\n\t\t\tlimits.maxPromptTokens ??\n\t\t\t(derivedInputCap !== undefined && derivedInputCap > 0 ? derivedInputCap : undefined) ??\n\t\t\tlimits.longContextMax;\n\t\t// Only carry the cap when the displayed long window actually exceeds it (the branded-total\n\t\t// case); when they coincide there is no gap and the input budget is just the window.\n\t\treturn longWindow > inputCap\n\t\t\t? { contextWindow: base, contextWindowOptions: [base, longWindow], maxInputTokens: inputCap }\n\t\t\t: { contextWindow: base, contextWindowOptions: [base, longWindow] };\n\t}\n\treturn { contextWindow: base };\n}\n\n/**\n * Parse a raw CAPI `/models` response body into an input-token context-window catalog.\n */\nexport function parseCopilotModelCatalog(body: unknown): CopilotModelCatalog {\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tconst data = asRecord(body)?.data;\n\tif (!Array.isArray(data)) return catalog;\n\n\tfor (const entry of data) {\n\t\tconst record = asRecord(entry);\n\t\tif (!record) continue;\n\t\tconst id = record.id;\n\t\tif (typeof id !== \"string\" || id.length === 0) continue;\n\n\t\tconst limits = asRecord(asRecord(record.capabilities)?.limits);\n\t\tconst prices = asRecord(asRecord(record.billing)?.token_prices);\n\t\tconst context = resolveCopilotModelContext({\n\t\t\tmaxPromptTokens: toPositiveInt(limits?.max_prompt_tokens),\n\t\t\tmaxContextWindowTokens: toPositiveInt(limits?.max_context_window_tokens),\n\t\t\tmaxOutputTokens: toPositiveInt(limits?.max_output_tokens),\n\t\t\tdefaultContextMax: toPositiveInt(asRecord(prices?.default)?.context_max),\n\t\t\tlongContextMax: toPositiveInt(asRecord(prices?.long_context)?.context_max),\n\t\t});\n\t\tif (context) catalog.set(id, context);\n\t}\n\n\treturn catalog;\n}\n\nexport interface FetchCopilotModelCatalogOptions {\n\t/** Valid Copilot CAPI bearer token (e.g. from `modelRegistry.getApiKeyForProvider`). */\n\ttoken: string;\n\t/** Override the resolved base URL; defaults to one derived from the token. */\n\tbaseUrl?: string;\n\t/** Enterprise domain, used for base-URL resolution when the token lacks a `proxy-ep`. */\n\tenterpriseDomain?: string;\n\t/** Extra/override request headers. */\n\theaders?: Record<string, string>;\n\t/** Injectable `fetch` for testing. */\n\tfetchImpl?: typeof fetch;\n\t/** Abort signal. */\n\tsignal?: AbortSignal;\n}\n\n/** Fetch and parse the live Copilot model catalog from CAPI `GET {baseUrl}/models`. */\nexport async function fetchCopilotModelCatalog(options: FetchCopilotModelCatalogOptions): Promise<CopilotModelCatalog> {\n\tconst fetchImpl = options.fetchImpl ?? fetch;\n\tconst baseUrl = options.baseUrl ?? copilotApiBaseUrlFromToken(options.token, options.enterpriseDomain);\n\tconst response = await fetchImpl(`${trimTrailingSlash(baseUrl)}/models`, {\n\t\tmethod: \"GET\",\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Bearer ${options.token}`,\n\t\t\t...COPILOT_CATALOG_HEADERS,\n\t\t\t...options.headers,\n\t\t},\n\t\t...(options.signal ? { signal: options.signal } : {}),\n\t});\n\tif (!response.ok) {\n\t\tthrow new Error(`GitHub Copilot /models request failed: ${response.status} ${response.statusText}`);\n\t}\n\treturn parseCopilotModelCatalog(await response.json());\n}\n\n// ----------------------------------------------------------------------------\n// Active in-memory catalog (consulted by the model registry).\n//\n// Empty by default, so with no Copilot auth / no successful fetch the registry leaves Copilot\n// model context windows untouched and the picker never appears.\n// ----------------------------------------------------------------------------\n\nlet activeCatalog: CopilotModelCatalog = new Map();\n\n/** Replace the active catalog the registry derives context windows from. */\nexport function setActiveCopilotModelCatalog(catalog: CopilotModelCatalog): void {\n\tactiveCatalog = catalog;\n}\n\n/** The active catalog (empty until a successful auth-gated fetch/cache load). */\nexport function getActiveCopilotModelCatalog(): CopilotModelCatalog {\n\treturn activeCatalog;\n}\n\n/** Reset the active catalog (primarily for tests). */\nexport function clearActiveCopilotModelCatalog(): void {\n\tactiveCatalog = new Map();\n}\n\n// ----------------------------------------------------------------------------\n// Disk cache.\n// ----------------------------------------------------------------------------\n\ninterface CopilotCatalogCacheFile {\n\tversion: typeof COPILOT_CATALOG_CACHE_VERSION;\n\t/** CAPI host the catalog was fetched from; cache misses on host change (e.g. enterprise switch). */\n\thost: string;\n\t/** Epoch ms the catalog was fetched. */\n\tfetchedAt: number;\n\tmodels: Record<string, CopilotModelContext>;\n}\n\nfunction hostFromBaseUrl(baseUrl: string): string {\n\ttry {\n\t\treturn new URL(baseUrl).host;\n\t} catch {\n\t\treturn baseUrl;\n\t}\n}\n\nexport interface ReadCopilotCatalogCacheOptions {\n\t/** Expected CAPI host; a cached file from a different host is ignored. */\n\thost: string;\n\t/** Current epoch ms (injectable for tests). */\n\tnow?: number;\n\t/** Freshness window; defaults to {@link COPILOT_CATALOG_CACHE_TTL_MS}. */\n\tttlMs?: number;\n}\n\nfunction sanitizeCachedContext(value: unknown): CopilotModelContext | undefined {\n\tconst record = asRecord(value);\n\tconst contextWindow = toPositiveInt(record?.contextWindow);\n\tif (contextWindow === undefined) return undefined;\n\tconst maxInputTokens = toPositiveInt(record?.maxInputTokens);\n\tconst rawOptions = record?.contextWindowOptions;\n\tif (Array.isArray(rawOptions)) {\n\t\tconst options = rawOptions.map(toPositiveInt).filter((n): n is number => n !== undefined);\n\t\tif (options.length > 1) {\n\t\t\treturn maxInputTokens !== undefined\n\t\t\t\t? { contextWindow, contextWindowOptions: options, maxInputTokens }\n\t\t\t\t: { contextWindow, contextWindowOptions: options };\n\t\t}\n\t}\n\treturn maxInputTokens !== undefined ? { contextWindow, maxInputTokens } : { contextWindow };\n}\n\n/** Read a fresh, host-matching catalog from the cache file, or `undefined` if missing/stale/invalid. */\nexport function readCopilotCatalogCache(\n\tpath: string,\n\toptions: ReadCopilotCatalogCacheOptions,\n): CopilotModelCatalog | undefined {\n\tlet parsed: CopilotCatalogCacheFile;\n\ttry {\n\t\tif (!existsSync(path)) return undefined;\n\t\tparsed = JSON.parse(readFileSync(path, \"utf8\")) as CopilotCatalogCacheFile;\n\t} catch {\n\t\treturn undefined;\n\t}\n\tif (!parsed || parsed.version !== COPILOT_CATALOG_CACHE_VERSION) return undefined;\n\tif (parsed.host !== options.host) return undefined;\n\tconst now = options.now ?? Date.now();\n\tconst ttlMs = options.ttlMs ?? COPILOT_CATALOG_CACHE_TTL_MS;\n\tif (typeof parsed.fetchedAt !== \"number\" || now - parsed.fetchedAt >= ttlMs) return undefined;\n\tconst models = asRecord(parsed.models);\n\tif (!models) return undefined;\n\n\tconst catalog = new Map<string, CopilotModelContext>();\n\tfor (const [id, value] of Object.entries(models)) {\n\t\tconst context = sanitizeCachedContext(value);\n\t\tif (context) catalog.set(id, context);\n\t}\n\treturn catalog;\n}\n\n/** Write the catalog to the cache file (creating parent dirs). Best-effort; never throws. */\nexport function writeCopilotCatalogCache(\n\tpath: string,\n\tbaseUrl: string,\n\tcatalog: CopilotModelCatalog,\n\tnow?: number,\n): void {\n\tconst payload: CopilotCatalogCacheFile = {\n\t\tversion: COPILOT_CATALOG_CACHE_VERSION,\n\t\thost: hostFromBaseUrl(baseUrl),\n\t\tfetchedAt: now ?? Date.now(),\n\t\tmodels: Object.fromEntries(catalog),\n\t};\n\ttry {\n\t\tmkdirSync(dirname(path), { recursive: true });\n\t\twriteFileSync(path, JSON.stringify(payload), \"utf8\");\n\t} catch {\n\t\t// best-effort cache; ignore write failures\n\t}\n}\n\n/** Host component of a base URL, for matching {@link readCopilotCatalogCache} `host`. */\nexport function copilotCatalogCacheHost(baseUrl: string): string {\n\treturn hostFromBaseUrl(baseUrl);\n}\n\n/** Standard on-disk cache path for the Copilot model catalog under an agent directory. */\nexport function copilotCatalogCachePath(agentDir: string): string {\n\treturn join(agentDir, \"cache\", \"copilot-models.json\");\n}\n\n/**\n * Seed the active catalog synchronously from the on-disk cache, gated on a Copilot access token.\n *\n * Called at model-registry construction so a returning user's previously selected long-context\n * window is recognized before startup validation runs — otherwise the persisted choice would warn\n * (\"context window 936k is not supported…\") and reset until the async refresh completes. The cache\n * TTL is intentionally ignored here: stale-but-present windows are still valid for selection, and\n * the async loader independently refetches on its own freshness window. Returns true when a catalog\n * was applied. No-op (returns false) without a token or a host-matching cached catalog.\n */\nexport function seedActiveCopilotModelCatalogFromCache(\n\taccessToken: string | undefined,\n\tcachePath: string,\n\tnow?: number,\n): boolean {\n\tif (typeof accessToken !== \"string\" || accessToken.length === 0) return false;\n\tconst host = copilotCatalogCacheHost(copilotApiBaseUrlFromToken(accessToken));\n\tconst cached = readCopilotCatalogCache(cachePath, { host, now, ttlMs: Number.POSITIVE_INFINITY });\n\tif (!cached) return false;\n\tsetActiveCopilotModelCatalog(cached);\n\treturn true;\n}\n"]}