@pure01fx/dsh-openai-codex-auth 0.10.1 → 0.11.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 +12 -0
- package/README.md +3 -1
- package/client.js +115 -2
- package/lib/catalog.js +2 -7
- package/lib/cloud-context.d.ts +1 -1
- package/lib/cloud-context.js +3 -6
- package/lib/cloud-http.d.ts +0 -3
- package/lib/cloud-http.js +3 -12
- package/lib/cloud-images.d.ts +2 -3
- package/lib/cloud-images.js +9 -14
- package/lib/cloud-media.js +9 -12
- package/lib/cloud-search.d.ts +1 -1
- package/lib/cloud-search.js +11 -30
- package/lib/cloud-tools.js +2 -2
- package/lib/cloud-vision.js +2 -10
- package/lib/cloud-web-tool.js +11 -21
- package/lib/index.d.ts +1 -0
- package/lib/index.js +11 -0
- package/lib/native-http.d.ts +4 -5
- package/lib/native-http.js +43 -25
- package/lib/native-websocket-session.js +1 -3
- package/lib/native-websocket-socket.d.ts +2 -1
- package/lib/native-websocket-socket.js +22 -31
- package/lib/native-websocket.d.ts +1 -2
- package/lib/native-websocket.js +53 -46
- package/lib/network-telemetry.d.ts +61 -0
- package/lib/network-telemetry.js +104 -0
- package/lib/replay.d.ts +2 -3
- package/lib/replay.js +13 -43
- package/lib/responses.d.ts +0 -2
- package/lib/responses.js +1 -14
- package/lib/sse.d.ts +1 -3
- package/lib/sse.js +2 -17
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.11.1
|
|
6
|
+
|
|
7
|
+
- Adds live network status and process-wide request, retry, and received-payload statistics to the quota panel, distinguishing network activity from model output.
|
|
8
|
+
- Shows a breathing quota indicator during unbounded connection recovery, including while the panel is closed; respects reduced-motion preferences and clears the indicator after recovery, cancellation, or stale status updates.
|
|
9
|
+
- Exposes bounded, content-free telemetry through the protected local network endpoint without additional upstream quota requests or changes to timeout and retry behavior.
|
|
10
|
+
|
|
11
|
+
## 0.11.0
|
|
12
|
+
|
|
13
|
+
- Removes plugin payload byte ceilings for native and cloud requests, SSE events, WebSocket messages and queues, response accumulation, encrypted replay state, model catalogs, and image data; removes the corresponding transport options, including `maxRequestBodyBytes`. WebSocket disables the underlying `ws` message-size ceiling explicitly.
|
|
14
|
+
- Keeps WebSocket incremental request preparation separate from HTTP serialization, preserving image history across incremental reuse, reconnects, and HTTP fallback.
|
|
15
|
+
- Preserves complete search output and result arrays, processes all inline result images, forwards selected human search context in full, and removes search batch/domain count ceilings and image-tool prompt length ceilings. Protocol validation, service errors, cancellation, retry/session lifecycle controls, and DSH attachment/file policies still apply.
|
|
16
|
+
|
|
5
17
|
## 0.10.1
|
|
6
18
|
|
|
7
19
|
- Targets DSH 0.1.5-rc.1 with exact SDK peers and runtime helpers; migrates Responses tool calls to ToolCallId.
|
package/README.md
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
## 快速开始
|
|
19
19
|
|
|
20
|
-
版本 `0.
|
|
20
|
+
版本 `0.11.1` 明确适配 DeepSeek Harness `0.1.5-rc.1`,SDK peers/runtime helpers 精确锁定该版本;不声明兼容旧 SDK。旧 DSH `0.1.1-rc.2` profile 应保留原插件 `0.10.0`,升级时在独立 profile 安装新版插件并保留原 profile/锁文件以便回滚。无需迁移账号文件或浏览器隐藏列表;`dsh.engines` 仅作兼容性说明,不当作宿主硬门禁。
|
|
21
21
|
|
|
22
22
|
本版本通过真实目标 LLM/tools 服务与本地 HTTP/SSE/WebSocket 回放、错误/取消回归;模型隐藏覆盖真实目录/catalog/store 的投影更新、重连和重新加载。验证命令为 `pnpm typecheck && pnpm build && pnpm test`;当前版本未执行线上 OAuth/付费模型验收。
|
|
23
23
|
|
|
24
|
+
`0.11.0` 移除了插件额外设置的请求、响应、SSE/WebSocket、图片和重放状态大小上限,以及相应的 transport 选项(含 `maxRequestBodyBytes`);旧配置中的该字段应删除。搜索结果完整保留,WebSocket 继续优先复用增量上下文。服务端限制、协议校验及 DSH 文件/附件策略仍生效。
|
|
25
|
+
|
|
24
26
|
将插件安装到 DSH 的 `web` profile:
|
|
25
27
|
|
|
26
28
|
```sh
|
package/client.js
CHANGED
|
@@ -225,6 +225,8 @@ window.__ModuleLoader__.load({
|
|
|
225
225
|
.codexQuotaTrack{fill:none;stroke:var(--dsw-alias-border-l3);stroke-width:2px}.codexQuotaAvailable{fill:none;stroke:var(--dsw-alias-state-success-primary);stroke-width:2px;stroke-linecap:round;transition:stroke .25s ease,stroke-dasharray .25s ease,stroke-dashoffset .25s ease}.codexQuotaAvailable.high{stroke:var(--dsw-alias-state-warn-label)}.codexQuotaAvailable.critical{stroke:var(--dsw-alias-state-error-primary)}.codexQuotaLoading{animation:codexQuotaPulse .8s ease-in-out infinite alternate}
|
|
226
226
|
.codexQuotaPanel{z-index:100;box-sizing:border-box;width:272px;border:1px solid var(--dsw-alias-border-inverted);border-radius:12px;padding:12px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3);color:var(--dsw-alias-label-secondary);cursor:default;font-size:12px;line-height:20px;position:absolute;right:0;bottom:calc(100% + 8px)}.codexQuotaPanelHead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:8px}.codexQuotaPanelHead strong{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}.codexQuotaPanelHead span{color:var(--dsw-alias-label-tertiary)}
|
|
227
227
|
.codexQuotaWindow{padding:7px 0}.codexQuotaWindow+.codexQuotaWindow{border-top:1px solid var(--dsw-alias-border-l2)}.codexQuotaWindowHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.codexQuotaWindowHead strong{font-weight:500;color:var(--dsw-alias-label-secondary)}.codexQuotaWindowHead span{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}.codexQuotaBar{height:4px;margin:6px 0;border-radius:999px;overflow:hidden;background:var(--dsw-alias-interactive-bg-hover)}.codexQuotaBarFill{height:100%;border-radius:inherit;background:var(--dsw-alias-state-success-primary)}.codexQuotaBarFill.high{background:var(--dsw-alias-state-warn-label)}.codexQuotaBarFill.critical{background:var(--dsw-alias-state-error-primary)}.codexQuotaReset{color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage{margin:4px 0;color:var(--dsw-alias-label-tertiary)}.codexQuotaMessage.error{color:var(--dsw-alias-state-error-primary)}
|
|
228
|
+
.codexNetwork{margin-top:8px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}.codexNetworkHead,.codexNetworkRow{display:flex;align-items:baseline;justify-content:space-between;gap:12px}.codexNetworkHead{margin-bottom:5px}.codexNetworkHead strong{font-weight:500;color:var(--dsw-alias-label-primary)}.codexNetworkRow>span:first-child,.codexNetworkScope{color:var(--dsw-alias-label-tertiary)}.codexNetworkRow>span:last-child{font-variant-numeric:tabular-nums;text-align:right}.codexNetworkScope{margin:6px 0 0;font-size:11px;line-height:16px}.codexNetworkWarning{color:var(--dsw-alias-state-warn-label)}.codexQuotaPanel{max-height:min(680px,75vh);overflow-y:auto;width:min(300px,calc(100vw - 24px))}
|
|
229
|
+
.codexQuotaReconnecting .codexQuotaTrigger svg,.codexQuotaReconnecting .codexQuotaBarFill{animation:codexReconnectBreath 2s ease-in-out infinite}.codexQuotaReconnecting .codexQuotaTrack,.codexQuotaReconnecting .codexQuotaAvailable{stroke:var(--dsw-alias-state-warn-label)}@keyframes codexReconnectBreath{0%,100%{opacity:1}50%{opacity:.3}}@media(prefers-reduced-motion:reduce){.codexQuotaReconnecting .codexQuotaTrigger svg,.codexQuotaReconnecting .codexQuotaBarFill{animation:none}}
|
|
228
230
|
@keyframes codexPulse{to{background-position:-200% 0}}@keyframes codexQuotaPulse{to{opacity:.35}}@media(prefers-reduced-motion:reduce){.codexSkeleton,.codexQuotaLoading{animation:none}}@media(max-width:620px){.codexHero{padding:14px;flex-direction:column}.codexBody{padding:14px}.codexGrid,.codexModelVisibilityList{grid-template-columns:1fr}.codexAccountOption{align-items:stretch;flex-direction:column;gap:10px}.codexAccountActions{align-self:flex-start}.codexCode{font-size:20px}}
|
|
229
231
|
`
|
|
230
232
|
if (typeof document !== 'undefined' && document.querySelector('style[data-plugin="' + PLUGIN_ID + '"]') === null) {
|
|
@@ -408,10 +410,120 @@ window.__ModuleLoader__.load({
|
|
|
408
410
|
)
|
|
409
411
|
}
|
|
410
412
|
|
|
413
|
+
function networkDuration(ms) {
|
|
414
|
+
const seconds = Math.max(0, Math.floor(ms / 1000))
|
|
415
|
+
if (seconds < 60) return seconds + ' 秒'
|
|
416
|
+
return Math.floor(seconds / 60) + ' 分 ' + seconds % 60 + ' 秒'
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function networkBytes(bytes) {
|
|
420
|
+
if (bytes < 1024) return Math.round(bytes) + ' B'
|
|
421
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
|
|
422
|
+
return (bytes / 1048576).toFixed(1) + ' MB'
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function networkSummary(snapshot) {
|
|
426
|
+
const active = snapshot.requests.filter((request) => !['completed', 'failed', 'cancelled'].includes(request.state))
|
|
427
|
+
const priority = { retrying: 0, waiting: 1, connecting: 2, preparing: 3, streaming: 4 }
|
|
428
|
+
const current = active.sort((a, b) => priority[a.state] - priority[b.state] || a.startedAt - b.startedAt)[0]
|
|
429
|
+
|| snapshot.requests.slice().sort((a, b) => b.startedAt - a.startedAt)[0]
|
|
430
|
+
const labels = { preparing: '准备请求', connecting: '连接中', waiting: '等待响应', streaming: '接收中', retrying: '等待重试', completed: '最近请求已完成', failed: '最近请求失败', cancelled: '最近请求已取消' }
|
|
431
|
+
const stalled = current && current.state === 'streaming' && snapshot.updatedAt - (current.lastActivityAt || current.startedAt) >= 10000
|
|
432
|
+
return { current, label: stalled ? '等待后续数据' : current ? labels[current.state] || '状态未知' : '尚无请求', active: active.length > 0 }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function useNetworkStats(enabled) {
|
|
436
|
+
const [sample, setSample] = useState(null)
|
|
437
|
+
const [error, setError] = useState('')
|
|
438
|
+
useEffect(() => {
|
|
439
|
+
if (!enabled) return undefined
|
|
440
|
+
let disposed = false
|
|
441
|
+
let timer
|
|
442
|
+
let controller
|
|
443
|
+
let timeout
|
|
444
|
+
let previous
|
|
445
|
+
const poll = async () => {
|
|
446
|
+
if (disposed || document.visibilityState === 'hidden') return
|
|
447
|
+
controller = new AbortController()
|
|
448
|
+
timeout = window.setTimeout(() => controller.abort(), 5000)
|
|
449
|
+
try {
|
|
450
|
+
const response = await fetch(BASE + '/network', { cache: 'no-store', signal: controller.signal })
|
|
451
|
+
if (!response.ok) throw new Error('HTTP ' + response.status)
|
|
452
|
+
const snapshot = await response.json()
|
|
453
|
+
if (!snapshot.totals || !Array.isArray(snapshot.requests)) throw new Error('网络统计暂不可用')
|
|
454
|
+
const elapsed = previous && snapshot.updatedAt - previous.updatedAt
|
|
455
|
+
const delta = previous && snapshot.totals.receivedBytes - previous.totals.receivedBytes
|
|
456
|
+
const rate = previous && previous.startedAt === snapshot.startedAt && elapsed > 0 && delta >= 0
|
|
457
|
+
? delta * 1000 / elapsed : null
|
|
458
|
+
previous = snapshot
|
|
459
|
+
if (!disposed) { setSample({ snapshot, rate }); setError('') }
|
|
460
|
+
} catch (failure) {
|
|
461
|
+
if (!disposed) setError('网络状态更新失败(' + messageOf(failure) + ')')
|
|
462
|
+
} finally {
|
|
463
|
+
window.clearTimeout(timeout)
|
|
464
|
+
controller = null
|
|
465
|
+
if (!disposed) timer = window.setTimeout(poll, 2000)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const onVisibility = () => {
|
|
469
|
+
if (document.visibilityState !== 'hidden' && !controller) {
|
|
470
|
+
window.clearTimeout(timer)
|
|
471
|
+
previous = null
|
|
472
|
+
void poll()
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
void poll()
|
|
476
|
+
document.addEventListener('visibilitychange', onVisibility)
|
|
477
|
+
return () => {
|
|
478
|
+
disposed = true
|
|
479
|
+
window.clearTimeout(timer)
|
|
480
|
+
window.clearTimeout(timeout)
|
|
481
|
+
if (controller) controller.abort()
|
|
482
|
+
document.removeEventListener('visibilitychange', onVisibility)
|
|
483
|
+
}
|
|
484
|
+
}, [enabled])
|
|
485
|
+
return { sample, error }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function networkReconnecting(network) {
|
|
489
|
+
return !network.error && network.sample && network.sample.snapshot.requests.some((request) =>
|
|
490
|
+
request.retryKind === 'connection' && ['retrying', 'preparing', 'connecting'].includes(request.state))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function CodexNetworkStats({ sample, error }) {
|
|
494
|
+
const snapshot = sample && sample.snapshot
|
|
495
|
+
const summary = snapshot && networkSummary(snapshot)
|
|
496
|
+
const current = summary && summary.current
|
|
497
|
+
const row = (label, value) => h('div', { className: 'codexNetworkRow', key: label }, h('span', null, label), h('span', null, value))
|
|
498
|
+
return h('section', { className: 'codexNetwork', 'aria-label': 'Codex 网络状态' },
|
|
499
|
+
h('div', { className: 'codexNetworkHead' }, h('strong', null, '网络'),
|
|
500
|
+
h('span', { className: error ? 'codexNetworkWarning' : '' }, error ? sample ? '数据已过期' : '读取失败' : summary ? summary.label : '读取中…')),
|
|
501
|
+
error ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, error) : null,
|
|
502
|
+
snapshot ? h(React.Fragment, null,
|
|
503
|
+
row('进行中', snapshot.activeCount + ' 个请求'),
|
|
504
|
+
current ? row('传输', current.transport === 'websocket' ? 'WebSocket' : 'HTTP / SSE') : null,
|
|
505
|
+
summary.active && current ? h(React.Fragment, null,
|
|
506
|
+
row('本次已等待', networkDuration(snapshot.updatedAt - current.startedAt)),
|
|
507
|
+
row('最近收包', current.lastActivityAt === null ? '尚未收到' : networkDuration(snapshot.updatedAt - current.lastActivityAt) + '前'),
|
|
508
|
+
row('最近输出', current.lastOutputAt === null ? '尚无输出' : networkDuration(snapshot.updatedAt - current.lastOutputAt) + '前'),
|
|
509
|
+
current.state === 'retrying' && current.retryAt !== null ? row('下次重试', networkDuration(current.retryAt - snapshot.updatedAt) + '后') : null,
|
|
510
|
+
row('本次尝试 / 重试', current.attempts + ' / ' + current.retries),
|
|
511
|
+
) : null,
|
|
512
|
+
row('接收速率', error || sample.rate === null ? '—' : networkBytes(sample.rate) + '/s'),
|
|
513
|
+
row('累计接收', networkBytes(snapshot.totals.receivedBytes)),
|
|
514
|
+
row('请求 / 成功 / 失败', snapshot.totals.requests + ' / ' + snapshot.totals.succeeded + ' / ' + snapshot.totals.failed),
|
|
515
|
+
row('累计重试 / 取消', snapshot.totals.retries + ' / ' + snapshot.totals.cancelled),
|
|
516
|
+
h('p', { className: 'codexNetworkScope' }, '本次加载以来 · 所有会话的原生 Codex 请求。接收量为响应及心跳载荷,不含协议开销。' + (snapshot.activeCount > 1 ? '上方显示优先等待的请求。' : '') + (snapshot.truncated ? '仅保留最近请求明细。' : '')),
|
|
517
|
+
) : null,
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
411
521
|
function CodexQuotaRing(props) {
|
|
412
522
|
const running = props.useSession((snapshot) => snapshot.running)
|
|
413
523
|
const status = useSharedStatus()
|
|
414
524
|
const [open, setOpen] = useState(false)
|
|
525
|
+
const network = useNetworkStats(Boolean(status && status.loggedIn))
|
|
526
|
+
const reconnecting = networkReconnecting(network)
|
|
415
527
|
const [loading, setLoading] = useState(false)
|
|
416
528
|
const [error, setError] = useState('')
|
|
417
529
|
const rootRef = useRef(null)
|
|
@@ -462,7 +574,7 @@ window.__ModuleLoader__.load({
|
|
|
462
574
|
const used = meter ? Math.max(0, Math.min(100, Number(meter.usedPercent) || 0)) : 0
|
|
463
575
|
const remaining = Math.max(0, 100 - used)
|
|
464
576
|
const reading = meter ? Math.round(remaining) + '%' : '待更新'
|
|
465
|
-
const title = loading ? '正在刷新 Codex 额度' : 'Codex 额度剩余 ' + reading + ';点击刷新'
|
|
577
|
+
const title = reconnecting ? 'Codex 网络不可用,正在持续重连' : loading ? '正在刷新 Codex 额度' : 'Codex 额度剩余 ' + reading + ';点击刷新'
|
|
466
578
|
const circumference = 2 * Math.PI * 5.5
|
|
467
579
|
const usedLength = circumference * used / 100
|
|
468
580
|
const remainingLength = circumference - usedLength
|
|
@@ -472,7 +584,7 @@ window.__ModuleLoader__.load({
|
|
|
472
584
|
|
|
473
585
|
return h('span', {
|
|
474
586
|
ref: rootRef,
|
|
475
|
-
className: 'codexQuotaRoot',
|
|
587
|
+
className: 'codexQuotaRoot' + (reconnecting ? ' codexQuotaReconnecting' : ''),
|
|
476
588
|
onMouseEnter: () => { setOpen(true) },
|
|
477
589
|
onMouseLeave: () => { setOpen(false) },
|
|
478
590
|
},
|
|
@@ -510,6 +622,7 @@ window.__ModuleLoader__.load({
|
|
|
510
622
|
windows.length > 0
|
|
511
623
|
? windows.map((row) => h(QuotaWindow, { key: row.name, name: row.name, window: row.window }))
|
|
512
624
|
: h('p', { className: 'codexQuotaMessage' }, '点击圆圈刷新最新额度。'),
|
|
625
|
+
h(CodexNetworkStats, network),
|
|
513
626
|
status.usageError ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, '额度读取失败:' + status.usageError) : null,
|
|
514
627
|
error ? h('p', { className: 'codexQuotaMessage error', role: 'status' }, '无法连接额度接口:' + error) : null,
|
|
515
628
|
) : null,
|
package/lib/catalog.js
CHANGED
|
@@ -8,7 +8,6 @@ export const CODEX_MODELS_URL = 'https://chatgpt.com/backend-api/codex/models';
|
|
|
8
8
|
export const CODEX_CATALOG_CACHE_TTL_MS = 5 * 60_000;
|
|
9
9
|
const CODEX_CATALOG_MAX_STALE_MS = 7 * 24 * 60 * 60_000;
|
|
10
10
|
const CODEX_CATALOG_TIMEOUT_MS = 5_000;
|
|
11
|
-
const MAX_CATALOG_BODY_LENGTH = 2 * 1024 * 1024;
|
|
12
11
|
const ORIGINATOR = 'dsh';
|
|
13
12
|
class CatalogFetchError extends LlmError {
|
|
14
13
|
allowsStale;
|
|
@@ -92,7 +91,7 @@ function parseServiceTiers(value) {
|
|
|
92
91
|
}
|
|
93
92
|
return tiers;
|
|
94
93
|
}
|
|
95
|
-
async function
|
|
94
|
+
async function responseText(response) {
|
|
96
95
|
if (response.body === null)
|
|
97
96
|
return '';
|
|
98
97
|
const reader = response.body.getReader();
|
|
@@ -104,10 +103,6 @@ async function boundedResponseText(response) {
|
|
|
104
103
|
if (done)
|
|
105
104
|
break;
|
|
106
105
|
total += value.byteLength;
|
|
107
|
-
if (total > MAX_CATALOG_BODY_LENGTH) {
|
|
108
|
-
await reader.cancel();
|
|
109
|
-
throw new CatalogFetchError('native Codex catalog response exceeded the size limit', 'CATALOG_INVALID_RESPONSE', true);
|
|
110
|
-
}
|
|
111
106
|
chunks.push(value);
|
|
112
107
|
}
|
|
113
108
|
}
|
|
@@ -366,7 +361,7 @@ export class NativeCodexCatalog {
|
|
|
366
361
|
}
|
|
367
362
|
let body;
|
|
368
363
|
try {
|
|
369
|
-
body = await
|
|
364
|
+
body = await responseText(response);
|
|
370
365
|
}
|
|
371
366
|
catch (error) {
|
|
372
367
|
if (error instanceof CatalogFetchError)
|
package/lib/cloud-context.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function chooseCloudModel(params: {
|
|
|
13
13
|
export declare function visibleCloudImages(messages: readonly Message[]): ImageAttachmentRef[];
|
|
14
14
|
/** Last two human texts plus intervening visible assistant text. UTF-8 bytes conservatively
|
|
15
15
|
* upper-bound byte-level tokenizer tokens; a single 1000-byte assistant budget avoids
|
|
16
|
-
* relying on an unavailable model tokenizer.
|
|
16
|
+
* relying on an unavailable model tokenizer. The selected human texts are preserved in full. */
|
|
17
17
|
export declare function recentSearchInput(messages: readonly Message[]): {
|
|
18
18
|
role: 'user' | 'assistant';
|
|
19
19
|
content: string;
|
package/lib/cloud-context.js
CHANGED
|
@@ -61,14 +61,13 @@ function boundedText(text, bytes, tail = false) {
|
|
|
61
61
|
}
|
|
62
62
|
/** Last two human texts plus intervening visible assistant text. UTF-8 bytes conservatively
|
|
63
63
|
* upper-bound byte-level tokenizer tokens; a single 1000-byte assistant budget avoids
|
|
64
|
-
* relying on an unavailable model tokenizer.
|
|
64
|
+
* relying on an unavailable model tokenizer. The selected human texts are preserved in full. */
|
|
65
65
|
export function recentSearchInput(messages) {
|
|
66
66
|
const users = messages.map((message, i) => message.role === 'user' && message.source.kind === 'user' && message.content.some(block => block.type === 'text') ? i : -1).filter(i => i >= 0).slice(-2);
|
|
67
67
|
if (!users.length)
|
|
68
68
|
return [];
|
|
69
69
|
const selected = [];
|
|
70
70
|
let assistantBytes = 1000;
|
|
71
|
-
let userBytes = 64 * 1024;
|
|
72
71
|
// Budget newest text first, then restore chronology.
|
|
73
72
|
for (let i = messages.length - 1; i >= users[0]; i--) {
|
|
74
73
|
const message = messages[i];
|
|
@@ -76,12 +75,10 @@ export function recentSearchInput(messages) {
|
|
|
76
75
|
if (!user && (message.role !== 'assistant' || message.source.kind !== 'model'))
|
|
77
76
|
continue;
|
|
78
77
|
const text = message.content.filter(block => block.type === 'text').map(block => block.text).join('\n');
|
|
79
|
-
const content =
|
|
78
|
+
const content = user ? text : boundedText(text, assistantBytes, true);
|
|
80
79
|
if (!content)
|
|
81
80
|
continue;
|
|
82
|
-
if (user)
|
|
83
|
-
userBytes -= Buffer.byteLength(content);
|
|
84
|
-
else
|
|
81
|
+
if (!user)
|
|
85
82
|
assistantBytes -= Buffer.byteLength(content);
|
|
86
83
|
selected.push({ role: user ? 'user' : 'assistant', content });
|
|
87
84
|
}
|
package/lib/cloud-http.d.ts
CHANGED
|
@@ -5,15 +5,12 @@ export interface NativeCodexCloudOptions {
|
|
|
5
5
|
fetch?: typeof fetch;
|
|
6
6
|
endpoint?: string;
|
|
7
7
|
requestTimeoutMs?: number;
|
|
8
|
-
maxRequestBodyBytes?: number;
|
|
9
|
-
maxResponseBytes?: number;
|
|
10
8
|
}
|
|
11
9
|
export interface NativeCodexCloudPostOptions {
|
|
12
10
|
credential: NativeCodexCredential;
|
|
13
11
|
signal: AbortSignal;
|
|
14
12
|
headers?: Record<string, string>;
|
|
15
13
|
timeoutMs?: number;
|
|
16
|
-
maxResponseBytes?: number;
|
|
17
14
|
}
|
|
18
15
|
export type NativeCodexCloudPath = 'alpha/search' | 'images/generations' | 'images/edits';
|
|
19
16
|
export declare class NativeCodexCloudClient {
|
package/lib/cloud-http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Nonstreaming Codex cloud requests. Never retry ambiguous POST failures. */
|
|
2
2
|
import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
|
|
3
3
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
4
4
|
const fail = (message, code = 'CODEX_CLOUD_FAILED') => new LlmError(message, code);
|
|
@@ -54,8 +54,6 @@ export class NativeCodexCloudClient {
|
|
|
54
54
|
if (!['alpha/search', 'images/generations', 'images/edits'].includes(path))
|
|
55
55
|
throw fail('Invalid Codex cloud path', 'INVALID_ARGS');
|
|
56
56
|
const timeoutMs = positive(options.timeoutMs ?? this.options.requestTimeoutMs, 120_000);
|
|
57
|
-
const maxBytes = positive(options.maxResponseBytes ?? this.options.maxResponseBytes, 48 * 1024 * 1024);
|
|
58
|
-
const requestLimit = positive(this.options.maxRequestBodyBytes, 48 * 1024 * 1024);
|
|
59
57
|
let encoded;
|
|
60
58
|
try {
|
|
61
59
|
encoded = JSON.stringify(body);
|
|
@@ -63,8 +61,8 @@ export class NativeCodexCloudClient {
|
|
|
63
61
|
catch {
|
|
64
62
|
throw fail('Invalid cloud JSON request', 'INVALID_ARGS');
|
|
65
63
|
}
|
|
66
|
-
if (typeof encoded !== 'string'
|
|
67
|
-
throw fail('
|
|
64
|
+
if (typeof encoded !== 'string')
|
|
65
|
+
throw fail('Invalid cloud JSON request', 'INVALID_ARGS');
|
|
68
66
|
let credential = credentialCopy(options.credential);
|
|
69
67
|
const accountId = credential.accountId;
|
|
70
68
|
const controller = new AbortController();
|
|
@@ -118,11 +116,7 @@ export class NativeCodexCloudClient {
|
|
|
118
116
|
if (!reader)
|
|
119
117
|
throw fail('Codex cloud response is empty');
|
|
120
118
|
const chunks = [];
|
|
121
|
-
let size = 0;
|
|
122
119
|
try {
|
|
123
|
-
const declared = response.headers.get('content-length');
|
|
124
|
-
if (declared !== null && Number(declared) > maxBytes)
|
|
125
|
-
throw fail('Codex cloud response exceeds byte limit');
|
|
126
120
|
while (true) {
|
|
127
121
|
let part;
|
|
128
122
|
try {
|
|
@@ -133,9 +127,6 @@ export class NativeCodexCloudClient {
|
|
|
133
127
|
}
|
|
134
128
|
if (part.done)
|
|
135
129
|
break;
|
|
136
|
-
size += part.value.byteLength;
|
|
137
|
-
if (size > maxBytes)
|
|
138
|
-
throw fail('Codex cloud response exceeds byte limit');
|
|
139
130
|
chunks.push(part.value);
|
|
140
131
|
}
|
|
141
132
|
}
|
package/lib/cloud-images.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
/** Pure image endpoint validation and
|
|
1
|
+
/** Pure image endpoint validation and decoding. */
|
|
2
2
|
import type { ImageMediaType } from '@deepseek-ai/dsh-attachment';
|
|
3
|
-
export declare const MAX_IMAGE_BYTES: number;
|
|
4
3
|
export type ImageSource = {
|
|
5
4
|
attachment_id: string;
|
|
6
5
|
} | {
|
|
@@ -36,7 +35,7 @@ export declare function buildImageEditRequest(value: unknown, images: readonly {
|
|
|
36
35
|
size: string;
|
|
37
36
|
n?: number;
|
|
38
37
|
};
|
|
39
|
-
export declare function decodeImageBase64(value: unknown
|
|
38
|
+
export declare function decodeImageBase64(value: unknown): Uint8Array;
|
|
40
39
|
export declare function imageMediaType(data: Uint8Array): ImageMediaType;
|
|
41
40
|
export interface ParsedCloudImage {
|
|
42
41
|
data: Uint8Array;
|
package/lib/cloud-images.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
export const MAX_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
2
1
|
function object(value, label) {
|
|
3
2
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
4
3
|
throw new Error(label + ' must be an object');
|
|
5
4
|
return value;
|
|
6
5
|
}
|
|
7
|
-
function text(value, label, max
|
|
8
|
-
if (typeof value !== 'string' || !value.trim() || value.length > max)
|
|
9
|
-
throw new Error(label + ' must be a nonempty
|
|
6
|
+
function text(value, label, max) {
|
|
7
|
+
if (typeof value !== 'string' || !value.trim() || (max !== undefined && value.length > max))
|
|
8
|
+
throw new Error(label + ' must be a valid nonempty string');
|
|
10
9
|
return value;
|
|
11
10
|
}
|
|
12
11
|
function count(value, label, max) {
|
|
@@ -68,16 +67,14 @@ export function buildImageEditRequest(value, images, defaultModel) {
|
|
|
68
67
|
}
|
|
69
68
|
return { ...validateImageGenerateArgs(value, defaultModel), images: images.map(({ image_url }) => ({ image_url })) };
|
|
70
69
|
}
|
|
71
|
-
export function decodeImageBase64(value
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
throw new Error('Image base64 is empty or exceeds byte limit');
|
|
75
|
-
// Check length before scanning or allocating the decoded payload.
|
|
70
|
+
export function decodeImageBase64(value) {
|
|
71
|
+
if (typeof value !== 'string' || !value.length)
|
|
72
|
+
throw new Error('Image base64 is empty');
|
|
76
73
|
if (value.length % 4 !== 0 || /[^A-Za-z0-9+/=]/.test(value) || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
|
|
77
74
|
throw new Error('Invalid image base64');
|
|
78
75
|
const data = Buffer.from(value, 'base64');
|
|
79
|
-
if (!data.length || data.
|
|
80
|
-
throw new Error('Invalid image base64
|
|
76
|
+
if (!data.length || data.toString('base64') !== value)
|
|
77
|
+
throw new Error('Invalid image base64');
|
|
81
78
|
return data;
|
|
82
79
|
}
|
|
83
80
|
export function imageMediaType(data) {
|
|
@@ -108,11 +105,9 @@ export function parseImageResponse(value, requestId) {
|
|
|
108
105
|
metadata.size = text(response.size, 'response size', 64);
|
|
109
106
|
if (requestId !== undefined)
|
|
110
107
|
metadata.request_id = text(requestId, 'request_id', 512);
|
|
111
|
-
let total = 0;
|
|
112
108
|
const images = response.data.map((entry) => {
|
|
113
109
|
const item = object(entry, 'Image response data');
|
|
114
|
-
const data = decodeImageBase64(item.b64_json
|
|
115
|
-
total += data.length;
|
|
110
|
+
const data = decodeImageBase64(item.b64_json);
|
|
116
111
|
const generation_id = item.generation_id == null ? undefined : text(item.generation_id, 'generation_id', 512);
|
|
117
112
|
// Unknown fields may contain payloads or private service state; never copy them to history.
|
|
118
113
|
return { data, mediaType: imageMediaType(data), metadata: generation_id === undefined ? {} : { generation_id },
|
package/lib/cloud-media.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { posix, win32 } from 'node:path';
|
|
4
4
|
import { imageSize } from 'image-size';
|
|
5
|
-
import {
|
|
5
|
+
import { imageMediaType, validateImageSelection } from './cloud-images.js';
|
|
6
6
|
// Check both process-world grammars without resolving against the plugin host OS.
|
|
7
7
|
function absoluteWorldPath(path) {
|
|
8
8
|
return !path.includes('\0') && (posix.isAbsolute(path) || (win32.isAbsolute(path) && /^[A-Za-z]:|^\\\\[^\\]+\\[^\\]+/.test(path)));
|
|
@@ -25,7 +25,6 @@ export async function resolveImageSources(value, deps) {
|
|
|
25
25
|
if ('attachment_id' in source && !refs.has(source.attachment_id))
|
|
26
26
|
throw new Error('Image attachment is not visible in this session: ' + source.attachment_id);
|
|
27
27
|
const result = [];
|
|
28
|
-
let total = 0;
|
|
29
28
|
for (const source of sources) {
|
|
30
29
|
deps.signal?.throwIfAborted();
|
|
31
30
|
let data;
|
|
@@ -33,8 +32,8 @@ export async function resolveImageSources(value, deps) {
|
|
|
33
32
|
let path;
|
|
34
33
|
if ('attachment_id' in source) {
|
|
35
34
|
attachment = refs.get(source.attachment_id);
|
|
36
|
-
if (!Number.isSafeInteger(attachment.bytes) || attachment.bytes <= 0
|
|
37
|
-
throw new Error('
|
|
35
|
+
if (!Number.isSafeInteger(attachment.bytes) || attachment.bytes <= 0)
|
|
36
|
+
throw new Error('Invalid image byte count');
|
|
38
37
|
const stored = await deps.attachments.readImage(attachment, deps.signal);
|
|
39
38
|
data = stored.data;
|
|
40
39
|
if (stored.ref.attachmentId !== attachment.attachmentId || stored.ref.bytes !== attachment.bytes ||
|
|
@@ -46,12 +45,12 @@ export async function resolveImageSources(value, deps) {
|
|
|
46
45
|
if (!absoluteWorldPath(source.path))
|
|
47
46
|
throw new Error('Image path must be absolute');
|
|
48
47
|
const target = await deps.fs.resolve(source.path, { cwd: deps.workspace, ...(deps.signal ? { signal: deps.signal } : {}) });
|
|
49
|
-
|
|
48
|
+
// The filesystem API requires a numeric bound; impose no plugin-specific byte ceiling.
|
|
49
|
+
data = await deps.fs.readBytes(target, deps.signal, Number.MAX_SAFE_INTEGER);
|
|
50
50
|
path = deps.fs.processPath(target);
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
throw new Error('Image inputs are empty or exceed byte limit');
|
|
52
|
+
if (!data.length)
|
|
53
|
+
throw new Error('Image inputs are empty');
|
|
55
54
|
const mediaType = imageMediaType(data);
|
|
56
55
|
await deps.attachments.validateImage({ data, mediaType });
|
|
57
56
|
const dimensions = encodedDimensions(data);
|
|
@@ -74,13 +73,11 @@ export async function saveImageOutputs(response, deps) {
|
|
|
74
73
|
throw new Error('Caller workspace must be absolute');
|
|
75
74
|
if (!deps.policy || !deps.shell.sandboxMode)
|
|
76
75
|
throw new Error('Original image writes require a policy-enforcing DSH shell');
|
|
77
|
-
let total = 0;
|
|
78
76
|
const dimensions = [];
|
|
79
77
|
for (const image of response.images) {
|
|
80
78
|
deps.signal?.throwIfAborted();
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new Error('Image outputs exceed byte limit');
|
|
79
|
+
if (!image.data.length)
|
|
80
|
+
throw new Error('Image outputs are empty');
|
|
84
81
|
if (imageMediaType(image.data) !== image.mediaType)
|
|
85
82
|
throw new Error('Output media type does not match bytes');
|
|
86
83
|
await deps.attachments.validateImage({ data: image.data, mediaType: image.mediaType });
|
package/lib/cloud-search.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export declare function buildCloudSearchRequest(args: CloudSearchArgs, context:
|
|
|
22
22
|
model: string;
|
|
23
23
|
input?: unknown;
|
|
24
24
|
}): Record<string, unknown>;
|
|
25
|
-
/**
|
|
25
|
+
/** Preserve complete output and opaque result entries. */
|
|
26
26
|
export declare function parseCloudSearchResponse(body: unknown): {
|
|
27
27
|
output: string;
|
|
28
28
|
results?: unknown[];
|
package/lib/cloud-search.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/** Wire contract: Codex b348fc26674189f758d5941cdab3f78f258b2aa7, codex-api/src/search.rs. */
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
|
-
const MAX_REQUEST_BYTES = 256 * 1024;
|
|
5
|
-
const MAX_OUTPUT_BYTES = 128 * 1024;
|
|
6
|
-
const MAX_RESULTS_BYTES = 256 * 1024;
|
|
7
4
|
const invalid = () => new LlmError('Invalid codex_web arguments; check command fields and limits', 'INVALID_ARGS');
|
|
8
5
|
function object(value) {
|
|
9
6
|
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
@@ -11,7 +8,7 @@ function object(value) {
|
|
|
11
8
|
return value;
|
|
12
9
|
}
|
|
13
10
|
function text(value, empty = false) {
|
|
14
|
-
if (typeof value !== 'string' || (!empty && !value.trim())
|
|
11
|
+
if (typeof value !== 'string' || (!empty && !value.trim()))
|
|
15
12
|
throw invalid();
|
|
16
13
|
return value;
|
|
17
14
|
}
|
|
@@ -22,7 +19,7 @@ const uint = value => {
|
|
|
22
19
|
return value;
|
|
23
20
|
};
|
|
24
21
|
const strings = value => {
|
|
25
|
-
if (!Array.isArray(value)
|
|
22
|
+
if (!Array.isArray(value))
|
|
26
23
|
throw invalid();
|
|
27
24
|
return value.map(item => text(item));
|
|
28
25
|
};
|
|
@@ -67,10 +64,9 @@ const operations = {
|
|
|
67
64
|
return s;
|
|
68
65
|
} }, required: ['utc_offset'] },
|
|
69
66
|
};
|
|
70
|
-
function
|
|
67
|
+
function validateRequestJson(value) {
|
|
71
68
|
try {
|
|
72
|
-
|
|
73
|
-
throw invalid();
|
|
69
|
+
JSON.stringify(value);
|
|
74
70
|
}
|
|
75
71
|
catch {
|
|
76
72
|
throw invalid();
|
|
@@ -88,13 +84,13 @@ export function parseCloudSearchArgs(value) {
|
|
|
88
84
|
result[key] = choice('short', 'medium', 'long')(items);
|
|
89
85
|
continue;
|
|
90
86
|
}
|
|
91
|
-
if (!Object.hasOwn(operations, key) || !Array.isArray(items) || items.length === 0
|
|
87
|
+
if (!Object.hasOwn(operations, key) || !Array.isArray(items) || items.length === 0)
|
|
92
88
|
throw invalid();
|
|
93
89
|
const operation = operations[key];
|
|
94
90
|
count += items.length;
|
|
95
91
|
result[key] = items.map(item => fields(item, operation.rules, operation.required));
|
|
96
92
|
}
|
|
97
|
-
if (count === 0
|
|
93
|
+
if (count === 0)
|
|
98
94
|
throw invalid();
|
|
99
95
|
return result;
|
|
100
96
|
},
|
|
@@ -104,7 +100,7 @@ export function parseCloudSearchArgs(value) {
|
|
|
104
100
|
filters: value => fields(value, { allowed_domains: strings, blocked_domains: strings }),
|
|
105
101
|
image_settings: value => fields(value, { max_results: uint, caption: bool }),
|
|
106
102
|
}, ['commands']);
|
|
107
|
-
|
|
103
|
+
validateRequestJson(parsed);
|
|
108
104
|
return parsed;
|
|
109
105
|
}
|
|
110
106
|
/** The caller selects the model and visible context using the same request credential. */
|
|
@@ -122,10 +118,10 @@ export function buildCloudSearchRequest(args, context) {
|
|
|
122
118
|
if (input !== undefined && typeof input !== 'string' && !Array.isArray(input))
|
|
123
119
|
throw invalid();
|
|
124
120
|
const result = { id, model: text(context.model), commands: parsed.commands, settings, ...(input === undefined ? {} : { input }) };
|
|
125
|
-
|
|
121
|
+
validateRequestJson(result);
|
|
126
122
|
return result;
|
|
127
123
|
}
|
|
128
|
-
/**
|
|
124
|
+
/** Preserve complete output and opaque result entries. */
|
|
129
125
|
export function parseCloudSearchResponse(body) {
|
|
130
126
|
let data;
|
|
131
127
|
try {
|
|
@@ -136,17 +132,10 @@ export function parseCloudSearchResponse(body) {
|
|
|
136
132
|
}
|
|
137
133
|
if (typeof data.output !== 'string' || (data.results != null && !Array.isArray(data.results)))
|
|
138
134
|
throw new LlmError('Invalid Codex search response', 'CODEX_CLOUD_FAILED');
|
|
139
|
-
|
|
140
|
-
let truncated = false;
|
|
141
|
-
if (Buffer.byteLength(output) > MAX_OUTPUT_BYTES) {
|
|
142
|
-
// Truncate only at complete UTF-8 boundaries; the notice warns that references may be omitted.
|
|
143
|
-
output = new TextDecoder().decode(Buffer.from(output).subarray(0, MAX_OUTPUT_BYTES)).replace(/\uFFFD$/, '');
|
|
144
|
-
truncated = true;
|
|
145
|
-
}
|
|
135
|
+
const output = data.output;
|
|
146
136
|
let results;
|
|
147
137
|
if (Array.isArray(data.results)) {
|
|
148
138
|
results = [];
|
|
149
|
-
let bytes = 2;
|
|
150
139
|
for (const item of data.results) {
|
|
151
140
|
let serialized;
|
|
152
141
|
try {
|
|
@@ -157,16 +146,8 @@ export function parseCloudSearchResponse(body) {
|
|
|
157
146
|
}
|
|
158
147
|
if (serialized === undefined)
|
|
159
148
|
throw new LlmError('Invalid Codex search results', 'CODEX_CLOUD_FAILED');
|
|
160
|
-
const size = Buffer.byteLength(serialized) + 1;
|
|
161
|
-
if (results.length >= 100 || bytes + size > MAX_RESULTS_BYTES) {
|
|
162
|
-
truncated = true;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
149
|
results.push(JSON.parse(serialized));
|
|
166
|
-
bytes += size;
|
|
167
150
|
}
|
|
168
151
|
}
|
|
169
|
-
|
|
170
|
-
output += '\n[Search response truncated; some content or references were omitted. Narrow the request to retrieve them.]';
|
|
171
|
-
return { output, ...(results === undefined ? {} : { results }), truncated };
|
|
152
|
+
return { output, ...(results === undefined ? {} : { results }), truncated: false };
|
|
172
153
|
}
|
package/lib/cloud-tools.js
CHANGED
|
@@ -10,7 +10,7 @@ const imageRefs = { type: 'array', minItems: 1, maxItems: 5, description: 'Image
|
|
|
10
10
|
],
|
|
11
11
|
} };
|
|
12
12
|
const common = {
|
|
13
|
-
prompt: { type: 'string', description: 'Describe the image to generate or the edits to perform.', minLength: 1
|
|
13
|
+
prompt: { type: 'string', description: 'Describe the image to generate or the edits to perform.', minLength: 1 },
|
|
14
14
|
model: { type: 'string', description: 'Optional Codex image model; defaults to configured imageModel or gpt-image-2.' },
|
|
15
15
|
quality: { type: 'string', enum: ['auto', 'low', 'medium', 'high'] },
|
|
16
16
|
background: { type: 'string', enum: ['auto', 'transparent', 'opaque'] },
|
|
@@ -105,7 +105,7 @@ export function registerCodexImageTools(ctx, deps) {
|
|
|
105
105
|
name: 'codex_image_inspect',
|
|
106
106
|
description: 'Ask a Codex vision model to analyze images, OCR, screenshots or charts. This makes one additional cloud inference call, without recursive tools. Use explicit model or configured visionModel for non-Codex callers. Reports effective detail and actual dimensions.',
|
|
107
107
|
parameters: { type: 'object', additionalProperties: false, required: ['prompt', 'images'], properties: {
|
|
108
|
-
prompt: { type: 'string', minLength: 1
|
|
108
|
+
prompt: { type: 'string', minLength: 1 }, images: imageRefs,
|
|
109
109
|
model: { type: 'string' }, detail: { type: 'string', enum: ['auto', 'low', 'high', 'original'] }, reasoning_effort: { type: 'string' },
|
|
110
110
|
} },
|
|
111
111
|
output, timeoutMs: 120_000, isConcurrencySafe: () => true,
|