@magpie-community/opencode-qoder-auth 0.2.2 → 0.2.4
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 +15 -4
- package/index.mjs +86 -22
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -26,10 +26,21 @@ What is kept: the job token, its refresh token and expiry, the uid, the
|
|
|
26
26
|
device token and a machine id made for this sign-in. OpenCode keeps them in
|
|
27
27
|
`auth.json`; magpie keeps them in `plugin-auth.json`.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
refresh token once, so the
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
Refreshing:
|
|
30
|
+
- Qoder spends a refresh token once, so the plugin never sends one twice.
|
|
31
|
+
- magpie renews the job token 10 minutes before it runs out, through the
|
|
32
|
+
plugin's `auth.refresh`, once for the account and before its requests,
|
|
33
|
+
models and usage need it; magpie saves the new pair. OpenCode doesn't
|
|
34
|
+
call that hook: there the job token is refreshed 5 minutes before it runs
|
|
35
|
+
out, before a request, and the new pair is saved straight away.
|
|
36
|
+
- Only one refresh runs at a time. A request that reads the sign-in before
|
|
37
|
+
magpie saved a renewal uses the renewed pair, and a renewal handed a pair
|
|
38
|
+
a request already spent gives what that request got.
|
|
39
|
+
- The device token, which reads usage, has no end on record: it is rotated
|
|
40
|
+
when Qoder refuses it. On a [Qoder CN](#qoder-cn) device-token account it
|
|
41
|
+
is the chat token too, and is renewed as above.
|
|
42
|
+
- When Qoder refuses a refresh (401 or 403), the account needs signing in
|
|
43
|
+
again. Any other failure is tried again later.
|
|
33
44
|
|
|
34
45
|
## Requests
|
|
35
46
|
|
package/index.mjs
CHANGED
|
@@ -32,7 +32,9 @@ const CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=
|
|
|
32
32
|
const MODELS_PATH = "/algo/api/v2/model/list?Encode=1"
|
|
33
33
|
const COSY_VERSION = "1.1.49"
|
|
34
34
|
const SIGN_IN_TIMEOUT = 15 * 60 * 1000
|
|
35
|
-
const REFRESH_LEAD = 5 * 60 * 1000
|
|
35
|
+
const REFRESH_LEAD = 5 * 60 * 1000 // a chat token this close to its end is refreshed before a request
|
|
36
|
+
const RENEW_LEAD = 10 * 60 * 1000 // and this close, magpie renews it ahead of time (auth.refresh)
|
|
37
|
+
const KEEP_SPENT_MS = 60 * 60 * 1000 // how long what a spent refresh token gave is remembered
|
|
36
38
|
const DAY = 24 * 60 * 60 * 1000
|
|
37
39
|
const CHAT = "@ai-sdk/openai-compatible"
|
|
38
40
|
|
|
@@ -724,6 +726,7 @@ async function* events(body) {
|
|
|
724
726
|
yield { error: failure(Number(env.statusCodeValue), inner || payload) }
|
|
725
727
|
return
|
|
726
728
|
}
|
|
729
|
+
if (inner.trim() === "[DONE]") break
|
|
727
730
|
let chunk
|
|
728
731
|
try {
|
|
729
732
|
chunk = JSON.parse(inner)
|
|
@@ -747,12 +750,19 @@ async function* events(body) {
|
|
|
747
750
|
yield { tool: { index: ++index, id: tc.id || "call_" + hexID(), name: tc.function?.name ?? "", args: tc.function?.arguments ?? "" } }
|
|
748
751
|
} else if (tc.function?.arguments) yield { args: { index, text: tc.function.arguments } }
|
|
749
752
|
}
|
|
750
|
-
fr = chunk?.choices?.[0]?.finish_reason
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
753
|
+
fr = chunk?.choices?.[0]?.finish_reason || fr
|
|
754
|
+
// the usage comes in a chunk of its own after the finish, with no
|
|
755
|
+
// choices, as the built-in read it (qoder.go): every request was
|
|
756
|
+
// counted as 0 tokens when the finish ended the reading
|
|
757
|
+
const u = chunk?.usage
|
|
758
|
+
if (u)
|
|
759
|
+
usage = {
|
|
760
|
+
prompt_tokens: u.prompt_tokens ?? 0,
|
|
761
|
+
completion_tokens: u.completion_tokens ?? 0,
|
|
762
|
+
total_tokens: (u.prompt_tokens ?? 0) + (u.completion_tokens ?? 0),
|
|
763
|
+
...(u.prompt_tokens_details?.cached_tokens ? { prompt_tokens_details: { cached_tokens: u.prompt_tokens_details.cached_tokens } } : {}),
|
|
764
|
+
...(u.completion_tokens_details?.reasoning_tokens ? { completion_tokens_details: { reasoning_tokens: u.completion_tokens_details.reasoning_tokens } } : {}),
|
|
765
|
+
}
|
|
756
766
|
}
|
|
757
767
|
yield* flushed()
|
|
758
768
|
yield { stop: a.sawTool ? "tool_calls" : fr === "length" ? "length" : "stop", usage }
|
|
@@ -842,6 +852,13 @@ function signed(res, renewed) {
|
|
|
842
852
|
return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
|
|
843
853
|
}
|
|
844
854
|
|
|
855
|
+
// changed is the fields of b that differ from a's: what auth.refresh gives.
|
|
856
|
+
function changed(a, b) {
|
|
857
|
+
const out = {}
|
|
858
|
+
for (const k of ["access", "refresh", "expires", "deviceToken", "deviceRefresh"]) if (b[k] !== a[k]) out[k] = b[k]
|
|
859
|
+
return out
|
|
860
|
+
}
|
|
861
|
+
|
|
845
862
|
// ---- usage --------------------------------------------------------------------
|
|
846
863
|
//
|
|
847
864
|
// The account's allowance as magpie's built-in Qoder account shows it
|
|
@@ -995,6 +1012,38 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
995
1012
|
lock = run.catch(() => {})
|
|
996
1013
|
return run
|
|
997
1014
|
}
|
|
1015
|
+
// spent is what each refresh token this process spent gave, by that
|
|
1016
|
+
// token: the chat pair it renewed. magpie saves what auth.refresh gives
|
|
1017
|
+
// only once the hook has returned, and a request may read the account in
|
|
1018
|
+
// between, or the hook be handed the account as it was before a request
|
|
1019
|
+
// renewed it: either follows spent rather than spending the token again.
|
|
1020
|
+
const spent = new Map()
|
|
1021
|
+
const latest = (a) => {
|
|
1022
|
+
for (let i = 0; i < 16 && a?.refresh; i++) {
|
|
1023
|
+
const s = spent.get(a.refresh)
|
|
1024
|
+
if (!s) break
|
|
1025
|
+
a = { ...a, ...s.got }
|
|
1026
|
+
}
|
|
1027
|
+
return a
|
|
1028
|
+
}
|
|
1029
|
+
const spend = (refresh, got) => {
|
|
1030
|
+
const now = Date.now()
|
|
1031
|
+
for (const [k, v] of spent) if (now - v.at > KEEP_SPENT_MS) spent.delete(k)
|
|
1032
|
+
spent.set(refresh, { got, at: now })
|
|
1033
|
+
return got
|
|
1034
|
+
}
|
|
1035
|
+
// renew spends a's chat refresh token, under the lock: the fields it
|
|
1036
|
+
// changes, recorded in spent.
|
|
1037
|
+
const renew = async (a) => {
|
|
1038
|
+
if (a.deviceChat) {
|
|
1039
|
+
// the device token is the chat token: renewed as a device token,
|
|
1040
|
+
// both pairs being the one pair
|
|
1041
|
+
const dt = await refreshDevice(site, a.refresh, true)
|
|
1042
|
+
return spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh })
|
|
1043
|
+
}
|
|
1044
|
+
const jt = await refreshJob(site, a.refresh, a.accountId || a.email || a.uid)
|
|
1045
|
+
return spend(a.refresh, { access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) })
|
|
1046
|
+
}
|
|
998
1047
|
// each account's listing, the model configs a request carries
|
|
999
1048
|
const listings = new Map()
|
|
1000
1049
|
// the accounts fresh renewed: the built-in took the lapse mark off on a
|
|
@@ -1013,19 +1062,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1013
1062
|
// its end.
|
|
1014
1063
|
const fresh = (getAuth) =>
|
|
1015
1064
|
locked(async () => {
|
|
1016
|
-
const
|
|
1017
|
-
if (
|
|
1065
|
+
const stored = await getAuth()
|
|
1066
|
+
if (stored?.type !== "oauth" || !stored.access || !stored.uid) throw new SignInGone(`${site.name}: not signed in`)
|
|
1067
|
+
// renewed already (by magpie's auth.refresh), the store not yet saying so
|
|
1068
|
+
const a = latest(stored)
|
|
1018
1069
|
if (a.expires - Date.now() > REFRESH_LEAD) return a
|
|
1019
|
-
|
|
1020
|
-
if (a.deviceChat) {
|
|
1021
|
-
// the device token is the chat token: renewed as a device token,
|
|
1022
|
-
// both pairs being the one pair
|
|
1023
|
-
const dt = await refreshDevice(site, a.refresh, true)
|
|
1024
|
-
next = { ...a, access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh }
|
|
1025
|
-
} else {
|
|
1026
|
-
const jt = await refreshJob(site, a.refresh, a.accountId || a.email || a.uid)
|
|
1027
|
-
next = { ...a, access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) }
|
|
1028
|
-
}
|
|
1070
|
+
const next = { ...a, ...(await renew(a)) }
|
|
1029
1071
|
await client.auth.set({ path: { id: ID }, body: next })
|
|
1030
1072
|
renewals.add(next)
|
|
1031
1073
|
return next
|
|
@@ -1044,12 +1086,12 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1044
1086
|
// the one saved since, else a rotated one, saved.
|
|
1045
1087
|
const deviceToken = (getAuth, attempted) =>
|
|
1046
1088
|
locked(async () => {
|
|
1047
|
-
const a = await getAuth()
|
|
1089
|
+
const a = latest(await getAuth())
|
|
1048
1090
|
if (a?.deviceToken !== attempted) return a?.deviceToken
|
|
1049
1091
|
const dt = await refreshDevice(site, a.deviceRefresh, !!a.deviceChat)
|
|
1050
1092
|
const next = { ...a, deviceToken: dt.token, deviceRefresh: dt.refresh }
|
|
1051
1093
|
// the pair it spent was the chat pair too
|
|
1052
|
-
if (a.deviceChat) Object.assign(next, { access: dt.token, refresh: dt.refresh, expires: dt.expires })
|
|
1094
|
+
if (a.deviceChat) Object.assign(next, spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh }))
|
|
1053
1095
|
await client.auth.set({ path: { id: ID }, body: next })
|
|
1054
1096
|
return dt.token
|
|
1055
1097
|
})
|
|
@@ -1115,6 +1157,28 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1115
1157
|
return {
|
|
1116
1158
|
auth: {
|
|
1117
1159
|
provider: ID,
|
|
1160
|
+
// magpie renews the chat token RENEW_LEAD before its end, once for
|
|
1161
|
+
// the account, before its requests, models and usage ask for it; the
|
|
1162
|
+
// check before each request (REFRESH_LEAD) stays for OpenCode, which
|
|
1163
|
+
// doesn't call this. The device token of a job-token account has no
|
|
1164
|
+
// end on record: usage rotates it once Qoder refuses it, as before.
|
|
1165
|
+
refreshLead: RENEW_LEAD,
|
|
1166
|
+
async refresh(auth) {
|
|
1167
|
+
if (auth?.type !== "oauth" || !auth.access || !auth.uid || !String(auth.refresh ?? "").trim()) return undefined
|
|
1168
|
+
return locked(async () => {
|
|
1169
|
+
// a request renewed it meanwhile (or magpie hasn't saved this
|
|
1170
|
+
// hook's last answer): what that gave, not a second refresh
|
|
1171
|
+
const a = latest(auth)
|
|
1172
|
+
if (a.access !== auth.access && a.expires - Date.now() > RENEW_LEAD) return changed(auth, a)
|
|
1173
|
+
try {
|
|
1174
|
+
// magpie saves what this gives
|
|
1175
|
+
return changed(auth, { ...a, ...(await renew(a)) })
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
if (e?.expired) throw Object.assign(new Error(e.message), { signIn: "expired" })
|
|
1178
|
+
throw e
|
|
1179
|
+
}
|
|
1180
|
+
})
|
|
1181
|
+
},
|
|
1118
1182
|
async loader(getAuth) {
|
|
1119
1183
|
const a = await getAuth()
|
|
1120
1184
|
if (a?.type !== "oauth") return {}
|
|
@@ -1189,4 +1253,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1189
1253
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1190
1254
|
|
|
1191
1255
|
// for tests
|
|
1192
|
-
export const _internal = { parseUsage, gfmt, when, failure, modelInfo, SITES, decodeBody }
|
|
1256
|
+
export const _internal = { parseUsage, gfmt, when, failure, modelInfo, SITES, decodeBody, events }
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-qoder-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Qoder subscriptions in OpenCode and magpie: Qoder (qoder.com) and Qoder CN (qoder.cn)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
8
|
"files": ["index.mjs", "README.md", "LICENSE-CLIProxyAPI"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "magpie", "qoder", "qoder-cn"],
|
|
10
|
-
"license": "MIT"
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/qoder" }
|
|
11
12
|
}
|