dsh-codex-ws 1.3.0 → 1.3.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/README.md CHANGED
@@ -8,11 +8,13 @@
8
8
  - **WS 模式**:`wss://<baseURL>/responses`,发送 `{ "type": "response.create", ... }` 后接收与 SSE 完全相同的 Responses 事件流;连接按池复用(干净结束后保留 3 分钟,50 分钟强制换新)。
9
9
  - **SSE 模式**:Fast 关闭时不拦截,走 DSH 原有 openai 渠道的流式请求(auto 模式下的回退则是普通 `POST /responses` + `stream: true` 打到同一中转站)。
10
10
  - 拦截层复刻了适配器边界的投影(文件 → 可读路径句柄、图片 → 文本占位、developer 消息丢弃、deferLoading 工具声明展开),WS 路径目前仅发送文本,不会把图片原始内容发送给模型;不能视为与原生多模态适配器完全等价。
11
- - 接口地址与 Key 的解析顺序(`lib/config.js`):
12
- 1. 插件配置里的 `baseURL` / `apiKey`
13
- 2. `envFile`(.env)或环境变量 `apiKeyEnv`(默认 `OPENAI_API_KEY`)
14
- 3. **`~/.codex/config.toml`** 中 `supports_websockets = true` 的 `[model_providers.*]`(取其 `base_url` 与 `experimental_bearer_token`)
15
- 4. 内置默认(`https://sub.cxc2.cn/v1`)
11
+ - **默认继承 DSH 当前 OpenAI 渠道的接口地址与凭据**:通过 `llm/listConfigurableProviders` 定位 openai 路由的设置命名空间与 profile 路径,读取其 `baseURL`(缺省用渠道默认地址),`apiKeyEnv` 凭据引用经 credentials 服务解析——与 llm-pi-ai 适配器自身的解析方式一致。每次请求重新读取,渠道配置改动即时生效;不读取本地 Codex CLI 配置,也没有写死的中转地址。
12
+ - 插件的 `baseURL` / `apiKey` / `apiKeyEnv` / `envFile` 留空即可继承。显式填写任意一项会进入独立覆盖模式,必须同时提供接口地址和 Key,不会与 DSH 的地址或凭据混用。
13
+ - 覆盖模式 Key 顺序:`apiKey` → `envFile` 中对应变量 → 显式指定的 `apiKeyEnv` 环境变量。
14
+ - `apiKeyEnv` 默认留空;仅配置 `envFile` 时默认读取文件中的 `OPENAI_API_KEY`。不会自动使用进程中的 `OPENAI_API_KEY`。
15
+ - 不再自动沿用 `~/.codex/config.toml`;若要使用同一服务,请在 DSH OpenAI 渠道中配置,或显式提供插件覆盖配置。
16
+ - 升级后若旧配置保存了 `apiKeyEnv: OPENAI_API_KEY`,请清空该字段及其他覆盖项以启用继承。
17
+ - 继承模式下渠道未配置或凭据无法解析时请求明确报错(`INVALID_CONFIGURATION`),不会退回其他端点或凭据。
16
18
  - 输入框右侧、模型选择器旁的 **WS / SSE** 小按钮实时切换传输模式(下一次模型请求生效),选择持久化在 `$DSH_HOME/codex-ws/state.json`。**按钮只在该会话当前模型位于 openai 渠道时出现**,其他模型旁不存在。
17
19
 
18
20
  ### Fast 模式
@@ -20,13 +22,20 @@
20
22
  WS / SSE 按钮旁新增独立 **Fast** 按钮,默认关闭,开启后高亮。开关与传输模式分别保存,全局影响 OpenAI 请求,从下一次模型请求生效,重启后保留。
21
23
 
22
24
  - 开启时发送 `service_tier: "priority"`,不改变模型、推理强度或输出长度;是否支持、实际速度和额外费用由服务端决定。
