dsh-lcx-codex 0.4.0 → 0.4.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,194 +1,213 @@
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%" />
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%" />
4
4
 
5
- # DSH-LCX-CODEX
6
-
7
- **OpenAI Responses / Codex-native capabilities for DeepSeek Harness.**
8
-
9
- [![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)
5
+ [![npm](https://img.shields.io/npm/v/dsh-lcx-codex?color=1677ff&label=npm%20latest)](https://www.npmjs.com/package/dsh-lcx-codex)
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 adds missing native capabilities to 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
+ ## Current stable release
21
18
 
22
- > `LCX` is only the project name. This project is not affiliated with OpenAI, DeepSeek, Sub2API or NewAPI.
19
+ `0.4.1` is the current stable release. It is a **zero-functional-change stable promotion** of `0.4.1-pre.1`, which already passed full Installed Candidate and Cross-feature QA.
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
+ ```powershell
22
+ dsh plugin --profile web add dsh-lcx-codex
23
+ dsh web
24
+ ```
25
25
 
26
- ## Features
26
+ npm `latest` points to `0.4.1`. Historical `prelatest=0.4.1-pre.1` uses the same accepted runtime baseline; new users should install `latest`.
27
27
 
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 |
28
+ ## What it solves
38
29
 
39
- ## rc.8: DSH 0.1.1-rc.2 native alignment
30
+ If DSH already has a working GPT `openai-responses` route, `dsh-lcx-codex` fills Responses capability gaps while preserving DSH entry points and lifecycle ownership.
40
31
 
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.
32
+ | Scenario | Existing DSH surface | With LCX |
33
+ |---|---|---|
34
+ | Ordinary web search | DSH `web_search` | Keep the same `web_search`; LCX maps its SearchProvider to GPT Hosted Search |
35
+ | Advanced Hosted controls | Ordinary search stays simple | Optional `websearch_gpt_advanced` for domain filters, approximate location, search context, image search, and related controls |
36
+ | Stateful page / PDF browsing | DSH keeps Web lifecycle ownership | Optional Codex / Alpha actions: `search → open → find/click → screenshot` |
37
+ | Long-session compaction | DSH keeps pressure, transactions, and recovery | Prefer Responses Native V2 checkpoints at the existing compaction seam |
42
38
 
43
- Search routing still crosses DSH's `tools/execute` SearchProvider boundary without changing the model-visible `web_search` schema:
39
+ **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.**
44
40
 
45
- ```text
46
- Agent: lcx / gpt-5.6-luna
47
- └─ DSH web_search
48
- └─ Hosted Search: lcx / gpt-5.6-luna
49
- ```
41
+ ## Core capabilities
50
42
 
51
- The settings-page model is now a **fallback** used only when no active Agent route is available.
43
+ ### GPT Hosted Search
52
44
 
53
- Hosted Search also receives its own stable cache namespace:
45
+ The model still sees DSH's ordinary `web_search`; LCX changes the SearchProvider path instead of registering a duplicate ordinary-search tool. Enable `websearch_gpt_advanced` only when you need additional Hosted Search controls.
54
46
 
55
- ```text
56
- conversation replay: dsh-lcx:<route hash>
57
- hosted search: dsh-lcx-search:<route hash>
58
- ```
47
+ ### Codex-style Web Actions
59
48
 
60
- A search row with a low cache hit in NewAPI therefore does not imply conversation truncation.
61
-
62
- ## Native V2 Compaction
63
-
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:
49
+ `websearch_alpha` supports stateful browsing:
65
50
 
66
51
  ```text
67
- DSH compaction transaction
68
- └─ purpose=compaction
69
- └─ /responses + compaction_trigger
52
+ search open → find / click → screenshot
70
53
  ```
71
54
 
72
- Native success means zero basic-summary request. Basic summary runs only if Native fails and fallback is enabled.
55
+ It is off by default and registered only when the current endpoint / provider / model / schema passes capability checks. Unknown deployments fail closed.
56
+
57
+ `0.4.1` includes the URL/stateful-continuation and capability fail-closed fixes that were accepted on the prerelease line.
73
58
 
74
- ## Conversation fidelity layer
59
+ ### Native V2 Compaction
75
60
 
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:
61
+ LCX does not create a second compaction engine. DSH still owns pressure, compactable ranges, `/compact`, durable session transactions, pruning, and overflow recovery. LCX requests Responses Native V2 through the existing compaction seam.
77
62
 
78
63
  ```text
79
- bounded client-visible history
80
- + bounded assistant-visible final answers
81
- + opaque Native V2 compaction item
64
+ 0% ─────────────────── 90% ───── 95% ───── 100%
65
+ normal Native emergency hard cap
66
+ V2 DSH prune
82
67
  ```
83
68
 
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.
69
+ - **90%**: prefer Native V2.
70
+ - **95%**: allow emergency DSH pruning.
71
+ - provider-confirmed context overflow: keep DSH's original recovery.
72
+ - manual `/compact`: keep the native DSH transaction.
85
73
 
86
- The goal is not byte-for-byte replay. It is **drop process noise, retain conversation facts**.
74
+ Native opaque state stays source-session bound. Forks and model/route migrations use portable migration rather than sending a parent's opaque state across sessions; restart/resume rebuilds from the DSH session log.
87
75
 
88
- ## Automatic compaction
76
+ ## What changed from 0.4.0
89
77
 
90
- ```text
91
- 0% -------------------- 90% ----- 95% ----- 100%
92
- normal Native emergency hard cap
93
- V2 DSH prune
94
- ```
78
+ `0.4.1` promotes the already accepted `0.4.1-pre.1` changes to stable:
95
79
 
96
- Default behavior:
80
+ - Alpha URL/stateful continuation and fail-closed capability fixes;
81
+ - DSH compatibility-seam isolation;
82
+ - typed protocol core for route / Native V2 / checkpoint handling;
83
+ - unified conservative token budgeting for Native retention and portable migration;
84
+ - installed settings namespace / configuration-card lifecycle fix;
85
+ - Installed Candidate + Cross-feature QA.
97
86
 
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.
87
+ The stable promotion itself adds no new runtime behavior.
103
88
 
104
- ## Cache behavior
89
+ ## Get started
105
90
 
106
- A provider cache miss is not the same thing as DSH deleting history.
91
+ ### Install stable
107
92
 
108
- Observed real-world pattern:
109
-
110
- ```text
111
- turn N: ~155k uncached, cacheRead 0
112
- turn N+1: ~1k new input, ~155k cacheRead
93
+ ```powershell
94
+ dsh plugin --profile web add dsh-lcx-codex
95
+ dsh web
113
96
  ```
114
97
 
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.
98
+ Pin the exact version if needed:
116
99
 
117
- ## Search modes
100
+ ```powershell
101
+ dsh plugin --profile web add dsh-lcx-codex@0.4.1
102
+ ```
118
103
 
119
- ### Ordinary `web_search`
104
+ Use the official DSH plugin remove/add path when upgrading. Do not delete user sessions or `$DSH_HOME/storages/lcx-codex/` just to change versions.
120
105
 
121
- Recommended default. It follows the current Agent GPT Responses route.
106
+ ### Suggested first-run settings
122
107
 
123
- ### `websearch_gpt_advanced`
108
+ | Setting | Suggested value |
109
+ |---|---:|
110
+ | Enable plugin | **On** |
111
+ | Use GPT Hosted Search | On when needed |
112
+ | Advanced Hosted Search | Off until needed |
113
+ | Alpha Search | Off until route capability is confirmed |
114
+ | Native V2 remote compaction | On only when upstream supports it |
115
+ | Native-first auto compaction | On when using Native V2 |
124
116
 
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.
117
+ Requirements: Node.js `>=20`, a working GPT `openai-responses` route in DSH, and actual upstream support for the capabilities you enable.
126
118
 
127
- ### `websearch_alpha`
119
+ ## Verify observable behavior
128
120
 
129
- Stateful Codex/Alpha-style `search/open/find/click/screenshot`; registered only after a matching capability probe.
121
+ | Capability | Expected behavior |
122
+ |---|---|
123
+ | GPT Hosted Search | Ordinary entry remains DSH `web_search`; the request follows the active GPT Responses route |
124
+ | Advanced Hosted | `websearch_gpt_advanced` appears only when enabled |
125
+ | Alpha Web Actions | `websearch_alpha` appears only after capability validation |
126
+ | Native V2 | Compaction follows the provider-native checkpoint path; Basic Compaction is not mislabeled as Native V2 |
130
127
 
131
- ## Installation
128
+ ## Which search tool should I use?
132
129
 
133
- Stable channel:
130
+ | Goal | Entry | Best for |
131
+ |---|---|---|
132
+ | Ordinary web lookup | DSH `web_search` | General search, research, and retrieval |
133
+ | Control Hosted Search parameters | `websearch_gpt_advanced` | Domain allow/block, approximate location, search context, image search, and related controls |
134
+ | Browse pages or PDFs statefully | `websearch_alpha` | `search/open/find/click/screenshot` and structured Web actions |
134
135
 
135
- ```powershell
136
- dsh plugin --profile web add dsh-lcx-codex
136
+ ## How it fits into DSH
137
+
138
+ ```text
139
+ DSH Agent / Session / Web
140
+ ├─ web_search ──────────> LCX SearchProvider ──> GPT Hosted Search
141
+ ├─ Advanced / Alpha ────> LCX Web tools ───────> GPT Web actions
142
+ └─ compact / replay ────> LCX Native bridge ───> Responses Native V2
137
143
  ```
138
144
 
139
- Pre-release channel (current rc.8):
145
+ **DSH is the host; LCX is a compatibility extension.** It reuses the active DSH route's `baseURL`, configured authentication reference, headers, model, and retry policy rather than maintaining a second account configuration.
140
146
 
141
- ```powershell
142
- dsh plugin --profile web add dsh-lcx-codex@next
143
- ```
147
+ See [ARCHITECTURE.md](ARCHITECTURE.md) for checkpoint, replay, Pi serialization, cache identity, fork safety, and pressure-coordination details.
144
148
 
145
- Local RC:
149
+ ## Compatibility
146
150
 
147
- ```powershell
148
- dsh plugin --profile web remove dsh-lcx-codex
149
- dsh plugin --profile web add .\dsh-lcx-codex-0.4.0-rc.8.tgz
150
- dsh web
151
- ```
151
+ Current formally stable stack:
152
152
 
153
- Do not delete existing sessions or `$DSH_HOME/storages/lcx-codex/` just to upgrade. The legacy v3 sidecar remains read-only compatibility state.
153
+ | Plugin | DSH | DSH host Pi | Plugin Pi | Status |
154
+ |---|---|---|---|---|
155
+ | `0.4.1` | `0.1.1-rc.2` | `0.82.1` | `0.82.1` | **VERIFIED STABLE** |
154
156
 
155
- ## Recommended settings
157
+ `0.4.1` uses the fully QA-passed `0.4.1-pre.1` runtime, and the stable promotion is zero-functional-change. New DSH releases are **not** assumed compatible without seam review and risk-appropriate testing.
156
158
 
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
- ```
159
+ <details>
160
+ <summary><strong>Recommended settings</strong></summary>
161
+
162
+ | Setting | Recommended | Notes |
163
+ |---|---:|---|
164
+ | Enable plugin | On | Enables LCX |
165
+ | Use GPT Hosted Search | As needed | Routes DSH `web_search` through GPT Hosted Search |
166
+ | Advanced Hosted Search | Off | Enable only for advanced Hosted parameters |
167
+ | Alpha Search | Off | Enable after capability verification |
168
+ | Native V2 remote compaction | On* | *When the upstream route actually supports Native V2 |
169
+ | Native-first auto compaction | On | Enables automatic pressure coordination |
170
+ | Native threshold | 90% | Prefer Native V2 from 90% |
171
+ | Emergency DSH prune | 95% | Allow emergency prune from 95% |
172
+ | `web_search` timeout | 240 s | Avoid false timeout on longer searches |
168
173
 
169
- ## Requirements
174
+ </details>
170
175
 
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
176
+ <details>
177
+ <summary><strong>Common cases</strong></summary>
175
178
 
176
- Typical paths include direct Sub2API and NewAPI-relayed Responses routes.
179
+ **`web_search` is not using GPT Hosted Search**
180
+ Make sure the plugin and Hosted Search are enabled and that the active Agent resolves to a compatible GPT `openai-responses` route.
177
181
 
178
- ## Release channels
182
+ **`websearch_alpha` is missing**
183
+ That is expected fail-closed behavior. Alpha is registered only after a trusted capability probe succeeds for the current route/schema.
179
184
 
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.
185
+ **Native V2 is not taking effect**
186
+ 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.
181
187
 
182
- ## Development
188
+ **Installed but no configuration card is visible**
189
+ Make sure you are on `0.4.1` or later and restart/refresh DSH Web. `0.4.1` includes the installed settings lifecycle fix.
190
+
191
+ </details>
192
+
193
+ <details>
194
+ <summary><strong>Development</strong></summary>
183
195
 
184
196
  ```bash
197
+ npm run typecheck
185
198
  npm test
186
199
  npm run test:schema
187
200
  npm pack --ignore-scripts
188
201
  ```
189
202
 
190
- See [ARCHITECTURE.md](ARCHITECTURE.md) and [CHANGELOG.md](CHANGELOG.md) for implementation details.
203
+ - Design: [ARCHITECTURE.md](ARCHITECTURE.md)
204
+ - Releases: [CHANGELOG.md](CHANGELOG.md)
205
+ - npm: [`dsh-lcx-codex`](https://www.npmjs.com/package/dsh-lcx-codex)
206
+
207
+ </details>
191
208
 
192
209
  ## License
193
210
 
194
211
  MIT
212
+
213
+ > `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
  }