@magpie-community/opencode-qoder-auth 0.2.6 → 0.2.8
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 +52 -0
- package/index.mjs +187 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,6 +151,58 @@ 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
|
+
### Enterprise VPC
|
|
155
|
+
|
|
156
|
+
A Qoder CN enterprise on its own VPC instance signs in with **Sign in with
|
|
157
|
+
Qoder CN Enterprise (VPC)** (since 0.2.8). It asks for the instance: its
|
|
158
|
+
name (`acme`) or one of its addresses (`acme.vpc.qoder.com.cn`,
|
|
159
|
+
`https://acme.vpc.qoder.com.cn`). Only https and hosts under
|
|
160
|
+
`vpc.qoder.com.cn` are taken.
|
|
161
|
+
|
|
162
|
+
The account then uses the instance's own hosts, as Qoder CN's CLI
|
|
163
|
+
(`@qodercn-ai/qoderclicn` 1.1.64) does once a VPC instance is set. The
|
|
164
|
+
paths, client id and protocol are the public ones:
|
|
165
|
+
|
|
166
|
+
| | Public Qoder CN | VPC instance `acme` |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| Sign-in page | qoder.cn | acme.vpc.qoder.com.cn |
|
|
169
|
+
| Accounts: poll, tokens, user info, usage | openapi.qoder.com.cn | acme-openapi.vpc.qoder.com.cn |
|
|
170
|
+
| Models and chat | gateway.qoder.com.cn | acme-gateway.vpc.qoder.com.cn |
|
|
171
|
+
|
|
172
|
+
- The instance is kept with the account, so its refreshes, models, usage
|
|
173
|
+
and check-in go to the instance too. An account kept with an instance
|
|
174
|
+
that isn't one is asked to sign in again; it is never sent to the public
|
|
175
|
+
hosts.
|
|
176
|
+
- The account is named `<email> (<instance>)`, so it is listed apart from
|
|
177
|
+
the same person's public account.
|
|
178
|
+
- An enterprise's usage shows as the Enterprise plan.
|
|
179
|
+
- **Sign in with Qoder CN** is unchanged and still the first way. An
|
|
180
|
+
account signed in without an instance works as before.
|
|
181
|
+
|
|
182
|
+
The hosts and reply shapes come from a real VPC account's report
|
|
183
|
+
(yetone/magpie#312). This plugin has not been tried with a VPC account.
|
|
184
|
+
|
|
185
|
+
## Daily check-in
|
|
186
|
+
|
|
187
|
+
Qoder gives credits for a daily claim (the Qoder client's campaign
|
|
188
|
+
`CLAIM_BENEFIT`, 100 Credits a day as reported), on both sites. magpie can
|
|
189
|
+
press it once a day for each account when its Qoder check-in switch is on
|
|
190
|
+
(Settings). It asks these pages through this plugin's fetch (since 0.2.7),
|
|
191
|
+
which sends them as the account, on the device token (`Bearer`,
|
|
192
|
+
`Cosy-ClientType: 10`) as usage is read; a refused device token is
|
|
193
|
+
rotated once and the page asked again.
|
|
194
|
+
|
|
195
|
+
- **List:** `GET <openapi>/sash/api/v1/me/campaigns`: `campaigns[]` with
|
|
196
|
+
`campaignId`, `actionType`, `claimStatus` (`CLAIMABLE`, `CLAIMED`),
|
|
197
|
+
`startAt`, `endAt`, `benefit.amount`.
|
|
198
|
+
- **Claim:** `POST <openapi>/sash/api/v1/me/campaigns/{id}/claim`, body
|
|
199
|
+
`{}`: `status` `CLAIMED` (or under `data`).
|
|
200
|
+
- `<openapi>` is `openapi.qoder.sh` (Qoder) or `openapi.qoder.com.cn`
|
|
201
|
+
(Qoder CN). Other pages on those hosts are still refused.
|
|
202
|
+
|
|
203
|
+
Taken from [wallechfox/qoder-checkin](https://github.com/wallechfox/qoder-checkin),
|
|
204
|
+
which a user reports works. Not yet tried here with a real Qoder account.
|
|
205
|
+
|
|
154
206
|
## Not here
|
|
155
207
|
|
|
156
208
|
- Qoder's usage and quota display.
|
package/index.mjs
CHANGED
|
@@ -25,6 +25,15 @@
|
|
|
25
25
|
// gateway.qoder.com.cn, with the device-flow client id that CLI sends in
|
|
26
26
|
// production and no redirect_uri. That CLI chats on the device token itself;
|
|
27
27
|
// if qoder.cn won't trade it for a job token, the account does the same.
|
|
28
|
+
//
|
|
29
|
+
// A Qoder CN enterprise VPC account (yetone/magpie#312) is signed in to with
|
|
30
|
+
// its own method, which asks for the enterprise's instance name. Its calls go
|
|
31
|
+
// to the same paths on the instance's own hosts, as Qoder CN's CLI 1.1.64
|
|
32
|
+
// rewrites them once a VPC instance is set: qoder.cn becomes
|
|
33
|
+
// <instance>.vpc.qoder.com.cn, openapi.qoder.com.cn becomes
|
|
34
|
+
// <instance>-openapi.vpc.qoder.com.cn, and gateway.qoder.com.cn becomes
|
|
35
|
+
// <instance>-gateway.vpc.qoder.com.cn. The instance is kept on the account,
|
|
36
|
+
// and an account with none is served as before.
|
|
28
37
|
import { createCipheriv, createHash, publicEncrypt, randomBytes, randomUUID, constants } from "node:crypto"
|
|
29
38
|
import { STATUS_CODES } from "node:http"
|
|
30
39
|
|
|
@@ -180,6 +189,59 @@ function deviceExpiry(dt) {
|
|
|
180
189
|
return Date.now() + DAY
|
|
181
190
|
}
|
|
182
191
|
|
|
192
|
+
// ---- enterprise VPC (Qoder CN) --------------------------------------------------
|
|
193
|
+
|
|
194
|
+
const VPC_NAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i
|
|
195
|
+
|
|
196
|
+
// vpcInstance is the instance named by what the user entered, as Qoder CN's
|
|
197
|
+
// CLI reads its VPC endpoint: a bare name ("acme"), or one of the instance's
|
|
198
|
+
// hosts (acme.vpc.qoder.com.cn, acme-openapi.…, acme-gateway.…), here also as
|
|
199
|
+
// an https:// address with no path. Anything else (http://, an IP, another
|
|
200
|
+
// domain, a path or query) names none: undefined.
|
|
201
|
+
function vpcInstance(input, domain) {
|
|
202
|
+
let e = String(input ?? "").trim().toLowerCase()
|
|
203
|
+
if (!e || !domain) return undefined
|
|
204
|
+
if (/^[a-z][a-z\d+.-]*:\/\//.test(e)) {
|
|
205
|
+
let u
|
|
206
|
+
try {
|
|
207
|
+
u = new URL(e)
|
|
208
|
+
} catch {
|
|
209
|
+
return undefined
|
|
210
|
+
}
|
|
211
|
+
if (u.protocol !== "https:" || u.username || u.password || u.port || u.search || u.hash || (u.pathname !== "" && u.pathname !== "/")) return undefined
|
|
212
|
+
if (e.endsWith("/")) e = e.slice(0, -1)
|
|
213
|
+
if (u.hostname + "" !== e.slice("https://".length)) return undefined
|
|
214
|
+
e = u.hostname
|
|
215
|
+
}
|
|
216
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(e)) return undefined
|
|
217
|
+
let name = e
|
|
218
|
+
if (e.endsWith("." + domain)) {
|
|
219
|
+
name = e.slice(0, e.length - domain.length - 1)
|
|
220
|
+
if (name.endsWith("-openapi") || name.endsWith("-gateway")) name = name.slice(0, -8)
|
|
221
|
+
} else if (e.includes(".")) return undefined
|
|
222
|
+
return VPC_NAME.test(name) ? name : undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// vpcSite is site served from a VPC instance's own hosts, the paths
|
|
226
|
+
// unchanged; undefined when the site has no VPC or instance names none.
|
|
227
|
+
function vpcSite(site, instance) {
|
|
228
|
+
const name = vpcInstance(instance, site.vpcDomain)
|
|
229
|
+
if (!name) return undefined
|
|
230
|
+
const d = site.vpcDomain
|
|
231
|
+
return { ...site, web: `https://${name}.${d}`, openapi: `https://${name}-openapi.${d}`, api: `https://${name}-gateway.${d}`, vpc: name }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// siteOf is the site an account is served from: its VPC instance's when it
|
|
235
|
+
// was signed in to one, else site. An instance on record that names none
|
|
236
|
+
// is not read as "no VPC", which would send the enterprise's tokens to the
|
|
237
|
+
// public hosts: the account has to be signed in to again.
|
|
238
|
+
function siteOf(site, a) {
|
|
239
|
+
if (a?.vpc === undefined || a?.vpc === null || a?.vpc === "") return site
|
|
240
|
+
const s = vpcSite(site, a.vpc)
|
|
241
|
+
if (!s) throw new SignInGone(`${site.name}: the account's VPC instance "${a.vpc}" isn't one; sign in again`)
|
|
242
|
+
return s
|
|
243
|
+
}
|
|
244
|
+
|
|
183
245
|
async function deviceSignIn(site) {
|
|
184
246
|
const verifier = b64url(randomBytes(64))
|
|
185
247
|
const challenge = b64url(createHash("sha256").update(verifier).digest())
|
|
@@ -217,20 +279,29 @@ async function deviceSignIn(site) {
|
|
|
217
279
|
} else throw new Error(`${site.name} job token: status ${jr.status}: ${jr.text.trim().slice(0, 300)}`)
|
|
218
280
|
let email = ""
|
|
219
281
|
let name = ""
|
|
282
|
+
let uid = dt.user_id
|
|
220
283
|
try {
|
|
221
284
|
const ui = await openapi(site, "/api/v1/userinfo", { token: dt.token })
|
|
222
|
-
if (ui.status >= 200 && ui.status < 300)
|
|
285
|
+
if (ui.status >= 200 && ui.status < 300) {
|
|
286
|
+
;[email, name] = [ui.json?.email ?? "", ui.json?.name ?? ""]
|
|
287
|
+
// a VPC poll may give the token alone: the user is then the one
|
|
288
|
+
// user info names (id, or its user_id / uid aliases)
|
|
289
|
+
if (site.vpc && !String(uid ?? "").trim()) uid = String(ui.json?.id ?? ui.json?.user_id ?? ui.json?.uid ?? "") || undefined
|
|
290
|
+
}
|
|
223
291
|
} catch {}
|
|
292
|
+
if (site.vpc && !String(uid ?? "").trim()) throw new Error(`${site.name} (${site.vpc}): the sign-in named no user`)
|
|
224
293
|
return {
|
|
225
294
|
type: "success",
|
|
226
295
|
...chat,
|
|
227
|
-
|
|
228
|
-
uid: dt.user_id,
|
|
296
|
+
// a VPC account is told apart from the same person's public one
|
|
297
|
+
accountId: site.vpc ? `${email || uid} (${site.vpc})` : email || dt.user_id,
|
|
298
|
+
uid,
|
|
229
299
|
email,
|
|
230
300
|
name: name || dt.user_name || "",
|
|
231
301
|
machineId,
|
|
232
302
|
deviceToken: dt.token,
|
|
233
303
|
deviceRefresh: dt.refresh_token ?? "",
|
|
304
|
+
...(site.vpc ? { vpc: site.vpc } : {}),
|
|
234
305
|
}
|
|
235
306
|
},
|
|
236
307
|
}
|
|
@@ -953,6 +1024,48 @@ async function fetchUsage(site, deviceToken) {
|
|
|
953
1024
|
return env
|
|
954
1025
|
}
|
|
955
1026
|
|
|
1027
|
+
// CAMPAIGNS is where Qoder lists the account's campaigns — the daily
|
|
1028
|
+
// credits among them (actionType CLAIM_BENEFIT) — and claims one
|
|
1029
|
+
// (POST …/{id}/claim), as Qoder's client does, on the device token.
|
|
1030
|
+
const CAMPAIGNS = "/sash/api/v1/me/campaigns"
|
|
1031
|
+
|
|
1032
|
+
// campaignPage: url is the site's campaigns page or a claim under it, which
|
|
1033
|
+
// the account's fetch sends as the account rather than as a chat.
|
|
1034
|
+
function campaignPage(site, url) {
|
|
1035
|
+
try {
|
|
1036
|
+
const u = new URL(url)
|
|
1037
|
+
return u.origin === new URL(site.openapi).origin && (u.pathname === CAMPAIGNS || /^\/sash\/api\/v1\/me\/campaigns\/[^/]+\/claim$/.test(u.pathname))
|
|
1038
|
+
} catch {
|
|
1039
|
+
return false
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// campaignTarget is where a campaigns page url is asked for an account
|
|
1044
|
+
// served from s: url itself on s's accounts host, or, on a VPC account, the
|
|
1045
|
+
// same page on its instance when url names the public host (magpie's
|
|
1046
|
+
// check-in knows only that), as Qoder CN's CLI rewrites it. null: url isn't
|
|
1047
|
+
// one.
|
|
1048
|
+
function campaignTarget(base, s, url) {
|
|
1049
|
+
if (campaignPage(s, url)) return url
|
|
1050
|
+
if (s === base || !campaignPage(base, url)) return null
|
|
1051
|
+
const u = new URL(url)
|
|
1052
|
+
const to = new URL(s.openapi)
|
|
1053
|
+
u.protocol = to.protocol
|
|
1054
|
+
u.host = to.host
|
|
1055
|
+
return u.href
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// campaignCall is url asked with the device token; Qoder's answer comes back
|
|
1059
|
+
// as it is.
|
|
1060
|
+
async function campaignCall(url, method, body, deviceToken, signal) {
|
|
1061
|
+
return fetch(url, {
|
|
1062
|
+
method,
|
|
1063
|
+
headers: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${deviceToken}`, "Cosy-ClientType": "10", "User-Agent": "Qoder" },
|
|
1064
|
+
body: method === "GET" ? undefined : body || "{}",
|
|
1065
|
+
signal: signal ?? AbortSignal.timeout(20_000),
|
|
1066
|
+
})
|
|
1067
|
+
}
|
|
1068
|
+
|
|
956
1069
|
// refreshDevice trades the device refresh token for a new pair; it rotates
|
|
957
1070
|
// too, so the caller saves it.
|
|
958
1071
|
async function refreshDevice(site, refresh, chat = false) {
|
|
@@ -1053,13 +1166,15 @@ const SITES = {
|
|
|
1053
1166
|
api: "https://gateway.qoder.com.cn",
|
|
1054
1167
|
redirect: "", // Qoder CN's CLI sends none
|
|
1055
1168
|
deviceChat: true, // a refused job token falls back to the CLI's device-token chat
|
|
1169
|
+
// an enterprise's own instance, <instance>.vpc.qoder.com.cn and its
|
|
1170
|
+
// -openapi and -gateway hosts, as Qoder CN's CLI builds it in
|
|
1171
|
+
vpcDomain: "vpc.qoder.com.cn",
|
|
1056
1172
|
models: CN_MODELS,
|
|
1057
1173
|
},
|
|
1058
1174
|
}
|
|
1059
1175
|
|
|
1060
1176
|
const makePlugin = (site) => async ({ client }) => {
|
|
1061
1177
|
const ID = site.id
|
|
1062
|
-
const CHAT_URL = site.api + CHAT_PATH
|
|
1063
1178
|
// serializes checking, rotating and saving tokens: a refresh token is
|
|
1064
1179
|
// spent once, so two refreshes would spend it twice
|
|
1065
1180
|
let lock = Promise.resolve()
|
|
@@ -1091,13 +1206,14 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1091
1206
|
// renew spends a's chat refresh token, under the lock: the fields it
|
|
1092
1207
|
// changes, recorded in spent.
|
|
1093
1208
|
const renew = async (a) => {
|
|
1209
|
+
const s = siteOf(site, a)
|
|
1094
1210
|
if (a.deviceChat) {
|
|
1095
1211
|
// the device token is the chat token: renewed as a device token,
|
|
1096
1212
|
// both pairs being the one pair
|
|
1097
|
-
const dt = await refreshDevice(
|
|
1213
|
+
const dt = await refreshDevice(s, a.refresh, true)
|
|
1098
1214
|
return spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh })
|
|
1099
1215
|
}
|
|
1100
|
-
const jt = await refreshJob(
|
|
1216
|
+
const jt = await refreshJob(s, a.refresh, a.accountId || a.email || a.uid)
|
|
1101
1217
|
return spend(a.refresh, { access: jt.token, refresh: jt.refresh_token, expires: expiresAt(jt) })
|
|
1102
1218
|
}
|
|
1103
1219
|
// each account's listing, the model configs a request carries
|
|
@@ -1120,6 +1236,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1120
1236
|
locked(async () => {
|
|
1121
1237
|
const stored = await getAuth()
|
|
1122
1238
|
if (stored?.type !== "oauth" || !stored.access || !stored.uid) throw new SignInGone(`${site.name}: not signed in`)
|
|
1239
|
+
siteOf(site, stored) // a VPC instance on record that names none is no sign-in
|
|
1123
1240
|
// renewed already (by magpie's auth.refresh), the store not yet saying so
|
|
1124
1241
|
const a = latest(stored)
|
|
1125
1242
|
if (a.expires - Date.now() > REFRESH_LEAD) return a
|
|
@@ -1132,7 +1249,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1132
1249
|
const models = async (cred, again = false) => {
|
|
1133
1250
|
let l = listings.get(cred.uid)
|
|
1134
1251
|
if (!l || again) {
|
|
1135
|
-
l = modelInfos(await fetchListing(site, cred))
|
|
1252
|
+
l = modelInfos(await fetchListing(siteOf(site, cred), cred))
|
|
1136
1253
|
listings.set(cred.uid, l)
|
|
1137
1254
|
}
|
|
1138
1255
|
return l
|
|
@@ -1144,7 +1261,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1144
1261
|
locked(async () => {
|
|
1145
1262
|
const a = latest(await getAuth())
|
|
1146
1263
|
if (a?.deviceToken !== attempted) return a?.deviceToken
|
|
1147
|
-
const dt = await refreshDevice(site, a.deviceRefresh, !!a.deviceChat)
|
|
1264
|
+
const dt = await refreshDevice(siteOf(site, a), a.deviceRefresh, !!a.deviceChat)
|
|
1148
1265
|
const next = { ...a, deviceToken: dt.token, deviceRefresh: dt.refresh }
|
|
1149
1266
|
// the pair it spent was the chat pair too
|
|
1150
1267
|
if (a.deviceChat) Object.assign(next, spend(a.refresh, { access: dt.token, refresh: dt.refresh, expires: dt.expires, deviceToken: dt.token, deviceRefresh: dt.refresh }))
|
|
@@ -1163,10 +1280,10 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1163
1280
|
if (renewed(cred)) signIn = "renewed"
|
|
1164
1281
|
let env
|
|
1165
1282
|
try {
|
|
1166
|
-
env = await fetchUsage(site, cred.deviceToken)
|
|
1283
|
+
env = await fetchUsage(siteOf(site, cred), cred.deviceToken)
|
|
1167
1284
|
} catch (e) {
|
|
1168
1285
|
if (!(e instanceof UsageStatus) || (e.status !== 401 && e.status !== 403)) throw e
|
|
1169
|
-
env = await fetchUsage(site, await deviceToken(getAuth, cred.deviceToken))
|
|
1286
|
+
env = await fetchUsage(siteOf(site, cred), await deviceToken(getAuth, cred.deviceToken))
|
|
1170
1287
|
}
|
|
1171
1288
|
return { ...parseUsage(env), signIn }
|
|
1172
1289
|
} catch (e) {
|
|
@@ -1174,6 +1291,29 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1174
1291
|
}
|
|
1175
1292
|
}
|
|
1176
1293
|
|
|
1294
|
+
// campaign is a campaigns page (the daily check-in) asked as the account:
|
|
1295
|
+
// a refused device token is rotated once, as usage does, and asked again
|
|
1296
|
+
const campaign = async (getAuth, input, url, init) => {
|
|
1297
|
+
const body = await bodyText(input, init)
|
|
1298
|
+
let cred
|
|
1299
|
+
try {
|
|
1300
|
+
cred = await fresh(getAuth)
|
|
1301
|
+
url = campaignTarget(site, siteOf(site, cred), url)
|
|
1302
|
+
} catch (e) {
|
|
1303
|
+
return signedInError(e)
|
|
1304
|
+
}
|
|
1305
|
+
if (!url) return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1306
|
+
const method = String(init.method ?? input?.method ?? "GET").toUpperCase()
|
|
1307
|
+
try {
|
|
1308
|
+
let res = await campaignCall(url, method, body, cred.deviceToken, init.signal)
|
|
1309
|
+
if (res.status === 401 || res.status === 403) res = await campaignCall(url, method, body, await deviceToken(getAuth, cred.deviceToken), init.signal)
|
|
1310
|
+
return signed(res, renewed(cred))
|
|
1311
|
+
} catch (e) {
|
|
1312
|
+
if (e?.expired) return signedInError(e)
|
|
1313
|
+
return errorResponse({ status: 502, message: e?.message ?? String(e) })
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1177
1317
|
const signedInError = (e) =>
|
|
1178
1318
|
errorResponse({
|
|
1179
1319
|
status: e instanceof SignInGone ? 401 : 502,
|
|
@@ -1192,6 +1332,7 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1192
1332
|
return errorResponse({ status: 400, message: e.message })
|
|
1193
1333
|
}
|
|
1194
1334
|
if (!m) return errorResponse({ status: 400, message: `unknown or disabled model "${chat.model}"` })
|
|
1335
|
+
const CHAT_URL = siteOf(site, cred).api + CHAT_PATH
|
|
1195
1336
|
const wire = encodeBody(JSON.stringify(qoderBody(chat, m)))
|
|
1196
1337
|
const headers = {
|
|
1197
1338
|
...cosyHeaders(CHAT_URL, cred, wire),
|
|
@@ -1238,12 +1379,17 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1238
1379
|
async loader(getAuth) {
|
|
1239
1380
|
const a = await getAuth()
|
|
1240
1381
|
if (a?.type !== "oauth") return {}
|
|
1382
|
+
let base = site
|
|
1383
|
+
try {
|
|
1384
|
+
base = siteOf(site, a)
|
|
1385
|
+
} catch {} // its requests say so
|
|
1241
1386
|
return {
|
|
1242
|
-
baseURL:
|
|
1387
|
+
baseURL: base.api,
|
|
1243
1388
|
apiKey: "qoder",
|
|
1244
1389
|
// every chat completion written as Qoder's own request
|
|
1245
1390
|
async fetch(input, init = {}) {
|
|
1246
1391
|
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url
|
|
1392
|
+
if (campaignPage(site, url) || campaignPage(base, url)) return campaign(getAuth, input, url, init)
|
|
1247
1393
|
if (!/\/chat\/completions$/.test(new URL(url).pathname))
|
|
1248
1394
|
return errorResponse({ status: 404, message: "only chat completions are served" })
|
|
1249
1395
|
let chat
|
|
@@ -1262,7 +1408,33 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1262
1408
|
},
|
|
1263
1409
|
}
|
|
1264
1410
|
},
|
|
1265
|
-
methods: [
|
|
1411
|
+
methods: [
|
|
1412
|
+
{ type: "oauth", label: `Sign in with ${site.name}`, authorize: () => deviceSignIn(site) },
|
|
1413
|
+
// an enterprise VPC instance (Qoder CN), a method of its own so
|
|
1414
|
+
// that the sign-in above asks nothing new
|
|
1415
|
+
...(site.vpcDomain
|
|
1416
|
+
? [
|
|
1417
|
+
{
|
|
1418
|
+
type: "oauth",
|
|
1419
|
+
label: `Sign in with ${site.name} Enterprise (VPC)`,
|
|
1420
|
+
prompts: [
|
|
1421
|
+
{
|
|
1422
|
+
type: "text",
|
|
1423
|
+
key: "vpc",
|
|
1424
|
+
message: `Your enterprise's VPC instance: its name, or its address (<instance>.${site.vpcDomain})`,
|
|
1425
|
+
placeholder: `acme or acme.${site.vpcDomain}`,
|
|
1426
|
+
validate: (v) => (vpcInstance(v, site.vpcDomain) ? undefined : `Not a VPC instance: enter its name (letters, digits, -) or https://<instance>.${site.vpcDomain}`),
|
|
1427
|
+
},
|
|
1428
|
+
],
|
|
1429
|
+
authorize: (inputs) => {
|
|
1430
|
+
const s = vpcSite(site, inputs?.vpc)
|
|
1431
|
+
if (!s) throw new Error(`Not a VPC instance: ${String(inputs?.vpc ?? "").slice(0, 100)}`)
|
|
1432
|
+
return deviceSignIn(s)
|
|
1433
|
+
},
|
|
1434
|
+
},
|
|
1435
|
+
]
|
|
1436
|
+
: []),
|
|
1437
|
+
],
|
|
1266
1438
|
usage,
|
|
1267
1439
|
},
|
|
1268
1440
|
async config(config) {
|
|
@@ -1294,8 +1466,9 @@ const makePlugin = (site) => async ({ client }) => {
|
|
|
1294
1466
|
try {
|
|
1295
1467
|
const ms = await models(cred, true)
|
|
1296
1468
|
if (!ms.length) return provider.models
|
|
1469
|
+
const s = siteOf(site, cred)
|
|
1297
1470
|
return Object.fromEntries(
|
|
1298
|
-
ms.map((m) => [m.key, runtimeModel(
|
|
1471
|
+
ms.map((m) => [m.key, runtimeModel(s, { id: m.key, name: m.name, context: m.context, images: m.images, efforts: m.thinks ? m.efforts : [], free: m.free, rate: m.rate, rateWas: m.rateWas })]),
|
|
1299
1472
|
)
|
|
1300
1473
|
} catch {
|
|
1301
1474
|
return provider.models
|
|
@@ -1309,4 +1482,4 @@ export const QoderAuthPlugin = makePlugin(SITES.qoder)
|
|
|
1309
1482
|
export const QoderCNAuthPlugin = makePlugin(SITES["qoder-cn"])
|
|
1310
1483
|
|
|
1311
1484
|
// for tests
|
|
1312
|
-
export const _internal = { parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
|
|
1485
|
+
export const _internal = { campaignPage, campaignTarget, vpcInstance, vpcSite, parseUsage, gfmt, when, failure, modelInfo, qoderBody, SITES, decodeBody, events }
|
package/package.json
CHANGED