@pure01fx/dsh-openai-codex-auth 0.11.2 → 0.13.0
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 +36 -1
- package/CODEX-COMPATIBILITY.md +39 -0
- package/README.md +3 -3
- package/lib/cloud-context.js +9 -13
- package/lib/cloud-media.d.ts +1 -1
- package/lib/cloud-media.js +2 -1
- package/lib/cloud-web-tool.js +4 -5
- package/lib/native-adapter.js +2 -1
- package/lib/native-http.js +49 -75
- package/lib/native-websocket-socket.js +4 -5
- package/lib/native-websocket.js +31 -45
- package/lib/replay.d.ts +3 -0
- package/lib/replay.js +8 -2
- package/lib/responses.d.ts +13 -11
- package/lib/responses.js +73 -30
- package/lib/retry-after.d.ts +9 -0
- package/lib/retry-after.js +109 -0
- package/lib/upstream.d.ts +2 -2
- package/lib/upstream.js +2 -2
- package/package.json +43 -37
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.13.0
|
|
6
|
+
|
|
7
|
+
- Targets DSH 0.2.0-rc.2 with exact SDK peers, development dependencies and runtime helpers; retains the tool-message, shell and provenance migration below.
|
|
8
|
+
- Merges GPT-6.1 catalog routing, shared Retry-After handling, assistant phases and deterministic developer instructions from the latest source.
|
|
9
|
+
- Target build/typecheck and all 517 regression tests pass on DSH 0.2.0-rc.2. No live OAuth/model requests were made; collection-level Web validation is recorded separately.
|
|
10
|
+
|
|
11
|
+
## 0.12.0
|
|
12
|
+
|
|
13
|
+
### Migration
|
|
14
|
+
|
|
15
|
+
- Migrated to DSH 0.1.7-rc.2 SDK
|
|
16
|
+
- Updated tool message format: tool results are now role:'tool' with top-level toolCallId/isError
|
|
17
|
+
- Migrated shell.run to shell.execute with result() method
|
|
18
|
+
- Updated peer dependencies to 0.1.7-rc.2
|
|
19
|
+
- Updated cordis to ~4.0.4
|
|
20
|
+
- Preserved developer messages in HTTP/WS wire requests
|
|
21
|
+
- Restored successful-result-only matched provenance in cloud context
|
|
22
|
+
- Added mismatch/failed-result regressions in cloud context tests
|
|
23
|
+
|
|
24
|
+
### Key Changes
|
|
25
|
+
|
|
26
|
+
- Tool messages: role:'tool' with top-level toolCallId/isError (no longer tool-result content blocks)
|
|
27
|
+
- Shell: execute() then .result() instead of run()
|
|
28
|
+
- Developer messages: preserved in Responses API requests
|
|
29
|
+
- Cloud context: requires toolCallId === source.callId and !isError
|
|
30
|
+
|
|
31
|
+
- Shares Retry-After parsing across HTTP, SSE, and WebSocket errors, including nested JSON headers and HTTP dates; preserves server deadlines across retries and HTTP fallback without truncating advice to local backoff limits.
|
|
32
|
+
|
|
33
|
+
- Preserves commentary, partial_answer, and final_answer phases through response events, durable replay, and WebSocket continuation matching; legacy phase-less replay remains readable.
|
|
34
|
+
|
|
35
|
+
- Moves Standard base instructions into the same deterministic developer-message prefix used by Lite, preserving each protocol’s tool and reasoning controls; changed instructions invalidate WebSocket prefix reuse.
|
|
36
|
+
|
|
37
|
+
- Requests the live model catalog as Codex 0.162.0 to discover GPT-6.1 Sol; verified that 0.155.0 omits it for the same account despite the bundled 0.153.0 minimum. Adds its exact Lite fallback and dynamic metadata/routing regressions without fabricating catalog availability.
|
|
38
|
+
|
|
39
|
+
|
|
5
40
|
## 0.11.2
|
|
6
41
|
|
|
7
42
|
- Requests the live model catalog as Codex 0.155.0 so version-gated GPT-6 Sol can be advertised; preserves dynamic account-specific discovery and routes exact Sol requests through Responses Lite when catalog metadata is unavailable. Fast remains catalog-authorized.
|
|
@@ -131,4 +166,4 @@
|
|
|
131
166
|
- Rejects redirects for credential-bearing OAuth, Catalog, Responses, and subscription-usage requests.
|
|
132
167
|
- Keeps OAuth credentials and account authority host-side, validates credential-bearing endpoint authorities, bounds request/response/replay/WebSocket state, and refuses managed/external credential mismatches.
|
|
133
168
|
- Pins Host/runtime contracts to DSH rc.6 and includes synchronized JavaScript/declaration artifacts plus the complete native transport modules.
|
|
134
|
-
- Keeps the package name `@pure01fx/dsh-openai-codex-auth`; a rename is intentionally deferred.
|
|
169
|
+
- Keeps the package name `@pure01fx/dsh-openai-codex-auth`; a rename is intentionally deferred.
|
package/CODEX-COMPATIBILITY.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Codex 云端能力兼容性跟踪
|
|
2
2
|
|
|
3
|
+
## 重试、消息阶段与 Standard 指令对齐(Unreleased)
|
|
4
|
+
|
|
5
|
+
本节记录对 `ea27864f` 的三项增量适配,取代下方 Retry-After、phase 和 Standard 指令位置的历史待办;其余工作仍独立跟踪。
|
|
6
|
+
|
|
7
|
+
- Standard 基础指令改为 input 首部的 developer message,和 Lite 共用稳定 UUID 与 `model.base_instructions` metadata 构造。Standard 顶层 tools、并行工具与 reasoning 参数保留各自协议;Lite 的工具前缀及目录模板/DSH 指令组合保持独立。回归验证相同指令保持 WS 前缀复用,改变指令会触发完整请求,HTTP 历史、图片、工具输出和加密 reasoning 顺序不变。
|
|
8
|
+
- 消息 phase 已接收并保留 commentary / partial_answer / final_answer,通过 v1 描述符的可选字段持久化再回放,并让 WS 复用比较使用同一有效 phase。added-only 的 phase 保留到 done,done 的显式 phase 优先;流中的 null/缺失值表示未提供,非法枚举拒绝。测试覆盖 JSON 保存/恢复、分叉和重启的请求构造、真实传输类的增量匹配与 phase 改变后完整重发。缺少 phase 的旧会话可读;旧插件的严格字段校验无法读取含 phase 的新描述符,降级时需考虑这一限制。
|
|
9
|
+
- Retry-After 共用秒数/HTTP-date 解析,支持 JSON 数字值,嵌套错误 headers 优先;保留单次收到建议的单调时钟截止时间,不用本地退避上限截短服务端等待。HTTP 读取错误正文及其超时不会重置或丢弃等待,WS 最后一次失败后切换 HTTP 也遵守剩余等待。长等待分段使用 Node timer,仍可取消。宿主 `LlmError` 只公开正数 duration,无法跨宿主持久化表达单调时钟 deadline;本插件默认 normal 重试策略在宿主等待超上限时交还错误,不会提前重试。宿主另行配置的 always 策略可能忽略长等待,不属于本次插件内改动。
|
|
10
|
+
|
|
11
|
+
验证:`npm run build`、`npm test`(27 文件 / 511 项)、`npm run typecheck`、`git diff --check`、`npm pack --dry-run --ignore-scripts` 均通过;所有生成模块与新鲜编译一致,新 retry-after 模块已加入打包清单。独立审查发现的非法日期归一化问题已修复,并加入内层非法日期回退外层有效等待的回归。本轮为源码、模拟传输与 loopback 验证,未执行线上推理、发布、替换 hu profile 或重启 GUI。
|
|
12
|
+
|
|
13
|
+
## GPT-6.1 Sol 增量对齐(Unreleased,2026-10-08)
|
|
14
|
+
|
|
15
|
+
`../codex` 已 fast-forward 到 `ea27864f99f0b086cec2f9f0251b7190fb9844f1`(本次获取的 origin/main),工作树干净。此轮针对模型发现与 Lite 路由,不宣称完整运行时对齐,原 Provider/cloud 跟踪提交保持不变。
|
|
16
|
+
|
|
17
|
+
### 已确认的缺失原因与修复
|
|
18
|
+
|
|
19
|
+
对同一当前账号执行只读线上目录请求(未调用推理、未记录凭据):
|
|
20
|
+
|
|
21
|
+
| client_version | originator | HTTP | 返回目录 |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| 0.153.4 | dsh | 200 | 不含 gpt-6-sol / gpt-6.1-sol |
|
|
24
|
+
| 0.155.0 | dsh | 200 | 含 gpt-6-sol,不含 gpt-6.1-sol |
|
|
25
|
+
| 0.162.0 | dsh | 200 | 含 gpt-6.1-sol,visibility=list |
|
|
26
|
+
| 0.162.0 | codex_cli_rs | 200 | 同样含 gpt-6.1-sol |
|
|
27
|
+
|
|
28
|
+
上游 `models-manager/models.json` 将 6.1 Sol 的 minimal_client_version 标为 0.153.0,但它不等于线上实际发现门槛。请求版本升至本次已验证的 0.162.0(上游已有 rust-v0.162.0-alpha.20;目录发送去预发布后缀的整版本)。不更改 originator,不将静态模型强行加入账号目录。
|
|
29
|
+
|
|
30
|
+
补充精确 `gpt-6.1-sol` 的 Lite 后备,目录存在时仍以 metadata 为准。新增精简上游 fixture,验证动态发现、六档 reasoning、默认 low / multi-agent xhigh、Fast 授权、metadata 优先及未知近似名称不误用 Lite。
|
|
31
|
+
|
|
32
|
+
当前 3080 GUI 的 hu profile 安装的是插件 0.11.1,请求版本 0.153.4。本轮只修改并构建仓库,未替换 profile 安装或重启当前会话;仓库修改不会自动改变正在运行的 GUI。
|
|
33
|
+
|
|
34
|
+
验证:`npm run build`、`npm test`(26 文件 / 469 项)、`npm run typecheck`、`git diff --check` 全部通过;生成产物一致性测试通过。使用新构建的 NativeCodexCatalog + NativeCodexAdapter 查询当前账号,已返回 `gpt-6.1-sol` 与 `gpt-6.1-sol-fast`,六档推理配置及默认 low 解析成功。本轮未执行模型推理,因此此结果验证发现与元数据,不代表线上推理端到端验收。
|
|
35
|
+
|
|
36
|
+
### 独立后续兼容性工作
|
|
37
|
+
|
|
38
|
+
- Retry-After:新上游从 response.failed.response.error.headers 读取等待提示,WebSocket 优先嵌套 headers,支持数字 JSON 值和 HTTP 日期;插件仍遗漏部分嵌套提示并将等待上限压到 120 秒。建议单独对齐共享解析及 deadline 传递,避免早于服务端建议重试。依据 `codex-api/src/sse/responses_error.rs`、`endpoint/responses_websocket.rs`、`http-client/src/retry_after.rs`。这不是 6.1 模型目录缺失原因。
|
|
39
|
+
- 数字自定义 reasoning effort、Standard instructions 移入 developer input、partial_answer phase 回放保真分别存在增量差异,应单独设计与验证。6.1 现有命名 effort 和 Lite 路径不依赖这些改动。
|
|
40
|
+
|
|
41
|
+
|
|
3
42
|
## 已实现:Sol 与错误协议增量适配(0.11.2)
|
|
4
43
|
|
|
5
44
|
参考上游 `c44deff7b1083e9660ac55d02122481f1cdf139b`,在既有 0.11.1 工作树上完成以下必要改动。此节取代下方历史审查中相应的“待适配”结论;不代表完整上游运行时对齐。
|
package/README.md
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
## 快速开始
|
|
19
19
|
|
|
20
|
-
版本 `0.
|
|
20
|
+
版本 `0.13.0` 明确适配 DeepSeek Harness `0.2.0-rc.2`,SDK peers/runtime helpers 精确锁定该版本;不声明兼容旧 SDK。旧 DSH `0.1.5-rc.1` profile 应保留原插件 `0.11.2`,升级时在独立 profile 安装新版插件并保留原 profile/锁文件以便回滚。无需迁移账号文件或浏览器隐藏列表;`dsh.engines` 仅作兼容性说明,不当作宿主硬门禁。
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
回归范围包含真实目标 LLM/tools 服务、本地 HTTP/SSE/WebSocket 回放、错误/取消,以及模型隐藏在真实目录/catalog/store 的投影更新、重连和重新加载。验证命令为 `pnpm typecheck && pnpm build && pnpm test`;0.13.0 候选版已通过目标版本构建、类型检查和 517 项测试;当前未执行线上 OAuth/付费模型验收。
|
|
23
23
|
|
|
24
24
|
`0.11.0` 移除了插件额外设置的请求、响应、SSE/WebSocket、图片和重放状态大小上限,以及相应的 transport 选项(含 `maxRequestBodyBytes`);旧配置中的该字段应删除。搜索结果完整保留,WebSocket 继续优先复用增量上下文。服务端限制、协议校验及 DSH 文件/附件策略仍生效。
|
|
25
25
|
|
|
@@ -356,4 +356,4 @@ node --check client.js
|
|
|
356
356
|
|
|
357
357
|
## License
|
|
358
358
|
|
|
359
|
-
[MIT](./LICENSE)
|
|
359
|
+
[MIT](./LICENSE)
|
package/lib/cloud-context.js
CHANGED
|
@@ -14,16 +14,6 @@ export function chooseCloudModel(params, models) {
|
|
|
14
14
|
export function visibleCloudImages(messages) {
|
|
15
15
|
const images = [];
|
|
16
16
|
const calls = new Set();
|
|
17
|
-
const visit = (blocks, depth = 0) => {
|
|
18
|
-
if (depth > 16)
|
|
19
|
-
return;
|
|
20
|
-
for (const block of blocks) {
|
|
21
|
-
if (block.type === 'image')
|
|
22
|
-
images.push(block.attachment);
|
|
23
|
-
else if (block.type === 'tool-result' && !block.isError && calls.has(block.toolCallId))
|
|
24
|
-
visit(block.content, depth + 1);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
27
17
|
for (const message of messages) {
|
|
28
18
|
if (message.role === 'assistant') {
|
|
29
19
|
for (const block of message.content)
|
|
@@ -36,10 +26,16 @@ export function visibleCloudImages(messages) {
|
|
|
36
26
|
if (block.type === 'image')
|
|
37
27
|
images.push(block.attachment);
|
|
38
28
|
}
|
|
39
|
-
else if (message.role === '
|
|
29
|
+
else if (message.role === 'tool' && message.source.kind === 'tool') {
|
|
30
|
+
// Require successful result with matching callId and earlier matching call
|
|
31
|
+
if (message.isError || message.toolCallId !== message.source.callId)
|
|
32
|
+
continue;
|
|
33
|
+
if (!calls.has(message.source.callId))
|
|
34
|
+
continue;
|
|
35
|
+
// Tool messages with images in their content
|
|
40
36
|
for (const block of message.content)
|
|
41
|
-
if (block.type === '
|
|
42
|
-
|
|
37
|
+
if (block.type === 'image')
|
|
38
|
+
images.push(block.attachment);
|
|
43
39
|
}
|
|
44
40
|
}
|
|
45
41
|
return images;
|
package/lib/cloud-media.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface ResolvedCloudImage {
|
|
|
24
24
|
export declare function resolveImageSources(value: ImageSelection, deps: MediaSourceDependencies): Promise<ResolvedCloudImage[]>;
|
|
25
25
|
export interface MediaOutputDependencies {
|
|
26
26
|
fs: Pick<FileSystem, 'resolve' | 'processPath' | 'contains'>;
|
|
27
|
-
shell: Pick<ShellExecutor, 'resolve' | '
|
|
27
|
+
shell: Pick<ShellExecutor, 'resolve' | 'execute' | 'sandboxMode'>;
|
|
28
28
|
/** Resolve from the calling session with the public sandboxPolicy service. */
|
|
29
29
|
policy: SandboxExecutionPolicy;
|
|
30
30
|
attachments: Pick<AttachmentStore, 'validateImage' | 'saveImage'>;
|
package/lib/cloud-media.js
CHANGED
|
@@ -92,11 +92,12 @@ export async function saveImageOutputs(response, deps) {
|
|
|
92
92
|
if (!deps.fs.contains(workspace, target))
|
|
93
93
|
throw new Error('Original image target escapes caller workspace');
|
|
94
94
|
const path = deps.fs.processPath(target);
|
|
95
|
-
const
|
|
95
|
+
const execution = await deps.shell.execute(deps.shell.resolve({
|
|
96
96
|
command: 'node -e ' + quote(WRITE_ORIGINAL) + ' -- ' + quote(path),
|
|
97
97
|
stdin: Buffer.from(image.data).toString('base64'), workdir: deps.fs.processPath(workspace),
|
|
98
98
|
sandboxPolicy: deps.policy, signal: deps.signal, timeoutMs: 30_000, stdoutMaxBytes: 1024,
|
|
99
99
|
}));
|
|
100
|
+
const outcome = await execution.result();
|
|
100
101
|
if (outcome.sandbox?.denied || outcome.sandbox?.runnerFailed || outcome.exitCode !== 0 || outcome.aborted || outcome.timedOut)
|
|
101
102
|
throw new Error('Original image write failed under caller filesystem policy');
|
|
102
103
|
const attachment = await deps.attachments.saveImage({ data: image.data, mediaType: image.mediaType, name });
|
package/lib/cloud-web-tool.js
CHANGED
|
@@ -124,12 +124,11 @@ function checkReferences(args, messages, id, recent) {
|
|
|
124
124
|
for (const block of message.content)
|
|
125
125
|
if (block.type === 'tool-call')
|
|
126
126
|
calls.set(block.id, block.name);
|
|
127
|
-
if (message.
|
|
127
|
+
if (message.role !== 'tool' || message.isError || message.source.kind !== 'tool'
|
|
128
|
+
|| message.toolCallId !== message.source.callId
|
|
129
|
+
|| !['codex_web', 'run_code'].includes(calls.get(message.toolCallId) ?? ''))
|
|
128
130
|
continue;
|
|
129
|
-
|
|
130
|
-
if (block.type === 'tool-result' && block.toolCallId === message.source.callId && !block.isError) {
|
|
131
|
-
outputs.push(block.content.filter(part => part.type === 'text').map(part => part.text).join('\n'));
|
|
132
|
-
}
|
|
131
|
+
outputs.push(message.content.filter(block => block.type === 'text').map(block => block.text).join('\n'));
|
|
133
132
|
}
|
|
134
133
|
outputs.push(...recent);
|
|
135
134
|
for (const ref of refs) {
|
package/lib/native-adapter.js
CHANGED
|
@@ -57,7 +57,8 @@ function responsesLiteMode(model, requestedModel = model?.slug) {
|
|
|
57
57
|
if (model !== undefined && !model.useResponsesLite)
|
|
58
58
|
return {};
|
|
59
59
|
// Never downgrade known Lite-only slugs to Standard Responses during catalog outages.
|
|
60
|
-
if (model === undefined && requestedModel !== 'gpt-6-astra'
|
|
60
|
+
if (model === undefined && requestedModel !== 'gpt-6-astra'
|
|
61
|
+
&& requestedModel !== 'gpt-6-sol' && requestedModel !== 'gpt-6.1-sol')
|
|
61
62
|
return {};
|
|
62
63
|
return {
|
|
63
64
|
responsesLite: {
|
package/lib/native-http.js
CHANGED
|
@@ -4,6 +4,7 @@ import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-
|
|
|
4
4
|
import { EMPTY_RESPONSE_CODE, LlmError, ProviderRequestId, attributionHeaders, } from '@deepseek-ai/dsh-llm';
|
|
5
5
|
import { nativeCodexAuthorityHash } from './catalog.js';
|
|
6
6
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
7
|
+
import { retryAfterMs, retryError, remainingRetryDelay, sleepRetry, inheritRetryDeadline } from './retry-after.js';
|
|
7
8
|
import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
|
|
8
9
|
import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, responsesFailure, } from './responses.js';
|
|
9
10
|
import { hasNativeCodexReplayKind } from './replay.js';
|
|
@@ -19,7 +20,7 @@ const INITIAL_CONNECTION_RETRY_DELAY_MS = 5_000;
|
|
|
19
20
|
const MAX_CONNECTION_RETRY_DELAY_MS = 60_000;
|
|
20
21
|
const MAX_ERROR_BODY_BYTES = 64 * 1024;
|
|
21
22
|
function aborted(message = 'native Codex request was aborted') {
|
|
22
|
-
return
|
|
23
|
+
return retryError(message, 'ABORTED');
|
|
23
24
|
}
|
|
24
25
|
function throwIfAborted(signal) {
|
|
25
26
|
if (!signal?.aborted)
|
|
@@ -29,7 +30,7 @@ function throwIfAborted(signal) {
|
|
|
29
30
|
throw aborted();
|
|
30
31
|
}
|
|
31
32
|
function fixedFailure(message, code, options) {
|
|
32
|
-
return
|
|
33
|
+
return retryError(message, code, options);
|
|
33
34
|
}
|
|
34
35
|
function safePositiveInteger(value, fallback, name) {
|
|
35
36
|
const resolved = value ?? fallback;
|
|
@@ -52,24 +53,6 @@ function stablePromptCacheKey(sessionId) {
|
|
|
52
53
|
const hex = bytes.toString('hex');
|
|
53
54
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
54
55
|
}
|
|
55
|
-
function sleep(delayMs, signal) {
|
|
56
|
-
throwIfAborted(signal);
|
|
57
|
-
return new Promise((resolve, reject) => {
|
|
58
|
-
const timer = setTimeout(done, delayMs);
|
|
59
|
-
const abort = () => {
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
signal?.removeEventListener('abort', abort);
|
|
62
|
-
const reason = signal?.reason;
|
|
63
|
-
reject(reason instanceof LlmError
|
|
64
|
-
? reason : aborted('native Codex retry wait was aborted'));
|
|
65
|
-
};
|
|
66
|
-
function done() {
|
|
67
|
-
signal?.removeEventListener('abort', abort);
|
|
68
|
-
resolve();
|
|
69
|
-
}
|
|
70
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
56
|
function attemptWatchdog(parent, requestMs, idleMs) {
|
|
74
57
|
const controller = new AbortController();
|
|
75
58
|
let timeout;
|
|
@@ -122,28 +105,29 @@ async function resolveImage(block, options, signal) {
|
|
|
122
105
|
...stored.detail === undefined ? {} : { detail: stored.detail },
|
|
123
106
|
};
|
|
124
107
|
}
|
|
125
|
-
async function resolveToolResult(block, options, signal) {
|
|
126
|
-
const content = [];
|
|
127
|
-
for (const part of block.content) {
|
|
128
|
-
throwIfAborted(signal);
|
|
129
|
-
if (part.type === 'text')
|
|
130
|
-
content.push(part);
|
|
131
|
-
else if (part.type === 'image')
|
|
132
|
-
content.push(await resolveImage(part, options, signal));
|
|
133
|
-
else {
|
|
134
|
-
throw fixedFailure('native Codex tool output contains an unsupported content block', 'UNSUPPORTED');
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
type: 'tool-result',
|
|
139
|
-
toolCallId: block.toolCallId,
|
|
140
|
-
content,
|
|
141
|
-
...block.isError === undefined ? {} : { isError: block.isError },
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
108
|
async function resolveMessages(generation, options) {
|
|
145
109
|
const messages = [];
|
|
146
110
|
for (const message of generation.messages) {
|
|
111
|
+
if (message.role === 'tool') {
|
|
112
|
+
const content = [];
|
|
113
|
+
for (const block of message.content) {
|
|
114
|
+
throwIfAborted(generation.signal);
|
|
115
|
+
if (block.type === 'text')
|
|
116
|
+
content.push(block);
|
|
117
|
+
else if (block.type === 'image')
|
|
118
|
+
content.push(await resolveImage(block, options, generation.signal));
|
|
119
|
+
else {
|
|
120
|
+
throw fixedFailure('native Codex tool output contains an unsupported content block', 'UNSUPPORTED');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
messages.push({
|
|
124
|
+
role: 'tool',
|
|
125
|
+
content,
|
|
126
|
+
toolCallId: message.toolCallId,
|
|
127
|
+
...message.isError !== undefined ? { isError: message.isError } : {},
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
147
131
|
const content = [];
|
|
148
132
|
const source = message.source;
|
|
149
133
|
const sourceKind = source?.kind;
|
|
@@ -177,12 +161,6 @@ async function resolveMessages(generation, options) {
|
|
|
177
161
|
// A settlement copies the child's assistant output into user-role context.
|
|
178
162
|
// Its tool calls belong to the child and must not become parent wire calls.
|
|
179
163
|
break;
|
|
180
|
-
case 'tool-result':
|
|
181
|
-
if (message.role !== 'user') {
|
|
182
|
-
throw fixedFailure('native Codex tool results require user messages', 'INVALID_ARGS');
|
|
183
|
-
}
|
|
184
|
-
content.push(await resolveToolResult(block, options, generation.signal));
|
|
185
|
-
break;
|
|
186
164
|
default:
|
|
187
165
|
throw fixedFailure('native Codex request contains an unsupported content block', 'UNSUPPORTED');
|
|
188
166
|
}
|
|
@@ -191,6 +169,7 @@ async function resolveMessages(generation, options) {
|
|
|
191
169
|
&& source.replayState !== undefined && hasNativeCodexReplayKind(source.replayState)
|
|
192
170
|
? { provider: source.provider, model: source.model, replayState: source.replayState }
|
|
193
171
|
: undefined;
|
|
172
|
+
// Developer messages are preserved for the Responses API
|
|
194
173
|
messages.push({
|
|
195
174
|
role: message.role,
|
|
196
175
|
content,
|
|
@@ -256,18 +235,8 @@ async function boundedError(response, signal, telemetry) {
|
|
|
256
235
|
return {};
|
|
257
236
|
}
|
|
258
237
|
}
|
|
259
|
-
function
|
|
260
|
-
const
|
|
261
|
-
if (raw === null)
|
|
262
|
-
return undefined;
|
|
263
|
-
const seconds = Number(raw);
|
|
264
|
-
const value = Number.isFinite(seconds)
|
|
265
|
-
? seconds * 1_000
|
|
266
|
-
: Date.parse(raw) - Date.now();
|
|
267
|
-
return Number.isFinite(value) && value > 0 ? Math.min(value, maxDelayMs) : undefined;
|
|
268
|
-
}
|
|
269
|
-
function errorFacts(response, maxDelayMs) {
|
|
270
|
-
const providerRetryAfterMs = retryAfterMs(response, maxDelayMs);
|
|
238
|
+
function errorFacts(response) {
|
|
239
|
+
const providerRetryAfterMs = retryAfterMs(response.headers.get('retry-after'));
|
|
271
240
|
const requestId = response.headers.get('x-request-id');
|
|
272
241
|
return {
|
|
273
242
|
status: response.status,
|
|
@@ -277,37 +246,43 @@ function errorFacts(response, maxDelayMs) {
|
|
|
277
246
|
: { requestId: ProviderRequestId(requestId) },
|
|
278
247
|
};
|
|
279
248
|
}
|
|
280
|
-
async function httpFailure(response,
|
|
249
|
+
async function httpFailure(response, signal, telemetry) {
|
|
250
|
+
const facts = errorFacts(response);
|
|
251
|
+
const advice = retryError('native Codex HTTP retry advice', 'SERVER', facts);
|
|
252
|
+
const fail = (message, code, options) => inheritRetryDeadline(fixedFailure(message, code, options), advice);
|
|
281
253
|
const detail = await boundedError(response, signal, telemetry);
|
|
282
|
-
const facts = errorFacts(response, maxDelayMs);
|
|
283
254
|
const classified = responsesFailure(detail.code, detail.message);
|
|
284
255
|
if (response.status === 401 || response.status === 403) {
|
|
285
|
-
return
|
|
256
|
+
return fail('native Codex rejected the configured credential', 'AUTH', facts);
|
|
286
257
|
}
|
|
287
258
|
if (classified.code !== 'SERVER') {
|
|
288
259
|
const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
|
|
289
|
-
return
|
|
260
|
+
return fail(classified.message, classified.code, {
|
|
290
261
|
...facts,
|
|
291
|
-
...(delay === undefined ? {} : { providerRetryAfterMs:
|
|
262
|
+
...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
|
|
292
263
|
});
|
|
293
264
|
}
|
|
294
265
|
if (response.status === 429) {
|
|
295
|
-
return
|
|
266
|
+
return fail('native Codex request was rate limited', 'RATE_LIMIT', facts);
|
|
296
267
|
}
|
|
297
268
|
if (response.status === 408 || response.status === 504) {
|
|
298
|
-
return
|
|
269
|
+
return fail('native Codex request timed out', 'TIMEOUT', facts);
|
|
299
270
|
}
|
|
300
271
|
if (response.status >= 500) {
|
|
301
|
-
return
|
|
272
|
+
return fail('native Codex server request failed', 'SERVER', facts);
|
|
302
273
|
}
|
|
303
|
-
return
|
|
274
|
+
return fail('native Codex request was rejected', 'INVALID_REQUEST', facts);
|
|
304
275
|
}
|
|
305
276
|
function mappedFailure(error, watchdog, parent) {
|
|
306
277
|
if (parent?.aborted) {
|
|
307
278
|
return parent.reason instanceof LlmError ? parent.reason : aborted();
|
|
308
279
|
}
|
|
309
280
|
if (watchdog.timedOut())
|
|
310
|
-
return fixedFailure('native Codex request timed out', 'TIMEOUT'
|
|
281
|
+
return fixedFailure('native Codex request timed out', 'TIMEOUT', {
|
|
282
|
+
cause: error,
|
|
283
|
+
...(error instanceof LlmError && error.failure.providerRetryAfterMs !== undefined
|
|
284
|
+
? { providerRetryAfterMs: error.failure.providerRetryAfterMs } : {}),
|
|
285
|
+
});
|
|
311
286
|
if (error instanceof LlmError)
|
|
312
287
|
return error;
|
|
313
288
|
return fixedFailure('native Codex transport failed', 'TRANSPORT', { cause: error });
|
|
@@ -335,8 +310,7 @@ function retryable(error) {
|
|
|
335
310
|
}
|
|
336
311
|
function failedStepRetry(error) {
|
|
337
312
|
const failure = error.failure;
|
|
338
|
-
const providerRetryAfterMs = failure.providerRetryAfterMs
|
|
339
|
-
? undefined : Math.min(failure.providerRetryAfterMs, DEFAULT_MAX_RETRY_DELAY_MS);
|
|
313
|
+
const providerRetryAfterMs = failure.providerRetryAfterMs;
|
|
340
314
|
return fixedFailure(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
|
|
341
315
|
cause: error,
|
|
342
316
|
...(failure.status === undefined ? {} : { status: failure.status }),
|
|
@@ -366,21 +340,21 @@ export class NativeCodexHttpTransport {
|
|
|
366
340
|
}
|
|
367
341
|
retryDelay(retry, providerDelay) {
|
|
368
342
|
if (providerDelay !== undefined)
|
|
369
|
-
return
|
|
343
|
+
return providerDelay;
|
|
370
344
|
const exponential = Math.min(this.initialDelayMs * (2 ** retry), this.maxDelayMs);
|
|
371
345
|
const random = this.options.random?.() ?? Math.random();
|
|
372
346
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
373
347
|
return Math.max(1, Math.round(exponential * jitter));
|
|
374
348
|
}
|
|
375
349
|
async wait(retry, error, signal, telemetry) {
|
|
376
|
-
const delay = this.retryDelay(retry, error
|
|
350
|
+
const delay = this.retryDelay(retry, remainingRetryDelay(error));
|
|
377
351
|
telemetry.retry(delay);
|
|
378
|
-
await (this.options.sleep ??
|
|
352
|
+
await (this.options.sleep ?? sleepRetry)(delay, signal);
|
|
379
353
|
}
|
|
380
354
|
async waitForConnection(delayMs, signal, telemetry) {
|
|
381
355
|
telemetry.retry(delayMs, 'connection');
|
|
382
356
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
383
|
-
await (this.options.sleep ??
|
|
357
|
+
await (this.options.sleep ?? sleepRetry)(delayMs, signal);
|
|
384
358
|
}
|
|
385
359
|
endpointUrl() { return this.endpoint; }
|
|
386
360
|
async prepare(generation, mode = {}) {
|
|
@@ -531,7 +505,7 @@ export class NativeCodexHttpTransport {
|
|
|
531
505
|
telemetry.retry(0);
|
|
532
506
|
continue;
|
|
533
507
|
}
|
|
534
|
-
throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response
|
|
508
|
+
throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response));
|
|
535
509
|
}
|
|
536
510
|
catch (error) {
|
|
537
511
|
const connection = connectionFailure(error, watchdog, generation.signal);
|
|
@@ -546,7 +520,7 @@ export class NativeCodexHttpTransport {
|
|
|
546
520
|
}
|
|
547
521
|
}
|
|
548
522
|
if (!response.ok) {
|
|
549
|
-
let failure = await httpFailure(response,
|
|
523
|
+
let failure = await httpFailure(response, watchdog.signal, telemetry);
|
|
550
524
|
if (generation.signal?.aborted || watchdog.timedOut()) {
|
|
551
525
|
failure = mappedFailure(failure, watchdog, generation.signal);
|
|
552
526
|
}
|
|
@@ -3,11 +3,12 @@ import { LlmError, ProviderRequestId } from '@deepseek-ai/dsh-llm';
|
|
|
3
3
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
4
4
|
import { getProxyForUrl } from 'proxy-from-env';
|
|
5
5
|
import { nativeCodexEndpoint } from './endpoint.js';
|
|
6
|
+
import { retryAfterMs, retryError } from './retry-after.js';
|
|
6
7
|
import { NATIVE_CODEX_CONNECTION_FAILED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
|
|
7
8
|
import WebSocket from 'ws';
|
|
8
9
|
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
|
|
9
10
|
function failure(message, code, cause) {
|
|
10
|
-
return
|
|
11
|
+
return retryError(message, code, cause === undefined ? undefined : { cause });
|
|
11
12
|
}
|
|
12
13
|
function abortFailure(signal) {
|
|
13
14
|
return signal?.reason instanceof LlmError
|
|
@@ -17,9 +18,7 @@ function abortFailure(signal) {
|
|
|
17
18
|
function handshakeFacts(status, headers) {
|
|
18
19
|
const first = (value) => Array.isArray(value) ? value[0] : value;
|
|
19
20
|
const requestId = first(headers['x-request-id']);
|
|
20
|
-
const
|
|
21
|
-
const providerRetryAfterMs = Number.isFinite(retryAfter) && retryAfter > 0
|
|
22
|
-
? Math.min(retryAfter * 1_000, 120_000) : undefined;
|
|
21
|
+
const providerRetryAfterMs = retryAfterMs(first(headers['retry-after']));
|
|
23
22
|
return {
|
|
24
23
|
...(status === undefined ? {} : { status }),
|
|
25
24
|
...(providerRetryAfterMs === undefined ? {} : { providerRetryAfterMs }),
|
|
@@ -202,7 +201,7 @@ export class NodeNativeCodexWebSocketFactory {
|
|
|
202
201
|
? 'WS_RETRYABLE'
|
|
203
202
|
: response.statusCode !== undefined && response.statusCode >= 400
|
|
204
203
|
? 'INVALID_REQUEST' : 'WS_HANDSHAKE';
|
|
205
|
-
reject(
|
|
204
|
+
reject(retryError(`native Codex WebSocket handshake failed (HTTP ${response.statusCode ?? 0})`, code, handshakeFacts(response.statusCode, response.headers)));
|
|
206
205
|
});
|
|
207
206
|
socket.on('open', () => {
|
|
208
207
|
options.signal?.removeEventListener('abort', abort);
|