@pure01fx/dsh-openai-codex-auth 0.11.1 → 0.12.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 CHANGED
@@ -2,6 +2,32 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.12.0
6
+
7
+ ### Migration
8
+
9
+ - Migrated to DSH 0.1.7-rc.2 SDK
10
+ - Updated tool message format: tool results are now role:'tool' with top-level toolCallId/isError
11
+ - Migrated shell.run to shell.execute with result() method
12
+ - Updated peer dependencies to 0.1.7-rc.2
13
+ - Updated cordis to ~4.0.4
14
+ - Preserved developer messages in HTTP/WS wire requests
15
+ - Restored successful-result-only matched provenance in cloud context
16
+ - Added mismatch/failed-result regressions in cloud context tests
17
+
18
+ ### Key Changes
19
+
20
+ - Tool messages: role:'tool' with top-level toolCallId/isError (no longer tool-result content blocks)
21
+ - Shell: execute() then .result() instead of run()
22
+ - Developer messages: preserved in Responses API requests
23
+ - Cloud context: requires toolCallId === source.callId and !isError
24
+
25
+ ## 0.11.2
26
+
27
+ - 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.
28
+ - Classifies `slow_down` as a rate limit with bounded provider wait hints, recognizes credit/organization/project quota exhaustion, and distinguishes `INVALID_PROMPT` across HTTP, SSE, and WebSocket errors without retrying permanent failures or switching transports.
29
+ - Preserves validated image request IDs and HTTP status on rejected responses and malformed JSON/UTF-8, without exposing credential echoes or retrying ambiguous image POSTs.
30
+
5
31
  ## 0.11.1
6
32
 
7
33
  - Adds live network status and process-wide request, retry, and received-payload statistics to the quota panel, distinguishing network activity from model output.
@@ -125,4 +151,4 @@
125
151
  - Rejects redirects for credential-bearing OAuth, Catalog, Responses, and subscription-usage requests.
126
152
  - 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.
127
153
  - Pins Host/runtime contracts to DSH rc.6 and includes synchronized JavaScript/declaration artifacts plus the complete native transport modules.
128
- - Keeps the package name `@pure01fx/dsh-openai-codex-auth`; a rename is intentionally deferred.
154
+ - Keeps the package name `@pure01fx/dsh-openai-codex-auth`; a rename is intentionally deferred.
@@ -1,5 +1,59 @@
1
1
  # Codex 云端能力兼容性跟踪
2
2
 
