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/CHANGELOG.md +29 -2
- package/README.md +138 -183
- package/README_EN.md +137 -118
- package/assets/dsh-lcx-codex-banner.jpg +0 -0
- package/lib/compact-v2.js +129 -16
- package/lib/dsh-compat.js +124 -0
- package/lib/index.js +126 -92
- package/lib/native-checkpoint.js +241 -22
- package/lib/route.js +174 -12
- package/lib/token-budget.js +165 -0
- package/lib/web-search-alpha.js +148 -7
- package/lib/web-search-capability.js +5 -3
- package/package.json +4 -2
package/README_EN.md
CHANGED
|
@@ -1,194 +1,213 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
**OpenAI Responses / Codex-native capabilities for DeepSeek Harness.**
|
|
8
|
-
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-lcx-codex)
|
|
10
|
-

|
|
5
|
+
[](https://www.npmjs.com/package/dsh-lcx-codex)
|
|
6
|
+
[](https://github.com/kk3ya03-star/dsh-lcx-codex/actions/workflows/publish.yml)
|
|
11
7
|

|
|
12
|
-

|
|
9
|
+

|
|
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
|
-
|
|
17
|
+
## Current stable release
|
|
21
18
|
|
|
22
|
-
|
|
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
|
-
|
|
21
|
+
```powershell
|
|
22
|
+
dsh plugin --profile web add dsh-lcx-codex
|
|
23
|
+
dsh web
|
|
24
|
+
```
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
+
### GPT Hosted Search
|
|
52
44
|
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
conversation replay: dsh-lcx:<route hash>
|
|
57
|
-
hosted search: dsh-lcx-search:<route hash>
|
|
58
|
-
```
|
|
47
|
+
### Codex-style Web Actions
|
|
59
48
|
|
|
60
|
-
|
|
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
|
-
|
|
68
|
-
└─ purpose=compaction
|
|
69
|
-
└─ /responses + compaction_trigger
|
|
52
|
+
search → open → find / click → screenshot
|
|
70
53
|
```
|
|
71
54
|
|
|
72
|
-
|
|
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
|
-
|
|
59
|
+
### Native V2 Compaction
|
|
75
60
|
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
64
|
+
0% ─────────────────── 90% ───── 95% ───── 100%
|
|
65
|
+
normal Native emergency hard cap
|
|
66
|
+
V2 DSH prune
|
|
82
67
|
```
|
|
83
68
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
76
|
+
## What changed from 0.4.0
|
|
89
77
|
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
89
|
+
## Get started
|
|
105
90
|
|
|
106
|
-
|
|
91
|
+
### Install stable
|
|
107
92
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
98
|
+
Pin the exact version if needed:
|
|
116
99
|
|
|
117
|
-
|
|
100
|
+
```powershell
|
|
101
|
+
dsh plugin --profile web add dsh-lcx-codex@0.4.1
|
|
102
|
+
```
|
|
118
103
|
|
|
119
|
-
|
|
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
|
-
|
|
106
|
+
### Suggested first-run settings
|
|
122
107
|
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
+
## Verify observable behavior
|
|
128
120
|
|
|
129
|
-
|
|
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
|
-
##
|
|
128
|
+
## Which search tool should I use?
|
|
132
129
|
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
## Compatibility
|
|
146
150
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
174
|
+
</details>
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|