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.
Files changed (53) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/CONTRIBUTING.en.md +54 -0
  3. package/CONTRIBUTING.md +54 -0
  4. package/LICENSE +201 -0
  5. package/NOTICE +12 -0
  6. package/README.en.md +98 -0
  7. package/README.md +98 -0
  8. package/SECURITY.md +41 -0
  9. package/SUPPORT.md +17 -0
  10. package/THIRD_PARTY_NOTICES.md +25 -0
  11. package/codex-community.patch.yml +13 -0
  12. package/dist/client/index.js +1037 -0
  13. package/dist/host/index.mjs +98 -0
  14. package/dist/internal/authorization-bridge.mjs +662 -0
  15. package/dist/internal/authorization-commit-tracker.mjs +49 -0
  16. package/dist/internal/codex-authorization.mjs +202 -0
  17. package/dist/internal/codex-credential-store.mjs +164 -0
  18. package/dist/internal/codex-identifiers.mjs +4 -0
  19. package/dist/internal/codex-pi-provider.mjs +137 -0
  20. package/dist/internal/codex-provider-runtime.mjs +256 -0
  21. package/dist/internal/codex-route-adapter.mjs +133 -0
  22. package/dist/internal/codex-session-resources.mjs +64 -0
  23. package/dist/internal/failure-normalizer.mjs +456 -0
  24. package/dist/internal/image-policy.mjs +45 -0
  25. package/dist/internal/quota-observer.mjs +142 -0
  26. package/dist/internal/reliability.mjs +12 -0
  27. package/dist/internal/remote-image-input.mjs +801 -0
  28. package/dist/internal/session-preference-command.mjs +52 -0
  29. package/dist/internal/session-preferences.mjs +93 -0
  30. package/dist/internal/stream-resilience.mjs +273 -0
  31. package/docs/README.en.md +15 -0
  32. package/docs/README.md +15 -0
  33. package/docs/architecture.en.md +106 -0
  34. package/docs/architecture.md +106 -0
  35. package/docs/compatibility.en.md +53 -0
  36. package/docs/compatibility.md +53 -0
  37. package/docs/configuration.en.md +61 -0
  38. package/docs/configuration.md +61 -0
  39. package/docs/contribution-sources.en.md +35 -0
  40. package/docs/contribution-sources.md +35 -0
  41. package/docs/github-about.md +15 -0
  42. package/docs/releases/v0.0.1.acceptance.json +160 -0
  43. package/docs/releases/v0.0.1.md +174 -0
  44. package/docs/releasing.en.md +290 -0
  45. package/docs/releasing.md +290 -0
  46. package/docs/testing.en.md +85 -0
  47. package/docs/testing.md +85 -0
  48. package/docs/troubleshooting.en.md +47 -0
  49. package/docs/troubleshooting.md +47 -0
  50. package/package.json +144 -0
  51. package/types/client.d.ts +160 -0
  52. package/types/index.d.ts +22 -0
  53. 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
+ }