dsh-lcx-codex 0.4.0 → 0.4.1-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README_EN.md CHANGED
@@ -1,185 +1,197 @@
1
1
  <div align="center">
2
2
 
3
- <img src="https://raw.githubusercontent.com/kk3ya03-star/dsh-lcx-codex/main/assets/dsh-lcx-codex-banner.jpg" alt="DSH-LCX-CODEX" width="100%" />
4
-
5
- # DSH-LCX-CODEX
6
-
7
- **OpenAI Responses / Codex-native capabilities for DeepSeek Harness.**
3
+ <img src="assets/dsh-lcx-codex-banner-en.png" alt="dsh-lcx-codex — GPT Hosted Search, Codex-style Web Actions, and Native V2 Compaction for DSH" width="100%" />
8
4
 
9
5
  [![npm](https://img.shields.io/npm/v/dsh-lcx-codex?color=1677ff&label=npm)](https://www.npmjs.com/package/dsh-lcx-codex)
10
- ![Node](https://img.shields.io/badge/Node-%3E%3D20-1677ff)
6
+ [![CI](https://github.com/kk3ya03-star/dsh-lcx-codex/actions/workflows/publish.yml/badge.svg)](https://github.com/kk3ya03-star/dsh-lcx-codex/actions/workflows/publish.yml)
11
7
  ![DSH](https://img.shields.io/badge/DSH-0.1.1--rc.2-4ea8ff)
12
- ![License](https://img.shields.io/badge/license-MIT-1677ff)
8
+ ![Node](https://img.shields.io/badge/Node-%3E%3D20-2f855a)
9
+ ![License](https://img.shields.io/badge/license-MIT-6b7280)
13
10
 
14
- [简体中文](README.md) · **English**
11
+ [简体中文](README.md) · **English** · [Architecture](ARCHITECTURE.md) · [Changelog](CHANGELOG.md)
15
12
 
16
13
  </div>
17
14
 
18
- ---
15
+ > **DSH remains the host. LCX fills missing native capabilities on GPT Responses routes without modifying DSH core or replacing Agent / Session / Web / Compaction.**
19
16
 
20
- `dsh-lcx-codex` is a community DSH plugin for GPT routes already configured through `llm-pi-ai / openai-responses`. It keeps DSH's Agent, Web, Session and Compaction ownership intact and fills the provider-native gaps.
17
+ ## What it solves
21
18
 
22
- > `LCX` is only the project name. This project is not affiliated with OpenAI, DeepSeek, Sub2API or NewAPI.
19
+ If DSH already has a working GPT `openai-responses` route, `dsh-lcx-codex` lets that route use more of the native GPT Responses capability set while keeping DSH's existing entries and lifecycle ownership intact.
23
20
 
24
- - **Current DSH image pipeline**: Native V2 reuses `readImageRequest()` and the active route image budgets, so compaction and ordinary GPT requests see the same deterministic image variants.
21
+ | Scenario | Existing DSH surface | With LCX |
22
+ |---|---|---|
23
+ | Ordinary web search | DSH `web_search` | **Keep the same `web_search` entry** while LCX maps its SearchProvider to the active GPT Hosted Search route |
24
+ | Advanced Hosted Search controls | Keep ordinary search simple | Optionally add `websearch_gpt_advanced` for domain filters, approximate location, search context, image search, and related controls |
25
+ | Stateful page / PDF browsing | DSH keeps Web lifecycle ownership | Optionally add Codex / Alpha Web Actions: `search → open → find/click → screenshot` |
26
+ | Long-session compaction | DSH keeps pressure, transactions, and recovery | Prefer Responses Native V2 checkpoints at the existing compaction seam |
25
27
 
26
- ## Features
28
+ **The goal is not to build another Agent stack. It is to let a DSH GPT route use the native capabilities it should already have access to.**
27
29
 
28
- | Capability | What it does | Default |
29
- |---|---|---:|
30
- | **DSH `web_search` → GPT Hosted Search** | Uses DSH's existing ordinary search tool; rc.7+ follows the active Agent GPT Responses model | opt-in |
31
- | **Advanced Hosted Search** | Native domain/location/context/image search controls | off |
32
- | **Alpha Search** | Stateful `search/open/find/click/screenshot`-style commands, capability-gated | off |
33
- | **Native Remote Compaction V2** | Uses Responses `compaction_trigger` and provider-native opaque state | opt-in |
34
- | **Conversation fidelity retention** | Bounded user + assistant-visible history protects low-salience facts | built-in |
35
- | **Session-native checkpoints** | Durable state lives in the DSH append-only session log | built-in |
36
- | **Native-first auto compaction** | 90% Native V2, 95% emergency DSH pruning; configurable | opt-in |
37
- | **Long web timeout** | DSH `web_search` deadline defaults to 240s, configurable 30–600s | built-in |
30
+ ## Three core capabilities
38
31
 
39
- ## rc.8: DSH 0.1.1-rc.2 native alignment
32
+ ### 1. GPT Hosted Search: keep the ordinary search entry
40
33
 
41
- rc.8 targets **DSH 0.1.1-rc.2** directly. Ordinary Hosted Search still follows the active Agent route, while Native V2 image serialization now uses DSH's `readImageRequest()` request-version pipeline instead of reading attachment master bytes.
34
+ When enabled, the model still sees the native DSH `web_search` tool for ordinary search. LCX changes the SearchProvider path instead of registering a second duplicate search tool.
42
35
 
43
- Search routing still crosses DSH's `tools/execute` → SearchProvider boundary without changing the model-visible `web_search` schema:
36
+ Enable `websearch_gpt_advanced` only when you need finer Hosted Search controls.
44
37
 
45
- ```text
46
- Agent: lcx / gpt-5.6-luna
47
- └─ DSH web_search
48
- └─ Hosted Search: lcx / gpt-5.6-luna
49
- ```
38
+ ### 2. Codex-style Web Actions: opt in when needed
50
39
 
51
- The settings-page model is now a **fallback** used only when no active Agent route is available.
52
-
53
- Hosted Search also receives its own stable cache namespace:
40
+ `websearch_alpha` is for stateful browsing and structured Web actions:
54
41
 
55
42
  ```text
56
- conversation replay: dsh-lcx:<route hash>
57
- hosted search: dsh-lcx-search:<route hash>
43
+ search → open → find / click → screenshot
58
44
  ```
59
45
 
60
- A search row with a low cache hit in NewAPI therefore does not imply conversation truncation.
46
+ It is off by default and is registered only after the exact endpoint / provider / model / schema passes a capability probe. Unknown deployments are **fail-closed** rather than mislabeled as Alpha-capable.
47
+
48
+ ### 3. Native V2 Compaction: preserve DSH session semantics
61
49
 
62
- ## Native V2 Compaction
50
+ LCX does not create a second compaction engine. DSH still owns pressure, compactable ranges, `/compact`, durable session transactions, tool-result pruning, and overflow recovery. LCX only requests Responses Native V2 at the existing compaction LLM seam.
63
51
 
64
- DSH continues to own pressure, range selection, pruning, durable replacement, `/compact`, and overflow recovery. The plugin replaces only the `purpose: compaction` summarizer transport with Native V2:
52
+ Default automatic policy:
65
53
 
66
54
  ```text
67
- DSH compaction transaction
68
- └─ purpose=compaction
69
- └─ /responses + compaction_trigger
55
+ 0% ─────────────────── 90% ───── 95% ───── 100%
56
+ normal Native emergency hard cap
57
+ V2 DSH prune
70
58
  ```
71
59
 
72
- Native success means zero basic-summary request. Basic summary runs only if Native fails and fallback is enabled.
60
+ - **90%**: prefer Native V2.
61
+ - **95%**: allow emergency DSH pruning.
62
+ - provider-confirmed context overflow: keep DSH's original recovery path.
63
+ - manual `/compact`: keep the native DSH session transaction.
73
64
 
74
- ## Conversation fidelity layer
65
+ The exact source session can reuse its own Native checkpoint. Forks or model migrations never send a parent's opaque state across sessions and use portable migration instead. Restart/resume rebuilds from the DSH session log.
75
66
 
76
- Provider-native opaque compaction is intentionally lossy. Real long-session tests showed that assistant-only details can disappear even when the opaque checkpoint itself is valid. The current checkpoint therefore keeps:
67
+ ## Get started in 30 seconds
77
68
 
78
- ```text
79
- bounded client-visible history
80
- + bounded assistant-visible final answers
81
- + opaque Native V2 compaction item
69
+ ### 1. Install
70
+
71
+ ```powershell
72
+ dsh plugin --profile web add dsh-lcx-codex
73
+ dsh web
82
74
  ```
83
75
 
84
- The explicit prefix is bounded at roughly 64k estimated tokens, with up to about 24k reserved for assistant answers and about 3k per retained assistant answer. Reasoning, raw tool traces, large search payloads and telemetry are not copied back into the prompt.
76
+ ### 2. Enable it in DSH Settings
85
77
 
86
- The goal is not byte-for-byte replay. It is **drop process noise, retain conversation facts**.
78
+ For a first run, start with the smallest useful configuration:
87
79
 
88
- ## Automatic compaction
80
+ | Setting | Suggested first-run value |
81
+ |---|---:|
82
+ | Enable plugin | **On** |
83
+ | Use GPT Hosted Search | **On** when you want GPT Hosted Search |
84
+ | Advanced Hosted Search | **Off** until you need advanced controls |
85
+ | Alpha Search | **Off** until the current route is confirmed compatible |
86
+ | Native V2 remote compaction | **On** only when the upstream actually supports Native V2 |
87
+ | Native-first auto compaction | **On** when using Native V2 |
89
88
 
90
- ```text
91
- 0% -------------------- 90% ----- 95% ----- 100%
92
- normal Native emergency hard cap
93
- V2 DSH prune
94
- ```
89
+ **Requirements**
95
90
 
96
- Default behavior:
91
+ - Node.js `>=20`
92
+ - a working GPT `openai-responses` route in DSH
93
+ - actual upstream support for the Hosted Search / Native V2 / Alpha capabilities you plan to enable
94
+
95
+ ### 3. Verify that it is actually active
96
+
97
+ Do not stop at checking toggles. Verify observable behavior for each capability:
98
+
99
+ | Capability | What you should observe |
100
+ |---|---|
101
+ | GPT Hosted Search | The ordinary tool entry remains DSH `web_search`, while the actual request follows the active GPT Responses route |
102
+ | Advanced Hosted | `websearch_gpt_advanced` appears after enablement and exposes the advanced controls separately |
103
+ | Alpha Web Actions | `websearch_alpha` appears only after the capability probe succeeds |
104
+ | Native V2 | Compaction follows the provider-native checkpoint path; ordinary Basic Compaction is never presented as a successful Native V2 run |
97
105
 
98
- - below 90%: suppress stock 80% pressure pruning for compatible Native sessions;
99
- - 90–95%: Native V2 first;
100
- - 95%+: DSH's replay-safe tool-result pruning may run as an emergency guard;
101
- - provider-confirmed context overflow keeps DSH's normal recovery path;
102
- - manual `/compact` is unaffected.
106
+ ## Which search tool should I use?
103
107
 
104
- ## Cache behavior
108
+ The three entries serve different jobs. **You do not need to enable all of them.**
105
109
 
106
- A provider cache miss is not the same thing as DSH deleting history.
110
+ | What you want to do | Entry | Best for |
111
+ |---|---|---|
112
+ | Ordinary web lookup | DSH `web_search` | General search, research, and retrieval |
113
+ | Control Hosted Search parameters | `websearch_gpt_advanced` | Domain allow/block, approximate location, search context, image search, and related controls |
114
+ | Browse pages or PDFs statefully | `websearch_alpha` | `search/open/find/click/screenshot` and structured Web actions |
107
115
 
108
- Observed real-world pattern:
116
+ ## How it fits into DSH
109
117
 
110
118
  ```text
111
- turn N: ~155k uncached, cacheRead 0
112
- turn N+1: ~1k new input, ~155k cacheRead
119
+ DSH Agent / Session / Web
120
+ ├─ web_search ──────────> LCX SearchProvider ──> GPT Hosted Search
121
+ ├─ Advanced / Alpha ────> LCX Web tools ───────> GPT Web actions
122
+ └─ compact / replay ────> LCX Native bridge ───> Responses Native V2
113
123
  ```
114
124
 
115
- That means the full context was resent and cached again. Prefix-changing operations include compaction, emergency pruning, route/model changes, and other DSH surface replacement. Restart/idle/provider cache TTL may also force a cold request independently of this plugin.
125
+ **DSH is the host; LCX is a compatibility extension.** The plugin prefers the active DSH route's `baseURL`, credential reference, headers, model, and retry policy instead of maintaining a second account configuration.
116
126
 
117
- ## Search modes
127
+ For checkpoint internals, canonical replay, Pi serialization, cache identity, fork safety, and pressure coordination, see [ARCHITECTURE.md](ARCHITECTURE.md).
118
128
 
119
- ### Ordinary `web_search`
129
+ ## Compatibility
120
130
 
121
- Recommended default. It follows the current Agent GPT Responses route.
131
+ Current formally verified stack:
122
132
 
123
- ### `websearch_gpt_advanced`
133
+ | Plugin | DSH | DSH host Pi | Plugin Pi | Status |
134
+ |---|---|---|---|---|
135
+ | `0.4.0` | `0.1.1-rc.2` | `0.82.1` | `0.82.1` | **VERIFIED** |
124
136
 
125
- Opt in only for Hosted-only parameters such as domain filters, approximate location, search-context size and image search. Keeping it disabled avoids unnecessary tool-catalog churn.
137
+ A new DSH release is **not** assumed compatible. The project first reviews the DSH / Pi seams that changed and then runs risk-appropriate tests. A newer Pi registry version alone does not automatically upgrade the plugin dependency.
126
138
 
127
- ### `websearch_alpha`
139
+ <details>
140
+ <summary><strong>Full recommended settings</strong></summary>
128
141
 
129
- Stateful Codex/Alpha-style `search/open/find/click/screenshot`; registered only after a matching capability probe.
142
+ | Setting | Recommended | Notes |
143
+ |---|---:|---|
144
+ | Enable plugin | On | Enables LCX |
145
+ | Use GPT Hosted Search | As needed | Routes DSH `web_search` through GPT Hosted Search |
146
+ | Advanced Hosted Search | Off | Enable only for advanced Hosted parameters |
147
+ | Alpha Search | Off | Enable after capability verification |
148
+ | Native V2 remote compaction | On* | *When the upstream route actually supports Native V2 |
149
+ | Native-first auto compaction | On | Enables automatic pressure coordination |
150
+ | Native threshold | 90% | Prefer Native V2 from 90% |
151
+ | Emergency DSH prune | 95% | Allow emergency prune from 95% |
152
+ | `web_search` timeout | 240 s | Avoid false timeout on longer searches |
130
153
 
131
- ## Installation
154
+ Key fields: `remoteCompaction`, `autoCompaction`, `fallbackToBasicCompaction`, `autoCompactionThresholdPercent`, `emergencyPruneThresholdPercent`, `webSearchTimeoutSeconds`, `advancedHostedSearch`, `alphaSearch`.
132
155
 
133
- Stable channel:
156
+ </details>
134
157
 
135
- ```powershell
136
- dsh plugin --profile web add dsh-lcx-codex
137
- ```
158
+ <details>
159
+ <summary><strong>Other installation options</strong></summary>
138
160
 
139
- Pre-release channel (current rc.8):
161
+ **Prerelease**
140
162
 
141
163
  ```powershell
142
164
  dsh plugin --profile web add dsh-lcx-codex@next
143
165
  ```
144
166
 
145
- Local RC:
167
+ **Local tarball**
146
168
 
147
169
  ```powershell
148
170
  dsh plugin --profile web remove dsh-lcx-codex
149
- dsh plugin --profile web add .\dsh-lcx-codex-0.4.0-rc.8.tgz
171
+ dsh plugin --profile web add .\dsh-lcx-codex-0.4.0.tgz
150
172
  dsh web
151
173
  ```
152
174
 
153
- Do not delete existing sessions or `$DSH_HOME/storages/lcx-codex/` just to upgrade. The legacy v3 sidecar remains read-only compatibility state.
175
+ Do not delete DSH sessions or `$DSH_HOME/storages/lcx-codex/` just to “clean up” during an upgrade. Older sessions may still reference compatibility data there.
154
176
 
155
- ## Recommended settings
177
+ </details>
156
178
 
157
- ```text
158
- Enable plugin on
159
- GPT Hosted Search on
160
- Advanced Hosted Search off
161
- Alpha Search off
162
- Native V2 remote compaction on
163
- Native-first auto compaction on
164
- Native threshold 90%
165
- Emergency DSH prune 95%
166
- web_search timeout 240s
167
- ```
168
-
169
- ## Requirements
179
+ <details>
180
+ <summary><strong>Common cases</strong></summary>
170
181
 
171
- - Node.js >= 20
172
- - DSH 0.1.1-rc.2
173
- - a working GPT `openai-responses` route in DSH
174
- - an upstream that actually supports the enabled Hosted Search / Native V2 / Alpha capabilities
182
+ **`web_search` is not using GPT Hosted Search**
183
+ Make sure the plugin and Hosted Search are enabled and that the active Agent resolves to a compatible GPT `openai-responses` route.
175
184
 
176
- Typical paths include direct Sub2API and NewAPI-relayed Responses routes.
185
+ **`websearch_alpha` is missing**
186
+ That is expected fail-closed behavior. Alpha is registered only after a trusted capability probe succeeds for the current route/schema.
177
187
 
178
- ## Release channels
188
+ **Native V2 is not taking effect**
189
+ Verify that the current GPT Responses endpoint actually supports `remote_compaction_v2`. The plugin does not present ordinary Basic Compaction as a successful Native V2 run.
179
190
 
180
- Stable releases use npm's `latest` tag. `0.4.0-rc.*` builds publish to npm's `next` tag. GitHub tags must match `package.json`; Trusted Publishing runs tests before publishing.
191
+ </details>
181
192
 
182
- ## Development
193
+ <details>
194
+ <summary><strong>Development</strong></summary>
183
195
 
184
196
  ```bash
185
197
  npm test
@@ -187,8 +199,14 @@ npm run test:schema
187
199
  npm pack --ignore-scripts
188
200
  ```
189
201
 
190
- See [ARCHITECTURE.md](ARCHITECTURE.md) and [CHANGELOG.md](CHANGELOG.md) for implementation details.
202
+ - Design and protocol details: [ARCHITECTURE.md](ARCHITECTURE.md)
203
+ - Release history: [CHANGELOG.md](CHANGELOG.md)
204
+ - npm: [`dsh-lcx-codex`](https://www.npmjs.com/package/dsh-lcx-codex)
205
+
206
+ </details>
191
207
 
192
208
  ## License
193
209
 
194
210
  MIT
211
+
212
+ > `LCX` is only the project name. This is a community project and is not affiliated with OpenAI, DeepSeek, Sub2API, or NewAPI.
Binary file
package/lib/compact-v2.js CHANGED
@@ -1,15 +1,89 @@
1
+ // @ts-check
2
+
1
3
  import { consumeSse, fetchSseWithRetry } from './transport.js'
2
4
  import { responsesTools } from './dsh-responses.js'
3
5
 
6
+ /** @typedef {Record<string, unknown>} UnknownRecord */
7
+ /** @typedef {Record<string, string>} HeaderMap */
8
+ /** @typedef {Error & { code?: string, status?: number, providerCode?: string, providerType?: string, providerParam?: string }} LcxError */
9
+ /** @typedef {{ type: 'compaction', encrypted_content: string }} CompactionItem */
10
+ /** @typedef {{ type: 'function_call', call_id: string }} FunctionCallItem */
11
+ /** @typedef {{ type: 'function_call_output', call_id: string }} FunctionCallOutputItem */
12
+ /** @typedef {CompactionItem | FunctionCallItem | FunctionCallOutputItem | { type: string }} ValidatedOutputItem */
13
+ /** @typedef {{ inputTokens: number, outputTokens: number, cacheReadTokens?: number, cacheWriteTokens?: number, reasoningTokens?: number }} CanonicalUsage */
14
+ /** @typedef {{ reasoningEffort?: unknown, temperature?: unknown, maxTokens?: unknown }} GenerationControls */
15
+ /** @typedef {{ reasoning?: { effort: string, summary: 'auto' }, include?: string[], temperature?: number, max_output_tokens?: number }} GenerationEnvelope */
16
+ /**
17
+ * @typedef {object} NativeCompactionBodyOptions
18
+ * @property {string} model
19
+ * @property {unknown[]} input
20
+ * @property {string} [instructions]
21
+ * @property {unknown} [tools]
22
+ * @property {string} [promptCacheKey]
23
+ * @property {string} [promptCacheRetention]
24
+ * @property {unknown} [reasoningEffort]
25
+ * @property {unknown} [temperature]
26
+ * @property {unknown} [maxTokens]
27
+ */
28
+ /**
29
+ * @typedef {object} NativeCompactionBody
30
+ * @property {string} model
31
+ * @property {unknown[]} input
32
+ * @property {true} stream
33
+ * @property {false} store
34
+ * @property {'auto'} tool_choice
35
+ * @property {true} parallel_tool_calls
36
+ * @property {string} [instructions]
37
+ * @property {unknown[]} [tools]
38
+ * @property {string} [prompt_cache_key]
39
+ * @property {string} [prompt_cache_retention]
40
+ * @property {{ effort: string, summary: 'auto' }} [reasoning]
41
+ * @property {string[]} [include]
42
+ * @property {number} [temperature]
43
+ * @property {number} [max_output_tokens]
44
+ */
45
+ /**
46
+ * @typedef {NativeCompactionBodyOptions & {
47
+ * baseURL: string,
48
+ * idempotencyKey?: string,
49
+ * headers?: HeaderMap,
50
+ * signal?: AbortSignal,
51
+ * timeoutMs?: number,
52
+ * maxAttempts?: number,
53
+ * maxResponseBytes?: number
54
+ * }} NativeCompactionRequestOptions
55
+ */
56
+ /** @typedef {{ signal?: AbortSignal, maxResponseBytes?: number }} SseParseOptions */
57
+ /** @typedef {{ object: unknown, id?: string, output: ValidatedOutputItem[], compaction: CompactionItem, usage?: CanonicalUsage }} NativeCompactionResult */
58
+
4
59
  export const REMOTE_COMPACTION_V2_FEATURE = 'remote_compaction_v2'
5
60
 
61
+ /**
62
+ * @param {unknown} value
63
+ * @returns {value is UnknownRecord}
64
+ */
6
65
  function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value) }
7
- function fail(message, code = 'LCX_COMPACT_INVALID_RESPONSE') { const e = new Error(message); e.code = code; return e }
66
+ /**
67
+ * @param {string} message
68
+ * @param {string} [code]
69
+ * @returns {LcxError}
70
+ */
71
+ function fail(message, code = 'LCX_COMPACT_INVALID_RESPONSE') {
72
+ /** @type {LcxError} */
73
+ const e = new Error(message)
74
+ e.code = code
75
+ return e
76
+ }
77
+ /** @param {unknown} value */
8
78
  function safeMachineField(value) {
9
79
  if (typeof value !== 'string' || value.length === 0 || value.length > 96) return undefined
10
80
  return /^[A-Za-z0-9_.:\[\]-]+$/u.test(value) ? value : undefined
11
81
  }
12
82
 
83
+ /**
84
+ * @param {HeaderMap} [headers]
85
+ * @returns {HeaderMap}
86
+ */
13
87
  export function mergeFeatureHeader(headers = {}) {
14
88
  const result = { ...headers }
15
89
  const key = Object.keys(result).find((name) => name.toLowerCase() === 'x-codex-beta-features')
@@ -19,7 +93,12 @@ export function mergeFeatureHeader(headers = {}) {
19
93
  return result
20
94
  }
21
95
 
96
+ /**
97
+ * @param {GenerationControls} [controls]
98
+ * @returns {GenerationEnvelope}
99
+ */
22
100
  export function responsesGenerationEnvelope({ reasoningEffort, temperature, maxTokens } = {}) {
101
+ /** @type {GenerationEnvelope} */
23
102
  const result = {}
24
103
  if (reasoningEffort !== undefined && reasoningEffort !== 'off') {
25
104
  result.reasoning = { effort: String(reasoningEffort), summary: 'auto' }
@@ -30,15 +109,20 @@ export function responsesGenerationEnvelope({ reasoningEffort, temperature, maxT
30
109
  result.temperature = Number(temperature)
31
110
  }
32
111
  if (maxTokens !== undefined) {
33
- if (!Number.isSafeInteger(maxTokens) || maxTokens <= 0) throw fail('Responses maxTokens must be a positive safe integer', 'LCX_COMPACT_INVALID_INPUT')
34
- result.max_output_tokens = Math.max(16, maxTokens)
112
+ if (!Number.isSafeInteger(maxTokens) || /** @type {number} */ (maxTokens) <= 0) throw fail('Responses maxTokens must be a positive safe integer', 'LCX_COMPACT_INVALID_INPUT')
113
+ result.max_output_tokens = Math.max(16, /** @type {number} */ (maxTokens))
35
114
  }
36
115
  return result
37
116
  }
38
117
 
118
+ /**
119
+ * @param {NativeCompactionBodyOptions} options
120
+ * @returns {NativeCompactionBody}
121
+ */
39
122
  export function buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens }) {
40
123
  if (!Array.isArray(input)) throw fail('native compaction input must be an array', 'LCX_COMPACT_INVALID_INPUT')
41
- if (input.some((item) => item?.type === 'compaction_trigger')) throw fail('native compaction input already contains compaction_trigger', 'LCX_COMPACT_DUPLICATE_TRIGGER')
124
+ if (input.some((item) => /** @type {UnknownRecord | undefined} */ (item)?.type === 'compaction_trigger')) throw fail('native compaction input already contains compaction_trigger', 'LCX_COMPACT_DUPLICATE_TRIGGER')
125
+ /** @type {unknown[] | undefined} */
42
126
  const nativeTools = responsesTools(tools)
43
127
  return {
44
128
  model,
@@ -55,31 +139,43 @@ export function buildNativeCompactionBody({ model, input, instructions, tools, p
55
139
  }
56
140
  }
57
141
 
142
+ /**
143
+ * Provider usage stays unknown until the object guard and numeric normalization.
144
+ * @param {unknown} raw
145
+ * @returns {CanonicalUsage | undefined}
146
+ */
58
147
  function usageFrom(raw) {
59
148
  if (!isObject(raw)) return undefined
60
149
  const inputTotal = Number(raw.input_tokens ?? 0)
61
150
  const output = Number(raw.output_tokens ?? 0)
62
- const details = raw.input_tokens_details ?? raw.input_token_details ?? raw.prompt_tokens_details ?? {}
151
+ const details = /** @type {UnknownRecord} */ (raw.input_tokens_details ?? raw.input_token_details ?? raw.prompt_tokens_details ?? {})
63
152
  const cacheRead = Number(details.cached_tokens ?? 0)
64
153
  const cacheWrite = Number(details.cache_write_tokens ?? 0)
154
+ /** @type {CanonicalUsage} */
65
155
  const result = {
66
156
  inputTokens: Math.max(0, Number.isFinite(inputTotal) ? inputTotal - (Number.isFinite(cacheRead) ? cacheRead : 0) - (Number.isFinite(cacheWrite) ? cacheWrite : 0) : 0),
67
157
  outputTokens: Number.isFinite(output) && output > 0 ? output : 0,
68
158
  }
69
159
  if (Number.isFinite(cacheRead) && cacheRead > 0) result.cacheReadTokens = cacheRead
70
160
  if (Number.isFinite(cacheWrite) && cacheWrite > 0) result.cacheWriteTokens = cacheWrite
71
- const reasoning = Number(raw.output_tokens_details?.reasoning_tokens ?? raw.output_token_details?.reasoning_tokens ?? 0)
161
+ const reasoning = Number(/** @type {UnknownRecord | undefined} */ (raw.output_tokens_details)?.reasoning_tokens ?? /** @type {UnknownRecord | undefined} */ (raw.output_token_details)?.reasoning_tokens ?? 0)
72
162
  if (Number.isFinite(reasoning) && reasoning > 0) result.reasoningTokens = reasoning
73
163
  return Object.values(result).some((v) => Number(v) > 0) ? result : undefined
74
164
  }
75
165
 
166
+ /**
167
+ * @param {unknown} output
168
+ * @returns {{ output: ValidatedOutputItem[], compaction: CompactionItem }}
169
+ */
76
170
  function validateOutput(output) {
77
171
  if (!Array.isArray(output) || output.length === 0) throw fail('native compaction output is empty')
78
172
  const compactions = output.filter((item) => isObject(item) && item.type === 'compaction')
79
173
  if (compactions.length !== 1) throw fail(`native compaction must return exactly one compaction item, got ${compactions.length}`)
80
174
  const compact = compactions[0]
81
175
  if (typeof compact.encrypted_content !== 'string' || compact.encrypted_content.length === 0) throw fail('native compaction item has no encrypted_content')
176
+ /** @type {Set<string>} */
82
177
  const calls = new Set()
178
+ /** @type {Set<string>} */
83
179
  const results = new Set()
84
180
  for (const item of output) {
85
181
  if (!isObject(item) || typeof item.type !== 'string') throw fail('native compaction output contains a malformed item')
@@ -93,16 +189,29 @@ function validateOutput(output) {
93
189
  }
94
190
  for (const id of calls) if (!results.has(id)) throw fail(`native compaction output has orphan function_call ${id}`)
95
191
  for (const id of results) if (!calls.has(id)) throw fail(`native compaction output has orphan function_call_output ${id}`)
96
- return { output: structuredClone(output), compaction: structuredClone(compact) }
192
+ return {
193
+ output: /** @type {ValidatedOutputItem[]} */ (structuredClone(output)),
194
+ compaction: /** @type {CompactionItem} */ (structuredClone(compact)),
195
+ }
97
196
  }
98
197
 
198
+ /**
199
+ * Provider SSE events enter as unknown; response.completed remains authoritative.
200
+ * @param {Response} response
201
+ * @param {SseParseOptions} [options]
202
+ * @returns {Promise<NativeCompactionResult>}
203
+ */
99
204
  export async function parseNativeCompactionSse(response, options = {}) {
205
+ /** @type {Map<number, UnknownRecord>} */
100
206
  const byIndex = new Map()
207
+ /** @type {Map<string, UnknownRecord>} */
101
208
  const byId = new Map()
209
+ /** @type {UnknownRecord | undefined} */
102
210
  let terminal
211
+ /** @param {UnknownRecord} event */
103
212
  const mergeItem = (event) => {
104
213
  if (!isObject(event?.item)) return
105
- const index = Number.isInteger(event.output_index) ? event.output_index : undefined
214
+ const index = Number.isInteger(event.output_index) ? /** @type {number} */ (event.output_index) : undefined
106
215
  const id = typeof event.item.id === 'string' ? event.item.id : typeof event.item_id === 'string' ? event.item_id : undefined
107
216
  let record = (index !== undefined ? byIndex.get(index) : undefined) ?? (id ? byId.get(id) : undefined)
108
217
  if (!record) record = {}
@@ -112,27 +221,28 @@ export async function parseNativeCompactionSse(response, options = {}) {
112
221
  if (index !== undefined) byIndex.set(index, record)
113
222
  if (id) byId.set(id, record)
114
223
  }
115
- await consumeSse(response, (event) => {
224
+ await consumeSse(response, (/** @type {unknown} */ event) => {
116
225
  if (!isObject(event)) return
117
226
  if (event.type === 'error' || event.type === 'response.failed' || event.type === 'response.incomplete') {
118
- const upstream = event.type === 'response.failed' ? event.response?.error : event.error
119
- const providerCode = safeMachineField(upstream?.code)
120
- const providerType = safeMachineField(upstream?.type)
121
- const providerParam = safeMachineField(upstream?.param)
227
+ const upstream = event.type === 'response.failed' ? /** @type {{ error?: unknown } | undefined} */ (event.response)?.error : event.error
228
+ const providerCode = safeMachineField(/** @type {UnknownRecord | undefined} */ (upstream)?.code)
229
+ const providerType = safeMachineField(/** @type {UnknownRecord | undefined} */ (upstream)?.type)
230
+ const providerParam = safeMachineField(/** @type {UnknownRecord | undefined} */ (upstream)?.param)
122
231
  const diagnostics = [
123
232
  providerCode ? `providerCode=${providerCode}` : '',
124
233
  providerType ? `providerType=${providerType}` : '',
125
234
  providerParam ? `providerParam=${providerParam}` : '',
126
235
  ].filter(Boolean).join(' ')
236
+ /** @type {LcxError} */
127
237
  const e = fail(`native compaction ended with ${event.type}${diagnostics ? ` ${diagnostics}` : ''}`, 'LCX_COMPACT_UPSTREAM_ERROR')
128
- if (Number.isInteger(upstream?.status ?? event.error?.status)) e.status = upstream?.status ?? event.error?.status
238
+ if (Number.isInteger(/** @type {UnknownRecord | undefined} */ (upstream)?.status ?? /** @type {UnknownRecord | undefined} */ (event.error)?.status)) e.status = /** @type {number} */ (/** @type {UnknownRecord | undefined} */ (upstream)?.status ?? /** @type {UnknownRecord | undefined} */ (event.error)?.status)
129
239
  if (providerCode) e.providerCode = providerCode
130
240
  if (providerType) e.providerType = providerType
131
241
  if (providerParam) e.providerParam = providerParam
132
242
  throw e
133
243
  }
134
244
  if (event.type === 'response.output_item.added' || event.type === 'response.output_item.done') mergeItem(event)
135
- if (event.type === 'response.completed') terminal = event.response
245
+ if (event.type === 'response.completed') terminal = /** @type {UnknownRecord} */ (event.response)
136
246
  }, options)
137
247
  if (!isObject(terminal)) throw fail('native compaction stream ended without response.completed', 'LCX_COMPACT_INCOMPLETE_SSE')
138
248
  if (terminal.status !== 'completed') throw fail('native compaction response.completed did not carry completed status', 'LCX_COMPACT_INCOMPLETE_SSE')
@@ -147,12 +257,15 @@ export async function parseNativeCompactionSse(response, options = {}) {
147
257
  }
148
258
  }
149
259
 
260
+ /**
261
+ * @param {NativeCompactionRequestOptions} options
262
+ */
150
263
  export async function requestNativeCompaction({ baseURL, model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens, idempotencyKey, headers, signal, timeoutMs, maxAttempts, maxResponseBytes }) {
151
264
  const body = buildNativeCompactionBody({ model, input, instructions, tools, promptCacheKey, promptCacheRetention, reasoningEffort, temperature, maxTokens })
152
265
  const requestHeaders = mergeFeatureHeader({ ...headers, ...(idempotencyKey ? { 'idempotency-key': String(idempotencyKey) } : {}) })
153
266
  return fetchSseWithRetry(`${String(baseURL).replace(/\/+$/u, '')}/responses`, body, requestHeaders, signal, timeoutMs, {
154
267
  maxAttempts,
155
268
  maxResponseBytes,
156
- consume: (response, consumeOptions) => parseNativeCompactionSse(response, { signal: consumeOptions.requestSignal ?? signal, maxResponseBytes }),
269
+ consume: (/** @type {Response} */ response, /** @type {{ requestSignal?: AbortSignal }} */ consumeOptions) => parseNativeCompactionSse(response, { signal: consumeOptions.requestSignal ?? signal, maxResponseBytes }),
157
270
  })
158
271
  }