@magpie-community/opencode-qoder-auth 0.2.5 → 0.2.7
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 +21 -0
- package/index.mjs +57 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,6 +151,27 @@ Neither magpie's built-in Qoder CN nor this plugin has been checked against
|
|
|
151
151
|
every kind of qoder.cn account. If a sign-in or a chat fails, please open
|
|
152
152
|
an issue with the error.
|
|
153
153
|
|
|
154
|
+
## Daily check-in
|
|
155
|
+
|
|
156
|
+
Qoder gives credits for a daily claim (the Qoder client's campaign
|
|
157
|
+
`CLAIM_BENEFIT`, 100 Credits a day as reported), on both sites. magpie can
|
|
158
|
+
press it once a day for each account when its Qoder check-in switch is on
|
|
159
|
+
(Settings). It asks these pages through this plugin's fetch (since 0.2.7),
|
|
160
|
+
which sends them as the account, on the device token (`Bearer`,
|
|
161
|
+
`Cosy-ClientType: 10`) as usage is read; a refused device token is
|
|
162
|
+
rotated once and the page asked again.
|
|
163
|
+
|
|
164
|
+
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
165
|
+
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
|
166
|
+
`startAt`, `endAt`, `benefit.amount`.
|
|
167
|
+
- **Claim:** `POST <openapi>/sash/api/v1/me/campaigns/{id}/claim`, body
|
|
168
|
+
`{}`: `status` `CLAIMED` (or under `data`).
|
|
169
|
+
- `<openapi>` is `openapi.qoder.sh` (Qoder) or `openapi.qoder.com.cn`
|
|
170
|
+
(Qoder CN). Other pages on those hosts are still refused.
|
|
171
|
+
|
|
172
|
+
Taken from [wallechfox/qoder-checkin](https://github.com/wallechfox/qoder-checkin),
|
|
173
|
+
which a user reports works. Not yet tried here with a real Qoder account.
|
|
174
|
+
|
|
154
175
|
## Not here
|
|
155
176
|
|
|
156
177
|
- Qoder's usage and quota display.
|
package/index.mjs
CHANGED
|
@@ -576,11 +576,14 @@ function qoderMessages(msgs) {
|
|
|
576
576
|
// (Qoder: changing the Context "may change the rate"), so it is asked for
|
|
577
577
|
// only when the conversation needs it. The request's size is counted
|
|
578
578
|
// generously, a token for every 3 bytes of its UTF-8 JSON, so a window is
|
|
579
|
-
// raised a little early rather than late
|
|
580
|
-
|
|
579
|
+
// raised a little early rather than late, and the reply's max_tokens is
|
|
580
|
+
// counted in it: a window holds the prompt and the reply, so a
|
|
581
|
+
// conversation of 180K asking 32K for the reply is sent 400K, not the 200K
|
|
582
|
+
// it would overflow while magpie says the model takes 1M (yetone/magpie#700).
|
|
583
|
+
function windowFor(chat, m, reply = 0) {
|
|
581
584
|
const base = m.defaultWindow || (m.config?.max_input_tokens > 0 ? m.config.max_input_tokens : 0) || m.context
|
|
582
585
|
if (!m.windows?.length) return m.context
|
|
583
|
-
const need = Math.ceil(Buffer.byteLength(JSON.stringify({ m: chat.messages ?? [], t: chat.tools ?? [] })) / 3)
|
|
586
|
+
const need = Math.ceil(Buffer.byteLength(JSON.stringify({ m: chat.messages ?? [], t: chat.tools ?? [] })) / 3) + Math.max(0, reply)
|
|
584
587
|
if (base > 0 && need <= base) return base
|
|
585
588
|
return m.windows.find((w) => w >= need) ?? m.windows[m.windows.length - 1]
|
|
586
589
|
}
|
|
@@ -596,7 +599,7 @@ function qoderBody(chat, m) {
|
|
|
596
599
|
const [thinking, effort] = effortFor(chat.reasoning_effort, m)
|
|
597
600
|
const params = { enable_thinking: thinking, max_tokens: chat.max_completion_tokens || chat.max_tokens || 32000 }
|
|
598
601
|
if (effort) params.reasoning_effort = effort
|
|
599
|
-
const window = windowFor(chat, m)
|
|
602
|
+
const window = windowFor(chat, m, params.max_tokens)
|
|
600
603
|
if (window > 0) params.context_length = window
|
|
601
604
|
const body = {
|
|
602
605
|
parameters: params,
|
|
@@ -950,6 +953,33 @@ async function fetchUsage(site, deviceToken) {
|
|
|
950
953
|
return env
|
|
951
954
|
}
|
|
952
955
|
|
|
956
|
+
// CAMPAIGNS is where Qoder lists the account's campaigns — the daily
|
|
957
|
+
// credits among them (actionType CLAIM_BENEFIT) — and claims one
|
|
958
|
+
// (POST …/{id}/claim), as Qoder's client does, on the device token.
|
|
959
|
+
const CAMPAIGNS = "/sash/api/v1/me/campaigns"
|
|
960
|
+
|
|
961
|
+
// campaignPage: url is the site's campaigns page or a claim under it, which
|
|
962
|
+
// the account's fetch sends as the account rather than as a chat.
|
|
963
|
+
function campaignPage(site, url) {
|
|
964
|
+
try {
|
|
965
|
+
const u = new URL(url)
|
|
966
|
+
return u.origin === new URL(site.openapi).origin && (u.pathname === CAMPAIGNS || /^\/sash\/api\/v1\/me\/campaigns\/[^/]+\/claim$/.test(u.pathname))
|
|
967
|
+
} catch {
|
|
968
|
+
return false
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// campaignCall is url asked with the device token; Qoder's answer comes back
|
|
973
|
+
// as it is.
|
|
974
|
+
async function campaignCall(url, method, body, deviceToken, signal) {
|
|
975
|
+
return fetch(url, {
|
|
976
|
+
method,
|
|
977
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}`, "Cosy-ClientType": "10", "User-Agent": "Qoder" },
|
|
978
|
+
body: method === "GET" ? undefined : body || "{}",
|
|
979
|
+
signal: signal ?? AbortSignal.timeout(20_000),
|
|
980
|
+
})
|
|
981
|
+
}
|
|
982
|
+
|
|
953
983
|
// refreshDevice trades the device refresh token for a new pair; it rotates
|
|
954
984
|
// too, so the caller saves it.
|
|
955
985
|
async function refreshDevice(site, refresh, chat = false) {
|
|
@@ -1171,6 +1201,27 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1171
1201
|
}
|
|
1172
1202
|
}
|
|
1173
1203
|
|
|
1204
|
+
// campaign is a campaigns page (the daily check-in) asked as the account:
|
|
1205
|
+
// a refused device token is rotated once, as usage does, and asked again
|
|
1206
|
+
const campaign = async (getAuth, input, url, init) => {
|
|
1207
|
+
const body = await bodyText(input, init)
|
|
1208
|
+
let cred
|
|
1209
|
+
try {
|
|
1210
|
+
cred = await fresh(getAuth)
|
|
1211
|
+
} catch (e) {
|
|
1212
|
+
return signedInError(e)
|
|
1213
|
+
}
|
|
1214
|
+
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1215
|
+
try {
|
|
1216
|
+
let res = await campaignCall(url, method, body, cred.deviceToken, init.signal)
|
|
1217
|
+
if (res.status === 401 || res.status === 403) res = await campaignCall(url, method, body, await deviceToken(getAuth, cred.deviceToken), init.signal)
|
|
1218
|
+
return signed(res, renewed(cred))
|
|
1219
|
+
} catch (e) {
|
|
1220
|
+
if (e?.expired) return signedInError(e)
|
|
1221
|
+
return errorResponse({ status: 502, message: e?.message ?? String(e) })
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1174
1225
|
const signedInError = (e) =>
|
|
1175
1226
|
errorResponse({
|
|
1176
1227
|
status: e instanceof SignInGone ? 401 : 502,
|
|
@@ -1241,6 +1292,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1241
1292
|
// every chat completion written as Qoder's own request
|
|
1242
1293
|
async fetch(input, init = {}) {
|
|
1243
1294
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
|
1295
|
+
if (campaignPage(site, url)) return campaign(getAuth, input, url, init)
|
|
1244
1296
|
if (!/\/chat\/completions$/.test(new URL(url).pathname))
|
|
1245
1297
|
return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1246
1298
|
let chat
|
|
@@ -1306,4 +1358,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1306
1358
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1307
1359
|
|
|
1308
1360
|
// for tests
|
|
1309
|
-
export const _internal = { parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
|
|
1361
|
+
export const _internal = { campaignPage, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
|
package/package.json
CHANGED