23
- - WS / auto 及 auto 回退 SSE 均保留该参数。**SSE + Fast 使用插件配置的 Responses 端点与凭据,而非原生适配器**,并继承插件的仅文本投影限制。关闭 Fast 后 SSE 恢复原样放行。
24
- - 开启 Fast 但缺少插件凭据时明确报错,不静默丢弃优先处理参数。切换 Fast 后增量 WS 请求配置指纹改变,自动全量发送。
25
+ - WS / auto 及 auto 回退 SSE 均保留该参数。**SSE + Fast 由插件发起 Responses 请求,默认继承 DSH OpenAI 端点与凭据**,并继承插件的仅文本投影限制。关闭 Fast 后 SSE 恢复原样放行。
26
+ - WS / auto / SSE + Fast 配置不完整时明确报错,不静默丢弃优先处理参数。切换 Fast 后增量 WS 请求配置指纹改变,自动全量发送。
25
27
  - 离线验证:`node test/fast.mjs`(不调用真实 API)。
26
28
 
27
29
  ## 安装
28
30
 
29
- npm 发布完成后,可运行 `npm pack dsh-codex-ws@1.3.0 --registry=https://registry.npmjs.org` 获取安装包,在 DSH 插件管理中安装本地包;或克隆仓库后从本地目录安装。安装后刷新插件和界面。
31
+ 可运行 `npm pack dsh-codex-ws@1.3.1 --registry=https://registry.npmjs.org` 获取安装包,在 DSH 插件管理中安装本地包;或克隆仓库后从本地目录安装。安装后刷新插件和界面。
32
+
33
+ ### v1.3.1 升级说明
34
+
35
+ - 默认继承 DSH OpenAI 渠道的接口地址和凭据,每次请求重新读取渠道配置。
36
+ - 移除本地 Codex CLI 配置与内置中转地址回退;显式覆盖必须成对提供地址和 Key,避免混用端点与凭据。
37
+ - **从旧版升级**:若插件中保存了 `apiKeyEnv: OPENAI_API_KEY`,请清空它及其他覆盖项以启用继承;如需独立覆盖,请同时配置 `baseURL` 和 Key。
38
+ - 配置不完整时,WS / auto / SSE + Fast 明确报错;SSE 且 Fast 关闭仍由原生 OpenAI 适配器处理。
30
39
 
31
40
  ## npm 发布维护
32
41
 
