@pure01fx/dsh-openai-codex-auth 0.12.0 → 0.13.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 +24 -0
- package/CODEX-COMPATIBILITY.md +39 -0
- package/README.md +2 -2
- package/lib/catalog.js +2 -1
- package/lib/index.d.ts +11 -0
- package/lib/index.js +57 -2
- package/lib/native-adapter.d.ts +7 -0
- package/lib/native-adapter.js +25 -4
- package/lib/native-http.js +28 -50
- package/lib/native-websocket-socket.js +4 -5
- package/lib/native-websocket.js +68 -49
- package/lib/replay.d.ts +3 -0
- package/lib/replay.js +8 -2
- package/lib/responses.d.ts +8 -2
- package/lib/responses.js +43 -20
- 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/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +34 -30
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.13.1
|
|
6
|
+
|
|
7
|
+
- Serves a concurrent or pool-saturated request over HTTP/SSE instead of failing the turn.
|
|
8
|
+
- Moves a session to HTTP/SSE after a 400-class WebSocket rejection, so one rejected request can no longer fail every later turn in that session.
|
|
9
|
+
- Logs the provider's own rejection code, status and message instead of discarding them.
|
|
10
|
+
- Keeps a Fast alias resolvable when the catalog could not be read and retries one empty catalog read, so a transient catalog outage no longer fails the whole provider group in the model picker; a catalog that answers without Fast still refuses it.
|
|
11
|
+
- Memoizes the managed credential per credential-file signature with one shared resolution, so the model picker's per-model resolutions no longer take a dozen concurrent writer-lock acquisitions on the credential file (measured 2.1 s for 14 concurrent readers against a 2 s lock deadline before this change).
|
|
12
|
+
- Adds `locale/en.json` and `locale/zh.json` so the DSH 0.2.0 plugin panel shows a display name and description instead of the package id.
|
|
13
|
+
|
|
14
|
+
## 0.13.0
|
|
15
|
+
|
|
16
|
+
- 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.
|
|
17
|
+
- Merges GPT-6.1 catalog routing, shared Retry-After handling, assistant phases and deterministic developer instructions from the latest source.
|
|
18
|
+
- 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.
|
|
19
|
+
|
|
5
20
|
## 0.12.0
|
|
6
21
|
|
|
7
22
|
### Migration
|
|
@@ -22,6 +37,15 @@
|
|
|
22
37
|
- Developer messages: preserved in Responses API requests
|
|
23
38
|
- Cloud context: requires toolCallId === source.callId and !isError
|
|
24
39
|
|
|
40
|
+
- 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.
|
|
41
|
+
|
|
42
|
+
- Preserves commentary, partial_answer, and final_answer phases through response events, durable replay, and WebSocket continuation matching; legacy phase-less replay remains readable.
|
|
43
|
+
|
|
44
|
+
- 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.
|
|
45
|
+
|
|
46
|
+
- 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.
|
|
47
|
+
|
|
48
|
+
|
|
25
49
|
## 0.11.2
|
|
26
50
|
|
|
27
51
|
- 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.
|
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
|
|
package/lib/catalog.js
CHANGED
|
@@ -265,7 +265,8 @@ export class NativeCodexCatalog {
|
|
|
265
265
|
if (error instanceof LlmError && error.code === 'ABORTED')
|
|
266
266
|
throw error;
|
|
267
267
|
if (!(error instanceof LlmError) || error.code !== 'MISSING_CREDENTIAL') {
|
|
268
|
-
|
|
268
|
+
const code = error instanceof LlmError ? error.code : 'UNKNOWN';
|
|
269
|
+
this.options.warn?.(`native Codex credential is unavailable (${code}); model metadata is advisory`);
|
|
269
270
|
}
|
|
270
271
|
return { models: [] };
|
|
271
272
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -132,6 +132,8 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
132
132
|
private usageAccountId;
|
|
133
133
|
private responseUsage;
|
|
134
134
|
private credentialAccountId;
|
|
135
|
+
private credentialMemo;
|
|
136
|
+
private credentialResolution;
|
|
135
137
|
private usageError;
|
|
136
138
|
private usageRefresh;
|
|
137
139
|
private usageGeneration;
|
|
@@ -163,6 +165,15 @@ export declare class OpenAICodexAuth extends Service {
|
|
|
163
165
|
private commitCredential;
|
|
164
166
|
/** Return the current managed bearer token, refreshing and migrating it when needed. */
|
|
165
167
|
bearerToken(signal?: AbortSignal): Promise<string | undefined>;
|
|
168
|
+
/**
|
|
169
|
+
* Resolve the managed credential for one native call, reusing a lock-free memo of
|
|
170
|
+
* the file it was read from. The model picker resolves one credential per listed
|
|
171
|
+
* model, so one shared resolution replaces a burst of lock acquisitions on the
|
|
172
|
+
* same credential file.
|
|
173
|
+
*/
|
|
174
|
+
private cachedManagedCredential;
|
|
175
|
+
/** Resolve once for every concurrent caller; never memo a credential the file no longer matches. */
|
|
176
|
+
private resolveManagedCredential;
|
|
166
177
|
private managedCredential;
|
|
167
178
|
private externalNativeCredential;
|
|
168
179
|
private resolveNativeCredential;
|
package/lib/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { withFileLock, writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
|
6
6
|
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths';
|
|
7
7
|
import { createServer } from 'node:http';
|
|
8
8
|
import { createHash, randomBytes } from 'node:crypto';
|
|
9
|
-
import { readFile, unlink } from 'node:fs/promises';
|
|
9
|
+
import { readFile, stat, unlink } from 'node:fs/promises';
|
|
10
10
|
import { join, resolve } from 'node:path';
|
|
11
11
|
import { INVALID_CREDENTIAL_CODE, LlmError } from '@deepseek-ai/dsh-llm';
|
|
12
12
|
import { CODEX_PROVIDER, NATIVE_CODEX_PROVIDER, NativeCodexAdapter, } from './native-adapter.js';
|
|
@@ -256,6 +256,26 @@ async function readCredentialDocument(filename) {
|
|
|
256
256
|
throw error;
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Cheap lock-free identity of the credential file. Every writer replaces it
|
|
261
|
+
* atomically, so both parts change and a cached read cannot outlive its file.
|
|
262
|
+
*/
|
|
263
|
+
async function credentialFileSignature(filename) {
|
|
264
|
+
try {
|
|
265
|
+
const info = await stat(filename);
|
|
266
|
+
// An atomic replacement renames a fresh file, so the inode changes with it.
|
|
267
|
+
return `${info.ino}:${info.mtimeMs}:${info.size}`;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error.code === 'ENOENT')
|
|
271
|
+
return 'missing';
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/** Whether a managed credential still avoids the pre-refresh window `bearerToken` enforces. */
|
|
276
|
+
function managedCredentialUsable(credential, now = Date.now()) {
|
|
277
|
+
return credential.expires > now + TOKEN_REFRESH_PREEMPT_MS;
|
|
278
|
+
}
|
|
259
279
|
function currentCredential(document) {
|
|
260
280
|
if (document?.currentAccountId === null || document === undefined)
|
|
261
281
|
return undefined;
|
|
@@ -722,6 +742,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
722
742
|
usageAccountId;
|
|
723
743
|
responseUsage;
|
|
724
744
|
credentialAccountId;
|
|
745
|
+
credentialMemo;
|
|
746
|
+
credentialResolution;
|
|
725
747
|
usageError;
|
|
726
748
|
usageRefresh;
|
|
727
749
|
usageGeneration = 0;
|
|
@@ -1036,6 +1058,8 @@ export class OpenAICodexAuth extends Service {
|
|
|
1036
1058
|
throw failure;
|
|
1037
1059
|
}
|
|
1038
1060
|
async commitDocument(document, signal) {
|
|
1061
|
+
// A write may publish or replace the managed account, so no read memo survives it.
|
|
1062
|
+
this.credentialMemo = undefined;
|
|
1039
1063
|
const nextToken = currentCredential(document)?.access;
|
|
1040
1064
|
const previous = await this.ctx.credentials.resolve(TOKEN_REF);
|
|
1041
1065
|
throwIfCancelled(signal);
|
|
@@ -1143,6 +1167,37 @@ export class OpenAICodexAuth extends Service {
|
|
|
1143
1167
|
return access;
|
|
1144
1168
|
}
|
|
1145
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Resolve the managed credential for one native call, reusing a lock-free memo of
|
|
1172
|
+
* the file it was read from. The model picker resolves one credential per listed
|
|
1173
|
+
* model, so one shared resolution replaces a burst of lock acquisitions on the
|
|
1174
|
+
* same credential file.
|
|
1175
|
+
*/
|
|
1176
|
+
async cachedManagedCredential(signal) {
|
|
1177
|
+
const signature = await credentialFileSignature(this.filename);
|
|
1178
|
+
const memo = this.credentialMemo;
|
|
1179
|
+
if (memo !== undefined && memo.signature === signature && managedCredentialUsable(memo.credential)) {
|
|
1180
|
+
this.setCredentialAccount(memo.credential.accountId);
|
|
1181
|
+
return memo.credential;
|
|
1182
|
+
}
|
|
1183
|
+
const pending = this.credentialResolution ??= this.resolveManagedCredential(signature);
|
|
1184
|
+
return await waitForSignal(pending, signal);
|
|
1185
|
+
}
|
|
1186
|
+
/** Resolve once for every concurrent caller; never memo a credential the file no longer matches. */
|
|
1187
|
+
resolveManagedCredential(signature) {
|
|
1188
|
+
const resolution = this.managedCredential().then(async (credential) => {
|
|
1189
|
+
if (credential !== undefined && await credentialFileSignature(this.filename) === signature) {
|
|
1190
|
+
this.credentialMemo = { signature, credential };
|
|
1191
|
+
}
|
|
1192
|
+
return credential;
|
|
1193
|
+
});
|
|
1194
|
+
const settled = () => {
|
|
1195
|
+
if (this.credentialResolution === resolution)
|
|
1196
|
+
this.credentialResolution = undefined;
|
|
1197
|
+
};
|
|
1198
|
+
void resolution.then(settled, settled);
|
|
1199
|
+
return resolution;
|
|
1200
|
+
}
|
|
1146
1201
|
async managedCredential(signal) {
|
|
1147
1202
|
while (true) {
|
|
1148
1203
|
const access = await this.bearerToken(signal);
|
|
@@ -1172,7 +1227,7 @@ export class OpenAICodexAuth extends Service {
|
|
|
1172
1227
|
async resolveNativeCredential(signal) {
|
|
1173
1228
|
try {
|
|
1174
1229
|
throwIfCancelled(signal);
|
|
1175
|
-
const managed = await this.
|
|
1230
|
+
const managed = await this.cachedManagedCredential(signal);
|
|
1176
1231
|
if (managed !== undefined) {
|
|
1177
1232
|
throwIfCancelled(signal);
|
|
1178
1233
|
return { accessToken: managed.access, accountId: managed.accountId };
|
package/lib/native-adapter.d.ts
CHANGED
|
@@ -44,6 +44,13 @@ export declare class NativeCodexAdapter extends LlmAdapter {
|
|
|
44
44
|
private assertNotAborted;
|
|
45
45
|
providerInfo(provider: string): LlmProviderInfo;
|
|
46
46
|
providerRetryPolicy(provider: string): ResolvedRetryPolicy;
|
|
47
|
+
/**
|
|
48
|
+
* Read model metadata, retrying once when a fetch returned nothing. The catalog
|
|
49
|
+
* caches nothing on failure, so a momentary outage otherwise empties every model
|
|
50
|
+
* list it touches; metadata is advisory and one extra request is cheaper than a
|
|
51
|
+
* provider that disappears from the picker until the next attempt.
|
|
52
|
+
*/
|
|
53
|
+
private catalogModels;
|
|
47
54
|
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
48
55
|
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<LlmResolvedModelInfo>;
|
|
49
56
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
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: {
|
|
@@ -188,9 +189,22 @@ export class NativeCodexAdapter extends LlmAdapter {
|
|
|
188
189
|
this.assertProvider(provider);
|
|
189
190
|
return NATIVE_RETRY_POLICY;
|
|
190
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Read model metadata, retrying once when a fetch returned nothing. The catalog
|
|
194
|
+
* caches nothing on failure, so a momentary outage otherwise empties every model
|
|
195
|
+
* list it touches; metadata is advisory and one extra request is cheaper than a
|
|
196
|
+
* provider that disappears from the picker until the next attempt.
|
|
197
|
+
*/
|
|
198
|
+
async catalogModels(signal) {
|
|
199
|
+
const first = await this.catalog?.list(signal) ?? [];
|
|
200
|
+
if (first.length > 0)
|
|
201
|
+
return first;
|
|
202
|
+
this.assertNotAborted(signal);
|
|
203
|
+
return await this.catalog?.list(signal) ?? [];
|
|
204
|
+
}
|
|
191
205
|
async listModels(provider) {
|
|
192
206
|
this.assertProvider(provider);
|
|
193
|
-
const models = await this.
|
|
207
|
+
const models = await this.catalogModels();
|
|
194
208
|
const aliases = fastRoutes(models);
|
|
195
209
|
return models
|
|
196
210
|
.filter(model => model.visibility === 'list')
|
|
@@ -206,7 +220,7 @@ export class NativeCodexAdapter extends LlmAdapter {
|
|
|
206
220
|
async resolveModel(provider, model, signal) {
|
|
207
221
|
this.assertNotAborted(signal);
|
|
208
222
|
this.assertProvider(provider);
|
|
209
|
-
const models = await this.
|
|
223
|
+
const models = await this.catalogModels(signal);
|
|
210
224
|
this.assertNotAborted(signal);
|
|
211
225
|
const exact = models.find(candidate => candidate.slug === model);
|
|
212
226
|
if (exact !== undefined)
|
|
@@ -215,7 +229,14 @@ export class NativeCodexAdapter extends LlmAdapter {
|
|
|
215
229
|
if (fast !== undefined)
|
|
216
230
|
return resolvedModel(provider, fast, true);
|
|
217
231
|
if (model.endsWith(CODEX_FAST_ALIAS_SUFFIX)) {
|
|
218
|
-
|
|
232
|
+
const base = model.slice(0, -CODEX_FAST_ALIAS_SUFFIX.length);
|
|
233
|
+
// Only a catalog that knows the base model can answer this negatively.
|
|
234
|
+
// Without any entry for it the metadata is missing rather than negative, and
|
|
235
|
+
// a caller that resolves every listed model (the picker) must not lose the
|
|
236
|
+
// whole provider group to an advisory fetch.
|
|
237
|
+
if (models.some(candidate => candidate.slug === base)) {
|
|
238
|
+
throw new LlmError('native Codex Fast is not advertised for the selected model', 'FAST_UNSUPPORTED');
|
|
239
|
+
}
|
|
219
240
|
}
|
|
220
241
|
return { provider, id: model, name: model };
|
|
221
242
|
}
|
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;
|
|
@@ -252,18 +235,8 @@ async function boundedError(response, signal, telemetry) {
|
|
|
252
235
|
return {};
|
|
253
236
|
}
|
|
254
237
|
}
|
|
255
|
-
function
|
|
256
|
-
const
|
|
257
|
-
if (raw === null)
|
|
258
|
-
return undefined;
|
|
259
|
-
const seconds = Number(raw);
|
|
260
|
-
const value = Number.isFinite(seconds)
|
|
261
|
-
? seconds * 1_000
|
|
262
|
-
: Date.parse(raw) - Date.now();
|
|
263
|
-
return Number.isFinite(value) && value > 0 ? Math.min(value, maxDelayMs) : undefined;
|
|
264
|
-
}
|
|
265
|
-
function errorFacts(response, maxDelayMs) {
|
|
266
|
-
const providerRetryAfterMs = retryAfterMs(response, maxDelayMs);
|
|
238
|
+
function errorFacts(response) {
|
|
239
|
+
const providerRetryAfterMs = retryAfterMs(response.headers.get('retry-after'));
|
|
267
240
|
const requestId = response.headers.get('x-request-id');
|
|
268
241
|
return {
|
|
269
242
|
status: response.status,
|
|
@@ -273,37 +246,43 @@ function errorFacts(response, maxDelayMs) {
|
|
|
273
246
|
: { requestId: ProviderRequestId(requestId) },
|
|
274
247
|
};
|
|
275
248
|
}
|
|
276
|
-
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);
|
|
277
253
|
const detail = await boundedError(response, signal, telemetry);
|
|
278
|
-
const facts = errorFacts(response, maxDelayMs);
|
|
279
254
|
const classified = responsesFailure(detail.code, detail.message);
|
|
280
255
|
if (response.status === 401 || response.status === 403) {
|
|
281
|
-
return
|
|
256
|
+
return fail('native Codex rejected the configured credential', 'AUTH', facts);
|
|
282
257
|
}
|
|
283
258
|
if (classified.code !== 'SERVER') {
|
|
284
259
|
const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
|
|
285
|
-
return
|
|
260
|
+
return fail(classified.message, classified.code, {
|
|
286
261
|
...facts,
|
|
287
|
-
...(delay === undefined ? {} : { providerRetryAfterMs:
|
|
262
|
+
...(delay === undefined ? {} : { providerRetryAfterMs: delay }),
|
|
288
263
|
});
|
|
289
264
|
}
|
|
290
265
|
if (response.status === 429) {
|
|
291
|
-
return
|
|
266
|
+
return fail('native Codex request was rate limited', 'RATE_LIMIT', facts);
|
|
292
267
|
}
|
|
293
268
|
if (response.status === 408 || response.status === 504) {
|
|
294
|
-
return
|
|
269
|
+
return fail('native Codex request timed out', 'TIMEOUT', facts);
|
|
295
270
|
}
|
|
296
271
|
if (response.status >= 500) {
|
|
297
|
-
return
|
|
272
|
+
return fail('native Codex server request failed', 'SERVER', facts);
|
|
298
273
|
}
|
|
299
|
-
return
|
|
274
|
+
return fail('native Codex request was rejected', 'INVALID_REQUEST', facts);
|
|
300
275
|
}
|
|
301
276
|
function mappedFailure(error, watchdog, parent) {
|
|
302
277
|
if (parent?.aborted) {
|
|
303
278
|
return parent.reason instanceof LlmError ? parent.reason : aborted();
|
|
304
279
|
}
|
|
305
280
|
if (watchdog.timedOut())
|
|
306
|
-
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
|
+
});
|
|
307
286
|
if (error instanceof LlmError)
|
|
308
287
|
return error;
|
|
309
288
|
return fixedFailure('native Codex transport failed', 'TRANSPORT', { cause: error });
|
|
@@ -331,8 +310,7 @@ function retryable(error) {
|
|
|
331
310
|
}
|
|
332
311
|
function failedStepRetry(error) {
|
|
333
312
|
const failure = error.failure;
|
|
334
|
-
const providerRetryAfterMs = failure.providerRetryAfterMs
|
|
335
|
-
? undefined : Math.min(failure.providerRetryAfterMs, DEFAULT_MAX_RETRY_DELAY_MS);
|
|
313
|
+
const providerRetryAfterMs = failure.providerRetryAfterMs;
|
|
336
314
|
return fixedFailure(`native Codex response stream was interrupted: ${error.message}`, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, {
|
|
337
315
|
cause: error,
|
|
338
316
|
...(failure.status === undefined ? {} : { status: failure.status }),
|
|
@@ -362,21 +340,21 @@ export class NativeCodexHttpTransport {
|
|
|
362
340
|
}
|
|
363
341
|
retryDelay(retry, providerDelay) {
|
|
364
342
|
if (providerDelay !== undefined)
|
|
365
|
-
return
|
|
343
|
+
return providerDelay;
|
|
366
344
|
const exponential = Math.min(this.initialDelayMs * (2 ** retry), this.maxDelayMs);
|
|
367
345
|
const random = this.options.random?.() ?? Math.random();
|
|
368
346
|
const jitter = 0.9 + Math.max(0, Math.min(1, random)) * 0.2;
|
|
369
347
|
return Math.max(1, Math.round(exponential * jitter));
|
|
370
348
|
}
|
|
371
349
|
async wait(retry, error, signal, telemetry) {
|
|
372
|
-
const delay = this.retryDelay(retry, error
|
|
350
|
+
const delay = this.retryDelay(retry, remainingRetryDelay(error));
|
|
373
351
|
telemetry.retry(delay);
|
|
374
|
-
await (this.options.sleep ??
|
|
352
|
+
await (this.options.sleep ?? sleepRetry)(delay, signal);
|
|
375
353
|
}
|
|
376
354
|
async waitForConnection(delayMs, signal, telemetry) {
|
|
377
355
|
telemetry.retry(delayMs, 'connection');
|
|
378
356
|
this.options.warn?.(`native Codex network is unavailable; reconnecting in ${delayMs}ms`);
|
|
379
|
-
await (this.options.sleep ??
|
|
357
|
+
await (this.options.sleep ?? sleepRetry)(delayMs, signal);
|
|
380
358
|
}
|
|
381
359
|
endpointUrl() { return this.endpoint; }
|
|
382
360
|
async prepare(generation, mode = {}) {
|
|
@@ -527,7 +505,7 @@ export class NativeCodexHttpTransport {
|
|
|
527
505
|
telemetry.retry(0);
|
|
528
506
|
continue;
|
|
529
507
|
}
|
|
530
|
-
throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response
|
|
508
|
+
throw fixedFailure('native Codex rejected the configured credential', 'AUTH', errorFacts(response));
|
|
531
509
|
}
|
|
532
510
|
catch (error) {
|
|
533
511
|
const connection = connectionFailure(error, watchdog, generation.signal);
|
|
@@ -542,7 +520,7 @@ export class NativeCodexHttpTransport {
|
|
|
542
520
|
}
|
|
543
521
|
}
|
|
544
522
|
if (!response.ok) {
|
|
545
|
-
let failure = await httpFailure(response,
|
|
523
|
+
let failure = await httpFailure(response, watchdog.signal, telemetry);
|
|
546
524
|
if (generation.signal?.aborted || watchdog.timedOut()) {
|
|
547
525
|
failure = mappedFailure(failure, watchdog, generation.signal);
|
|
548
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);
|