open-multi-agent-kit 0.98.2 → 0.98.4

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 (286) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +11 -2
  3. package/dist/cli/args.d.ts +1 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +14 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/help.d.ts.map +1 -1
  8. package/dist/cli/help.js +3 -0
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/mcp-attach.d.ts +3 -3
  11. package/dist/cli/mcp-attach.d.ts.map +1 -1
  12. package/dist/cli/mcp-attach.js +5 -4
  13. package/dist/cli/mcp-attach.js.map +1 -1
  14. package/dist/cli/model-contract.d.ts +5 -0
  15. package/dist/cli/model-contract.d.ts.map +1 -0
  16. package/dist/cli/model-contract.js +45 -0
  17. package/dist/cli/model-contract.js.map +1 -0
  18. package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -1
  19. package/dist/commands/adaptorch-doctor-cli.js +32 -10
  20. package/dist/commands/adaptorch-doctor-cli.js.map +1 -1
  21. package/dist/commands/init-cli.d.ts +12 -0
  22. package/dist/commands/init-cli.d.ts.map +1 -0
  23. package/dist/commands/init-cli.js +76 -0
  24. package/dist/commands/init-cli.js.map +1 -0
  25. package/dist/commands/provider-sync-cli.d.ts +27 -0
  26. package/dist/commands/provider-sync-cli.d.ts.map +1 -0
  27. package/dist/commands/provider-sync-cli.js +177 -0
  28. package/dist/commands/provider-sync-cli.js.map +1 -0
  29. package/dist/commands/run-command.d.ts +8 -0
  30. package/dist/commands/run-command.d.ts.map +1 -0
  31. package/dist/commands/run-command.js +26 -0
  32. package/dist/commands/run-command.js.map +1 -0
  33. package/dist/core/active-skill-state.d.ts +7 -0
  34. package/dist/core/active-skill-state.d.ts.map +1 -0
  35. package/dist/core/active-skill-state.js +25 -0
  36. package/dist/core/active-skill-state.js.map +1 -0
  37. package/dist/core/advisory-judge-model.d.ts.map +1 -1
  38. package/dist/core/advisory-judge-model.js +8 -1
  39. package/dist/core/advisory-judge-model.js.map +1 -1
  40. package/dist/core/advisory-judge-types.d.ts +18 -1
  41. package/dist/core/advisory-judge-types.d.ts.map +1 -1
  42. package/dist/core/advisory-judge-types.js.map +1 -1
  43. package/dist/core/advisory-judge.d.ts.map +1 -1
  44. package/dist/core/advisory-judge.js +41 -6
  45. package/dist/core/advisory-judge.js.map +1 -1
  46. package/dist/core/agent-session-services.d.ts +2 -0
  47. package/dist/core/agent-session-services.d.ts.map +1 -1
  48. package/dist/core/agent-session-services.js +2 -0
  49. package/dist/core/agent-session-services.js.map +1 -1
  50. package/dist/core/agent-session.d.ts +0 -1
  51. package/dist/core/agent-session.d.ts.map +1 -1
  52. package/dist/core/agent-session.js +30 -85
  53. package/dist/core/agent-session.js.map +1 -1
  54. package/dist/core/codex-chatgpt-web-bridge.d.ts +99 -0
  55. package/dist/core/codex-chatgpt-web-bridge.d.ts.map +1 -0
  56. package/dist/core/codex-chatgpt-web-bridge.js +192 -0
  57. package/dist/core/codex-chatgpt-web-bridge.js.map +1 -0
  58. package/dist/core/codex-chatgpt-web-sync.d.ts +50 -0
  59. package/dist/core/codex-chatgpt-web-sync.d.ts.map +1 -0
  60. package/dist/core/codex-chatgpt-web-sync.js +119 -0
  61. package/dist/core/codex-chatgpt-web-sync.js.map +1 -0
  62. package/dist/core/compaction/compaction-headroom.d.ts +30 -0
  63. package/dist/core/compaction/compaction-headroom.d.ts.map +1 -0
  64. package/dist/core/compaction/compaction-headroom.js +48 -0
  65. package/dist/core/compaction/compaction-headroom.js.map +1 -0
  66. package/dist/core/compaction/compaction.d.ts +2 -29
  67. package/dist/core/compaction/compaction.d.ts.map +1 -1
  68. package/dist/core/compaction/compaction.js +19 -47
  69. package/dist/core/compaction/compaction.js.map +1 -1
  70. package/dist/core/compaction/overflow-retry-guard.d.ts +20 -0
  71. package/dist/core/compaction/overflow-retry-guard.d.ts.map +1 -0
  72. package/dist/core/compaction/overflow-retry-guard.js +38 -0
  73. package/dist/core/compaction/overflow-retry-guard.js.map +1 -0
  74. package/dist/core/context-budget-headroom-types.d.ts.map +1 -1
  75. package/dist/core/context-budget-headroom-types.js +1 -1
  76. package/dist/core/context-budget-headroom-types.js.map +1 -1
  77. package/dist/core/context-budget-token-counter-types.d.ts +27 -0
  78. package/dist/core/context-budget-token-counter-types.d.ts.map +1 -0
  79. package/dist/core/context-budget-token-counter-types.js +2 -0
  80. package/dist/core/context-budget-token-counter-types.js.map +1 -0
  81. package/dist/core/context-budget-token-counter.d.ts +2 -26
  82. package/dist/core/context-budget-token-counter.d.ts.map +1 -1
  83. package/dist/core/context-budget-token-counter.js +2 -0
  84. package/dist/core/context-budget-token-counter.js.map +1 -1
  85. package/dist/core/context-budget-v2-scoring.d.ts.map +1 -1
  86. package/dist/core/context-budget-v2-scoring.js +14 -10
  87. package/dist/core/context-budget-v2-scoring.js.map +1 -1
  88. package/dist/core/domain-router.d.ts.map +1 -1
  89. package/dist/core/domain-router.js +1 -1
  90. package/dist/core/domain-router.js.map +1 -1
  91. package/dist/core/grok-harness-dispatch.d.ts +4 -0
  92. package/dist/core/grok-harness-dispatch.d.ts.map +1 -1
  93. package/dist/core/grok-harness-dispatch.js +20 -3
  94. package/dist/core/grok-harness-dispatch.js.map +1 -1
  95. package/dist/core/grok-harness.d.ts +15 -9
  96. package/dist/core/grok-harness.d.ts.map +1 -1
  97. package/dist/core/grok-harness.js +34 -8
  98. package/dist/core/grok-harness.js.map +1 -1
  99. package/dist/core/mcp/tools.d.ts +2 -2
  100. package/dist/core/mcp/tools.d.ts.map +1 -1
  101. package/dist/core/mcp/tools.js +29 -3
  102. package/dist/core/mcp/tools.js.map +1 -1
  103. package/dist/core/model-registry-schema.d.ts +1081 -0
  104. package/dist/core/model-registry-schema.d.ts.map +1 -0
  105. package/dist/core/model-registry-schema.js +148 -0
  106. package/dist/core/model-registry-schema.js.map +1 -0
  107. package/dist/core/model-registry.d.ts.map +1 -1
  108. package/dist/core/model-registry.js +1 -146
  109. package/dist/core/model-registry.js.map +1 -1
  110. package/dist/core/model-resolver.d.ts +2 -1
  111. package/dist/core/model-resolver.d.ts.map +1 -1
  112. package/dist/core/model-resolver.js +2 -1
  113. package/dist/core/model-resolver.js.map +1 -1
  114. package/dist/core/prompt-preset.d.ts +1 -1
  115. package/dist/core/prompt-preset.d.ts.map +1 -1
  116. package/dist/core/prompt-preset.js +16 -1
  117. package/dist/core/prompt-preset.js.map +1 -1
  118. package/dist/core/provider-display-names.d.ts.map +1 -1
  119. package/dist/core/provider-display-names.js +1 -0
  120. package/dist/core/provider-display-names.js.map +1 -1
  121. package/dist/core/provider-resilience.d.ts +8 -0
  122. package/dist/core/provider-resilience.d.ts.map +1 -1
  123. package/dist/core/provider-resilience.js +20 -3
  124. package/dist/core/provider-resilience.js.map +1 -1
  125. package/dist/core/provider-usage-types.d.ts +42 -0
  126. package/dist/core/provider-usage-types.d.ts.map +1 -0
  127. package/dist/core/provider-usage-types.js +2 -0
  128. package/dist/core/provider-usage-types.js.map +1 -0
  129. package/dist/core/provider-usage.d.ts +2 -31
  130. package/dist/core/provider-usage.d.ts.map +1 -1
  131. package/dist/core/provider-usage.js +4 -0
  132. package/dist/core/provider-usage.js.map +1 -1
  133. package/dist/core/redaction.d.ts.map +1 -1
  134. package/dist/core/redaction.js +18 -2
  135. package/dist/core/redaction.js.map +1 -1
  136. package/dist/core/sandbox/policy-merge.d.ts +6 -0
  137. package/dist/core/sandbox/policy-merge.d.ts.map +1 -0
  138. package/dist/core/sandbox/policy-merge.js +74 -0
  139. package/dist/core/sandbox/policy-merge.js.map +1 -0
  140. package/dist/core/sandbox/policy-paths.d.ts +4 -0
  141. package/dist/core/sandbox/policy-paths.d.ts.map +1 -0
  142. package/dist/core/sandbox/policy-paths.js +21 -0
  143. package/dist/core/sandbox/policy-paths.js.map +1 -0
  144. package/dist/core/sandbox/policy-types.d.ts +75 -0
  145. package/dist/core/sandbox/policy-types.d.ts.map +1 -0
  146. package/dist/core/sandbox/policy-types.js +2 -0
  147. package/dist/core/sandbox/policy-types.js.map +1 -0
  148. package/dist/core/sandbox/policy.d.ts +3 -77
  149. package/dist/core/sandbox/policy.d.ts.map +1 -1
  150. package/dist/core/sandbox/policy.js +3 -98
  151. package/dist/core/sandbox/policy.js.map +1 -1
  152. package/dist/core/sdk-provider-stream.d.ts +14 -0
  153. package/dist/core/sdk-provider-stream.d.ts.map +1 -0
  154. package/dist/core/sdk-provider-stream.js +38 -0
  155. package/dist/core/sdk-provider-stream.js.map +1 -0
  156. package/dist/core/sdk.d.ts +5 -1
  157. package/dist/core/sdk.d.ts.map +1 -1
  158. package/dist/core/sdk.js +17 -33
  159. package/dist/core/sdk.js.map +1 -1
  160. package/dist/core/session-bash-runtime.d.ts +7 -0
  161. package/dist/core/session-bash-runtime.d.ts.map +1 -1
  162. package/dist/core/session-bash-runtime.js +10 -1
  163. package/dist/core/session-bash-runtime.js.map +1 -1
  164. package/dist/core/session-failure-cause.d.ts.map +1 -1
  165. package/dist/core/session-failure-cause.js +10 -3
  166. package/dist/core/session-failure-cause.js.map +1 -1
  167. package/dist/core/session-run-termination.d.ts +18 -0
  168. package/dist/core/session-run-termination.d.ts.map +1 -0
  169. package/dist/core/session-run-termination.js +61 -0
  170. package/dist/core/session-run-termination.js.map +1 -0
  171. package/dist/core/session-termination.d.ts.map +1 -1
  172. package/dist/core/session-termination.js +1 -1
  173. package/dist/core/session-termination.js.map +1 -1
  174. package/dist/core/skill-selector.d.ts +40 -0
  175. package/dist/core/skill-selector.d.ts.map +1 -0
  176. package/dist/core/skill-selector.js +102 -0
  177. package/dist/core/skill-selector.js.map +1 -0
  178. package/dist/core/tools/render-utils.d.ts.map +1 -1
  179. package/dist/core/tools/render-utils.js +2 -4
  180. package/dist/core/tools/render-utils.js.map +1 -1
  181. package/dist/core/turn-metrics-record.d.ts +55 -0
  182. package/dist/core/turn-metrics-record.d.ts.map +1 -0
  183. package/dist/core/turn-metrics-record.js +149 -0
  184. package/dist/core/turn-metrics-record.js.map +1 -0
  185. package/dist/core/turn-metrics.d.ts +4 -56
  186. package/dist/core/turn-metrics.d.ts.map +1 -1
  187. package/dist/core/turn-metrics.js +12 -60
  188. package/dist/core/turn-metrics.js.map +1 -1
  189. package/dist/core/verified-bash-runtime.d.ts +40 -0
  190. package/dist/core/verified-bash-runtime.d.ts.map +1 -1
  191. package/dist/core/verified-bash-runtime.js +60 -13
  192. package/dist/core/verified-bash-runtime.js.map +1 -1
  193. package/dist/guardrails/evidence-system.d.ts +1 -1
  194. package/dist/guardrails/evidence-system.d.ts.map +1 -1
  195. package/dist/guardrails/evidence-system.js +18 -29
  196. package/dist/guardrails/evidence-system.js.map +1 -1
  197. package/dist/guardrails/merge-gate-result.d.ts +4 -0
  198. package/dist/guardrails/merge-gate-result.d.ts.map +1 -0
  199. package/dist/guardrails/merge-gate-result.js +32 -0
  200. package/dist/guardrails/merge-gate-result.js.map +1 -0
  201. package/dist/index.d.ts +2 -0
  202. package/dist/index.d.ts.map +1 -1
  203. package/dist/index.js +1 -0
  204. package/dist/index.js.map +1 -1
  205. package/dist/main.d.ts.map +1 -1
  206. package/dist/main.js +22 -22
  207. package/dist/main.js.map +1 -1
  208. package/dist/modes/interactive/interactive-login-options.d.ts +27 -0
  209. package/dist/modes/interactive/interactive-login-options.d.ts.map +1 -0
  210. package/dist/modes/interactive/interactive-login-options.js +54 -0
  211. package/dist/modes/interactive/interactive-login-options.js.map +1 -0
  212. package/dist/modes/interactive/interactive-mode.d.ts +7 -4
  213. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  214. package/dist/modes/interactive/interactive-mode.js +138 -140
  215. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  216. package/dist/modes/interactive/interactive-resume-command.d.ts +3 -0
  217. package/dist/modes/interactive/interactive-resume-command.d.ts.map +1 -0
  218. package/dist/modes/interactive/interactive-resume-command.js +24 -0
  219. package/dist/modes/interactive/interactive-resume-command.js.map +1 -0
  220. package/dist/modes/interactive/interactive-tool-result.d.ts +32 -0
  221. package/dist/modes/interactive/interactive-tool-result.d.ts.map +1 -0
  222. package/dist/modes/interactive/interactive-tool-result.js +59 -0
  223. package/dist/modes/interactive/interactive-tool-result.js.map +1 -0
  224. package/dist/modes/print-mode.d.ts.map +1 -1
  225. package/dist/modes/print-mode.js +22 -0
  226. package/dist/modes/print-mode.js.map +1 -1
  227. package/dist/utils/clipboard-image.d.ts.map +1 -1
  228. package/dist/utils/clipboard-image.js +10 -60
  229. package/dist/utils/clipboard-image.js.map +1 -1
  230. package/dist/utils/terminal-links.d.ts +13 -0
  231. package/dist/utils/terminal-links.d.ts.map +1 -0
  232. package/dist/utils/terminal-links.js +68 -0
  233. package/dist/utils/terminal-links.js.map +1 -0
  234. package/dist/utils/windows-clipboard-image.d.ts +16 -0
  235. package/dist/utils/windows-clipboard-image.d.ts.map +1 -0
  236. package/dist/utils/windows-clipboard-image.js +87 -0
  237. package/dist/utils/windows-clipboard-image.js.map +1 -0
  238. package/docs/adaptorch-onboarding.md +80 -0
  239. package/docs/advisory-selection.md +112 -0
  240. package/docs/containerization.md +12 -0
  241. package/docs/context-files.md +97 -0
  242. package/docs/correctness-wall.md +8 -0
  243. package/docs/custom-provider.md +35 -0
  244. package/docs/development.md +18 -3
  245. package/docs/environment-variables.md +2 -0
  246. package/docs/grok-harness.md +4 -0
  247. package/docs/harness-boundaries.md +154 -0
  248. package/docs/harness-improvements.md +90 -0
  249. package/docs/json.md +19 -0
  250. package/docs/keybindings.md +1 -1
  251. package/docs/mcp.md +22 -2
  252. package/docs/metrics.md +66 -16
  253. package/docs/model-catalog-refresh.md +190 -0
  254. package/docs/model-contract.md +168 -0
  255. package/docs/models.md +21 -4
  256. package/docs/provider-resilience.md +5 -3
  257. package/docs/providers.md +68 -1
  258. package/docs/quickstart.md +4 -0
  259. package/docs/release-audit-0.98.3.md +97 -0
  260. package/docs/release-audit-0.98.4.md +112 -0
  261. package/docs/review-bundle-followup.md +195 -0
  262. package/docs/run-protocol.md +3 -1
  263. package/docs/runtime-algorithms.md +38 -2
  264. package/docs/sdk.md +35 -3
  265. package/docs/sessions.md +1 -1
  266. package/docs/settings.md +17 -1
  267. package/docs/skills.md +2 -0
  268. package/docs/tb21-audit.md +131 -0
  269. package/docs/terminal-setup.md +20 -0
  270. package/docs/usage.md +23 -1
  271. package/docs/windows.md +48 -0
  272. package/examples/README.md +4 -0
  273. package/examples/context/AGENTS.md +64 -0
  274. package/examples/context/CLAUDE.md +17 -0
  275. package/examples/context/INTERNET.md +42 -0
  276. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  277. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  278. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  279. package/examples/extensions/gondolin/package-lock.json +2 -2
  280. package/examples/extensions/gondolin/package.json +1 -1
  281. package/examples/extensions/sandbox/package-lock.json +2 -2
  282. package/examples/extensions/sandbox/package.json +1 -1
  283. package/examples/extensions/with-deps/package-lock.json +2 -2
  284. package/examples/extensions/with-deps/package.json +1 -1
  285. package/npm-shrinkwrap.json +18 -18
  286. package/package.json +6 -6