@@ -48,7 +57,7 @@ npm 发布完成后,可运行 `npm pack dsh-codex-ws@1.3.0 --registry=https://
48
57
 
49
58
  1. 确认 openai 渠道(llm-pi-ai 的 `openai`)已配置好 Key;模型选择器里选任意 GPT 模型。
50
59
  2. 模型选择器旁会出现 WS / SSE 按钮:默认即 WS 模式(可配置);点一下切回 SSE 流式,再点切回 WS。
51
- 3. 插件配置(设置 → 插件 → Codex WS → 配置)可改:`transport`(websocket / sse / auto,auto = WS 连接失败自动回退流式)、`baseURL`、`apiKey`、`apiKeyEnv`、`envFile`、超时等。
60
+ 3. 默认无需重复填写地址和 Key。插件配置(设置 → 插件 → Codex WS → 配置)可改:`transport`(websocket / sse / auto,auto = WS 连接失败自动回退流式)、`baseURL`、`apiKey`、`apiKeyEnv`、`envFile`、超时等。
52
61
 
53
62
  ## 说明
54
63
 
package/cordis.patch.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # GPT (Codex backend) WebSocket mode for the OpenAI channel.
2
- # Every field is optional; empty values fall back to ~/.codex/config.toml
3
- # (model_providers.* with supports_websockets = true), then to the plugin defaults.
2
+ # Leave endpoint/credential fields empty to inherit the DSH OpenAI provider.
3
+ # Explicit overrides require both an endpoint and credentials; no Codex CLI fallback.
4
4
  - insert:
5
5
  - id: codex-ws
6
6
  name: 'dsh-codex-ws'
package/index.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * harness `llm/stream` waterfall for the existing `openai` provider: while the
6
6
  * transport toggle is on (websocket / auto), those requests speak the Responses
7
7
  * protocol over WebSocket ("WS 模式", Codex CLI `supports_websockets`) against the
8
- * relay the local Codex CLI uses (base URL + bearer token resolved from this
9
- * plugin's config, then ~/.codex/config.toml, then defaults). SSE mode passes the
8
+ * current DSH OpenAI endpoint and credentials (or a complete explicit plugin
9
+ * override). No Codex CLI configuration is read. SSE mode passes the
10
10
  * request through to the normal openai adapter untouched, so OpenAI models stay a
11
11
  * single channel and non-OpenAI models never see the toggle at all.
12
12
  *
@@ -19,12 +19,12 @@
19
19
  import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
20
20
  import { homedir } from 'node:os'
21
21
  import { join } from 'node:path'
22
- import { Config, resolveSettings, TARGET_PROVIDER, TRANSPORTS } from './lib/config.js'
22
+ import { Config, resolveSettings, hasOverride, TARGET_PROVIDER, TRANSPORTS } from './lib/config.js'
23
23
  import { ResponseStreamMapper, buildCreateBody, projectOptions, abortedFinish, errorFinish } from './lib/responses.js'
24
24
  import { runTransport, closeIdleSockets, TransportError } from './lib/transport.js'
25
25
 
26
26
  export const name = 'codex-ws'
27
- export const inject = ['llm', 'webServer', 'connection']
27
+ export const inject = ['llm', 'webServer', 'connection', 'settings']
28
28
  export { Config }
29
29
 
30
30
  const STATE_PATH = '/codex-ws/state'
@@ -57,16 +57,16 @@ function writeState(state) {
57
57
  // ---------------------------------------------------------------------------------------------
58
58
 
59
59
  /** One openai-provider request over the Codex-style transport; yields harness chunks. */
60
- async function* interceptedStream(options, runtime, resolvePath) {
61
- const { settings, transport } = runtime
62
- if (settings.apiKey.length === 0) {
60
+ async function* interceptedStream(options, resolveRuntime, resolvePath) {
61
+ const { settings, transport } = await resolveRuntime()
62
+ if (settings.configurationError) {
63
63
  yield {
64
64
  type: 'finish',
65
65
  reason: {
66
66
  kind: 'error',
67
67
  failure: {
68
- message: `codex-ws: no API key for the plugin Responses transport; set the key on the plugin's config page (or ${settings.apiKeyEnv} / ~/.codex/config.toml), or disable Fast and switch to SSE`,
69
- code: 'MISSING_CREDENTIAL',
68
+ message: settings.configurationError,
69
+ code: 'INVALID_CONFIGURATION',
70
70
  },
71
71
  },
72
72
  }
@@ -117,11 +117,86 @@ export function apply(ctx, config) {
117
117
  }
118
118
  loadState()
119
119
 
