mohdel 3.5.0 → 3.6.0
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 +3 -2
- package/js/chatgpt/bin.js +29 -0
- package/js/chatgpt/index.js +7 -3
- package/js/chatgpt/store.js +9 -1
- package/js/session/adapters/chatgpt.js +14 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -152,8 +152,9 @@ reading them. Mohdel is built so neither is present.
|
|
|
152
152
|
starts a temporary HTTP callback listener on `127.0.0.1` with PKCE and state validation.
|
|
153
153
|
- **Session credentials come from the caller.** The provider credential rides
|
|
154
154
|
on each envelope and goes to the SDK. Optional ChatGPT sign-in saves OAuth
|
|
155
|
-
credentials in the caller's user-data directory
|
|
156
|
-
that store
|
|
155
|
+
credentials in the caller's user-data directory. The standalone gate and its
|
|
156
|
+
sessions never read that store. Only an embedder that opts into `ChatGptAuth`
|
|
157
|
+
reads it, through mohdel's helper.
|
|
157
158
|
- **The session subprocess starts from an empty environment.** It is given
|
|
158
159
|
back only what the runtime reads — `PATH`, proxy and TLS settings, mohdel's
|
|
159
160
|
own dials, `OTEL_*`. Every `*_API_SK`, cloud credential and database URL the
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Access-token helper for a supervisor that cannot run the OAuth refresh
|
|
4
|
+
* itself (thin-gate's `ChatGptAuth`). Invoked as
|
|
5
|
+
* `node <path-to-this-file> access [--account <id>]`.
|
|
6
|
+
*
|
|
7
|
+
* stdout: `{"accessToken": "…", "refreshAt": <epoch ms>}`, exit 0.
|
|
8
|
+
* stderr: the error message, exit 1. The token never reaches stderr.
|
|
9
|
+
*
|
|
10
|
+
* @module chatgpt/bin
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createChatGPT } from './index.js'
|
|
14
|
+
|
|
15
|
+
const USAGE = 'usage: node bin.js access [--account <id>]'
|
|
16
|
+
|
|
17
|
+
async function main (args) {
|
|
18
|
+
const [verb, flag, id, ...rest] = args
|
|
19
|
+
if (verb !== 'access' || rest.length || (flag !== undefined && (flag !== '--account' || !id))) {
|
|
20
|
+
throw new Error(USAGE)
|
|
21
|
+
}
|
|
22
|
+
const { accessToken, refreshAt } = await createChatGPT().access(id)
|
|
23
|
+
process.stdout.write(JSON.stringify({ accessToken, refreshAt }))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
main(process.argv.slice(2)).catch(err => {
|
|
27
|
+
process.stderr.write(err.message)
|
|
28
|
+
process.exitCode = 1
|
|
29
|
+
})
|
package/js/chatgpt/index.js
CHANGED
|
@@ -11,6 +11,7 @@ const PERMISSION = 'chatgpt.tokens.use.direct'
|
|
|
11
11
|
const SCOPE = `openid profile email offline_access resource.invoke ${PERMISSION}`
|
|
12
12
|
const jwks = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`))
|
|
13
13
|
const random = () => randomBytes(32).toString('base64url')
|
|
14
|
+
const REFRESH_MARGIN = 30000
|
|
14
15
|
export const usageURL = 'https://chatgpt.com/settings/usage'
|
|
15
16
|
|
|
16
17
|
async function jsonRequest (fetcher, url, options = {}) {
|
|
@@ -92,18 +93,21 @@ export function createChatGPT (options = {}) {
|
|
|
92
93
|
await save(store)
|
|
93
94
|
})
|
|
94
95
|
|
|
95
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* `refreshAt` is when this token stops being handed out; a caller may reuse it until then.
|
|
98
|
+
* @param {string} [id] @returns {Promise<{accessToken: string, accountId: string, refreshAt: number}>}
|
|
99
|
+
*/
|
|
96
100
|
const access = async (id) => withStore(directory, async (store, save) => {
|
|
97
101
|
const account = store.accounts[id ?? store.active]
|
|
98
102
|
if (!account?.access_token) throw new Error('ChatGPT is not connected; run mo chatgpt login')
|
|
99
103
|
if (!account.scopes?.includes(PERMISSION)) throw new Error('ChatGPT plan usage is not enabled; run mo chatgpt login and grant plan usage')
|
|
100
|
-
if (account.expires_at <= Date.now() +
|
|
104
|
+
if (account.expires_at <= Date.now() + REFRESH_MARGIN) {
|
|
101
105
|
const tokens = await tokenRequest({ grant_type: 'refresh_token', client_id: account.client_id, refresh_token: account.refresh_token })
|
|
102
106
|
Object.assign(account, credentials(tokens, account))
|
|
103
107
|
await save(store)
|
|
104
108
|
if (!account.scopes.includes(PERMISSION)) throw new Error('ChatGPT plan usage permission was removed; reconnect with mo chatgpt login')
|
|
105
109
|
}
|
|
106
|
-
return { accessToken: account.access_token, accountId: account.client_id }
|
|
110
|
+
return { accessToken: account.access_token, accountId: account.client_id, refreshAt: account.expires_at - REFRESH_MARGIN }
|
|
107
111
|
})
|
|
108
112
|
|
|
109
113
|
/** @param {string} [id] @returns {Promise<Array<{id: string, model: string, label: string}>>} */
|
package/js/chatgpt/store.js
CHANGED
|
@@ -7,12 +7,20 @@ import envPaths from 'env-paths'
|
|
|
7
7
|
export const defaultDirectory = () => join(envPaths('mohdel', { suffix: null }).data, 'chatgpt')
|
|
8
8
|
|
|
9
9
|
export async function readStore (directory) {
|
|
10
|
+
const path = join(directory, 'accounts.json')
|
|
11
|
+
let text
|
|
10
12
|
try {
|
|
11
|
-
|
|
13
|
+
text = await readFile(path, 'utf8')
|
|
12
14
|
} catch (err) {
|
|
13
15
|
if (err.code === 'ENOENT') return { host: `urn:uuid:${randomUUID()}`, active: null, accounts: {} }
|
|
14
16
|
throw err
|
|
15
17
|
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(text)
|
|
20
|
+
} catch {
|
|
21
|
+
// V8's parse error quotes the input, which here holds tokens.
|
|
22
|
+
throw new Error(`ChatGPT credentials are not valid JSON: ${path}`)
|
|
23
|
+
}
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
export async function writeStore (directory, store) {
|
|
@@ -7,6 +7,19 @@ import { getSpec } from './_catalog.js'
|
|
|
7
7
|
import { discoverModels } from '../../chatgpt/models.js'
|
|
8
8
|
import { abortedDone } from './_aborted.js'
|
|
9
9
|
|
|
10
|
+
// A token belongs to one account, so its model list only changes with the token.
|
|
11
|
+
const discovered = new Map()
|
|
12
|
+
const DISCOVERED_TOKENS = 8
|
|
13
|
+
|
|
14
|
+
async function accountModels (key, deps) {
|
|
15
|
+
const cached = discovered.get(key)
|
|
16
|
+
if (cached) return cached
|
|
17
|
+
const models = await discoverModels(key, deps)
|
|
18
|
+
discovered.set(key, models)
|
|
19
|
+
if (discovered.size > DISCOVERED_TOKENS) discovered.delete(discovered.keys().next().value)
|
|
20
|
+
return models
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
/**
|
|
11
24
|
* Uses only the caller's OAuth access token, including behind the gate.
|
|
12
25
|
* Session subprocesses never read the host's saved ChatGPT account.
|
|
@@ -18,7 +31,7 @@ export async function * chatgpt (envelope, deps = {}) {
|
|
|
18
31
|
const start = String(process.hrtime.bigint())
|
|
19
32
|
const model = getSpec(catalogKey(envelope.model))?.model ?? bareOf(catalogKey(envelope.model))
|
|
20
33
|
try {
|
|
21
|
-
const models = await
|
|
34
|
+
const models = await accountModels(key, deps)
|
|
22
35
|
if (!models.some(m => m.slug === model)) {
|
|
23
36
|
yield { type: 'error', error: { type: 'INVALID_REQUEST', severity: 'error', message: 'This model is not available to the selected ChatGPT account.', retryable: false } }
|
|
24
37
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mohdel",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Christophe Le Bars",
|
|
@@ -46,6 +46,10 @@
|
|
|
46
46
|
"types": "./js/chatgpt/index.d.ts",
|
|
47
47
|
"default": "./js/chatgpt/index.js"
|
|
48
48
|
},
|
|
49
|
+
"./chatgpt/bin": {
|
|
50
|
+
"types": "./js/chatgpt/bin.d.ts",
|
|
51
|
+
"default": "./js/chatgpt/bin.js"
|
|
52
|
+
},
|
|
49
53
|
".": {
|
|
50
54
|
"types": "./src/lib/index.d.ts",
|
|
51
55
|
"default": "./src/lib/index.js"
|
|
@@ -140,7 +144,7 @@
|
|
|
140
144
|
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
141
145
|
"@opentelemetry/sdk-node": "^0.222.0",
|
|
142
146
|
"chalk": "^6.0.1",
|
|
143
|
-
"mohdel-thin-gate-linux-x64-gnu": "3.
|
|
147
|
+
"mohdel-thin-gate-linux-x64-gnu": "3.6.0"
|
|
144
148
|
},
|
|
145
149
|
"dependencies": {
|
|
146
150
|
"@anthropic-ai/sdk": "^0.129.0",
|