3
+ ## 已实现:Sol 与错误协议增量适配(0.11.2)
4
+
5
+ 参考上游 `c44deff7b1083e9660ac55d02122481f1cdf139b`,在既有 0.11.1 工作树上完成以下必要改动。此节取代下方历史审查中相应的“待适配”结论;不代表完整上游运行时对齐。
6
+
7
+ - 模型目录仍动态获取,查询 `client_version` 升至 `0.155.0`,满足上游 Sol 条目的最低版本。仅为缺失目录 metadata 的精确 `gpt-6-sol` 增加 Lite 路由后备;正常目录的 `use_responses_lite` 仍是依据,Fast 仍须目录授权,未知模型不按名称前缀猜测协议。
8
+ - HTTP 错误正文、SSE `response.failed` 与 WS `error` / `response.failed` 共用安全分类。`slow_down` 按限流处理并保留有界等待提示;新增三类 credit/organization/project 额度错误映射宿主 `QUOTA`;`invalid_prompt` 映射独立的 `INVALID_PROMPT`。这些失败不会在传输内重试、重连或回退 HTTP;原有 WS error 包装的限流代码 `RATE_LIMITED` 保持兼容。
9
+ - 图片生成/编辑 HTTP 失败及 JSON/UTF-8 解码失败,通过正式 LlmError failure 元数据保留状态码和校验后的 requestId。图片专用响应头优先;拒绝非法 ID 与 token/account 回显,不增加图片 POST 重试。
10
+ - 新增来自上游的精简 Sol fixture(含全部六档 reasoning effort),覆盖动态目录、最低版本过滤的模拟服务、metadata 优先、缺目录后备、Fast 权限;HTTP/SSE/WS 回归覆盖上述错误的终止行为和隐私边界。模拟版本过滤验证客户端请求条件,不证明真实账号一定获得 Sol。
11
+
12
+ 验证:`pnpm build`、`pnpm test`(26 文件 / 467 项)、`pnpm typecheck`、`git diff --check` 全部通过;生成产物与新鲜编译一致。已更新相关 `lib/` 产物和 CHANGELOG,保留全部已有工作树改动。没有运行真实账号推理、查询线上目录或发布/替换常驻 GUI 插件;当前进程是否使用新构建未验证。Provider/cloud 原始跟踪提交仍分别保持 `ddf04ad` / `b348fc26`,为集合 0.3.7 准备插件版本 0.11.2;尚未发布。
13
+
14
+ ## 上游增量审查:064e701b(插件 0.11.1 工作树)
15
+
16
+ 本次 `../codex` 从 `b348fc26674189f758d5941cdab3f78f258b2aa7` fast-forward 到 `064e701b0fa4f5c0dcaf195a565d088f99bb6af8`,新增 596 个提交;更新后 HEAD 与本次 fetch 的 origin/main 一致,工作树干净。插件以当前已有未提交改动的 0.11.1 工作树为审查对象(Git HEAD 为 `188abd13430b3fff7782c1ee87e5809c4a2e8e96`),不是仅审查已提交版本。
17
+
18
+ 结论:建议做针对性的兼容适配;目前证据不支持“必须整体重写”或“已完整对齐最新上游”。本次只更新上游并记录审查,没有修改插件运行时代码、跟踪常量或发布版本。
19
+
20
+ ### 优先适配:失败响应分类
21
+
22
+ 上游 `codex-rs/codex-api/src/sse/responses.rs` 的变化直接影响现有推理路径:
23
+
24
+ - `slow_down` 现在与 `rate_limit_exceeded` 一样表示限流,并解析服务端建议等待时间。插件 `src/responses.ts:333` 的 `responsesFailure` 仅对后者返回 `RATE_LIMIT`,前者落入 `SERVER`。
25
+ - `credit_balance_exhausted`、`organization_spend_limit_exceeded`、`project_spend_limit_exceeded` 现在明确归为额度耗尽。插件未按这些 code 分类;若 message 未命中宿主的额度文本识别,会落入 `SERVER`,影响不可重试额度错误的判断。
26
+ - 已用现有 `lib/responses.js` 直接调用复现:上述四个 code 搭配中性 message 均返回 `SERVER`,对照 `rate_limit_exceeded` 返回 `RATE_LIMIT`。这是本地确定性复现,不是线上故障记录。
27
+ - `bio_policy` 在上游已独立分类;插件仍返回不可重试的 `INVALID_REQUEST`。这是诊断粒度差异,优先级低于限流和额度分类。
28
+
29
+ 建议首个适配单元覆盖错误分类、等待时间,以及 HTTP/SSE、WebSocket 两条传输中的终止/重试语义;仅修改版本号不能解决差异。
30
+
31
+ ### 可按需要扩展的能力
32
+
33
+ - **WebSocket 握手稳定性**:本次上游将已有 HTTP 基础设施 cookie store 接入 WSS 握手,并从成功或拒绝的 upgrade 响应回收 Set-Cookie(`websocket-client/src/lib.rs:145`)。插件没有对应 cookie store。可作为下一优先级的稳定性适配,但未验证当前账号存在握手故障。HTTP cookie store 是上次基线已有能力,不能将整个缺口归为本次新增;实现时应遵循 host/TLS 和基础设施 cookie allowlist,不共享账号/会话 cookie。
34
+ - **WebSocket 空增量续传**:上游 `core/src/client.rs:1377` 放宽为允许与已完成上下文相等的 input;插件 `src/native-websocket-session.ts:83` 默认仍要求严格增长,仅预热例外。可减少无新增 input 时的全量重发,当前没有数据丢失证据。
35
+ - **目录 access programs**:新增可选 `available_access_programs`,插件未建模;需要对应模型访问计划选择时再接入,不能单凭字段新增判定目录解析失败。
36
+
37
+ - **工作区路由与 gateway OAuth**:上游增加账号工作区路由发现以及独立 gateway 登录。插件仍限制凭据端点为 chatgpt.com 或 loopback(`src/endpoint.ts`),没有对应发现流程。需要面向这类工作区或网关时再设计端点信任、账号绑定及切换后的缓存/连接隔离。上游 `login/src/auth/manager/workspace_routing.rs:63` 明确保留无 resolver 调用者的旧行为,不能据此认定普通 ChatGPT 登录已失效。
38
+ - **推理强度增量更新**:目录新增 `supports_reasoning_effort_updates`;上游以 `configuration_update` 历史项保留请求基线、减少切换 effort 的缓存损失。该路径受功能开关、provider 和模型能力共同控制(`core/src/client.rs:548`)。插件继续使用请求级参数;属于缓存与功能对齐项目,尚无证据表明普通请求因此不兼容。
39
+ - **图片文件引用**:`protocol/src/models.rs:897` 新增 `ImageReference::File { file_id }`,原有 `image_url` wire 形状仍保留。插件继续发送内联图片;搜索结果只提取 `image_url` 图片,不能将不认识的 file_id 视为已下载附件。可在明确上传、下载和账号作用域后单独接入。
40
+
41
+ ### 深入评估:推理增量更新与 file_id
42
+
43
+ 推理增量更新建议先做有开关的实验,暂不默认启用。插件当前 `responses.ts:245` 将 effort 放在请求属性中,`native-websocket-session.ts:57` 对请求属性做复用哈希,因此 effort 改变会使增量链不满足复用条件。保持请求基线并追加 configuration_update 有明确的客户端复用收益机制,但服务端缓存收益尚未实测。真正成本是持久化:现有 `replay.ts:7` 只记录 message/reasoning/function_call,不能直接表示宿主生成的配置更新及其历史位置。需先确认宿主能否提供稳定的轮次、分叉、压缩边界和持久化扩展点,再决定插件内扩展是否足够,不能只做进程内 sessionId→effort 缓存。验收应覆盖 medium→high→medium、断线/重启、分叉、失败重试、成功/失败压缩、模型切换以及能力缺失时保留请求级 effort。收益验证比较匹配历史的缓存输入量、请求字节数和首 token 延迟,不预设改善比例。
44
+
45
+ file_id 建议暂缓产品化,先验证后端契约。补充检查发现上游 `attachment-store/src/lib.rs:161` 提供的 InlineAttachmentStore 直接返回原字节;`core/src/thread_manager.rs:498` 默认图片存储仍使用它。`core/src/image_preparation.rs:363` 接受可注入存储返回 File,并在上传失败时回退内联。因而协议存在 file_id 不等于默认 Codex 已经提供现成可复制的图片上传闭环。`codex-api/src/files.rs` 的通用/连接器文件上传也不能未经验证就当成 Responses 图片上传。下一步应以一张合成小图验证上传→finalize→同账号 Responses 识图→引用失效/解析契约;若不能确认接受性与生命周期,继续内联。后续实现优先将远端 ID 作为插件内部、按账号与内容隔离的缓存,保留 DSH 本地附件作恢复依据;应对重新上传、账号切换、并发、取消、签名 URL 凭据隔离,并避免中途改变历史图片表示破坏 WS 前缀。现有 WS 增量已避免重复发送不变历史,因此收益主要在 HTTP、多次新连接或跨请求复用,不能直接宣称节省视觉 token。
46
+
47
+ 本轮为源码评估,无线上请求和运行时代码修改;为读取上游接口追加了 `codex-rs/attachment-store` 稀疏检出目录。
48
+
49
+ ### 当前无需随上游机械改动的接口
50
+
51
+ 本次区间 `codex-api/src/search.rs`、`images.rs`、`endpoint/images.rs` 无变化;`endpoint/search.rs` 保留 `alpha/search`,主要是内部常量和测试类型调整。普通 Responses HTTP/WebSocket 仍使用 `/responses`,移除的是 Guardian 专用分支,不要求插件改路由。模型目录支持自定义 URL 和更多可选元数据,不等于默认目录的旧字段全部失效。
52
+
53
+ ### 验证与边界
54
+
55
+ `pnpm test`:26 个文件、384 项测试全部通过;`pnpm typecheck` 通过;测试包括现有生成产物与新鲜 TypeScript 编译的一致性校验。未执行真实 OAuth、刷新或付费推理;现有回归通过不覆盖上游所有新增行为。原 Provider 基线 `ddf04ad` 与云能力基线 `b348fc26` 保持不变。下方历史章节中的旧缺口、旧测试数量和账号验证不能自动视为当前工作树结论。
56
+
3
57
  ## DSH 0.1.5-rc.1 版本适配
