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 +18 -9
- package/cordis.patch.yml +2 -2
- package/index.js +98 -23
- package/lib/config.js +36 -73
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
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
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
24
|
-
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
#
|
|
3
|
-
#
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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,
|
|
61
|
-
const { settings, transport } =
|
|
62
|
-
if (settings.
|
|
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:
|
|
69
|
-
code: '
|
|
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
|
-
/**
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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 {
|
|
156
|
-
if (transport === 'sse' && !
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
return interceptedStream(options,
|
|
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
|
-
*
|
|
7
|
-
*
|
|
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
|
-
'
|
|
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
|
|
33
|
+
'Bearer API Key。默认沿用 DSH OpenAI 渠道;显式覆盖须同时填写 baseURL。覆盖模式下可通过 envFile / apiKeyEnv 提供 Key。Key 仅在 Host 使用。',
|
|
36
34
|
},
|
|
37
35
|
},
|
|
38
|
-
7: { type: 'string', meta: { default: '
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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)
|
|
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",
|