120
- /** Live settings plus the effective transport for the next request. */
121
- const runtime = () => {
122
- const settings = { ...resolveSettings(config), fast: state.fast }
123
- const transport = state.override ?? settings.transport
124
- return { settings, transport }
120
+ /**
121
+ * The DSH OpenAI provider's live endpoint and bearer credential, resolved the
122
+ * same way the pi-ai adapter resolves them: the configurable-provider
123
+ * directory names the settings namespace and profile path; the profile holds
124
+ * an optional baseURL (falling back to the route's catalog default) and an
125
+ * apiKeyEnv credential reference resolved through the credentials service
126
+ * (with the launch environment as a secondary source). Re-read per request
127
+ * so channel edits apply without a restart; every failure is contained and
128
+ * surfaces as the inherited-mode configuration error.
129
+ */
130
+ const inheritedProvider = async () => {
131
+ try {
132
+ const entry = ctx.llm.listConfigurableProviders().find((provider) => provider.provider === TARGET_PROVIDER)
133
+ if (!entry) return {}
134
+ let profile = {}
135
+ try {
136
+ const namespace = ctx.settings
137
+ ?.describe({ redactSecrets: true })
138
+ .find((candidate) => candidate.ns === entry.settingsNs)
139
+ let value = namespace?.value
140
+ for (const key of entry.settingsPath ?? []) value = value?.[key]
141
+ profile = value !== null && typeof value === 'object' ? value : {}
142
+ } catch {
143
+ /* settings service unavailable: fall back to the catalog default */
144
+ }
145
+ const baseURL = typeof profile.baseURL === 'string' ? profile.baseURL.trim() : ''
146
+ const reference = typeof profile.apiKeyEnv === 'string' ? profile.apiKeyEnv.trim() : ''
147
+ let apiKey = ''
148
+ if (reference) {
149
+ // Bare `ctx.credentials` throws on the Cordis context proxy when the
150
+ // service is not in this plugin's inject list ("cannot get property
151
+ // without inject"); ctx.get is the supported inject-free accessor and
152
+ // matches how llm-pi-ai itself reaches the credentials service.
153
+ let credentials
154
+ try {
155
+ credentials = typeof ctx.get === 'function' ? ctx.get('credentials') : undefined
156
+ } catch {
157
+ /* this composition mounts no credential plane */
158
+ }
159
+ try {
160
+ const resolved = credentials === undefined ? undefined : await credentials.resolve?.(reference)
161
+ if (typeof resolved?.value === 'string') apiKey = resolved.value.trim()
162
+ } catch {
163
+ /* an unresolvable reference surfaces as the inherited-mode error */
164
+ }
165
+ if (apiKey.length === 0) {
166
+ // Ambient fallback mirrors the adapter: the launch-environment
167
+ // snapshot (process / project .env / user .env layers), with raw
168
+ // process.env as the last resort for compositions without either.
169
+ let ambient
170
+ try {
171
+ const environment = typeof ctx.get === 'function' ? ctx.get('launchEnvironment') : undefined
172
+ ambient = environment?.get?.(reference)?.value
173
+ } catch {
174
+ /* optional service */
175
+ }
176
+ apiKey = (ambient ?? process.env[reference] ?? '').trim()
177
+ }
178
+ }
179
+ return {
180
+ baseURL: baseURL || (typeof entry.defaultBaseURL === 'string' ? entry.defaultBaseURL.trim() : ''),
181
+ apiKey,
182
+ }
183
+ } catch {
184
+ return {}
185
+ }
186
+ }
187
+
188
+ /** Sync transport/fast decision for the next request; never touches credentials. */
189
+ const runtimeSync = () => {
190
+ const { transport } = resolveSettings(config, {})
191
+ return { transport: state.override ?? transport, fast: state.fast }
192
+ }
193
+
194
+ /** Live settings plus the effective transport; the inherited endpoint and
195
+ * credential resolve asynchronously through the DSH services. */
196
+ const runtime = async () => {
197
+ const provider = hasOverride(config) ? {} : await inheritedProvider()
198
+ const settings = { ...resolveSettings(config, provider), fast: state.fast }
199
+ return { settings, transport: state.override ?? settings.transport }
125
200
  }
126
201
 
127
202
  const persistState = (patch) => {
@@ -152,15 +227,15 @@ export function apply(ctx, config) {
152
227
  */
153
228
  ctx.on('llm/stream', (options, next) => {
154
229
  if (options?.provider !== TARGET_PROVIDER) return next()
155
- const { settings, transport } = runtime()
156
- if (transport === 'sse' && !settings.fast) return next()
157
- // Do not silently drop an explicitly requested priority tier when credentials are missing.
158
- if (settings.apiKey.length === 0 && !settings.fast) return next()
159
- return interceptedStream(options, { settings, transport }, fileReadPath)
230
+ const { transport, fast } = runtimeSync()
231
+ if (transport === 'sse' && !fast) return next()
232
+ // Fail closed for incomplete overrides or unavailable inherited configuration.
233
+ // Never silently change endpoint, account, or requested transport.
234
+ return interceptedStream(options, runtime, fileReadPath)
160
235
  })
161
236
 
162
- const publicState = () => {
163
- const { settings, transport } = runtime()
237
+ const publicState = async () => {
238
+ const { settings, transport } = await runtime()
164
239
  let providerAvailable = true
165
240
  try {
166
241
  providerAvailable = ctx.llm.listProviders().some((provider) => provider.id === TARGET_PROVIDER)
@@ -230,7 +305,7 @@ export function apply(ctx, config) {
230
305
  try {
231
306
  if (rejected(req, res)) return
232
307
  if (req.method === 'GET') {
233
- sendJson(res, 200, publicState())
308
+ sendJson(res, 200, await publicState())
234
309
  return
235
310
  }
236
311
  if (req.method !== 'POST') {
@@ -262,7 +337,7 @@ export function apply(ctx, config) {
262
337
  patch.fast = body.fast
263
338
  }
264
339
  persistState(patch)
265
- sendJson(res, 200, publicState())
340
+ sendJson(res, 200, await publicState())
266
341
  } catch (error) {
267
342
  if (!res.headersSent) sendJson(res, 400, { error: error?.message ?? 'bad request' })
268
343
  else res.end()
package/lib/config.js CHANGED
@@ -3,13 +3,11 @@
3
3
  * normalizer the plugin reads them through (same inline-schemastery pattern as the
4
4
  * other local bundles — a profile-installed bundle cannot import DSH's own packages).
5
5
  *
6
- * Endpoint resolution mirrors the local Codex CLI setup this plugin adapts:
7
- * plugin config → ~/.codex/config.toml [model_providers.*] (supports_websockets) → defaults.
6
+ * Endpoints and credentials are inherited together from the DSH OpenAI provider.
7
+ * Explicit plugin overrides must supply their own endpoint and credential pair.
8
8
  */
9
9
 
10
10
  import { readFileSync } from 'node:fs'
11
- import { homedir } from 'node:os'
12
- import { join } from 'node:path'
13
11
 
14
12
  // ---------------------------------------------------------------------------------------------
15
13
  // Schema (serialized schemastery document; generated shape, do not hand-edit refs' uids)
@@ -23,7 +21,7 @@ const SCHEMA = {
23
21
  meta: {
24
22
  default: '',
25
23
  description:
26
- '中转站 / 接口地址(Codex CLI 的 base_url,如 https://sub.cxc2.cn/v1)。请求实际访问 <baseURL>/responses(WS 为 wss://…/responses)。留空读取 ~/.codex/config.toml 中 supports_websockets = true 的模型渠道,再回退默认值。',
24
+ '接口地址。默认与 Key 一起继承 DSH 的 OpenAI 渠道。显式覆盖时须同时提供本插件的 Key(或环境变量引用),不会混用 DSH 凭据。请求访问 <baseURL>/responses。',
27
25
  },
28
26
  },
29
27
  5: {
@@ -32,10 +30,10 @@ const SCHEMA = {
32
30
  role: 'secret',
33
31
  default: '',
34
32
  description:
35
- 'Bearer API Key(sk-…)。留空依次读取:envFile / 环境变量 apiKeyEnv / ~/.codex/config.toml 的 experimental_bearer_token。Key 只在 Host 侧使用,不会发送到浏览器。',
33
+ 'Bearer API Key。默认沿用 DSH OpenAI 渠道;显式覆盖须同时填写 baseURL。覆盖模式下可通过 envFile / apiKeyEnv 提供 Key。Key 仅在 Host 使用。',
36
34
  },
37
35
  },
38
- 7: { type: 'string', meta: { default: 'OPENAI_API_KEY', description: 'API Key 的环境变量 / 凭据引用名。' } },
36
+ 7: { type: 'string', meta: { default: '', description: '显式覆盖 Key 的环境变量名;留空不自动读取 OPENAI_API_KEY。使用 envFile 时留空默认读取其中的 OPENAI_API_KEY。' } },
39
37
  9: { type: 'string', meta: { default: '', description: '可选的 .env 文件绝对路径(KEY=VALUE,每行一条)。' } },
40
38
  11: {
41
39
  type: 'string',
@@ -110,7 +108,6 @@ export const Config = {
110
108
  // ---------------------------------------------------------------------------------------------
111
109
 
112
110
  export const TARGET_PROVIDER = 'openai'
113
- export const DEFAULT_BASE_URL = 'https://sub.cxc2.cn/v1'
114
111
  export const TRANSPORTS = ['websocket', 'sse', 'auto']
115
112
 
116
113
  function text(value) {
@@ -147,76 +144,40 @@ function readEnvFile(file) {
147
144
  return result
148
145
  }
149
146
 
150
- /**
151
- * Minimal TOML reader for the one thing this plugin needs from the Codex CLI config:
152
- * `[model_providers.<id>]` tables with string/boolean values. Ignores everything else
153
- * (arrays, nested tables, multiline strings), which is exactly the shape config.toml uses.
154
- */
155
- function readCodexModelProviders(file) {
156
- let raw
157
- try {
158
- raw = readFileSync(file, 'utf8')
159
- } catch {
160
- return []
161
- }
162
- const providers = []
163
- let current = null
164
- for (const line of raw.split(/\r?\n/)) {
165
- const trimmed = line.trim()
166
- if (!trimmed || trimmed.startsWith('#')) continue
167
- const header = /^\[\s*([^\]]+?)\s*\]$/.exec(trimmed)
168
- if (header) {
169
- const path = header[1].split('.').map((p) => (p.startsWith('"') || p.startsWith("'") ? p.slice(1, -1) : p.trim()))
170
- current = path.length === 2 && path[0] === 'model_providers' ? { id: path[1] } : null
171
- if (current) providers.push(current)
172
- continue
173
- }
174
- if (!current) continue
175
- const eq = trimmed.indexOf('=')
176
- if (eq <= 0) continue
177
- const key = trimmed.slice(0, eq).trim()
178
- let value = trimmed.slice(eq + 1).trim()
179
- if (/^".*"$/.test(value)) {
180
- try {
181
- value = JSON.parse(value)
182
- } catch {
183
- value = value.slice(1, -1)
184
- }
185
- } else if (/^'.*'$/.test(value)) value = value.slice(1, -1)
186
- else if (value === 'true' || value === 'false') value = value === 'true'
187
- else if (/^-?\d+$/.test(value)) value = Number(value)
188
- if (typeof value === 'string' || typeof value === 'boolean') current[key] = value
189
- }
190
- return providers.filter((p) => typeof p.base_url === 'string' && p.base_url.length > 0)
191
- }
192
-
193
- /** The WebSocket-capable provider the local Codex CLI uses, when there is one. */
194
- export function localCodexProvider(codexHome) {
195
- const providers = readCodexModelProviders(join(codexHome ?? join(homedir(), '.codex'), 'config.toml'))
196
- return (
197
- providers.find((p) => p.supports_websockets === true) ??
198
- providers.find((p) => typeof p.experimental_bearer_token === 'string' && p.experimental_bearer_token.length > 0) ??
199
- null
200
- )
147
+ /** True when any explicit endpoint/credential field opts out of inheritance. */
148
+ export function hasOverride(config) {
149
+ const cfg = config !== null && typeof config === 'object' ? config : {}
150
+ return ['baseURL', 'apiKey', 'apiKeyEnv', 'envFile'].some((key) => text(cfg[key]))
201
151
  }
202
152
 
203
- /**
204
- * Fully-defaulted settings for one request. Precedence per value: row config, then
205
- * `envFile`, then the process environment, then ~/.codex/config.toml, then built-in default.
206
- * Resolved on every read so an edited .env or config.toml applies without a restart.
153
+ /** Resolve one request without mixing endpoint and credentials from different sources.
154
+ * `provider` is the current, Host-only DSH OpenAI transport configuration
155
+ * ({ baseURL, apiKey }); inherited mode never reads local Codex files or
156
+ * ambient credentials.
207
157
  */
208
- export function resolveSettings(config) {
158
+ export function resolveSettings(config, provider = {}) {
209
159
  const cfg = config !== null && typeof config === 'object' ? config : {}
210
- const envFile = text(cfg.envFile)
211
- const fileEnv = envFile ? readEnvFile(envFile) : {}
212
- const env = (key) => text(fileEnv[key]) || text(process.env[key])
213
-
214
- const codex = localCodexProvider()
215
-
216
- const baseURL = (text(cfg.baseURL) || text(codex?.base_url) || DEFAULT_BASE_URL).replace(/\/+$/, '')
217
-
160
+ const override = hasOverride(config)
218
161
  const apiKeyEnv = text(cfg.apiKeyEnv) || 'OPENAI_API_KEY'
219
- const apiKey = text(cfg.apiKey) || env(apiKeyEnv) || text(codex?.experimental_bearer_token)
162
+ const envFile = text(cfg.envFile)
163
+ const fileEnv = override && envFile ? readEnvFile(envFile) : {}
164
+ const baseURL = text(override ? cfg.baseURL : provider?.baseURL).replace(/\/+$/, '')
165
+ const apiKey = override
166
+ ? text(cfg.apiKey) || text(fileEnv[apiKeyEnv]) || (text(cfg.apiKeyEnv) ? text(process.env[apiKeyEnv]) : '')
167
+ : text(provider?.apiKey)
168
+ let configurationError = ''
169
+ if (!baseURL || !apiKey) {
170
+ configurationError = override
171
+ ? 'codex-ws: plugin overrides require both baseURL and an API key (apiKey, envFile or apiKeyEnv); DSH credentials are never mixed with overrides'
172
+ : 'codex-ws: DSH OpenAI endpoint or credential is unavailable — configure the openai channel in DSH settings (an API-key credential with baseURL), or supply a complete plugin override (baseURL and key together)'
173
+ } else {
174
+ try {
175
+ const url = new URL(baseURL)
176
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error('invalid endpoint')
177
+ } catch {
178
+ configurationError = 'codex-ws: baseURL must be an HTTP(S) URL without userinfo, query or fragment'
179
+ }
180
+ }
220
181
 
221
182
  const rawTransport = text(cfg.transport).toLowerCase()
222
183
  const transport = TRANSPORTS.includes(rawTransport) ? rawTransport : 'websocket'
@@ -224,6 +185,8 @@ export function resolveSettings(config) {
224
185
  const verbosity = text(cfg.verbosity).toLowerCase()
225
186
 
226
187
  return {
188
+ configurationError,
189
+ source: override ? 'plugin' : 'dsh',
227
190
  baseURL,
228
191
  sseUrl: baseURL.endsWith('/responses') ? baseURL : `${baseURL}/responses`,
229
192
  wsUrl: (baseURL.endsWith('/responses') ? baseURL : `${baseURL}/responses`)
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Codex WS",
4
- "description": "GPT models over the Codex-style Responses WebSocket transport (Codex CLI's supports_websockets): no extra model channel — the openai provider is intercepted in place, with the endpoint and key resolved from the local Codex CLI config (~/.codex/config.toml). The composer WS/SSE button appears only while the current model is an OpenAI one."
4
+ "description": "GPT models over the Codex-style Responses WebSocket transport (Codex CLI's supports_websockets): no extra model channel — the openai provider is intercepted in place, with the endpoint and key inherited from the DSH OpenAI provider by default. Explicit plugin overrides require both an endpoint and credentials; local Codex CLI config is not read. The composer WS/SSE button appears only while the current model is an OpenAI one."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Codex WS",
4
- "description": "GPT 模型走 Codex 风格的 Responses WebSocket 传输(对应 Codex CLI 的 supports_websockets):不注册额外渠道,直接在 openai 渠道上拦截生效,接口地址与 Key 自动沿用本地 Codex CLI 配置(~/.codex/config.toml);输入框模型选择器旁的 WS/SSE 按钮仅在当前模型为 OpenAI 时出现。"
4
+ "description": "GPT 模型走 Codex 风格的 Responses WebSocket 传输(对应 Codex CLI 的 supports_websockets):不注册额外渠道,直接在 openai 渠道上拦截生效,接口地址与 Key 默认沿用 DSH 的 OpenAI 渠道配置,插件可显式成对覆盖,不自动读取 Codex CLI 配置;输入框模型选择器旁的 WS/SSE 按钮仅在当前模型为 OpenAI 时出现。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "dsh-codex-ws",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "GPT models over the Codex-style Responses WebSocket transport (like Codex CLI 'supports_websockets'), toggled in place on the openai channel with an in-composer WS/SSE button shown only for OpenAI models.",
5
5
  "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" },
6
6
  "scripts": {
7
- "test": "node test/client.mjs && node test/fast.mjs && node test/projection.mjs && node test/continuation.mjs && DSH_HOME=$(mktemp -d) OPENAI_API_KEY=test-only node test/intercept.mjs"
7
+ "test": "node test/config.mjs && node test/client.mjs && node test/fast.mjs && node test/projection.mjs && node test/continuation.mjs && DSH_HOME=$(mktemp -d) node test/intercept.mjs"
8
8
  },
9
9
  "repository": { "type": "git", "url": "https://github.com/kiritoko1029/dsh-codex-ws.git" },
10
10
  "homepage": "https://github.com/kiritoko1029/dsh-codex-ws#readme",