4
58
 
5
59
  `0.10.1` 迁移 ToolCallId、精确 SDK 依赖和客户端服务提供包,隐藏策略在目录发布投影之前执行。`tests/target-directory.spec.ts` 执行官方 shell 的真实 store/Zustand/Immer 与目标目录/catalog 类,使用合成 Remote 响应和投影输入;这不是浏览器联合验收。`tests/target-services.spec.ts` 使用真实 Cordis/LlmRuntime/ToolRuntime、真实 loopback HTTP/SSE/WebSocket,验证工具 ID、工具执行、含加密 reasoning 的历史回放、永久错误和取消,并确认 WS 没有偷偷回退 HTTP。实际 socket 测试也修复了 ws 成功回调为 null 时的误判。
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  ## 快速开始
19
19
 
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` 仅作兼容性说明,不当作宿主硬门禁。
20
+ 版本 `0.12.0` 明确适配 DeepSeek Harness `0.1.7-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`;当前版本未执行线上 OAuth/付费模型验收。
23
23
 
@@ -356,4 +356,4 @@ node --check client.js
356
356
 
357
357
  ## License
358
358
 
359
- [MIT](./LICENSE)
359
+ [MIT](./LICENSE)
@@ -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 === 'user' && message.source.kind === 'tool' && calls.has(message.source.callId)) {
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 === 'tool-result' && block.toolCallId === message.source.callId && !block.isError)
42
- visit(block.content);
37
+ if (block.type === 'image')
38
+ images.push(block.attachment);
43
39
  }
44
40
  }
45
41
  return images;
package/lib/cloud-http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Nonstreaming Codex cloud requests. Never retry ambiguous POST failures. */
2
- import { attributionHeaders, LlmError } from '@deepseek-ai/dsh-llm';
2
+ import { attributionHeaders, LlmError, ProviderRequestId } 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);
5
5
  function positive(value, fallback) {
@@ -93,6 +93,14 @@ export class NativeCodexCloudClient {
93
93
  catch {
94
94
  throw fail(signal.aborted ? 'Codex cloud request cancelled or timed out' : 'Codex cloud request failed; it was not retried', signal.aborted ? 'ABORTED' : 'CODEX_CLOUD_FAILED');
95
95
  }
96
+ const rawRequestId = path.startsWith('images/')
97
+ ? response.headers.get('x-codex-imagegen-request-id') ?? response.headers.get('x-request-id')
98
+ : response.headers.get('x-request-id');
99
+ // Validate before branding: response headers are untrusted, including credential echoes.
100
+ const requestId = rawRequestId && /^[a-zA-Z0-9_.:-]{1,256}$/.test(rawRequestId)
101
+ && ![options.credential.accessToken, credential.accessToken, accountId].some(secret => rawRequestId.includes(secret))
102
+ ? ProviderRequestId(rawRequestId) : undefined;
103
+ const responseFail = (message, code = 'CODEX_CLOUD_FAILED') => new LlmError(message, code, { status: response.status, ...(requestId === undefined ? {} : { requestId }) });
96
104
  if (!response.ok) {
97
105
  void response.body?.cancel().catch(() => { });
98
106
  if (response.status === 401 && attempt === 0 && this.options.recoverCredential) {
@@ -110,7 +118,7 @@ export class NativeCodexCloudClient {
110
118
  continue;
111
119
  }
112
120
  }
113
- throw fail('Codex cloud service returned HTTP ' + response.status, response.status === 401 ? 'AUTH_REQUIRED' : 'CODEX_CLOUD_HTTP_ERROR');
121
+ throw responseFail('Codex cloud service returned HTTP ' + response.status, response.status === 401 ? 'AUTH_REQUIRED' : 'CODEX_CLOUD_HTTP_ERROR');
114
122
  }
115
123
  const reader = response.body?.getReader();
116
124
  if (!reader)
@@ -144,12 +152,9 @@ export class NativeCodexCloudClient {
144
152
  parsed = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
145
153
  }
146
154
  catch {
147
- throw fail('Codex cloud service returned invalid JSON');
155
+ throw responseFail('Codex cloud service returned invalid JSON');
148
156
  }
149
- const requestId = path.startsWith('images/')
150
- ? response.headers.get('x-codex-imagegen-request-id') ?? response.headers.get('x-request-id')
151
- : response.headers.get('x-request-id');
152
- return { body: parsed, ...(requestId && /^[a-zA-Z0-9_.:-]{1,256}$/.test(requestId) && !requestId.includes(credential.accessToken) ? { requestId } : {}) };
157
+ return { body: parsed, ...(requestId ? { requestId } : {}) };
153
158
  }
154
159
  throw fail('Codex cloud authentication failed', 'AUTH_REQUIRED');
155
160
  }
@@ -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' | 'run' | 'sandboxMode'>;
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'>;
@@ -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 outcome = await deps.shell.run(deps.shell.resolve({
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 });
@@ -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.source.kind !== 'tool' || !['codex_web', 'run_code'].includes(calls.get(message.source.callId) ?? ''))
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
- for (const block of message.content)
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) {
@@ -56,8 +56,8 @@ function supportsFast(model) {
56
56
  function responsesLiteMode(model, requestedModel = model?.slug) {
57
57
  if (model !== undefined && !model.useResponsesLite)
58
58
  return {};
59
- // Never downgrade the tracked Lite-only Astra slug to Standard Responses during catalog outages.
60
- if (model === undefined && requestedModel !== 'gpt-6-astra')
59
+ // Never downgrade known Lite-only slugs to Standard Responses during catalog outages.
60
+ if (model === undefined && requestedModel !== 'gpt-6-astra' && requestedModel !== 'gpt-6-sol')
61
61
  return {};
62
62
  return {
63
63
  responsesLite: {
@@ -1,11 +1,11 @@
1
1
  /** Native ChatGPT Codex HTTP/SSE transport with safe pre-output retries. */
2
2
  import { createHash, randomUUID } from 'node:crypto';
3
3
  import { networkTelemetry, trackNetwork, observeNetworkOutput } from './network-telemetry.js';
4
- import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, isContextWindowExceededError, isQuotaExceededError, LlmError, ProviderRequestId, QUOTA_EXCEEDED_CODE, attributionHeaders, } from '@deepseek-ai/dsh-llm';
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
7
  import { NATIVE_CODEX_CONNECTION_FAILED_CODE, NATIVE_CODEX_STREAM_INTERRUPTED_CODE, isNativeCodexConnectionFailure, } from './native-adapter.js';
8
- import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, } from './responses.js';
8
+ import { boundedCodexTurnState, codexRequestBody, codexResponseTurnState, streamResponses, responsesFailure, } from './responses.js';
9
9
  import { hasNativeCodexReplayKind } from './replay.js';
10
10
  import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
11
11
  import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.js';
@@ -122,28 +122,29 @@ async function resolveImage(block, options, signal) {
122
122
  ...stored.detail === undefined ? {} : { detail: stored.detail },
123
123
  };
124
124
  }
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
125
  async function resolveMessages(generation, options) {
145
126
  const messages = [];
146
127
  for (const message of generation.messages) {
128
+ if (message.role === 'tool') {
129
+ const content = [];
130
+ for (const block of message.content) {
131
+ throwIfAborted(generation.signal);
132
+ if (block.type === 'text')
133
+ content.push(block);
134
+ else if (block.type === 'image')
135
+ content.push(await resolveImage(block, options, generation.signal));
136
+ else {
137
+ throw fixedFailure('native Codex tool output contains an unsupported content block', 'UNSUPPORTED');
138
+ }
139
+ }
140
+ messages.push({
141
+ role: 'tool',
142
+ content,
143
+ toolCallId: message.toolCallId,
144
+ ...message.isError !== undefined ? { isError: message.isError } : {},
145
+ });
146
+ continue;
147
+ }
147
148
  const content = [];
148
149
  const source = message.source;
149
150
  const sourceKind = source?.kind;
@@ -177,12 +178,6 @@ async function resolveMessages(generation, options) {
177
178
  // A settlement copies the child's assistant output into user-role context.
178
179
  // Its tool calls belong to the child and must not become parent wire calls.
179
180
  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
181
  default:
187
182
  throw fixedFailure('native Codex request contains an unsupported content block', 'UNSUPPORTED');
188
183
  }
@@ -191,6 +186,7 @@ async function resolveMessages(generation, options) {
191
186
  && source.replayState !== undefined && hasNativeCodexReplayKind(source.replayState)
192
187
  ? { provider: source.provider, model: source.model, replayState: source.replayState }
193
188
  : undefined;
189
+ // Developer messages are preserved for the Responses API
194
190
  messages.push({
195
191
  role: message.role,
196
192
  content,
@@ -280,24 +276,20 @@ function errorFacts(response, maxDelayMs) {
280
276
  async function httpFailure(response, maxDelayMs, signal, telemetry) {
281
277
  const detail = await boundedError(response, signal, telemetry);
282
278
  const facts = errorFacts(response, maxDelayMs);
283
- const classification = `${detail.code ?? ''} ${detail.message ?? ''}`;
279
+ const classified = responsesFailure(detail.code, detail.message);
284
280
  if (response.status === 401 || response.status === 403) {
285
281
  return fixedFailure('native Codex rejected the configured credential', 'AUTH', facts);
286
282
  }
283
+ if (classified.code !== 'SERVER') {
284
+ const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
285
+ return fixedFailure(classified.message, classified.code, {
286
+ ...facts,
287
+ ...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, maxDelayMs) }),
288
+ });
289
+ }
287
290
  if (response.status === 429) {
288
- if (isQuotaExceededError(classification)) {
289
- return fixedFailure('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE, facts);
290
- }
291
291
  return fixedFailure('native Codex request was rate limited', 'RATE_LIMIT', facts);
292
292
  }
293
- if (detail.code === 'context_length_exceeded'
294
- || detail.code === 'context_window_exceeded'
295
- || isContextWindowExceededError(classification)) {
296
- return fixedFailure('native Codex request exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE, facts);
297
- }
298
- if (isQuotaExceededError(classification)) {
299
- return fixedFailure('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE, facts);
300
- }
301
293
  if (response.status === 408 || response.status === 504) {
302
294
  return fixedFailure('native Codex request timed out', 'TIMEOUT', facts);
303
295
  }
@@ -8,7 +8,7 @@ import { NativeCodexHttpTransport, } from './native-http.js';
8
8
  import { replayableItemId } from './replay.js';
9
9
  import { parseCodexResponseUsageMetadata, publishCodexResponseUsage, } from './response-usage.js';
10
10
  import { parseCodexRateLimitEvent, parseCodexRateLimitHeaders, publishCodexRateLimits, } from './rate-limits.js';
11
- import { ResponsesStreamTranslator, codexResponseTurnState, } from './responses.js';
11
+ import { ResponsesStreamTranslator, responsesFailure, codexResponseTurnState, } from './responses.js';
12
12
  import { NodeNativeCodexWebSocketFactory, } from './native-websocket-socket.js';
13
13
  import { NativeCodexWebSocketSessionState } from './native-websocket-session.js';
14
14
  const WS_BETA = 'responses_websockets=2026-02-06';
@@ -98,7 +98,8 @@ function sessionKey(generation, routingId) {
98
98
  function turnKey(generation) {
99
99
  for (let index = generation.messages.length - 1; index >= 0; index--) {
100
100
  const message = generation.messages[index];
101
- if (message?.source.kind === 'user') {
101
+ // RequestUserInput doesn't have source or id
102
+ if (message && 'source' in message && message.source?.kind === 'user' && 'id' in message) {
102
103
  return createHash('sha256').update(String(message.id)).digest('base64url');
103
104
  }
104
105
  }
@@ -193,8 +194,15 @@ function eventFailure(event) {
193
194
  }
194
195
  if (status === 403)
195
196
  return new LlmError('native Codex WebSocket authorization failed', 'AUTH', facts);
196
- if (providerCode === 'rate_limit_exceeded' || providerCode === 'usage_limit_reached'
197
- || status === 429) {
197
+ const classified = responsesFailure(providerCode, typeof error.message === 'string' ? error.message : undefined);
198
+ if (classified.code !== 'SERVER') {
199
+ const delay = facts?.providerRetryAfterMs ?? classified.failure.providerRetryAfterMs;
200
+ return new LlmError(classified.message, classified.code === 'RATE_LIMIT' ? 'RATE_LIMITED' : classified.code, {
201
+ ...facts,
202
+ ...(delay === undefined ? {} : { providerRetryAfterMs: Math.min(delay, 120_000) }),
203
+ });
204
+ }
205
+ if (providerCode === 'usage_limit_reached' || status === 429) {
198
206
  return new LlmError('native Codex WebSocket rate limit was reached', 'RATE_LIMITED', facts);
199
207
  }
200
208
  if (status !== undefined && status >= 500) {
@@ -9,18 +9,14 @@ export interface ResolvedImagePart {
9
9
  dataBase64: string;
10
10
  detail?: CodexImageDetail;
11
11
  }
12
- export interface ResolvedToolResultPart {
13
- type: 'tool-result';
14
- toolCallId: ToolCallId;
15
- content: readonly ResolvedContentPart[];
16
- isError?: boolean;
17
- }
18
12
  export type ResolvedContentPart = Exclude<ContentBlock, {
19
- type: 'image' | 'tool-result';
20
- }> | ResolvedImagePart | ResolvedToolResultPart;
13
+ type: 'image';
14
+ }> | ResolvedImagePart;
21
15
  export interface ResolvedMessage {
22
- role: 'system' | 'user' | 'assistant';
16
+ role: 'system' | 'user' | 'assistant' | 'tool' | 'developer';
23
17
  content: readonly ResolvedContentPart[];
18
+ toolCallId?: ToolCallId;
19
+ isError?: boolean;
24
20
  replaySource?: NativeCodexReplaySource;
25
21
  }
26
22
  export interface ResponsesRequestInput {
package/lib/responses.js CHANGED
@@ -26,12 +26,12 @@ function imageItem(image) {
26
26
  return { type: 'input_image', image_url: `data:${image.mediaType};base64,${image.dataBase64}`,
27
27
  ...image.detail === undefined ? {} : { detail: image.detail } };
28
28
  }
29
- function toolOutput(block) {
30
- const images = block.content.some(part => part.type === 'image');
29
+ function toolOutput(content) {
30
+ const images = content.some(part => part.type === 'image');
31
31
  if (!images) {
32
- return block.content.map(part => part.type === 'text' ? part.text : '').join('');
32
+ return content.map(part => part.type === 'text' ? part.text : '').join('');
33
33
  }
34
- return block.content.map((part) => {
34
+ return content.map((part) => {
35
35
  if (part.type === 'text')
36
36
  return { type: 'input_text', text: part.text };
37
37
  if (part.type === 'image' && 'dataBase64' in part)
@@ -67,6 +67,29 @@ export function toResponsesInput(messages, system) {
67
67
  input.push({ type: 'message', role: message.role, content });
68
68
  content = [];
69
69
  };
70
+ // Handle tool messages separately - they have no content blocks, just top-level fields
71
+ if (message.role === 'tool') {
72
+ if (message.toolCallId === undefined) {
73
+ throw fixedError('native Codex tool message requires toolCallId', 'INVALID_ARGS');
74
+ }
75
+ input.push({
76
+ type: 'function_call_output',
77
+ call_id: String(message.toolCallId),
78
+ output: toolOutput(message.content),
79
+ ...message.isError === true ? { is_error: true } : {},
80
+ });
81
+ continue;
82
+ }
83
+ // Handle developer messages - map to developer role in Responses API
84
+ if (message.role === 'developer') {
85
+ for (const block of message.content) {
86
+ if (block.type === 'text') {
87
+ content.push({ type: 'input_text', text: block.text });
88
+ }
89
+ }
90
+ flush();
91
+ continue;
92
+ }
70
93
  for (const block of message.content) {
71
94
  if (message.role === 'system') {
72
95
  if (block.type === 'text')
@@ -89,18 +112,15 @@ export function toResponsesInput(messages, system) {
89
112
  content.push(imageItem(block));
90
113
  break;
91
114
  case 'tool-call':
115
+ if (message.role !== 'assistant') {
116
+ throw fixedError('native Codex tool calls require assistant messages', 'INVALID_ARGS');
117
+ }
92
118
  flush();
93
119
  input.push({
94
120
  type: 'function_call', call_id: String(block.id),
95
121
  name: block.name, arguments: block.arguments,
96
122
  });
97
123
  break;
98
- case 'tool-result':
99
- flush();
100
- input.push({
101
- type: 'function_call_output', call_id: String(block.toolCallId), output: toolOutput(block),
102
- });
103
- break;
104
124
  case 'reasoning':
105
125
  break; // Visible reasoning is never replayed; M4 may add encrypted items.
106
126
  default:
@@ -282,16 +302,21 @@ export function responsesFailure(code, message) {
282
302
  || isContextWindowExceededError(detail)) {
283
303
  return fixedError('native Codex request exceeded the model context window', CONTEXT_WINDOW_EXCEEDED_CODE);
284
304
  }
285
- if (code === 'insufficient_quota' || isQuotaExceededError(detail)) {
305
+ if (code === 'insufficient_quota' || code === 'credit_balance_exhausted'
306
+ || code === 'organization_spend_limit_exceeded' || code === 'project_spend_limit_exceeded'
307
+ || isQuotaExceededError(detail)) {
286
308
  return fixedError('native Codex account quota is exhausted', QUOTA_EXCEEDED_CODE);
287
309
  }
288
- if (code === 'rate_limit_exceeded') {
310
+ if (code === 'rate_limit_exceeded' || code === 'slow_down') {
289
311
  const delay = retryDelay(message);
290
312
  return new LlmError('native Codex request was rate limited', 'RATE_LIMIT', {
291
313
  ...delay === undefined ? {} : { providerRetryAfterMs: delay },
292
314
  });
293
315
  }
294
- if (code === 'invalid_prompt' || code === 'bio_policy') {
316
+ if (code === 'invalid_prompt') {
317
+ return fixedError('native Codex rejected the prompt', 'INVALID_PROMPT');
318
+ }
319
+ if (code === 'bio_policy') {
295
320
  return fixedError('native Codex rejected the request', 'INVALID_REQUEST');
296
321
  }
297
322
  return fixedError('native Codex reported a failed response', 'SERVER');
package/lib/upstream.d.ts CHANGED
@@ -5,4 +5,4 @@ export declare const TRACKED_CODEX_RELEASE = "main@ddf04ad";
5
5
  /** Independently audited cloud search/image contracts; not local Codex executor parity. */
6
6
  export declare const TRACKED_CODEX_CLOUD_COMMIT = "b348fc26674189f758d5941cdab3f78f258b2aa7";
7
7
  /** Whole stable release version sent to the Codex model-catalog endpoint. */
8
- export declare const CODEX_CLIENT_VERSION = "0.153.4";
8
+ export declare const CODEX_CLIENT_VERSION = "0.155.0";
package/lib/upstream.js CHANGED
@@ -5,4 +5,4 @@ export const TRACKED_CODEX_RELEASE = 'main@ddf04ad';
5
5
  /** Independently audited cloud search/image contracts; not local Codex executor parity. */
6
6
  export const TRACKED_CODEX_CLOUD_COMMIT = 'b348fc26674189f758d5941cdab3f78f258b2aa7';
7
7
  /** Whole stable release version sent to the Codex model-catalog endpoint. */
8
- export const CODEX_CLIENT_VERSION = '0.153.4';
8
+ export const CODEX_CLIENT_VERSION = '0.155.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pure01fx/dsh-openai-codex-auth",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Native ChatGPT Codex provider, device-code-first login, and same-origin Web integration for DeepSeek Harness",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -96,7 +96,7 @@
96
96
  },
97
97
  "dsh": {
98
98
  "engines": {
99
- "dsh": "0.1.5-rc.1"
99
+ "dsh": "0.1.7-rc.2"
100
100
  },
101
101
  "bundle": {
102
102
  "patch": "./cordis.patch.yml"
@@ -112,20 +112,20 @@
112
112
  }
113
113
  },
114
114
  "peerDependencies": {
115
- "@deepseek-ai/cordis": "^4.0.1",
116
- "@deepseek-ai/dsh-attachment": "0.1.5-rc.1",
117
- "@deepseek-ai/dsh-credentials": "0.1.5-rc.1",
118
- "@deepseek-ai/dsh-fs": "0.1.5-rc.1",
119
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
120
- "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
121
- "@deepseek-ai/dsh-sandbox": "0.1.5-rc.1",
122
- "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.1",
123
- "@deepseek-ai/dsh-shell": "0.1.5-rc.1",
124
- "@deepseek-ai/dsh-tools": "0.1.5-rc.1"
115
+ "@deepseek-ai/cordis": "~4.0.4",
116
+ "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
117
+ "@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
118
+ "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
119
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
120
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
121
+ "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
122
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
123
+ "@deepseek-ai/dsh-shell": "0.1.7-rc.2",
124
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2"
125
125
  },
126
126
  "dependencies": {
127
- "@deepseek-ai/dsh-atomic-write": "0.1.5-rc.1",
128
- "@deepseek-ai/dsh-home-paths": "0.1.5-rc.1",
127
+ "@deepseek-ai/dsh-atomic-write": "0.1.7-rc.2",
128
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.2",
129
129
  "@deepseek-ai/schemastery": "^3.18.1",
130
130
  "https-proxy-agent": "7.0.6",
131
131
  "image-size": "^2.0.2",
@@ -133,29 +133,32 @@
133
133
  "ws": "8.21.3"
134
134
  },
135
135
  "devDependencies": {
136
- "@deepseek-ai/dsh-client-ui-model-selection": "0.1.5-rc.1",
137
- "@deepseek-ai/dsh-web-frontend": "0.1.5-rc.1",
138
- "@deepseek-ai/cordis": "^4.0.1",
139
- "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
140
- "@deepseek-ai/dsh-attachment": "0.1.5-rc.1",
141
- "@deepseek-ai/dsh-attachment-local": "0.1.5-rc.1",
142
- "@deepseek-ai/dsh-credentials": "0.1.5-rc.1",
143
- "@deepseek-ai/dsh-fs": "0.1.5-rc.1",
144
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
145
- "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
146
- "@deepseek-ai/dsh-sandbox": "0.1.5-rc.1",
147
- "@deepseek-ai/dsh-sandbox-policy": "0.1.5-rc.1",
148
- "@deepseek-ai/dsh-scope": "0.1.5-rc.1",
149
- "@deepseek-ai/dsh-session": "0.1.5-rc.1",
150
- "@deepseek-ai/dsh-shell": "0.1.5-rc.1",
151
- "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.1",
152
- "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
136
+ "@deepseek-ai/cordis": "~4.0.4",
137
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
138
+ "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
139
+ "@deepseek-ai/dsh-attachment-local": "0.1.7-rc.2",
140
+ "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
141
+ "@deepseek-ai/dsh-client-ui-model-selection": "0.1.7-rc.2",
142
+ "@deepseek-ai/dsh-credentials": "0.1.7-rc.2",
143
+ "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
144
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.2",
145
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
146
+ "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
147
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.2",
148
+ "@deepseek-ai/dsh-scope": "0.1.7-rc.2",
149
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
150
+ "@deepseek-ai/dsh-shell": "0.1.7-rc.2",
151
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
152
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
153
+ "@deepseek-ai/dsh-web-frontend": "0.1.7-rc.2",
153
154
  "@types/node": "^22.20.0",
154
155
  "@types/proxy-from-env": "1.0.4",
155
156
  "@types/ws": "8.18.1",
157
+ "immer": "10.1.1",
156
158
  "sharp": "^0.35.4",
157
159
  "typescript": "^6.0.3",
158
- "vitest": "^4.1.8"
160
+ "vitest": "^4.1.8",
161
+ "zustand": "4.4.7"
159
162
  },
160
163
  "peerDependenciesMeta": {
161
164
  "@deepseek-ai/dsh-tools": {
@@ -177,4 +180,4 @@
177
180
  "optional": true
178
181
  }
179
182
  }
180
- }
183
+ }