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,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CODEX_CREDENTIAL_KEY,
|
|
3
|
+
CODEX_PROVIDER_ID,
|
|
4
|
+
} from "./codex-credential-store.mjs"
|
|
5
|
+
|
|
6
|
+
const MAX_MESSAGE_CHARS = 2_048
|
|
7
|
+
const MAX_PLACEHOLDER_CHARS = 512
|
|
8
|
+
const MAX_CODE_CHARS = 256
|
|
9
|
+
const MAX_URL_CHARS = 2_048
|
|
10
|
+
const MAX_OPTIONS = 64
|
|
11
|
+
|
|
12
|
+
/** Register the sole OAuth flow that writes the plugin-owned Codex grant. */
|
|
13
|
+
export function registerCodexAuthorizationFlow({
|
|
14
|
+
ownerContext,
|
|
15
|
+
authorization,
|
|
16
|
+
credentialStore,
|
|
17
|
+
authContext,
|
|
18
|
+
provider,
|
|
19
|
+
commitTracker,
|
|
20
|
+
}) {
|
|
21
|
+
void authContext
|
|
22
|
+
if (provider?.id !== CODEX_PROVIDER_ID || provider.auth?.oauth === undefined) {
|
|
23
|
+
throw new TypeError("provider must be the OAuth-capable Codex provider")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let closing = false
|
|
27
|
+
const flow = {
|
|
28
|
+
key: CODEX_CREDENTIAL_KEY,
|
|
29
|
+
label: "Codex (ChatGPT OAuth)",
|
|
30
|
+
methods: [{ id: "oauth", label: "使用 ChatGPT 登录 / Sign in with ChatGPT" }],
|
|
31
|
+
async run(session) {
|
|
32
|
+
if (closing) {
|
|
33
|
+
await waitForAbort(session.signal)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
const trackedAttempt = commitTracker?.begin()
|
|
37
|
+
try {
|
|
38
|
+
const credential = await provider.auth.oauth.login({
|
|
39
|
+
signal: session.signal,
|
|
40
|
+
notify(event) {
|
|
41
|
+
const notice = authorizationNotice(event)
|
|
42
|
+
if (notice !== undefined) session.notify(notice)
|
|
43
|
+
},
|
|
44
|
+
prompt(prompt) {
|
|
45
|
+
return session.prompt(authorizationPrompt(prompt))
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
if (session.signal.aborted) return
|
|
49
|
+
|
|
50
|
+
// The serialized mutate callback is the cancellation linearization point.
|
|
51
|
+
// Before it selects a replacement, abort is a no-op. After it selects one,
|
|
52
|
+
// this write owns the lock and must finish: a later compensating delete
|
|
53
|
+
// cannot distinguish this attempt from a sign-in already queued by another
|
|
54
|
+
// DSH process and could erase that newer grant. The tracker lets plugin
|
|
55
|
+
// surfaces reject a too-late cancel until authorization/settled fires.
|
|
56
|
+
await credentialStore.modify(CODEX_PROVIDER_ID, () => {
|
|
57
|
+
if (session.signal.aborted) return undefined
|
|
58
|
+
if (trackedAttempt !== undefined && !trackedAttempt.selectCommit()) return undefined
|
|
59
|
+
return credential
|
|
60
|
+
})
|
|
61
|
+
} finally {
|
|
62
|
+
trackedAttempt?.finish()
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const rootContext = ownerContext?.root
|
|
68
|
+
if (rootContext === undefined || rootContext === ownerContext) {
|
|
69
|
+
return authorization.registerFlow(flow)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// DSH rc.2 withdraws a flow and aborts its running controller in the same
|
|
73
|
+
// disposer. A selected credential write is already irreversible, so keep
|
|
74
|
+
// the flow under the app root and give the plugin one ordered cleanup that
|
|
75
|
+
// waits for the public authorization settlement before withdrawing it.
|
|
76
|
+
const rootAuthorization = rootContext.authorization
|
|
77
|
+
const settlementWaiters = new Set()
|
|
78
|
+
const disposeSettlementListener = rootContext.on(
|
|
79
|
+
"authorization/settled",
|
|
80
|
+
(key) => {
|
|
81
|
+
if (key !== CODEX_CREDENTIAL_KEY) return
|
|
82
|
+
commitTracker?.settle()
|
|
83
|
+
for (const resolve of settlementWaiters) resolve()
|
|
84
|
+
settlementWaiters.clear()
|
|
85
|
+
},
|
|
86
|
+
{ global: true },
|
|
87
|
+
)
|
|
88
|
+
let disposeFlow
|
|
89
|
+
let disposeLifecycle
|
|
90
|
+
try {
|
|
91
|
+
disposeFlow = rootAuthorization.registerFlow(flow)
|
|
92
|
+
disposeLifecycle = ownerContext.effect(
|
|
93
|
+
() => async () => {
|
|
94
|
+
closing = true
|
|
95
|
+
try {
|
|
96
|
+
while (rootAuthorization.describe(CODEX_CREDENTIAL_KEY)?.inFlight === true) {
|
|
97
|
+
const settled = new Promise((resolve) => { settlementWaiters.add(resolve) })
|
|
98
|
+
if (commitTracker?.isCommitPending() !== true) {
|
|
99
|
+
rootAuthorization.cancel(CODEX_CREDENTIAL_KEY)
|
|
100
|
+
}
|
|
101
|
+
await settled
|
|
102
|
+
}
|
|
103
|
+
} finally {
|
|
104
|
+
disposeFlow()
|
|
105
|
+
disposeSettlementListener()
|
|
106
|
+
for (const resolve of settlementWaiters) resolve()
|
|
107
|
+
settlementWaiters.clear()
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"dsh-codex: quiescent authorization flow",
|
|
111
|
+
)
|
|
112
|
+
} catch (error) {
|
|
113
|
+
disposeFlow?.()
|
|
114
|
+
disposeSettlementListener()
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
return disposeLifecycle
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function waitForAbort(signal) {
|
|
121
|
+
if (signal.aborted) return Promise.resolve()
|
|
122
|
+
return new Promise((resolve) => signal.addEventListener("abort", resolve, { once: true }))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function authorizationNotice(event) {
|
|
126
|
+
switch (event?.type) {
|
|
127
|
+
case "auth_url":
|
|
128
|
+
return compact({
|
|
129
|
+
message: bounded(event.instructions ?? "在浏览器中继续授权。 / Continue authorization in your browser.", MAX_MESSAGE_CHARS),
|
|
130
|
+
url: safeAuthorizationUrl(event.url),
|
|
131
|
+
})
|
|
132
|
+
case "device_code":
|
|
133
|
+
return compact({
|
|
134
|
+
message: "在浏览器中继续授权。 / Continue authorization in your browser.",
|
|
135
|
+
url: safeAuthorizationUrl(event.verificationUri),
|
|
136
|
+
code: bounded(event.userCode, MAX_CODE_CHARS),
|
|
137
|
+
})
|
|
138
|
+
case "info":
|
|
139
|
+
return compact({
|
|
140
|
+
message: bounded(event.message, MAX_MESSAGE_CHARS),
|
|
141
|
+
url: safeAuthorizationUrl(event.links?.[0]?.url),
|
|
142
|
+
})
|
|
143
|
+
case "progress":
|
|
144
|
+
return { message: bounded(event.message, MAX_MESSAGE_CHARS) }
|
|
145
|
+
default:
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function authorizationPrompt(prompt) {
|
|
151
|
+
const signal = prompt.signal === undefined ? {} : { signal: prompt.signal }
|
|
152
|
+
if (prompt.type === "select") {
|
|
153
|
+
return {
|
|
154
|
+
...signal,
|
|
155
|
+
kind: "select",
|
|
156
|
+
message: bounded(prompt.message, MAX_MESSAGE_CHARS),
|
|
157
|
+
options: prompt.options.slice(0, MAX_OPTIONS).map((option) => compact({
|
|
158
|
+
id: bounded(option.id, MAX_CODE_CHARS),
|
|
159
|
+
label: bounded(option.label, MAX_MESSAGE_CHARS),
|
|
160
|
+
description: option.description === undefined
|
|
161
|
+
? undefined
|
|
162
|
+
: bounded(option.description, MAX_MESSAGE_CHARS),
|
|
163
|
+
})),
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return compact({
|
|
167
|
+
...signal,
|
|
168
|
+
kind: prompt.type === "secret" ? "secret" : "text",
|
|
169
|
+
message: bounded(prompt.message, MAX_MESSAGE_CHARS),
|
|
170
|
+
placeholder: prompt.placeholder === undefined
|
|
171
|
+
? undefined
|
|
172
|
+
: bounded(prompt.placeholder, MAX_PLACEHOLDER_CHARS),
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function safeAuthorizationUrl(value) {
|
|
177
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_URL_CHARS) return undefined
|
|
178
|
+
try {
|
|
179
|
+
const url = new URL(value)
|
|
180
|
+
if (url.username !== "" || url.password !== "") return undefined
|
|
181
|
+
if (url.protocol === "https:") return url.href
|
|
182
|
+
if (url.protocol === "http:" && isLoopback(url.hostname)) return url.href
|
|
183
|
+
} catch {
|
|
184
|
+
// Invalid URLs are omitted from the neutral notice.
|
|
185
|
+
}
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isLoopback(hostname) {
|
|
190
|
+
return hostname === "localhost"
|
|
191
|
+
|| hostname === "[::1]"
|
|
192
|
+
|| /^127(?:\.\d{1,3}){3}$/u.test(hostname)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function bounded(value, maxLength) {
|
|
196
|
+
const text = String(value)
|
|
197
|
+
return text.length <= maxLength ? text : text.slice(0, maxLength)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function compact(value) {
|
|
201
|
+
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined))
|
|
202
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CODEX_CREDENTIAL_KEY,
|
|
3
|
+
CODEX_PROVIDER_ID,
|
|
4
|
+
} from "./codex-identifiers.mjs"
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
CODEX_CREDENTIAL_KEY,
|
|
8
|
+
CODEX_PROVIDER_ID,
|
|
9
|
+
} from "./codex-identifiers.mjs"
|
|
10
|
+
|
|
11
|
+
const MAX_JSON_DEPTH = 24
|
|
12
|
+
const MAX_JSON_NODES = 8_192
|
|
13
|
+
const MAX_STRING_CHARS = 1_048_576
|
|
14
|
+
|
|
15
|
+
export class CodexCredentialStoreError extends Error {
|
|
16
|
+
constructor(message, code = "INVALID_CODEX_CREDENTIAL", options) {
|
|
17
|
+
super(message, options)
|
|
18
|
+
this.name = "CodexCredentialStoreError"
|
|
19
|
+
this.code = code
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Adapt the Harness credential-record seam to pi-ai's provider-scoped store.
|
|
25
|
+
*
|
|
26
|
+
* The adapter owns exactly one record. It never enumerates or interprets any
|
|
27
|
+
* other plugin's records, and it accepts OAuth grants only: an API key cannot
|
|
28
|
+
* accidentally become an alternate authentication path for this provider.
|
|
29
|
+
*/
|
|
30
|
+
export function createCodexCredentialStore(credentials) {
|
|
31
|
+
if (credentials === null || typeof credentials !== "object") {
|
|
32
|
+
throw new TypeError("credentials must be a credential provider")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
async read(providerId) {
|
|
37
|
+
if (providerId !== CODEX_PROVIDER_ID) return undefined
|
|
38
|
+
return credentialFromRecord(await credentials.readRecord(CODEX_CREDENTIAL_KEY), true)
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async list() {
|
|
42
|
+
const info = await credentials.describeRecord(CODEX_CREDENTIAL_KEY)
|
|
43
|
+
if (info.configured !== true) return []
|
|
44
|
+
if (info.kind !== "grant") {
|
|
45
|
+
throw invalidCredential("stored Codex credential is not an OAuth grant")
|
|
46
|
+
}
|
|
47
|
+
return [{ providerId: CODEX_PROVIDER_ID, type: "oauth" }]
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async modify(providerId, mutate) {
|
|
51
|
+
if (providerId !== CODEX_PROVIDER_ID) {
|
|
52
|
+
throw new CodexCredentialStoreError(
|
|
53
|
+
"the Codex credential store does not own this provider",
|
|
54
|
+
"UNOWNED_PROVIDER",
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
if (typeof mutate !== "function") throw new TypeError("mutate must be a function")
|
|
58
|
+
|
|
59
|
+
const result = await credentials.modifyRecord(CODEX_CREDENTIAL_KEY, async (current) => {
|
|
60
|
+
// An explicit sign-in may repair an incompatible record. Request-time
|
|
61
|
+
// refresh still fails closed because a callback returning undefined
|
|
62
|
+
// leaves the incompatible record untouched and the final decode rejects.
|
|
63
|
+
const decoded = credentialFromRecord(current, false)
|
|
64
|
+
const next = await mutate(decoded)
|
|
65
|
+
return next === undefined ? undefined : recordFromCredential(next)
|
|
66
|
+
})
|
|
67
|
+
return credentialFromRecord(result, true)
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
async delete(providerId) {
|
|
71
|
+
if (providerId !== CODEX_PROVIDER_ID) return
|
|
72
|
+
await credentials.deleteRecord(CODEX_CREDENTIAL_KEY)
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function credentialFromRecord(record, strict) {
|
|
78
|
+
if (record === undefined) return undefined
|
|
79
|
+
if (record?.kind !== "grant") {
|
|
80
|
+
if (!strict) return undefined
|
|
81
|
+
throw invalidCredential("stored Codex credential is not an OAuth grant")
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
return oauthCredential(record.payload)
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!strict) return undefined
|
|
88
|
+
if (error instanceof CodexCredentialStoreError) throw error
|
|
89
|
+
throw invalidCredential("stored Codex OAuth grant is invalid", error)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function recordFromCredential(credential) {
|
|
94
|
+
return { kind: "grant", payload: oauthCredential(credential) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function oauthCredential(value) {
|
|
98
|
+
const payload = cloneJson(value)
|
|
99
|
+
if (!plainRecord(payload) || payload.type !== "oauth") {
|
|
100
|
+
throw invalidCredential("Codex credentials must use OAuth")
|
|
101
|
+
}
|
|
102
|
+
for (const field of ["access", "refresh"]) {
|
|
103
|
+
if (typeof payload[field] !== "string" || payload[field].length === 0) {
|
|
104
|
+
throw invalidCredential(`Codex OAuth credential is missing ${field}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!Number.isSafeInteger(payload.expires) || payload.expires <= 0) {
|
|
108
|
+
throw invalidCredential("Codex OAuth credential has an invalid expiry")
|
|
109
|
+
}
|
|
110
|
+
return payload
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cloneJson(value) {
|
|
114
|
+
const state = { nodes: 0, stack: new Set() }
|
|
115
|
+
validateJson(value, state, 0)
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(JSON.stringify(value))
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw invalidCredential("Codex OAuth credential is not JSON compatible", error)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function validateJson(value, state, depth) {
|
|
124
|
+
state.nodes += 1
|
|
125
|
+
if (state.nodes > MAX_JSON_NODES || depth > MAX_JSON_DEPTH) {
|
|
126
|
+
throw invalidCredential("Codex OAuth credential exceeds structural limits")
|
|
127
|
+
}
|
|
128
|
+
if (value === null || typeof value === "boolean") return
|
|
129
|
+
if (typeof value === "number") {
|
|
130
|
+
if (!Number.isFinite(value)) throw invalidCredential("Codex OAuth credential contains an invalid number")
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === "string") {
|
|
134
|
+
if (value.length > MAX_STRING_CHARS) throw invalidCredential("Codex OAuth credential contains an oversized string")
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
if (typeof value !== "object") {
|
|
138
|
+
throw invalidCredential("Codex OAuth credential contains a non-JSON value")
|
|
139
|
+
}
|
|
140
|
+
if (state.stack.has(value)) throw invalidCredential("Codex OAuth credential contains a cycle")
|
|
141
|
+
const prototype = Object.getPrototypeOf(value)
|
|
142
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
143
|
+
throw invalidCredential("Codex OAuth credential contains a non-plain object")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
state.stack.add(value)
|
|
147
|
+
if (Array.isArray(value)) {
|
|
148
|
+
for (const item of value) validateJson(item, state, depth + 1)
|
|
149
|
+
} else {
|
|
150
|
+
for (const [key, item] of Object.entries(value)) {
|
|
151
|
+
if (key.length > MAX_STRING_CHARS) throw invalidCredential("Codex OAuth credential contains an oversized key")
|
|
152
|
+
validateJson(item, state, depth + 1)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
state.stack.delete(value)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function plainRecord(value) {
|
|
159
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function invalidCredential(message, cause) {
|
|
163
|
+
return new CodexCredentialStoreError(message, "INVALID_CODEX_CREDENTIAL", cause === undefined ? undefined : { cause })
|
|
164
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clampThinkingLevel,
|
|
3
|
+
createProvider,
|
|
4
|
+
} from "@earendil-works/pi-ai"
|
|
5
|
+
import { buildBaseOptions } from "@earendil-works/pi-ai/api/simple-options"
|
|
6
|
+
import { stream as streamCodexResponses } from "@earendil-works/pi-ai/api/openai-codex-responses"
|
|
7
|
+
import { openaiCodexProvider } from "@earendil-works/pi-ai/providers/openai-codex"
|
|
8
|
+
|
|
9
|
+
import { codexTransportSessionId } from "./codex-session-resources.mjs"
|
|
10
|
+
|
|
11
|
+
const FACTORY_OPTION_KEYS = new Set([
|
|
12
|
+
"resolveSessionPreferences",
|
|
13
|
+
"resolveTransportSessionId",
|
|
14
|
+
"serviceTier",
|
|
15
|
+
])
|
|
16
|
+
const TRANSPORTS = new Set([
|
|
17
|
+
"auto",
|
|
18
|
+
"sse",
|
|
19
|
+
"websocket",
|
|
20
|
+
"websocket-cached",
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
function plainObject(value) {
|
|
24
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false
|
|
25
|
+
const prototype = Object.getPrototypeOf(value)
|
|
26
|
+
return prototype === Object.prototype || prototype === null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateFactoryOptions(options) {
|
|
30
|
+
if (!plainObject(options)) throw new TypeError("options must be a plain object")
|
|
31
|
+
for (const key of Object.keys(options)) {
|
|
32
|
+
if (!FACTORY_OPTION_KEYS.has(key)) throw new TypeError(`unknown option: ${key}`)
|
|
33
|
+
}
|
|
34
|
+
if (options.serviceTier !== undefined && options.serviceTier !== "priority") {
|
|
35
|
+
throw new TypeError("serviceTier must be priority when provided")
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
options.resolveSessionPreferences !== undefined
|
|
39
|
+
&& typeof options.resolveSessionPreferences !== "function"
|
|
40
|
+
) {
|
|
41
|
+
throw new TypeError("resolveSessionPreferences must be a function")
|
|
42
|
+
}
|
|
43
|
+
if (
|
|
44
|
+
options.resolveTransportSessionId !== undefined
|
|
45
|
+
&& typeof options.resolveTransportSessionId !== "function"
|
|
46
|
+
) {
|
|
47
|
+
throw new TypeError("resolveTransportSessionId must be a function")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function resolveSessionPreferenceOptions(resolver, sessionId) {
|
|
52
|
+
const preferences = resolver(sessionId) ?? { fast: false }
|
|
53
|
+
if (!plainObject(preferences)) {
|
|
54
|
+
throw new TypeError("session preferences must be a plain object")
|
|
55
|
+
}
|
|
56
|
+
for (const key of Object.keys(preferences)) {
|
|
57
|
+
if (key !== "fast" && key !== "transport") {
|
|
58
|
+
throw new TypeError(`unknown session preference: ${key}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const fast = preferences.fast ?? false
|
|
62
|
+
if (typeof fast !== "boolean") {
|
|
63
|
+
throw new TypeError("session preferences fast must be a boolean")
|
|
64
|
+
}
|
|
65
|
+
const transport = preferences.transport
|
|
66
|
+
if (transport !== undefined && !TRANSPORTS.has(transport)) {
|
|
67
|
+
throw new TypeError("session preferences transport is invalid")
|
|
68
|
+
}
|
|
69
|
+
return { fast, transport }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function reasoningOptions(model, reasoning) {
|
|
73
|
+
if (reasoning === undefined) return {}
|
|
74
|
+
const effort = clampThinkingLevel(model, reasoning)
|
|
75
|
+
return effort === "off" ? {} : { reasoningEffort: effort }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the public pi-ai Codex provider with an optional per-session Fast policy.
|
|
80
|
+
* Authentication, models, transports, and response conversion remain owned by
|
|
81
|
+
* the published provider and PiAiAdapter; only the documented service tier is
|
|
82
|
+
* added to the provider-specific request options.
|
|
83
|
+
*/
|
|
84
|
+
export function createCodexPiProvider(options = {}) {
|
|
85
|
+
validateFactoryOptions(options)
|
|
86
|
+
const source = openaiCodexProvider()
|
|
87
|
+
const resolveSessionPreferences = options.resolveSessionPreferences
|
|
88
|
+
const resolveTransportSessionId = options.resolveTransportSessionId
|
|
89
|
+
?? codexTransportSessionId
|
|
90
|
+
|
|
91
|
+
return createProvider({
|
|
92
|
+
id: source.id,
|
|
93
|
+
name: source.name,
|
|
94
|
+
baseUrl: source.baseUrl,
|
|
95
|
+
headers: source.headers,
|
|
96
|
+
auth: source.auth,
|
|
97
|
+
models: source.getModels(),
|
|
98
|
+
...(source.filterModels === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: {
|
|
101
|
+
filterModels: (models, credential) => source.filterModels(models, credential),
|
|
102
|
+
}),
|
|
103
|
+
api: {
|
|
104
|
+
stream: streamCodexResponses,
|
|
105
|
+
streamSimple(model, context, streamOptions = {}) {
|
|
106
|
+
const sessionPreferences = resolveSessionPreferences === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: resolveSessionPreferenceOptions(
|
|
109
|
+
resolveSessionPreferences,
|
|
110
|
+
streamOptions.sessionId,
|
|
111
|
+
)
|
|
112
|
+
const serviceTier = sessionPreferences === undefined
|
|
113
|
+
? options.serviceTier
|
|
114
|
+
: sessionPreferences.fast
|
|
115
|
+
? "priority"
|
|
116
|
+
: undefined
|
|
117
|
+
const transportSessionId = resolveTransportSessionId(streamOptions.sessionId)
|
|
118
|
+
if (
|
|
119
|
+
transportSessionId !== undefined
|
|
120
|
+
&& (typeof transportSessionId !== "string" || transportSessionId.length === 0)
|
|
121
|
+
) {
|
|
122
|
+
throw new TypeError("resolved transport session id must be a non-empty string")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return streamCodexResponses(model, context, {
|
|
126
|
+
...buildBaseOptions(model, context, streamOptions),
|
|
127
|
+
sessionId: transportSessionId,
|
|
128
|
+
...reasoningOptions(model, streamOptions.reasoning),
|
|
129
|
+
...(serviceTier === undefined ? {} : { serviceTier }),
|
|
130
|
+
...(sessionPreferences?.transport === undefined
|
|
131
|
+
? {}
|
|
132
|
+
: { transport: sessionPreferences.transport }),
|
|
133
|
+
})
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
}
|