dsh-codex-community 0.0.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 +90 -0
- package/CONTRIBUTING.en.md +54 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.en.md +98 -0
- package/README.md +98 -0
- package/SECURITY.md +41 -0
- package/SUPPORT.md +17 -0
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/codex-community.patch.yml +13 -0
- package/dist/client/index.js +1037 -0
- package/dist/host/index.mjs +98 -0
- package/dist/internal/authorization-bridge.mjs +662 -0
- package/dist/internal/authorization-commit-tracker.mjs +49 -0
- package/dist/internal/codex-authorization.mjs +202 -0
- package/dist/internal/codex-credential-store.mjs +164 -0
- package/dist/internal/codex-identifiers.mjs +4 -0
- package/dist/internal/codex-pi-provider.mjs +137 -0
- package/dist/internal/codex-provider-runtime.mjs +256 -0
- package/dist/internal/codex-route-adapter.mjs +133 -0
- package/dist/internal/codex-session-resources.mjs +64 -0
- package/dist/internal/failure-normalizer.mjs +456 -0
- package/dist/internal/image-policy.mjs +45 -0
- package/dist/internal/quota-observer.mjs +142 -0
- package/dist/internal/reliability.mjs +12 -0
- package/dist/internal/remote-image-input.mjs +801 -0
- package/dist/internal/session-preference-command.mjs +52 -0
- package/dist/internal/session-preferences.mjs +93 -0
- package/dist/internal/stream-resilience.mjs +273 -0
- package/docs/README.en.md +15 -0
- package/docs/README.md +15 -0
- package/docs/architecture.en.md +106 -0
- package/docs/architecture.md +106 -0
- package/docs/compatibility.en.md +53 -0
- package/docs/compatibility.md +53 -0
- package/docs/configuration.en.md +61 -0
- package/docs/configuration.md +61 -0
- package/docs/contribution-sources.en.md +35 -0
- package/docs/contribution-sources.md +35 -0
- package/docs/github-about.md +15 -0
- package/docs/releases/v0.0.1.acceptance.json +160 -0
- package/docs/releases/v0.0.1.md +174 -0
- package/docs/releasing.en.md +290 -0
- package/docs/releasing.md +290 -0
- package/docs/testing.en.md +85 -0
- package/docs/testing.md +85 -0
- package/docs/troubleshooting.en.md +47 -0
- package/docs/troubleshooting.md +47 -0
- package/package.json +144 -0
- package/types/client.d.ts +160 -0
- package/types/index.d.ts +22 -0
- package/types/reliability.d.ts +78 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { PiAiAdapter } from "@deepseek-ai/dsh-llm-pi-ai"
|
|
2
|
+
import { resolveRetryPolicy } from "@deepseek-ai/dsh-llm"
|
|
3
|
+
import {
|
|
4
|
+
installSettingsSection,
|
|
5
|
+
settingsNamespace,
|
|
6
|
+
} from "@deepseek-ai/dsh-settings"
|
|
7
|
+
import Schema from "@deepseek-ai/schemastery"
|
|
8
|
+
|
|
9
|
+
import { registerCodexAuthorizationFlow } from "./codex-authorization.mjs"
|
|
10
|
+
import {
|
|
11
|
+
CODEX_PROVIDER_ID,
|
|
12
|
+
createCodexCredentialStore,
|
|
13
|
+
} from "./codex-credential-store.mjs"
|
|
14
|
+
import { createCodexPiProvider } from "./codex-pi-provider.mjs"
|
|
15
|
+
import {
|
|
16
|
+
CODEX_ROUTE_ID,
|
|
17
|
+
CodexRouteAdapter,
|
|
18
|
+
} from "./codex-route-adapter.mjs"
|
|
19
|
+
import {
|
|
20
|
+
DEFAULT_IMAGE_POLICY,
|
|
21
|
+
resolveImagePolicy,
|
|
22
|
+
} from "./image-policy.mjs"
|
|
23
|
+
|
|
24
|
+
export const CODEX_SETTINGS_NAMESPACE = settingsNamespace("dsh-codex")
|
|
25
|
+
|
|
26
|
+
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 300_000
|
|
27
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647
|
|
28
|
+
const MODEL_SCHEMA = Schema.object({
|
|
29
|
+
id: Schema.string().required(),
|
|
30
|
+
name: Schema.string(),
|
|
31
|
+
contextWindow: Schema.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
|
|
32
|
+
maxTokens: Schema.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const Config = Schema.object({
|
|
36
|
+
partialResponseRecovery: Schema.boolean()
|
|
37
|
+
.default(true)
|
|
38
|
+
.description("Preserve safe partial text instead of replaying an already-visible Codex request"),
|
|
39
|
+
models: Schema.array(MODEL_SCHEMA).default(undefined),
|
|
40
|
+
cacheRetention: Schema.union(["none", "short", "long"]).default("short"),
|
|
41
|
+
streamIdleTimeoutMs: Schema.number()
|
|
42
|
+
.min(1)
|
|
43
|
+
.max(MAX_TIMER_DELAY_MS)
|
|
44
|
+
.default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
45
|
+
maxRequestImageBytes: Schema.number()
|
|
46
|
+
.step(1)
|
|
47
|
+
.min(1)
|
|
48
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
49
|
+
.default(DEFAULT_IMAGE_POLICY.maxRequestImageBytes),
|
|
50
|
+
requestImagePixelBudget: Schema.number()
|
|
51
|
+
.step(1)
|
|
52
|
+
.min(1)
|
|
53
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
54
|
+
.default(DEFAULT_IMAGE_POLICY.requestImagePixelBudget),
|
|
55
|
+
requestImageMaxBytes: Schema.number()
|
|
56
|
+
.step(1)
|
|
57
|
+
.min(1)
|
|
58
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
59
|
+
.default(DEFAULT_IMAGE_POLICY.requestImageMaxBytes),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const RETRY_POLICY = resolveRetryPolicy({
|
|
63
|
+
mode: "normal",
|
|
64
|
+
maxRetries: 2,
|
|
65
|
+
retryableCodes: [
|
|
66
|
+
"EMPTY_RESPONSE",
|
|
67
|
+
"RATE_LIMIT",
|
|
68
|
+
"SERVER",
|
|
69
|
+
"TIMEOUT",
|
|
70
|
+
"TRANSPORT",
|
|
71
|
+
],
|
|
72
|
+
backoff: {
|
|
73
|
+
initialDelayMs: 500,
|
|
74
|
+
maxDelayMs: 10_000,
|
|
75
|
+
jitterRatio: 0.1,
|
|
76
|
+
},
|
|
77
|
+
}, "dsh-codex provider retryPolicy")
|
|
78
|
+
|
|
79
|
+
const DEFAULT_SESSION_PREFERENCES = Object.freeze({ fast: false, transport: "auto" })
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Materialize the one immutable PiAiAdapter profile for a settings snapshot.
|
|
83
|
+
* Model filtering and all request/image bounds are decided here; OAuth and
|
|
84
|
+
* wire behavior stay inside the injected public pi-ai provider.
|
|
85
|
+
*/
|
|
86
|
+
export function createCodexProfile(config, provider = createCodexPiProvider()) {
|
|
87
|
+
if (provider?.id !== CODEX_PROVIDER_ID || provider.auth?.oauth === undefined) {
|
|
88
|
+
throw new TypeError("provider must be the OAuth-capable Codex provider")
|
|
89
|
+
}
|
|
90
|
+
if (config.models !== undefined && !Array.isArray(config.models)) {
|
|
91
|
+
throw new TypeError("models must be omitted or an array")
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isFinite(config.streamIdleTimeoutMs)
|
|
94
|
+
|| config.streamIdleTimeoutMs < 1
|
|
95
|
+
|| config.streamIdleTimeoutMs > MAX_TIMER_DELAY_MS) {
|
|
96
|
+
throw new TypeError(`streamIdleTimeoutMs must be a finite number from 1 through ${MAX_TIMER_DELAY_MS}`)
|
|
97
|
+
}
|
|
98
|
+
const image = resolveImagePolicy(config)
|
|
99
|
+
const models = configuredModels(config.models, provider.getModels())
|
|
100
|
+
const configuredMaxTokens = new Map()
|
|
101
|
+
for (const row of config.models ?? []) {
|
|
102
|
+
if (row.maxTokens !== undefined) configuredMaxTokens.set(row.id, row.maxTokens)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Object.freeze({
|
|
106
|
+
provider: CODEX_PROVIDER_ID,
|
|
107
|
+
displayName: "Codex (ChatGPT OAuth)",
|
|
108
|
+
cacheRetention: config.cacheRetention,
|
|
109
|
+
transport: "auto",
|
|
110
|
+
streamIdleTimeoutMs: config.streamIdleTimeoutMs,
|
|
111
|
+
...image,
|
|
112
|
+
retryPolicy: RETRY_POLICY,
|
|
113
|
+
piProvider: providerWithModels(provider, models),
|
|
114
|
+
configuredMaxTokens,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Install the plugin-owned route, OAuth flow, and settings namespace.
|
|
120
|
+
* The returned interface intentionally exposes only the live resolved config;
|
|
121
|
+
* callers do not coordinate adapter snapshots or credential writes themselves.
|
|
122
|
+
*/
|
|
123
|
+
export function installCodexProviderRuntime(ctx, entryConfig = {}, options = {}) {
|
|
124
|
+
const entry = Config(entryConfig)
|
|
125
|
+
const sessionPreferences = options.sessionPreferences
|
|
126
|
+
const sessionResources = options.sessionResources
|
|
127
|
+
const resolveSessionPreferences = (sessionId) => sessionPreferences?.resolve(sessionId)
|
|
128
|
+
?? DEFAULT_SESSION_PREFERENCES
|
|
129
|
+
const provider = options.provider ?? createCodexPiProvider({
|
|
130
|
+
resolveSessionPreferences,
|
|
131
|
+
...(sessionResources === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: {
|
|
134
|
+
resolveTransportSessionId: (sessionId) => (
|
|
135
|
+
sessionResources.transportSessionId(sessionId)
|
|
136
|
+
),
|
|
137
|
+
}),
|
|
138
|
+
})
|
|
139
|
+
const credentialStore = createCodexCredentialStore(ctx.credentials)
|
|
140
|
+
const authContext = Object.freeze({
|
|
141
|
+
env: async () => undefined,
|
|
142
|
+
fileExists: async () => false,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
let source = () => entry
|
|
146
|
+
let previousConfig
|
|
147
|
+
let previousProfiles
|
|
148
|
+
const profiles = () => {
|
|
149
|
+
const current = source()
|
|
150
|
+
if (current === previousConfig && previousProfiles !== undefined) return previousProfiles
|
|
151
|
+
const profile = createCodexProfile(current, provider)
|
|
152
|
+
previousConfig = current
|
|
153
|
+
previousProfiles = new Map([[CODEX_PROVIDER_ID, profile]])
|
|
154
|
+
return previousProfiles
|
|
155
|
+
}
|
|
156
|
+
profiles()
|
|
157
|
+
|
|
158
|
+
const canonicalAdapter = new PiAiAdapter({
|
|
159
|
+
profiles,
|
|
160
|
+
resolveApiKey: async () => undefined,
|
|
161
|
+
auth: { credentials: credentialStore, authContext },
|
|
162
|
+
resolveAttachments: () => ctx.get?.("attachments"),
|
|
163
|
+
onReplayDegrade: ({ model, reason }) => {
|
|
164
|
+
ctx.logger.warn(`dsh-codex: unusable replay state for ${CODEX_PROVIDER_ID}/${model}; using provider-neutral history (${reason})`)
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
registerCodexAuthorizationFlow({
|
|
169
|
+
ownerContext: ctx,
|
|
170
|
+
authorization: ctx.authorization,
|
|
171
|
+
credentialStore,
|
|
172
|
+
authContext,
|
|
173
|
+
provider,
|
|
174
|
+
commitTracker: options.authorizationCommitTracker,
|
|
175
|
+
})
|
|
176
|
+
const routeAdapter = new CodexRouteAdapter(canonicalAdapter, {
|
|
177
|
+
filterModels(models) {
|
|
178
|
+
const configured = source().models
|
|
179
|
+
if (configured === undefined) return models
|
|
180
|
+
const visible = new Set(configured.map(({ id }) => id))
|
|
181
|
+
return models.filter(({ id }) => visible.has(id))
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
ctx.llm.registerAdapter([CODEX_ROUTE_ID], routeAdapter)
|
|
185
|
+
ctx.llm.registerModelDiscovery(CODEX_SETTINGS_NAMESPACE, (request) => {
|
|
186
|
+
if (request.signal?.aborted === true) {
|
|
187
|
+
return Promise.reject(request.signal.reason ?? new Error("model discovery cancelled"))
|
|
188
|
+
}
|
|
189
|
+
if (request.provider !== undefined && request.provider !== CODEX_ROUTE_ID) {
|
|
190
|
+
return Promise.reject(new Error("dsh-codex model discovery does not own this route"))
|
|
191
|
+
}
|
|
192
|
+
return Promise.resolve(provider.getModels().map((model) => ({
|
|
193
|
+
id: model.id,
|
|
194
|
+
name: model.name,
|
|
195
|
+
contextWindow: model.contextWindow,
|
|
196
|
+
maxTokens: model.maxTokens,
|
|
197
|
+
})))
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
installSettingsSection(ctx, CODEX_SETTINGS_NAMESPACE, Config, entry, {
|
|
201
|
+
validate: (candidate) => {
|
|
202
|
+
createCodexProfile(candidate, provider)
|
|
203
|
+
},
|
|
204
|
+
setSource(current) {
|
|
205
|
+
source = current
|
|
206
|
+
},
|
|
207
|
+
onChange() {
|
|
208
|
+
// PiAiAdapter reads the profile map once per operation; invalid settings
|
|
209
|
+
// are rejected by validate before this source can become authoritative.
|
|
210
|
+
profiles()
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
return Object.freeze({ getConfig: () => source() })
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function configuredModels(configured, catalog) {
|
|
218
|
+
const byId = new Map(catalog.map((model) => [model.id, model]))
|
|
219
|
+
if (configured === undefined) return [...catalog]
|
|
220
|
+
|
|
221
|
+
const seen = new Set()
|
|
222
|
+
const overrides = new Map()
|
|
223
|
+
for (const [index, row] of configured.entries()) {
|
|
224
|
+
for (const field of ["name", "contextWindow", "maxTokens"]) {
|
|
225
|
+
if (row[field] === null) throw new TypeError(`models[${index}].${field} must not be null`)
|
|
226
|
+
}
|
|
227
|
+
if (seen.has(row.id)) throw new Error(`Codex model "${row.id}" is listed more than once`)
|
|
228
|
+
seen.add(row.id)
|
|
229
|
+
const base = byId.get(row.id)
|
|
230
|
+
if (base === undefined) throw new Error(`unknown Codex model "${row.id}"`)
|
|
231
|
+
overrides.set(row.id, Object.freeze({
|
|
232
|
+
...base,
|
|
233
|
+
...(row.name === undefined ? {} : { name: row.name }),
|
|
234
|
+
...(row.contextWindow === undefined ? {} : { contextWindow: row.contextWindow }),
|
|
235
|
+
...(row.maxTokens === undefined ? {} : { maxTokens: row.maxTokens }),
|
|
236
|
+
}))
|
|
237
|
+
}
|
|
238
|
+
return catalog.map((model) => overrides.get(model.id) ?? model)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function providerWithModels(provider, models) {
|
|
242
|
+
const selected = Object.freeze([...models])
|
|
243
|
+
return Object.freeze({
|
|
244
|
+
id: provider.id,
|
|
245
|
+
name: provider.name,
|
|
246
|
+
...(provider.baseUrl === undefined ? {} : { baseUrl: provider.baseUrl }),
|
|
247
|
+
...(provider.headers === undefined ? {} : { headers: provider.headers }),
|
|
248
|
+
auth: provider.auth,
|
|
249
|
+
getModels: () => selected,
|
|
250
|
+
...(provider.filterModels === undefined
|
|
251
|
+
? {}
|
|
252
|
+
: { filterModels: (entries, credential) => provider.filterModels(entries, credential) }),
|
|
253
|
+
stream: (model, context, options) => provider.stream(model, context, options),
|
|
254
|
+
streamSimple: (model, context, options) => provider.streamSimple(model, context, options),
|
|
255
|
+
})
|
|
256
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LlmAdapter,
|
|
3
|
+
LlmError,
|
|
4
|
+
freezeMessage,
|
|
5
|
+
} from "@deepseek-ai/dsh-llm"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CODEX_PROVIDER_ID,
|
|
9
|
+
CODEX_ROUTE_ID,
|
|
10
|
+
} from "./codex-identifiers.mjs"
|
|
11
|
+
|
|
12
|
+
export { CODEX_ROUTE_ID } from "./codex-identifiers.mjs"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Expose one plugin-owned Harness route while keeping pi-ai's native provider
|
|
16
|
+
* identity at the wire/replay boundary. The Harness owns the external route;
|
|
17
|
+
* PiAiAdapter continues to own conversion, tool-call correlation, and replay.
|
|
18
|
+
*/
|
|
19
|
+
export class CodexRouteAdapter extends LlmAdapter {
|
|
20
|
+
constructor(delegate, options = {}) {
|
|
21
|
+
super()
|
|
22
|
+
if (delegate === null || typeof delegate !== "object") {
|
|
23
|
+
throw new TypeError("delegate must be an LLM adapter")
|
|
24
|
+
}
|
|
25
|
+
for (const method of [
|
|
26
|
+
"providerInfo",
|
|
27
|
+
"providerRetryPolicy",
|
|
28
|
+
"listModels",
|
|
29
|
+
"resolveModel",
|
|
30
|
+
"prepareCall",
|
|
31
|
+
"stream",
|
|
32
|
+
]) {
|
|
33
|
+
if (typeof delegate[method] !== "function") {
|
|
34
|
+
throw new TypeError(`delegate.${method} must be a function`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
options === null
|
|
39
|
+
|| typeof options !== "object"
|
|
40
|
+
|| Array.isArray(options)
|
|
41
|
+
|| Object.keys(options).some((key) => key !== "filterModels")
|
|
42
|
+
) {
|
|
43
|
+
throw new TypeError("options must contain only filterModels")
|
|
44
|
+
}
|
|
45
|
+
if (options.filterModels !== undefined && typeof options.filterModels !== "function") {
|
|
46
|
+
throw new TypeError("filterModels must be a function")
|
|
47
|
+
}
|
|
48
|
+
this.delegate = delegate
|
|
49
|
+
this.filterModels = options.filterModels
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
providerInfo(provider) {
|
|
53
|
+
this.#assertRoute(provider)
|
|
54
|
+
const info = this.delegate.providerInfo(CODEX_PROVIDER_ID)
|
|
55
|
+
return { ...info, id: CODEX_ROUTE_ID }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
providerRetryPolicy(provider) {
|
|
59
|
+
this.#assertRoute(provider)
|
|
60
|
+
return this.delegate.providerRetryPolicy(CODEX_PROVIDER_ID)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async listModels(provider) {
|
|
64
|
+
this.#assertRoute(provider)
|
|
65
|
+
const catalog = await this.delegate.listModels(CODEX_PROVIDER_ID)
|
|
66
|
+
const visible = this.filterModels === undefined
|
|
67
|
+
? catalog
|
|
68
|
+
: this.filterModels(catalog)
|
|
69
|
+
if (!Array.isArray(visible)) throw new TypeError("filterModels must return an array")
|
|
70
|
+
return visible
|
|
71
|
+
.map((model) => ({ ...model, provider: CODEX_ROUTE_ID }))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async resolveModel(provider, model, signal) {
|
|
75
|
+
this.#assertRoute(provider)
|
|
76
|
+
return {
|
|
77
|
+
...await this.delegate.resolveModel(CODEX_PROVIDER_ID, model, signal),
|
|
78
|
+
provider: CODEX_ROUTE_ID,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async prepareCall(provider, model, signal) {
|
|
83
|
+
this.#assertRoute(provider)
|
|
84
|
+
const prepared = await this.delegate.prepareCall(
|
|
85
|
+
CODEX_PROVIDER_ID,
|
|
86
|
+
model,
|
|
87
|
+
signal,
|
|
88
|
+
)
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
model: Object.freeze({ ...prepared.model, provider: CODEX_ROUTE_ID }),
|
|
91
|
+
stream: (options) => prepared.stream(this.#canonicalOptions(options)),
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
stream(options) {
|
|
96
|
+
return this.delegate.stream(this.#canonicalOptions(options))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#canonicalOptions(options) {
|
|
100
|
+
this.#assertRoute(options?.provider)
|
|
101
|
+
return {
|
|
102
|
+
...options,
|
|
103
|
+
provider: CODEX_PROVIDER_ID,
|
|
104
|
+
messages: options.messages.map(toCanonicalHistoryMessage),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#assertRoute(provider) {
|
|
109
|
+
if (provider !== CODEX_ROUTE_ID) {
|
|
110
|
+
throw new LlmError(
|
|
111
|
+
`dsh-codex adapter does not own provider "${String(provider)}"`,
|
|
112
|
+
"NO_ADAPTER",
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toCanonicalHistoryMessage(message) {
|
|
119
|
+
if (
|
|
120
|
+
message.role !== "assistant"
|
|
121
|
+
|| message.source.kind !== "model"
|
|
122
|
+
|| message.source.provider !== CODEX_ROUTE_ID
|
|
123
|
+
) {
|
|
124
|
+
return message
|
|
125
|
+
}
|
|
126
|
+
return freezeMessage({
|
|
127
|
+
...message,
|
|
128
|
+
source: {
|
|
129
|
+
...message.source,
|
|
130
|
+
provider: CODEX_PROVIDER_ID,
|
|
131
|
+
},
|
|
132
|
+
})
|
|
133
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeOpenAICodexWebSocketSessions,
|
|
3
|
+
resetOpenAICodexWebSocketDebugStats,
|
|
4
|
+
} from "@earendil-works/pi-ai/api/openai-codex-responses"
|
|
5
|
+
|
|
6
|
+
const TRANSPORT_SESSION_PREFIX = "dsh-codex:"
|
|
7
|
+
|
|
8
|
+
/** Keep pi-ai transport/cache state distinct from other consumers of the same session id. */
|
|
9
|
+
export function codexTransportSessionId(sessionId) {
|
|
10
|
+
if (sessionId === undefined) return undefined
|
|
11
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
12
|
+
throw new TypeError("transport session id must be a non-empty string")
|
|
13
|
+
}
|
|
14
|
+
return `${TRANSPORT_SESSION_PREFIX}${sessionId}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function clearTransportSession(sessionId) {
|
|
18
|
+
try {
|
|
19
|
+
closeOpenAICodexWebSocketSessions(sessionId)
|
|
20
|
+
} finally {
|
|
21
|
+
resetOpenAICodexWebSocketDebugStats(sessionId)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Own only the public pi-ai transport state created by this plugin instance. */
|
|
26
|
+
export function createCodexSessionResourceManager() {
|
|
27
|
+
let disposed = false
|
|
28
|
+
const owned = new Set()
|
|
29
|
+
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
transportSessionId(sessionId) {
|
|
32
|
+
const resolved = codexTransportSessionId(sessionId)
|
|
33
|
+
if (resolved === undefined) return undefined
|
|
34
|
+
if (disposed) throw new Error("Codex session resources are disposed")
|
|
35
|
+
owned.add(resolved)
|
|
36
|
+
return resolved
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
reset(sessionId) {
|
|
40
|
+
if (disposed) return
|
|
41
|
+
const resolved = codexTransportSessionId(sessionId)
|
|
42
|
+
if (resolved === undefined) return
|
|
43
|
+
owned.delete(resolved)
|
|
44
|
+
clearTransportSession(resolved)
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
dispose() {
|
|
48
|
+
if (disposed) return
|
|
49
|
+
disposed = true
|
|
50
|
+
const errors = []
|
|
51
|
+
for (const sessionId of owned) {
|
|
52
|
+
try {
|
|
53
|
+
clearTransportSession(sessionId)
|
|
54
|
+
} catch (error) {
|
|
55
|
+
errors.push(error)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
owned.clear()
|
|
59
|
+
if (errors.length > 0) {
|
|
60
|
+
throw new AggregateError(errors, "Failed to clear Codex session resources")
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
}
|