@@ -0,0 +1,99 @@
1
+ /**
2
+ * codex-chatgpt-web bridge catalog.
3
+ *
4
+ * The launcher (https://github.com/miuuyy/codex-chatgpt-web) serves a loopback Responses bridge
5
+ * whose `/v1/models` document is account-aware: a `chatgpt-web/*` slug advertises a 90K context
6
+ * window on Plus and 333K on Pro with Bigger Context. This module reads that document and derives
7
+ * the models.json rows for the provider, so OMK compacts on the bridge's real limits instead of a
8
+ * hardcoded guess. Network and file I/O stay in the caller.
9
+ */
10
+ export declare const CODEX_CHATGPT_WEB_MODEL_PREFIX = "chatgpt-web/";
11
+ /**
12
+ * Codex CLI version the bridge catalog was last verified against (2026-09-07). The Codex backend
13
+ * gates model visibility by `client_version`; a stale value yields an empty or partial catalog.
14
+ */
15
+ export declare const CODEX_CHATGPT_WEB_FALLBACK_CLIENT_VERSION = "0.147.0";
16
+ export type ModelInput = "text" | "image";
17
+ export interface BridgeHealth {
18
+ readonly version: string;
19
+ readonly mode: string;
20
+ readonly acceptingTurns: boolean;
21
+ }
22
+ export interface BridgeCatalogRow {
23
+ readonly id: string;
24
+ readonly name: string;
25
+ readonly contextWindow: number;
26
+ readonly autoCompactTokenLimit: number | undefined;
27
+ readonly reasoningLevel: string;
28
+ readonly input: readonly ModelInput[];
29
+ }
30
+ /** One models.json model row. Keys OMK does not derive are carried through untouched. */
31
+ export interface ModelsJsonModel {
32
+ readonly id: string;
33
+ readonly name?: string;
34
+ readonly contextWindow?: number;
35
+ readonly maxTokens?: number;
36
+ readonly reasoning?: boolean;
37
+ readonly input?: readonly ModelInput[];
38
+ readonly thinkingLevelMap?: Readonly<Record<string, string>>;
39
+ readonly cost?: Readonly<Record<string, number>>;
40
+ readonly [extra: string]: unknown;
41
+ }
42
+ export type SyncedField = "name" | "contextWindow" | "reasoning" | "input";
43
+ export interface ModelFieldChange {
44
+ readonly id: string;
45
+ readonly field: SyncedField;
46
+ readonly from: unknown;
47
+ readonly to: unknown;
48
+ }
49
+ export interface BridgeSyncPlan {
50
+ readonly models: readonly ModelsJsonModel[];
51
+ readonly added: readonly string[];
52
+ readonly removed: readonly string[];
53
+ readonly changes: readonly ModelFieldChange[];
54
+ }
55
+ /** Where the provider's rows came from, stored on the provider so the numbers stay explainable. */
56
+ export interface BridgeCatalogProvenance {
57
+ readonly bridgeVersion: string;
58
+ readonly clientVersion: string;
59
+ readonly syncedAt: string;
60
+ /** The bridge's rejection ceiling per row; `contextWindow` follows its compaction budget instead. */
61
+ readonly contextCeilings: Readonly<Record<string, number>>;
62
+ }
63
+ export interface ProviderSyncUpdate {
64
+ readonly models: readonly ModelsJsonModel[];
65
+ readonly bridgeCatalog: BridgeCatalogProvenance;
66
+ }
67
+ export declare function bridgeHealthUrl(baseUrl: string): string;
68
+ export declare function bridgeModelsUrl(baseUrl: string, clientVersion: string): string;
69
+ /** The models.json provider shape this module can sync; other keys pass through untouched. */
70
+ export interface CodexChatGptWebProviderConfig {
71
+ readonly api: "openai-responses";
72
+ readonly compat: {
73
+ readonly sendCodexTurnMetadata: true;
74
+ };
75
+ readonly baseUrl?: unknown;
76
+ readonly models?: unknown;
77
+ readonly [extra: string]: unknown;
78
+ }
79
+ /** A models.json provider is the bridge when it speaks Responses and opts into Codex turn metadata. */
80
+ export declare function isCodexChatGptWebProvider(provider: unknown): provider is CodexChatGptWebProviderConfig;
81
+ export declare function parseBridgeHealth(value: unknown): BridgeHealth | undefined;
82
+ /** `chatgpt-web/*` rows of a `/v1/models` document; undefined when the document has no models array. */
83
+ export declare function parseBridgeCatalog(value: unknown): readonly BridgeCatalogRow[] | undefined;
84
+ /**
85
+ * Merge the bridge catalog into the provider's models: derived fields follow the catalog, rows the
86
+ * bridge no longer serves are dropped, and everything else (user-owned fields, other namespaces,
87
+ * ordering) is preserved.
88
+ */
89
+ export declare function planBridgeModelSync(existing: readonly ModelsJsonModel[], rows: readonly BridgeCatalogRow[]): BridgeSyncPlan;
90
+ export declare function bridgeCatalogProvenance(source: Omit<BridgeCatalogProvenance, "contextCeilings">, rows: readonly BridgeCatalogRow[]): BridgeCatalogProvenance;
91
+ /** True when the stored provenance already describes this catalog; `syncedAt` alone never forces a rewrite. */
92
+ export declare function provenanceMatches(stored: unknown, next: BridgeCatalogProvenance): boolean;
93
+ /**
94
+ * Replace one provider's `models` array and `bridgeCatalog` provenance in a models.json document,
95
+ * preserving every other key and the file's indentation. Undefined when the text is not a JSON
96
+ * document with that provider, so a caller never writes a guess.
97
+ */
98
+ export declare function rewriteProviderModels(modelsJsonText: string, providerId: string, update: ProviderSyncUpdate): string | undefined;
99
+ //# sourceMappingURL=codex-chatgpt-web-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-chatgpt-web-bridge.d.ts","sourceRoot":"","sources":["../../src/core/codex-chatgpt-web-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * codex-chatgpt-web bridge catalog.\n *\n * The launcher (https://github.com/miuuyy/codex-chatgpt-web) serves a loopback Responses bridge\n * whose `/v1/models` document is account-aware: a `chatgpt-web/*` slug advertises a 90K context\n * window on Plus and 333K on Pro with Bigger Context. This module reads that document and derives\n * the models.json rows for the provider, so OMK compacts on the bridge's real limits instead of a\n * hardcoded guess. Network and file I/O stay in the caller.\n */\n\nexport const CODEX_CHATGPT_WEB_MODEL_PREFIX = \"chatgpt-web/\";\n/**\n * Codex CLI version the bridge catalog was last verified against (2026-09-07). The Codex backend\n * gates model visibility by `client_version`; a stale value yields an empty or partial catalog.\n */\nexport const CODEX_CHATGPT_WEB_FALLBACK_CLIENT_VERSION = \"0.147.0\";\nconst BRIDGE_SERVICE_NAME = \"codex-chatgpt-web\";\nconst DEFAULT_MAX_TOKENS = 32_768;\nconst ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;\n\nexport type ModelInput = \"text\" | \"image\";\n\nexport interface BridgeHealth {\n\treadonly version: string;\n\treadonly mode: string;\n\treadonly acceptingTurns: boolean;\n}\n\nexport interface BridgeCatalogRow {\n\treadonly id: string;\n\treadonly name: string;\n\treadonly contextWindow: number;\n\treadonly autoCompactTokenLimit: number | undefined;\n\treadonly reasoningLevel: string;\n\treadonly input: readonly ModelInput[];\n}\n\n/** One models.json model row. Keys OMK does not derive are carried through untouched. */\nexport interface ModelsJsonModel {\n\treadonly id: string;\n\treadonly name?: string;\n\treadonly contextWindow?: number;\n\treadonly maxTokens?: number;\n\treadonly reasoning?: boolean;\n\treadonly input?: readonly ModelInput[];\n\treadonly thinkingLevelMap?: Readonly<Record<string, string>>;\n\treadonly cost?: Readonly<Record<string, number>>;\n\treadonly [extra: string]: unknown;\n}\n\nexport type SyncedField = \"name\" | \"contextWindow\" | \"reasoning\" | \"input\";\n\nexport interface ModelFieldChange {\n\treadonly id: string;\n\treadonly field: SyncedField;\n\treadonly from: unknown;\n\treadonly to: unknown;\n}\n\nexport interface BridgeSyncPlan {\n\treadonly models: readonly ModelsJsonModel[];\n\treadonly added: readonly string[];\n\treadonly removed: readonly string[];\n\treadonly changes: readonly ModelFieldChange[];\n}\n\n/** Where the provider's rows came from, stored on the provider so the numbers stay explainable. */\nexport interface BridgeCatalogProvenance {\n\treadonly bridgeVersion: string;\n\treadonly clientVersion: string;\n\treadonly syncedAt: string;\n\t/** The bridge's rejection ceiling per row; `contextWindow` follows its compaction budget instead. */\n\treadonly contextCeilings: Readonly<Record<string, number>>;\n}\n\nexport interface ProviderSyncUpdate {\n\treadonly models: readonly ModelsJsonModel[];\n\treadonly bridgeCatalog: BridgeCatalogProvenance;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isPositiveInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\n\nfunction isModelInput(value: unknown): value is ModelInput {\n\treturn value === \"text\" || value === \"image\";\n}\n\nexport function bridgeHealthUrl(baseUrl: string): string {\n\treturn new URL(\"/healthz\", baseUrl).toString();\n}\n\nexport function bridgeModelsUrl(baseUrl: string, clientVersion: string): string {\n\treturn `${baseUrl.replace(/\\/+$/, \"\")}/models?client_version=${encodeURIComponent(clientVersion)}`;\n}\n\n/** The models.json provider shape this module can sync; other keys pass through untouched. */\nexport interface CodexChatGptWebProviderConfig {\n\treadonly api: \"openai-responses\";\n\treadonly compat: { readonly sendCodexTurnMetadata: true };\n\treadonly baseUrl?: unknown;\n\treadonly models?: unknown;\n\treadonly [extra: string]: unknown;\n}\n\n/** A models.json provider is the bridge when it speaks Responses and opts into Codex turn metadata. */\nexport function isCodexChatGptWebProvider(provider: unknown): provider is CodexChatGptWebProviderConfig {\n\treturn (\n\t\tisRecord(provider) &&\n\t\tprovider.api === \"openai-responses\" &&\n\t\tisRecord(provider.compat) &&\n\t\tprovider.compat.sendCodexTurnMetadata === true\n\t);\n}\n\nexport function parseBridgeHealth(value: unknown): BridgeHealth | undefined {\n\tif (!isRecord(value) || value.service !== BRIDGE_SERVICE_NAME) return undefined;\n\tif (typeof value.version !== \"string\" || typeof value.mode !== \"string\") return undefined;\n\treturn { version: value.version, mode: value.mode, acceptingTurns: value.accepting_turns === true };\n}\n\nfunction parseCatalogRow(value: unknown): BridgeCatalogRow | undefined {\n\tif (!isRecord(value) || typeof value.slug !== \"string\") return undefined;\n\tif (!value.slug.startsWith(CODEX_CHATGPT_WEB_MODEL_PREFIX)) return undefined;\n\tif (!isPositiveInteger(value.context_window)) return undefined;\n\tconst modalities = Array.isArray(value.input_modalities) ? value.input_modalities.filter(isModelInput) : [];\n\treturn {\n\t\tid: value.slug,\n\t\tname: typeof value.display_name === \"string\" && value.display_name.length > 0 ? value.display_name : value.slug,\n\t\tcontextWindow: value.context_window,\n\t\tautoCompactTokenLimit: isPositiveInteger(value.auto_compact_token_limit)\n\t\t\t? value.auto_compact_token_limit\n\t\t\t: undefined,\n\t\treasoningLevel: typeof value.default_reasoning_level === \"string\" ? value.default_reasoning_level : \"low\",\n\t\tinput: modalities.length > 0 ? modalities : [\"text\"],\n\t};\n}\n\n/** `chatgpt-web/*` rows of a `/v1/models` document; undefined when the document has no models array. */\nexport function parseBridgeCatalog(value: unknown): readonly BridgeCatalogRow[] | undefined {\n\tif (!isRecord(value) || !Array.isArray(value.models)) return undefined;\n\treturn value.models.flatMap((entry) => {\n\t\tconst row = parseCatalogRow(entry);\n\t\treturn row ? [row] : [];\n\t});\n}\n\n/** OMK thinking levels the bridge's fixed effort corresponds to; the bridge ignores the request effort. */\nfunction thinkingLevelMap(reasoningLevel: string): Readonly<Record<string, string>> | undefined {\n\tswitch (reasoningLevel) {\n\t\tcase \"xhigh\":\n\t\t\treturn { xhigh: \"xhigh\" };\n\t\tcase \"ultra\":\n\t\t\treturn { max: \"max\" };\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\n/**\n * OMK compacts within `contextWindow` by its own headroom policy, so the row carries the budget the\n * bridge expects a client to compact within (`auto_compact_token_limit`, Codex's effective window)\n * rather than the rejection ceiling. Rows without a budget (Luna) use the ceiling itself.\n */\nfunction derivedFields(row: BridgeCatalogRow): Pick<ModelsJsonModel, SyncedField> {\n\treturn {\n\t\tname: row.name,\n\t\tcontextWindow: row.autoCompactTokenLimit ?? row.contextWindow,\n\t\treasoning: row.reasoningLevel !== \"low\",\n\t\tinput: row.input,\n\t};\n}\n\nfunction newModel(row: BridgeCatalogRow): ModelsJsonModel {\n\tconst levels = thinkingLevelMap(row.reasoningLevel);\n\treturn {\n\t\tid: row.id,\n\t\t...derivedFields(row),\n\t\tmaxTokens: DEFAULT_MAX_TOKENS,\n\t\tcost: ZERO_COST,\n\t\t...(levels ? { thinkingLevelMap: levels } : {}),\n\t};\n}\n\nfunction fieldChanges(existing: ModelsJsonModel, row: BridgeCatalogRow): readonly ModelFieldChange[] {\n\tconst next = derivedFields(row);\n\tconst fields: readonly SyncedField[] = [\"name\", \"contextWindow\", \"reasoning\", \"input\"];\n\treturn fields.flatMap((field) => {\n\t\tconst from = existing[field];\n\t\tconst to = next[field];\n\t\treturn JSON.stringify(from) === JSON.stringify(to) ? [] : [{ id: existing.id, field, from, to }];\n\t});\n}\n\n/**\n * Merge the bridge catalog into the provider's models: derived fields follow the catalog, rows the\n * bridge no longer serves are dropped, and everything else (user-owned fields, other namespaces,\n * ordering) is preserved.\n */\nexport function planBridgeModelSync(\n\texisting: readonly ModelsJsonModel[],\n\trows: readonly BridgeCatalogRow[],\n): BridgeSyncPlan {\n\tconst byId = new Map(rows.map((row) => [row.id, row]));\n\tconst changes: ModelFieldChange[] = [];\n\tconst removed: string[] = [];\n\tconst kept: ModelsJsonModel[] = [];\n\tfor (const model of existing) {\n\t\tif (!model.id.startsWith(CODEX_CHATGPT_WEB_MODEL_PREFIX)) {\n\t\t\tkept.push(model);\n\t\t\tcontinue;\n\t\t}\n\t\tconst row = byId.get(model.id);\n\t\tif (!row) {\n\t\t\tremoved.push(model.id);\n\t\t\tcontinue;\n\t\t}\n\t\tchanges.push(...fieldChanges(model, row));\n\t\tkept.push({ ...model, ...derivedFields(row) });\n\t}\n\tconst seen = new Set(existing.map((model) => model.id));\n\tconst added = rows.filter((row) => !seen.has(row.id));\n\treturn {\n\t\tmodels: [...kept, ...added.map(newModel)],\n\t\tadded: added.map((row) => row.id),\n\t\tremoved,\n\t\tchanges,\n\t};\n}\n\nexport function bridgeCatalogProvenance(\n\tsource: Omit<BridgeCatalogProvenance, \"contextCeilings\">,\n\trows: readonly BridgeCatalogRow[],\n): BridgeCatalogProvenance {\n\treturn { ...source, contextCeilings: Object.fromEntries(rows.map((row) => [row.id, row.contextWindow])) };\n}\n\n/** True when the stored provenance already describes this catalog; `syncedAt` alone never forces a rewrite. */\nexport function provenanceMatches(stored: unknown, next: BridgeCatalogProvenance): boolean {\n\tif (!isRecord(stored)) return false;\n\treturn (\n\t\tstored.bridgeVersion === next.bridgeVersion &&\n\t\tstored.clientVersion === next.clientVersion &&\n\t\tJSON.stringify(stored.contextCeilings) === JSON.stringify(next.contextCeilings)\n\t);\n}\n\nfunction detectIndent(text: string): string {\n\tif (/^\\t+\"/m.test(text)) return \"\\t\";\n\tconst spaces = /^( +)\"/m.exec(text);\n\treturn spaces?.[1] ?? \" \";\n}\n\n/**\n * Replace one provider's `models` array and `bridgeCatalog` provenance in a models.json document,\n * preserving every other key and the file's indentation. Undefined when the text is not a JSON\n * document with that provider, so a caller never writes a guess.\n */\nexport function rewriteProviderModels(\n\tmodelsJsonText: string,\n\tproviderId: string,\n\tupdate: ProviderSyncUpdate,\n): string | undefined {\n\tlet document: unknown;\n\ttry {\n\t\tdocument = JSON.parse(modelsJsonText);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) return undefined;\n\t\tthrow error;\n\t}\n\tif (!isRecord(document) || !isRecord(document.providers)) return undefined;\n\tconst provider = document.providers[providerId];\n\tif (!isRecord(provider)) return undefined;\n\tconst providers = { ...document.providers, [providerId]: { ...provider, ...update } };\n\tconst serialized = JSON.stringify({ ...document, providers }, null, detectIndent(modelsJsonText));\n\treturn modelsJsonText.endsWith(\"\\n\") ? `${serialized}\\n` : serialized;\n}\n"]}
@@ -0,0 +1,192 @@
1
+ /**
2
+ * codex-chatgpt-web bridge catalog.
3
+ *
4
+ * The launcher (https://github.com/miuuyy/codex-chatgpt-web) serves a loopback Responses bridge
5
+ * whose `/v1/models` document is account-aware: a `chatgpt-web/*` slug advertises a 90K context
6
+ * window on Plus and 333K on Pro with Bigger Context. This module reads that document and derives
7
+ * the models.json rows for the provider, so OMK compacts on the bridge's real limits instead of a
8
+ * hardcoded guess. Network and file I/O stay in the caller.
9
+ */
10
+ export const CODEX_CHATGPT_WEB_MODEL_PREFIX = "chatgpt-web/";
11
+ /**
12
+ * Codex CLI version the bridge catalog was last verified against (2026-09-07). The Codex backend
13
+ * gates model visibility by `client_version`; a stale value yields an empty or partial catalog.
14
+ */
15
+ export const CODEX_CHATGPT_WEB_FALLBACK_CLIENT_VERSION = "0.147.0";
16
+ const BRIDGE_SERVICE_NAME = "codex-chatgpt-web";
17
+ const DEFAULT_MAX_TOKENS = 32_768;
18
+ const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
19
+ function isRecord(value) {
20
+ return typeof value === "object" && value !== null && !Array.isArray(value);
21
+ }
22
+ function isPositiveInteger(value) {
23
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
24
+ }
25
+ function isModelInput(value) {
26
+ return value === "text" || value === "image";
27
+ }
28
+ export function bridgeHealthUrl(baseUrl) {
29
+ return new URL("/healthz", baseUrl).toString();
30
+ }
31
+ export function bridgeModelsUrl(baseUrl, clientVersion) {
32
+ return `${baseUrl.replace(/\/+$/, "")}/models?client_version=${encodeURIComponent(clientVersion)}`;
33
+ }
34
+ /** A models.json provider is the bridge when it speaks Responses and opts into Codex turn metadata. */
35
+ export function isCodexChatGptWebProvider(provider) {
36
+ return (isRecord(provider) &&
37
+ provider.api === "openai-responses" &&
38
+ isRecord(provider.compat) &&
39
+ provider.compat.sendCodexTurnMetadata === true);
40
+ }
41
+ export function parseBridgeHealth(value) {
42
+ if (!isRecord(value) || value.service !== BRIDGE_SERVICE_NAME)
43
+ return undefined;
44
+ if (typeof value.version !== "string" || typeof value.mode !== "string")
45
+ return undefined;
46
+ return { version: value.version, mode: value.mode, acceptingTurns: value.accepting_turns === true };
47
+ }
48
+ function parseCatalogRow(value) {
49
+ if (!isRecord(value) || typeof value.slug !== "string")
50
+ return undefined;
51
+ if (!value.slug.startsWith(CODEX_CHATGPT_WEB_MODEL_PREFIX))
52
+ return undefined;
53
+ if (!isPositiveInteger(value.context_window))
54
+ return undefined;
55
+ const modalities = Array.isArray(value.input_modalities) ? value.input_modalities.filter(isModelInput) : [];
56
+ return {
57
+ id: value.slug,
58
+ name: typeof value.display_name === "string" && value.display_name.length > 0 ? value.display_name : value.slug,
59
+ contextWindow: value.context_window,
60
+ autoCompactTokenLimit: isPositiveInteger(value.auto_compact_token_limit)
61
+ ? value.auto_compact_token_limit
62
+ : undefined,
63
+ reasoningLevel: typeof value.default_reasoning_level === "string" ? value.default_reasoning_level : "low",
64
+ input: modalities.length > 0 ? modalities : ["text"],
65
+ };
66
+ }
67
+ /** `chatgpt-web/*` rows of a `/v1/models` document; undefined when the document has no models array. */
68
+ export function parseBridgeCatalog(value) {
69
+ if (!isRecord(value) || !Array.isArray(value.models))
70
+ return undefined;
71
+ return value.models.flatMap((entry) => {
72
+ const row = parseCatalogRow(entry);
73
+ return row ? [row] : [];
74
+ });
75
+ }
76
+ /** OMK thinking levels the bridge's fixed effort corresponds to; the bridge ignores the request effort. */
77
+ function thinkingLevelMap(reasoningLevel) {
78
+ switch (reasoningLevel) {
79
+ case "xhigh":
80
+ return { xhigh: "xhigh" };
81
+ case "ultra":
82
+ return { max: "max" };
83
+ default:
84
+ return undefined;
85
+ }
86
+ }
87
+ /**
88
+ * OMK compacts within `contextWindow` by its own headroom policy, so the row carries the budget the
89
+ * bridge expects a client to compact within (`auto_compact_token_limit`, Codex's effective window)
90
+ * rather than the rejection ceiling. Rows without a budget (Luna) use the ceiling itself.
91
+ */
92
+ function derivedFields(row) {
93
+ return {
94
+ name: row.name,
95
+ contextWindow: row.autoCompactTokenLimit ?? row.contextWindow,
96
+ reasoning: row.reasoningLevel !== "low",
97
+ input: row.input,
98
+ };
99
+ }
100
+ function newModel(row) {
101
+ const levels = thinkingLevelMap(row.reasoningLevel);
102
+ return {
103
+ id: row.id,
104
+ ...derivedFields(row),
105
+ maxTokens: DEFAULT_MAX_TOKENS,
106
+ cost: ZERO_COST,
107
+ ...(levels ? { thinkingLevelMap: levels } : {}),
108
+ };
109
+ }
110
+ function fieldChanges(existing, row) {
111
+ const next = derivedFields(row);
112
+ const fields = ["name", "contextWindow", "reasoning", "input"];
113
+ return fields.flatMap((field) => {
114
+ const from = existing[field];
115
+ const to = next[field];
116
+ return JSON.stringify(from) === JSON.stringify(to) ? [] : [{ id: existing.id, field, from, to }];
117
+ });
118
+ }
119
+ /**
120
+ * Merge the bridge catalog into the provider's models: derived fields follow the catalog, rows the
121
+ * bridge no longer serves are dropped, and everything else (user-owned fields, other namespaces,
122
+ * ordering) is preserved.
123
+ */
124
+ export function planBridgeModelSync(existing, rows) {
125
+ const byId = new Map(rows.map((row) => [row.id, row]));
126
+ const changes = [];
127
+ const removed = [];
128
+ const kept = [];
129
+ for (const model of existing) {
130
+ if (!model.id.startsWith(CODEX_CHATGPT_WEB_MODEL_PREFIX)) {
131
+ kept.push(model);
132
+ continue;
133
+ }
134
+ const row = byId.get(model.id);
135
+ if (!row) {
136
+ removed.push(model.id);
137
+ continue;
138
+ }
139
+ changes.push(...fieldChanges(model, row));
140
+ kept.push({ ...model, ...derivedFields(row) });
141
+ }
142
+ const seen = new Set(existing.map((model) => model.id));
143
+ const added = rows.filter((row) => !seen.has(row.id));
144
+ return {
145
+ models: [...kept, ...added.map(newModel)],
146
+ added: added.map((row) => row.id),
147
+ removed,
148
+ changes,
149
+ };
150
+ }
151
+ export function bridgeCatalogProvenance(source, rows) {
152
+ return { ...source, contextCeilings: Object.fromEntries(rows.map((row) => [row.id, row.contextWindow])) };
153
+ }
154
+ /** True when the stored provenance already describes this catalog; `syncedAt` alone never forces a rewrite. */
155
+ export function provenanceMatches(stored, next) {
156
+ if (!isRecord(stored))
157
+ return false;
158
+ return (stored.bridgeVersion === next.bridgeVersion &&
159
+ stored.clientVersion === next.clientVersion &&
160
+ JSON.stringify(stored.contextCeilings) === JSON.stringify(next.contextCeilings));
161
+ }
162
+ function detectIndent(text) {
163
+ if (/^\t+"/m.test(text))
164
+ return "\t";
165
+ const spaces = /^( +)"/m.exec(text);
166
+ return spaces?.[1] ?? " ";
167
+ }
168
+ /**
169
+ * Replace one provider's `models` array and `bridgeCatalog` provenance in a models.json document,
170
+ * preserving every other key and the file's indentation. Undefined when the text is not a JSON
171
+ * document with that provider, so a caller never writes a guess.
172
+ */
173
+ export function rewriteProviderModels(modelsJsonText, providerId, update) {
174
+ let document;
175
+ try {
176
+ document = JSON.parse(modelsJsonText);
177
+ }
178
+ catch (error) {
179
+ if (error instanceof SyntaxError)
180
+ return undefined;
181
+ throw error;
182
+ }
183
+ if (!isRecord(document) || !isRecord(document.providers))
184
+ return undefined;
185
+ const provider = document.providers[providerId];
186
+ if (!isRecord(provider))
187
+ return undefined;
188
+ const providers = { ...document.providers, [providerId]: { ...provider, ...update } };
189
+ const serialized = JSON.stringify({ ...document, providers }, null, detectIndent(modelsJsonText));
190
+ return modelsJsonText.endsWith("\n") ? `${serialized}\n` : serialized;
191
+ }
192
+ //# sourceMappingURL=codex-chatgpt-web-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-chatgpt-web-bridge.js","sourceRoot":"","sources":["../../src/core/codex-chatgpt-web-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * codex-chatgpt-web bridge catalog.\n *\n * The launcher (https://github.com/miuuyy/codex-chatgpt-web) serves a loopback Responses bridge\n * whose `/v1/models` document is account-aware: a `chatgpt-web/*` slug advertises a 90K context\n * window on Plus and 333K on Pro with Bigger Context. This module reads that document and derives\n * the models.json rows for the provider, so OMK compacts on the bridge's real limits instead of a\n * hardcoded guess. Network and file I/O stay in the caller.\n */\n\nexport const CODEX_CHATGPT_WEB_MODEL_PREFIX = \"chatgpt-web/\";\n/**\n * Codex CLI version the bridge catalog was last verified against (2026-09-07). The Codex backend\n * gates model visibility by `client_version`; a stale value yields an empty or partial catalog.\n */\nexport const CODEX_CHATGPT_WEB_FALLBACK_CLIENT_VERSION = \"0.147.0\";\nconst BRIDGE_SERVICE_NAME = \"codex-chatgpt-web\";\nconst DEFAULT_MAX_TOKENS = 32_768;\nconst ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } as const;\n\nexport type ModelInput = \"text\" | \"image\";\n\nexport interface BridgeHealth {\n\treadonly version: string;\n\treadonly mode: string;\n\treadonly acceptingTurns: boolean;\n}\n\nexport interface BridgeCatalogRow {\n\treadonly id: string;\n\treadonly name: string;\n\treadonly contextWindow: number;\n\treadonly autoCompactTokenLimit: number | undefined;\n\treadonly reasoningLevel: string;\n\treadonly input: readonly ModelInput[];\n}\n\n/** One models.json model row. Keys OMK does not derive are carried through untouched. */\nexport interface ModelsJsonModel {\n\treadonly id: string;\n\treadonly name?: string;\n\treadonly contextWindow?: number;\n\treadonly maxTokens?: number;\n\treadonly reasoning?: boolean;\n\treadonly input?: readonly ModelInput[];\n\treadonly thinkingLevelMap?: Readonly<Record<string, string>>;\n\treadonly cost?: Readonly<Record<string, number>>;\n\treadonly [extra: string]: unknown;\n}\n\nexport type SyncedField = \"name\" | \"contextWindow\" | \"reasoning\" | \"input\";\n\nexport interface ModelFieldChange {\n\treadonly id: string;\n\treadonly field: SyncedField;\n\treadonly from: unknown;\n\treadonly to: unknown;\n}\n\nexport interface BridgeSyncPlan {\n\treadonly models: readonly ModelsJsonModel[];\n\treadonly added: readonly string[];\n\treadonly removed: readonly string[];\n\treadonly changes: readonly ModelFieldChange[];\n}\n\n/** Where the provider's rows came from, stored on the provider so the numbers stay explainable. */\nexport interface BridgeCatalogProvenance {\n\treadonly bridgeVersion: string;\n\treadonly clientVersion: string;\n\treadonly syncedAt: string;\n\t/** The bridge's rejection ceiling per row; `contextWindow` follows its compaction budget instead. */\n\treadonly contextCeilings: Readonly<Record<string, number>>;\n}\n\nexport interface ProviderSyncUpdate {\n\treadonly models: readonly ModelsJsonModel[];\n\treadonly bridgeCatalog: BridgeCatalogProvenance;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction isPositiveInteger(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\n\nfunction isModelInput(value: unknown): value is ModelInput {\n\treturn value === \"text\" || value === \"image\";\n}\n\nexport function bridgeHealthUrl(baseUrl: string): string {\n\treturn new URL(\"/healthz\", baseUrl).toString();\n}\n\nexport function bridgeModelsUrl(baseUrl: string, clientVersion: string): string {\n\treturn `${baseUrl.replace(/\\/+$/, \"\")}/models?client_version=${encodeURIComponent(clientVersion)}`;\n}\n\n/** The models.json provider shape this module can sync; other keys pass through untouched. */\nexport interface CodexChatGptWebProviderConfig {\n\treadonly api: \"openai-responses\";\n\treadonly compat: { readonly sendCodexTurnMetadata: true };\n\treadonly baseUrl?: unknown;\n\treadonly models?: unknown;\n\treadonly [extra: string]: unknown;\n}\n\n/** A models.json provider is the bridge when it speaks Responses and opts into Codex turn metadata. */\nexport function isCodexChatGptWebProvider(provider: unknown): provider is CodexChatGptWebProviderConfig {\n\treturn (\n\t\tisRecord(provider) &&\n\t\tprovider.api === \"openai-responses\" &&\n\t\tisRecord(provider.compat) &&\n\t\tprovider.compat.sendCodexTurnMetadata === true\n\t);\n}\n\nexport function parseBridgeHealth(value: unknown): BridgeHealth | undefined {\n\tif (!isRecord(value) || value.service !== BRIDGE_SERVICE_NAME) return undefined;\n\tif (typeof value.version !== \"string\" || typeof value.mode !== \"string\") return undefined;\n\treturn { version: value.version, mode: value.mode, acceptingTurns: value.accepting_turns === true };\n}\n\nfunction parseCatalogRow(value: unknown): BridgeCatalogRow | undefined {\n\tif (!isRecord(value) || typeof value.slug !== \"string\") return undefined;\n\tif (!value.slug.startsWith(CODEX_CHATGPT_WEB_MODEL_PREFIX)) return undefined;\n\tif (!isPositiveInteger(value.context_window)) return undefined;\n\tconst modalities = Array.isArray(value.input_modalities) ? value.input_modalities.filter(isModelInput) : [];\n\treturn {\n\t\tid: value.slug,\n\t\tname: typeof value.display_name === \"string\" && value.display_name.length > 0 ? value.display_name : value.slug,\n\t\tcontextWindow: value.context_window,\n\t\tautoCompactTokenLimit: isPositiveInteger(value.auto_compact_token_limit)\n\t\t\t? value.auto_compact_token_limit\n\t\t\t: undefined,\n\t\treasoningLevel: typeof value.default_reasoning_level === \"string\" ? value.default_reasoning_level : \"low\",\n\t\tinput: modalities.length > 0 ? modalities : [\"text\"],\n\t};\n}\n\n/** `chatgpt-web/*` rows of a `/v1/models` document; undefined when the document has no models array. */\nexport function parseBridgeCatalog(value: unknown): readonly BridgeCatalogRow[] | undefined {\n\tif (!isRecord(value) || !Array.isArray(value.models)) return undefined;\n\treturn value.models.flatMap((entry) => {\n\t\tconst row = parseCatalogRow(entry);\n\t\treturn row ? [row] : [];\n\t});\n}\n\n/** OMK thinking levels the bridge's fixed effort corresponds to; the bridge ignores the request effort. */\nfunction thinkingLevelMap(reasoningLevel: string): Readonly<Record<string, string>> | undefined {\n\tswitch (reasoningLevel) {\n\t\tcase \"xhigh\":\n\t\t\treturn { xhigh: \"xhigh\" };\n\t\tcase \"ultra\":\n\t\t\treturn { max: \"max\" };\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\n/**\n * OMK compacts within `contextWindow` by its own headroom policy, so the row carries the budget the\n * bridge expects a client to compact within (`auto_compact_token_limit`, Codex's effective window)\n * rather than the rejection ceiling. Rows without a budget (Luna) use the ceiling itself.\n */\nfunction derivedFields(row: BridgeCatalogRow): Pick<ModelsJsonModel, SyncedField> {\n\treturn {\n\t\tname: row.name,\n\t\tcontextWindow: row.autoCompactTokenLimit ?? row.contextWindow,\n\t\treasoning: row.reasoningLevel !== \"low\",\n\t\tinput: row.input,\n\t};\n}\n\nfunction newModel(row: BridgeCatalogRow): ModelsJsonModel {\n\tconst levels = thinkingLevelMap(row.reasoningLevel);\n\treturn {\n\t\tid: row.id,\n\t\t...derivedFields(row),\n\t\tmaxTokens: DEFAULT_MAX_TOKENS,\n\t\tcost: ZERO_COST,\n\t\t...(levels ? { thinkingLevelMap: levels } : {}),\n\t};\n}\n\nfunction fieldChanges(existing: ModelsJsonModel, row: BridgeCatalogRow): readonly ModelFieldChange[] {\n\tconst next = derivedFields(row);\n\tconst fields: readonly SyncedField[] = [\"name\", \"contextWindow\", \"reasoning\", \"input\"];\n\treturn fields.flatMap((field) => {\n\t\tconst from = existing[field];\n\t\tconst to = next[field];\n\t\treturn JSON.stringify(from) === JSON.stringify(to) ? [] : [{ id: existing.id, field, from, to }];\n\t});\n}\n\n/**\n * Merge the bridge catalog into the provider's models: derived fields follow the catalog, rows the\n * bridge no longer serves are dropped, and everything else (user-owned fields, other namespaces,\n * ordering) is preserved.\n */\nexport function planBridgeModelSync(\n\texisting: readonly ModelsJsonModel[],\n\trows: readonly BridgeCatalogRow[],\n): BridgeSyncPlan {\n\tconst byId = new Map(rows.map((row) => [row.id, row]));\n\tconst changes: ModelFieldChange[] = [];\n\tconst removed: string[] = [];\n\tconst kept: ModelsJsonModel[] = [];\n\tfor (const model of existing) {\n\t\tif (!model.id.startsWith(CODEX_CHATGPT_WEB_MODEL_PREFIX)) {\n\t\t\tkept.push(model);\n\t\t\tcontinue;\n\t\t}\n\t\tconst row = byId.get(model.id);\n\t\tif (!row) {\n\t\t\tremoved.push(model.id);\n\t\t\tcontinue;\n\t\t}\n\t\tchanges.push(...fieldChanges(model, row));\n\t\tkept.push({ ...model, ...derivedFields(row) });\n\t}\n\tconst seen = new Set(existing.map((model) => model.id));\n\tconst added = rows.filter((row) => !seen.has(row.id));\n\treturn {\n\t\tmodels: [...kept, ...added.map(newModel)],\n\t\tadded: added.map((row) => row.id),\n\t\tremoved,\n\t\tchanges,\n\t};\n}\n\nexport function bridgeCatalogProvenance(\n\tsource: Omit<BridgeCatalogProvenance, \"contextCeilings\">,\n\trows: readonly BridgeCatalogRow[],\n): BridgeCatalogProvenance {\n\treturn { ...source, contextCeilings: Object.fromEntries(rows.map((row) => [row.id, row.contextWindow])) };\n}\n\n/** True when the stored provenance already describes this catalog; `syncedAt` alone never forces a rewrite. */\nexport function provenanceMatches(stored: unknown, next: BridgeCatalogProvenance): boolean {\n\tif (!isRecord(stored)) return false;\n\treturn (\n\t\tstored.bridgeVersion === next.bridgeVersion &&\n\t\tstored.clientVersion === next.clientVersion &&\n\t\tJSON.stringify(stored.contextCeilings) === JSON.stringify(next.contextCeilings)\n\t);\n}\n\nfunction detectIndent(text: string): string {\n\tif (/^\\t+\"/m.test(text)) return \"\\t\";\n\tconst spaces = /^( +)\"/m.exec(text);\n\treturn spaces?.[1] ?? \" \";\n}\n\n/**\n * Replace one provider's `models` array and `bridgeCatalog` provenance in a models.json document,\n * preserving every other key and the file's indentation. Undefined when the text is not a JSON\n * document with that provider, so a caller never writes a guess.\n */\nexport function rewriteProviderModels(\n\tmodelsJsonText: string,\n\tproviderId: string,\n\tupdate: ProviderSyncUpdate,\n): string | undefined {\n\tlet document: unknown;\n\ttry {\n\t\tdocument = JSON.parse(modelsJsonText);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) return undefined;\n\t\tthrow error;\n\t}\n\tif (!isRecord(document) || !isRecord(document.providers)) return undefined;\n\tconst provider = document.providers[providerId];\n\tif (!isRecord(provider)) return undefined;\n\tconst providers = { ...document.providers, [providerId]: { ...provider, ...update } };\n\tconst serialized = JSON.stringify({ ...document, providers }, null, detectIndent(modelsJsonText));\n\treturn modelsJsonText.endsWith(\"\\n\") ? `${serialized}\\n` : serialized;\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * codex-chatgpt-web provider sync workflow: health check, account-aware catalog fetch, sync plan,
3
+ * and the models.json rewrite. I/O arrives through `BridgeSyncIo`; expected failures are reported
4
+ * as `BridgeSyncFailure` with a user-facing message that never carries a credential.
5
+ */
6
+ import { type BridgeCatalogProvenance, type BridgeHealth, type BridgeSyncPlan } from "./codex-chatgpt-web-bridge.ts";
7
+ interface BridgeSyncResponse {
8
+ readonly status: number;
9
+ json(): Promise<unknown>;
10
+ text(): Promise<string>;
11
+ }
12
+ interface BridgeSyncRequestInit {
13
+ readonly signal: AbortSignal;
14
+ readonly headers?: Readonly<Record<string, string>>;
15
+ }
16
+ export interface BridgeSyncIo {
17
+ readonly readFile: (path: string) => string;
18
+ readonly writeFile: (path: string, text: string) => void;
19
+ readonly fetch: (url: string, init: BridgeSyncRequestInit) => Promise<BridgeSyncResponse>;
20
+ readonly getCodexAccessToken: () => Promise<string | undefined>;
21
+ /** ISO timestamp for `bridgeCatalog.syncedAt`. */
22
+ readonly now: () => string;
23
+ }
24
+ export interface BridgeSyncRequest {
25
+ readonly providerId: string;
26
+ readonly modelsPath: string;
27
+ readonly timeoutMs: number;
28
+ readonly clientVersion: string;
29
+ readonly dryRun: boolean;
30
+ }
31
+ export interface BridgeSyncReport {
32
+ readonly modelsPath: string;
33
+ readonly baseUrl: string;
34
+ readonly health: BridgeHealth;
35
+ readonly clientVersion: string;
36
+ readonly plan: BridgeSyncPlan;
37
+ readonly bridgeCatalog: BridgeCatalogProvenance;
38
+ readonly written: boolean;
39
+ }
40
+ export declare class BridgeSyncFailure extends Error {
41
+ constructor(message: string);
42
+ }
43
+ export declare function hasBridgeSyncChanges(plan: BridgeSyncPlan): boolean;
44
+ /**
45
+ * Run the sync. models.json is rewritten when a row changes or the recorded provenance no longer
46
+ * describes the bridge (its version, the client version, or a ceiling moved); `dryRun` only reports.
47
+ */
48
+ export declare function syncCodexChatGptWebProvider(sync: BridgeSyncRequest, io: BridgeSyncIo): Promise<BridgeSyncReport>;
49
+ export {};
50
+ //# sourceMappingURL=codex-chatgpt-web-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-chatgpt-web-sync.d.ts","sourceRoot":"","sources":["../../src/core/codex-chatgpt-web-sync.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,YAAY,EACjB,KAAK,cAAc,EAWnB,MAAM,+BAA+B,CAAC;AAMvC,UAAU,kBAAkB;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,IAAI,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACxB;AAED,UAAU,qBAAqB;IAC9B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACpD;AAED,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,qBAAqB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC1F,QAAQ,CAAC,mBAAmB,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAChE,kDAAkD;IAClD,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,uBAAuB,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC1B;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACD;AAaD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAElE;AA8ED;;;GAGG;AACH,wBAAsB,2BAA2B,CAChD,IAAI,EAAE,iBAAiB,EACvB,EAAE,EAAE,YAAY,GACd,OAAO,CAAC,gBAAgB,CAAC,CAyB3B","sourcesContent":["/**\n * codex-chatgpt-web provider sync workflow: health check, account-aware catalog fetch, sync plan,\n * and the models.json rewrite. I/O arrives through `BridgeSyncIo`; expected failures are reported\n * as `BridgeSyncFailure` with a user-facing message that never carries a credential.\n */\n\nimport {\n\ttype BridgeCatalogProvenance,\n\ttype BridgeHealth,\n\ttype BridgeSyncPlan,\n\tbridgeCatalogProvenance,\n\tbridgeHealthUrl,\n\tbridgeModelsUrl,\n\tisCodexChatGptWebProvider,\n\ttype ModelsJsonModel,\n\tparseBridgeCatalog,\n\tparseBridgeHealth,\n\tplanBridgeModelSync,\n\tprovenanceMatches,\n\trewriteProviderModels,\n} from \"./codex-chatgpt-web-bridge.ts\";\n\nconst LAUNCHER_HINT =\n\t\"Start the Codex Web GPT launcher (it hosts the signed-in ChatGPT Web session and the local bridge), then retry.\";\nconst LOGIN_HINT = \"run /login openai-codex in OMK\";\n\ninterface BridgeSyncResponse {\n\treadonly status: number;\n\tjson(): Promise<unknown>;\n\ttext(): Promise<string>;\n}\n\ninterface BridgeSyncRequestInit {\n\treadonly signal: AbortSignal;\n\treadonly headers?: Readonly<Record<string, string>>;\n}\n\nexport interface BridgeSyncIo {\n\treadonly readFile: (path: string) => string;\n\treadonly writeFile: (path: string, text: string) => void;\n\treadonly fetch: (url: string, init: BridgeSyncRequestInit) => Promise<BridgeSyncResponse>;\n\treadonly getCodexAccessToken: () => Promise<string | undefined>;\n\t/** ISO timestamp for `bridgeCatalog.syncedAt`. */\n\treadonly now: () => string;\n}\n\nexport interface BridgeSyncRequest {\n\treadonly providerId: string;\n\treadonly modelsPath: string;\n\treadonly timeoutMs: number;\n\treadonly clientVersion: string;\n\treadonly dryRun: boolean;\n}\n\nexport interface BridgeSyncReport {\n\treadonly modelsPath: string;\n\treadonly baseUrl: string;\n\treadonly health: BridgeHealth;\n\treadonly clientVersion: string;\n\treadonly plan: BridgeSyncPlan;\n\treadonly bridgeCatalog: BridgeCatalogProvenance;\n\treadonly written: boolean;\n}\n\nexport class BridgeSyncFailure extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"BridgeSyncFailure\";\n\t}\n}\n\ninterface LoadedProvider {\n\treadonly text: string;\n\treadonly baseUrl: string;\n\treadonly models: readonly ModelsJsonModel[];\n\treadonly bridgeCatalog: unknown;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function hasBridgeSyncChanges(plan: BridgeSyncPlan): boolean {\n\treturn plan.changes.length + plan.added.length + plan.removed.length > 0;\n}\n\n/** Parse models.json once and narrow the target provider to the bridge shape this workflow can sync. */\nfunction loadProvider(io: BridgeSyncIo, path: string, providerId: string): LoadedProvider {\n\tconst text = io.readFile(path);\n\tlet document: unknown;\n\ttry {\n\t\tdocument = JSON.parse(text);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) throw new BridgeSyncFailure(`Could not read ${path}: ${error.message}`);\n\t\tthrow error;\n\t}\n\tconst provider = isRecord(document) && isRecord(document.providers) ? document.providers[providerId] : undefined;\n\tif (provider === undefined) throw new BridgeSyncFailure(`Provider \"${providerId}\" is not defined in ${path}.`);\n\tif (!isCodexChatGptWebProvider(provider)) {\n\t\tthrow new BridgeSyncFailure(\n\t\t\t`Provider \"${providerId}\" is not a codex-chatgpt-web bridge: provider sync supports openai-responses providers with compat.sendCodexTurnMetadata: true.`,\n\t\t);\n\t}\n\tif (typeof provider.baseUrl !== \"string\") throw new BridgeSyncFailure(`Provider \"${providerId}\" has no baseUrl.`);\n\tconst rows: readonly unknown[] = Array.isArray(provider.models) ? provider.models : [];\n\tconst models = rows.flatMap((model) =>\n\t\tisRecord(model) && typeof model.id === \"string\" ? [{ ...model, id: model.id }] : [],\n\t);\n\treturn { text, baseUrl: provider.baseUrl, models, bridgeCatalog: provider.bridgeCatalog };\n}\n\nasync function request(io: BridgeSyncIo, url: string, init: BridgeSyncRequestInit): Promise<BridgeSyncResponse> {\n\ttry {\n\t\treturn await io.fetch(url, init);\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tthrow new BridgeSyncFailure(`Bridge request to ${url} failed (${error.message}). ${LAUNCHER_HINT}`);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nasync function checkHealth(io: BridgeSyncIo, baseUrl: string, timeoutMs: number): Promise<BridgeHealth> {\n\tconst url = bridgeHealthUrl(baseUrl);\n\tconst response = await request(io, url, { signal: AbortSignal.timeout(timeoutMs) });\n\tif (response.status !== 200) {\n\t\tthrow new BridgeSyncFailure(`Bridge health check at ${url} returned HTTP ${response.status}. ${LAUNCHER_HINT}`);\n\t}\n\tconst health = parseBridgeHealth(await response.json());\n\tif (!health)\n\t\tthrow new BridgeSyncFailure(`${url} did not answer as codex-chatgpt-web; check the provider's baseUrl.`);\n\treturn health;\n}\n\nasync function fetchCatalog(io: BridgeSyncIo, sync: BridgeSyncRequest, baseUrl: string) {\n\tconst token = await io.getCodexAccessToken();\n\tif (!token) {\n\t\tthrow new BridgeSyncFailure(\n\t\t\t`The bridge forwards /v1/models to the Codex backend with your ChatGPT OAuth; ${LOGIN_HINT} first.`,\n\t\t);\n\t}\n\tconst response = await request(io, bridgeModelsUrl(baseUrl, sync.clientVersion), {\n\t\tsignal: AbortSignal.timeout(sync.timeoutMs),\n\t\theaders: { authorization: `Bearer ${token}` },\n\t});\n\tif (response.status === 401 || response.status === 403) {\n\t\tthrow new BridgeSyncFailure(`The Codex backend rejected OMK's openai-codex OAuth token; ${LOGIN_HINT} again.`);\n\t}\n\tif (response.status !== 200) {\n\t\tconst body = await response.text();\n\t\tthrow new BridgeSyncFailure(`Bridge catalog request returned HTTP ${response.status}: ${body.slice(0, 300)}`);\n\t}\n\tconst rows = parseBridgeCatalog(await response.json());\n\tif (rows === undefined) throw new BridgeSyncFailure(\"Bridge catalog response has no models array.\");\n\tif (rows.length === 0) {\n\t\tthrow new BridgeSyncFailure(\n\t\t\t`Bridge advertised no chatgpt-web/* models for client_version ${sync.clientVersion}; sign in to the launcher and press Install models.`,\n\t\t);\n\t}\n\treturn rows;\n}\n\n/**\n * Run the sync. models.json is rewritten when a row changes or the recorded provenance no longer\n * describes the bridge (its version, the client version, or a ceiling moved); `dryRun` only reports.\n */\nexport async function syncCodexChatGptWebProvider(\n\tsync: BridgeSyncRequest,\n\tio: BridgeSyncIo,\n): Promise<BridgeSyncReport> {\n\tconst provider = loadProvider(io, sync.modelsPath, sync.providerId);\n\tconst health = await checkHealth(io, provider.baseUrl, sync.timeoutMs);\n\tconst rows = await fetchCatalog(io, sync, provider.baseUrl);\n\tconst plan = planBridgeModelSync(provider.models, rows);\n\tconst bridgeCatalog = bridgeCatalogProvenance(\n\t\t{ bridgeVersion: health.version, clientVersion: sync.clientVersion, syncedAt: io.now() },\n\t\trows,\n\t);\n\tconst stale = hasBridgeSyncChanges(plan) || !provenanceMatches(provider.bridgeCatalog, bridgeCatalog);\n\tconst written = stale && !sync.dryRun;\n\tif (written) {\n\t\tconst rewritten = rewriteProviderModels(provider.text, sync.providerId, { models: plan.models, bridgeCatalog });\n\t\tif (rewritten === undefined) throw new BridgeSyncFailure(`Could not rewrite ${sync.modelsPath}.`);\n\t\tio.writeFile(sync.modelsPath, rewritten);\n\t}\n\treturn {\n\t\tmodelsPath: sync.modelsPath,\n\t\tbaseUrl: provider.baseUrl,\n\t\thealth,\n\t\tclientVersion: sync.clientVersion,\n\t\tplan,\n\t\tbridgeCatalog,\n\t\twritten,\n\t};\n}\n"]}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * codex-chatgpt-web provider sync workflow: health check, account-aware catalog fetch, sync plan,
3
+ * and the models.json rewrite. I/O arrives through `BridgeSyncIo`; expected failures are reported
4
+ * as `BridgeSyncFailure` with a user-facing message that never carries a credential.
5
+ */
6
+ import { bridgeCatalogProvenance, bridgeHealthUrl, bridgeModelsUrl, isCodexChatGptWebProvider, parseBridgeCatalog, parseBridgeHealth, planBridgeModelSync, provenanceMatches, rewriteProviderModels, } from "./codex-chatgpt-web-bridge.js";
7
+ const LAUNCHER_HINT = "Start the Codex Web GPT launcher (it hosts the signed-in ChatGPT Web session and the local bridge), then retry.";
8
+ const LOGIN_HINT = "run /login openai-codex in OMK";
9
+ export class BridgeSyncFailure extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "BridgeSyncFailure";
13
+ }
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ export function hasBridgeSyncChanges(plan) {
19
+ return plan.changes.length + plan.added.length + plan.removed.length > 0;
20
+ }
21
+ /** Parse models.json once and narrow the target provider to the bridge shape this workflow can sync. */
22
+ function loadProvider(io, path, providerId) {
23
+ const text = io.readFile(path);
24
+ let document;
25
+ try {
26
+ document = JSON.parse(text);
27
+ }
28
+ catch (error) {
29
+ if (error instanceof SyntaxError)
30
+ throw new BridgeSyncFailure(`Could not read ${path}: ${error.message}`);
31
+ throw error;
32
+ }
33
+ const provider = isRecord(document) && isRecord(document.providers) ? document.providers[providerId] : undefined;
34
+ if (provider === undefined)
35
+ throw new BridgeSyncFailure(`Provider "${providerId}" is not defined in ${path}.`);
36
+ if (!isCodexChatGptWebProvider(provider)) {
37
+ throw new BridgeSyncFailure(`Provider "${providerId}" is not a codex-chatgpt-web bridge: provider sync supports openai-responses providers with compat.sendCodexTurnMetadata: true.`);
38
+ }
39
+ if (typeof provider.baseUrl !== "string")
40
+ throw new BridgeSyncFailure(`Provider "${providerId}" has no baseUrl.`);
41
+ const rows = Array.isArray(provider.models) ? provider.models : [];
42
+ const models = rows.flatMap((model) => isRecord(model) && typeof model.id === "string" ? [{ ...model, id: model.id }] : []);
43
+ return { text, baseUrl: provider.baseUrl, models, bridgeCatalog: provider.bridgeCatalog };
44
+ }
45
+ async function request(io, url, init) {
46
+ try {
47
+ return await io.fetch(url, init);
48
+ }
49
+ catch (error) {
50
+ if (error instanceof Error) {
51
+ throw new BridgeSyncFailure(`Bridge request to ${url} failed (${error.message}). ${LAUNCHER_HINT}`);
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+ async function checkHealth(io, baseUrl, timeoutMs) {
57
+ const url = bridgeHealthUrl(baseUrl);
58
+ const response = await request(io, url, { signal: AbortSignal.timeout(timeoutMs) });
59
+ if (response.status !== 200) {
60
+ throw new BridgeSyncFailure(`Bridge health check at ${url} returned HTTP ${response.status}. ${LAUNCHER_HINT}`);
61
+ }
62
+ const health = parseBridgeHealth(await response.json());
63
+ if (!health)
64
+ throw new BridgeSyncFailure(`${url} did not answer as codex-chatgpt-web; check the provider's baseUrl.`);
65
+ return health;
66
+ }
67
+ async function fetchCatalog(io, sync, baseUrl) {
68
+ const token = await io.getCodexAccessToken();
69
+ if (!token) {
70
+ throw new BridgeSyncFailure(`The bridge forwards /v1/models to the Codex backend with your ChatGPT OAuth; ${LOGIN_HINT} first.`);
71
+ }
72
+ const response = await request(io, bridgeModelsUrl(baseUrl, sync.clientVersion), {
73
+ signal: AbortSignal.timeout(sync.timeoutMs),
74
+ headers: { authorization: `Bearer ${token}` },
75
+ });
76
+ if (response.status === 401 || response.status === 403) {
77
+ throw new BridgeSyncFailure(`The Codex backend rejected OMK's openai-codex OAuth token; ${LOGIN_HINT} again.`);
78
+ }
79
+ if (response.status !== 200) {
80
+ const body = await response.text();
81
+ throw new BridgeSyncFailure(`Bridge catalog request returned HTTP ${response.status}: ${body.slice(0, 300)}`);
82
+ }
83
+ const rows = parseBridgeCatalog(await response.json());
84
+ if (rows === undefined)
85
+ throw new BridgeSyncFailure("Bridge catalog response has no models array.");
86
+ if (rows.length === 0) {
87
+ throw new BridgeSyncFailure(`Bridge advertised no chatgpt-web/* models for client_version ${sync.clientVersion}; sign in to the launcher and press Install models.`);
88
+ }
89
+ return rows;
90
+ }
91
+ /**
92
+ * Run the sync. models.json is rewritten when a row changes or the recorded provenance no longer
93
+ * describes the bridge (its version, the client version, or a ceiling moved); `dryRun` only reports.
94
+ */
95
+ export async function syncCodexChatGptWebProvider(sync, io) {
96
+ const provider = loadProvider(io, sync.modelsPath, sync.providerId);
97
+ const health = await checkHealth(io, provider.baseUrl, sync.timeoutMs);
98
+ const rows = await fetchCatalog(io, sync, provider.baseUrl);
99
+ const plan = planBridgeModelSync(provider.models, rows);
100
+ const bridgeCatalog = bridgeCatalogProvenance({ bridgeVersion: health.version, clientVersion: sync.clientVersion, syncedAt: io.now() }, rows);
101
+ const stale = hasBridgeSyncChanges(plan) || !provenanceMatches(provider.bridgeCatalog, bridgeCatalog);
102
+ const written = stale && !sync.dryRun;
103
+ if (written) {
104
+ const rewritten = rewriteProviderModels(provider.text, sync.providerId, { models: plan.models, bridgeCatalog });
105
+ if (rewritten === undefined)
106
+ throw new BridgeSyncFailure(`Could not rewrite ${sync.modelsPath}.`);
107
+ io.writeFile(sync.modelsPath, rewritten);
108
+ }
109
+ return {
110
+ modelsPath: sync.modelsPath,
111
+ baseUrl: provider.baseUrl,
112
+ health,
113
+ clientVersion: sync.clientVersion,
114
+ plan,
115
+ bridgeCatalog,
116
+ written,
117
+ };
118
+ }
119
+ //# sourceMappingURL=codex-chatgpt-web-sync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-chatgpt-web-sync.js","sourceRoot":"","sources":["../../src/core/codex-chatgpt-web-sync.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * codex-chatgpt-web provider sync workflow: health check, account-aware catalog fetch, sync plan,\n * and the models.json rewrite. I/O arrives through `BridgeSyncIo`; expected failures are reported\n * as `BridgeSyncFailure` with a user-facing message that never carries a credential.\n */\n\nimport {\n\ttype BridgeCatalogProvenance,\n\ttype BridgeHealth,\n\ttype BridgeSyncPlan,\n\tbridgeCatalogProvenance,\n\tbridgeHealthUrl,\n\tbridgeModelsUrl,\n\tisCodexChatGptWebProvider,\n\ttype ModelsJsonModel,\n\tparseBridgeCatalog,\n\tparseBridgeHealth,\n\tplanBridgeModelSync,\n\tprovenanceMatches,\n\trewriteProviderModels,\n} from \"./codex-chatgpt-web-bridge.ts\";\n\nconst LAUNCHER_HINT =\n\t\"Start the Codex Web GPT launcher (it hosts the signed-in ChatGPT Web session and the local bridge), then retry.\";\nconst LOGIN_HINT = \"run /login openai-codex in OMK\";\n\ninterface BridgeSyncResponse {\n\treadonly status: number;\n\tjson(): Promise<unknown>;\n\ttext(): Promise<string>;\n}\n\ninterface BridgeSyncRequestInit {\n\treadonly signal: AbortSignal;\n\treadonly headers?: Readonly<Record<string, string>>;\n}\n\nexport interface BridgeSyncIo {\n\treadonly readFile: (path: string) => string;\n\treadonly writeFile: (path: string, text: string) => void;\n\treadonly fetch: (url: string, init: BridgeSyncRequestInit) => Promise<BridgeSyncResponse>;\n\treadonly getCodexAccessToken: () => Promise<string | undefined>;\n\t/** ISO timestamp for `bridgeCatalog.syncedAt`. */\n\treadonly now: () => string;\n}\n\nexport interface BridgeSyncRequest {\n\treadonly providerId: string;\n\treadonly modelsPath: string;\n\treadonly timeoutMs: number;\n\treadonly clientVersion: string;\n\treadonly dryRun: boolean;\n}\n\nexport interface BridgeSyncReport {\n\treadonly modelsPath: string;\n\treadonly baseUrl: string;\n\treadonly health: BridgeHealth;\n\treadonly clientVersion: string;\n\treadonly plan: BridgeSyncPlan;\n\treadonly bridgeCatalog: BridgeCatalogProvenance;\n\treadonly written: boolean;\n}\n\nexport class BridgeSyncFailure extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"BridgeSyncFailure\";\n\t}\n}\n\ninterface LoadedProvider {\n\treadonly text: string;\n\treadonly baseUrl: string;\n\treadonly models: readonly ModelsJsonModel[];\n\treadonly bridgeCatalog: unknown;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nexport function hasBridgeSyncChanges(plan: BridgeSyncPlan): boolean {\n\treturn plan.changes.length + plan.added.length + plan.removed.length > 0;\n}\n\n/** Parse models.json once and narrow the target provider to the bridge shape this workflow can sync. */\nfunction loadProvider(io: BridgeSyncIo, path: string, providerId: string): LoadedProvider {\n\tconst text = io.readFile(path);\n\tlet document: unknown;\n\ttry {\n\t\tdocument = JSON.parse(text);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) throw new BridgeSyncFailure(`Could not read ${path}: ${error.message}`);\n\t\tthrow error;\n\t}\n\tconst provider = isRecord(document) && isRecord(document.providers) ? document.providers[providerId] : undefined;\n\tif (provider === undefined) throw new BridgeSyncFailure(`Provider \"${providerId}\" is not defined in ${path}.`);\n\tif (!isCodexChatGptWebProvider(provider)) {\n\t\tthrow new BridgeSyncFailure(\n\t\t\t`Provider \"${providerId}\" is not a codex-chatgpt-web bridge: provider sync supports openai-responses providers with compat.sendCodexTurnMetadata: true.`,\n\t\t);\n\t}\n\tif (typeof provider.baseUrl !== \"string\") throw new BridgeSyncFailure(`Provider \"${providerId}\" has no baseUrl.`);\n\tconst rows: readonly unknown[] = Array.isArray(provider.models) ? provider.models : [];\n\tconst models = rows.flatMap((model) =>\n\t\tisRecord(model) && typeof model.id === \"string\" ? [{ ...model, id: model.id }] : [],\n\t);\n\treturn { text, baseUrl: provider.baseUrl, models, bridgeCatalog: provider.bridgeCatalog };\n}\n\nasync function request(io: BridgeSyncIo, url: string, init: BridgeSyncRequestInit): Promise<BridgeSyncResponse> {\n\ttry {\n\t\treturn await io.fetch(url, init);\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tthrow new BridgeSyncFailure(`Bridge request to ${url} failed (${error.message}). ${LAUNCHER_HINT}`);\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nasync function checkHealth(io: BridgeSyncIo, baseUrl: string, timeoutMs: number): Promise<BridgeHealth> {\n\tconst url = bridgeHealthUrl(baseUrl);\n\tconst response = await request(io, url, { signal: AbortSignal.timeout(timeoutMs) });\n\tif (response.status !== 200) {\n\t\tthrow new BridgeSyncFailure(`Bridge health check at ${url} returned HTTP ${response.status}. ${LAUNCHER_HINT}`);\n\t}\n\tconst health = parseBridgeHealth(await response.json());\n\tif (!health)\n\t\tthrow new BridgeSyncFailure(`${url} did not answer as codex-chatgpt-web; check the provider's baseUrl.`);\n\treturn health;\n}\n\nasync function fetchCatalog(io: BridgeSyncIo, sync: BridgeSyncRequest, baseUrl: string) {\n\tconst token = await io.getCodexAccessToken();\n\tif (!token) {\n\t\tthrow new BridgeSyncFailure(\n\t\t\t`The bridge forwards /v1/models to the Codex backend with your ChatGPT OAuth; ${LOGIN_HINT} first.`,\n\t\t);\n\t}\n\tconst response = await request(io, bridgeModelsUrl(baseUrl, sync.clientVersion), {\n\t\tsignal: AbortSignal.timeout(sync.timeoutMs),\n\t\theaders: { authorization: `Bearer ${token}` },\n\t});\n\tif (response.status === 401 || response.status === 403) {\n\t\tthrow new BridgeSyncFailure(`The Codex backend rejected OMK's openai-codex OAuth token; ${LOGIN_HINT} again.`);\n\t}\n\tif (response.status !== 200) {\n\t\tconst body = await response.text();\n\t\tthrow new BridgeSyncFailure(`Bridge catalog request returned HTTP ${response.status}: ${body.slice(0, 300)}`);\n\t}\n\tconst rows = parseBridgeCatalog(await response.json());\n\tif (rows === undefined) throw new BridgeSyncFailure(\"Bridge catalog response has no models array.\");\n\tif (rows.length === 0) {\n\t\tthrow new BridgeSyncFailure(\n\t\t\t`Bridge advertised no chatgpt-web/* models for client_version ${sync.clientVersion}; sign in to the launcher and press Install models.`,\n\t\t);\n\t}\n\treturn rows;\n}\n\n/**\n * Run the sync. models.json is rewritten when a row changes or the recorded provenance no longer\n * describes the bridge (its version, the client version, or a ceiling moved); `dryRun` only reports.\n */\nexport async function syncCodexChatGptWebProvider(\n\tsync: BridgeSyncRequest,\n\tio: BridgeSyncIo,\n): Promise<BridgeSyncReport> {\n\tconst provider = loadProvider(io, sync.modelsPath, sync.providerId);\n\tconst health = await checkHealth(io, provider.baseUrl, sync.timeoutMs);\n\tconst rows = await fetchCatalog(io, sync, provider.baseUrl);\n\tconst plan = planBridgeModelSync(provider.models, rows);\n\tconst bridgeCatalog = bridgeCatalogProvenance(\n\t\t{ bridgeVersion: health.version, clientVersion: sync.clientVersion, syncedAt: io.now() },\n\t\trows,\n\t);\n\tconst stale = hasBridgeSyncChanges(plan) || !provenanceMatches(provider.bridgeCatalog, bridgeCatalog);\n\tconst written = stale && !sync.dryRun;\n\tif (written) {\n\t\tconst rewritten = rewriteProviderModels(provider.text, sync.providerId, { models: plan.models, bridgeCatalog });\n\t\tif (rewritten === undefined) throw new BridgeSyncFailure(`Could not rewrite ${sync.modelsPath}.`);\n\t\tio.writeFile(sync.modelsPath, rewritten);\n\t}\n\treturn {\n\t\tmodelsPath: sync.modelsPath,\n\t\tbaseUrl: provider.baseUrl,\n\t\thealth,\n\t\tclientVersion: sync.clientVersion,\n\t\tplan,\n\t\tbridgeCatalog,\n\t\twritten,\n\t};\n}\n"]}
@@ -0,0 +1,30 @@
1
+ export interface CompactionSettings {
2
+ enabled: boolean;
3
+ reserveTokens: number;
4
+ reservedOutputTokens?: number;
5
+ reservedToolResultTokens?: number;
6
+ safetyMarginTokens?: number;
7
+ imageReserveTokens?: number;
8
+ keepRecentTokens: number;
9
+ /** Maximum fraction of the context window to use before compaction. Default: 0.9. */
10
+ maxUsageRatio?: number;
11
+ rearmRatio?: number;
12
+ emergencyRatio?: number;
13
+ }
14
+ export declare const DEFAULT_COMPACTION_MAX_USAGE_RATIO = 0.9;
15
+ export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
16
+ export type CompactionHeadroomLimit = "max_usage_ratio" | "reserve_tokens";
17
+ export interface CompactionHeadroomThreshold {
18
+ triggerTokens: number;
19
+ headroomTokens: number;
20
+ maxUsageRatioTokens: number;
21
+ reserveBoundaryTokens: number;
22
+ limitedBy: CompactionHeadroomLimit;
23
+ }
24
+ /** Return the earliest compaction threshold from ratio-based headroom and absolute reserve. */
25
+ export declare function getCompactionHeadroomThreshold(contextWindow: number, settings: CompactionSettings): CompactionHeadroomThreshold | undefined;
26
+ /**
27
+ * Check if compaction should trigger based on context usage.
28
+ */
29
+ export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
30
+ //# sourceMappingURL=compaction-headroom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction-headroom.d.ts","sourceRoot":"","sources":["../../../src/core/compaction/compaction-headroom.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,qFAAqF;IACrF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,kCAAkC,MAAM,CAAC;AAEtD,eAAO,MAAM,2BAA2B,EAAE,kBAKzC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,iBAAiB,GAAG,gBAAgB,CAAC;AAE3E,MAAM,WAAW,2BAA2B;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,SAAS,EAAE,uBAAuB,CAAC;CACnC;AAED,+FAA+F;AAC/F,wBAAgB,8BAA8B,CAC7C,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,kBAAkB,GAC1B,2BAA2B,GAAG,SAAS,CAkCzC;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAGjH","sourcesContent":["import { computeReservedTokenBudget } from \"../context-budget-reserved-tokens.ts\";\n\nexport interface CompactionSettings {\n\tenabled: boolean;\n\treserveTokens: number;\n\treservedOutputTokens?: number;\n\treservedToolResultTokens?: number;\n\tsafetyMarginTokens?: number;\n\timageReserveTokens?: number;\n\tkeepRecentTokens: number;\n\t/** Maximum fraction of the context window to use before compaction. Default: 0.9. */\n\tmaxUsageRatio?: number;\n\trearmRatio?: number;\n\temergencyRatio?: number;\n}\n\nexport const DEFAULT_COMPACTION_MAX_USAGE_RATIO = 0.9;\n\nexport const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {\n\tenabled: true,\n\treserveTokens: 16384,\n\tkeepRecentTokens: 20000,\n\tmaxUsageRatio: DEFAULT_COMPACTION_MAX_USAGE_RATIO,\n};\n\nexport type CompactionHeadroomLimit = \"max_usage_ratio\" | \"reserve_tokens\";\n\nexport interface CompactionHeadroomThreshold {\n\ttriggerTokens: number;\n\theadroomTokens: number;\n\tmaxUsageRatioTokens: number;\n\treserveBoundaryTokens: number;\n\tlimitedBy: CompactionHeadroomLimit;\n}\n\n/** Return the earliest compaction threshold from ratio-based headroom and absolute reserve. */\nexport function getCompactionHeadroomThreshold(\n\tcontextWindow: number,\n\tsettings: CompactionSettings,\n): CompactionHeadroomThreshold | undefined {\n\tif (!settings.enabled || !Number.isFinite(contextWindow) || contextWindow <= 0) {\n\t\treturn undefined;\n\t}\n\n\tconst windowTokens = Math.floor(contextWindow);\n\tconst reserveTokens = Number.isFinite(settings.reserveTokens) ? Math.max(0, Math.floor(settings.reserveTokens)) : 0;\n\tconst configuredMaxUsageRatio = settings.maxUsageRatio ?? DEFAULT_COMPACTION_MAX_USAGE_RATIO;\n\tconst maxUsageRatio =\n\t\tNumber.isFinite(configuredMaxUsageRatio) && configuredMaxUsageRatio > 0 && configuredMaxUsageRatio < 1\n\t\t\t? configuredMaxUsageRatio\n\t\t\t: DEFAULT_COMPACTION_MAX_USAGE_RATIO;\n\tconst maxUsageRatioTokens = Math.max(1, Math.floor(windowTokens * maxUsageRatio));\n\tconst reservedBudget = computeReservedTokenBudget({\n\t\tmodelContextWindow: windowTokens,\n\t\tsystemPromptTokens: 0,\n\t\treservedOutputTokens: settings.reservedOutputTokens ?? reserveTokens,\n\t\treservedToolResultTokens: settings.reservedToolResultTokens ?? 0,\n\t\tsafetyMarginTokens: settings.safetyMarginTokens ?? 0,\n\t\timageReserveTokens: settings.imageReserveTokens ?? 0,\n\t});\n\tconst reserveBoundaryTokens =\n\t\treservedBudget.overflow || reservedBudget.effectiveBudget === 0\n\t\t\t? maxUsageRatioTokens\n\t\t\t: Math.max(1, reservedBudget.effectiveBudget);\n\tconst triggerTokens = Math.min(maxUsageRatioTokens, reserveBoundaryTokens);\n\n\treturn {\n\t\ttriggerTokens,\n\t\theadroomTokens: windowTokens - triggerTokens,\n\t\tmaxUsageRatioTokens,\n\t\treserveBoundaryTokens,\n\t\tlimitedBy: reserveBoundaryTokens <= maxUsageRatioTokens ? \"reserve_tokens\" : \"max_usage_ratio\",\n\t};\n}\n\n/**\n * Check if compaction should trigger based on context usage.\n */\nexport function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {\n\tconst threshold = getCompactionHeadroomThreshold(contextWindow, settings);\n\treturn threshold !== undefined && Number.isFinite(contextTokens) && contextTokens >= threshold.triggerTokens;\n}\n"]}