opencode-ufr 0.2.8 → 0.2.10
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 +65 -62
- package/models.json +11 -5
- package/package.json +14 -6
- package/skills/pdf2md/SKILL.md +32 -0
- package/src/client/gateway.ts +101 -0
- package/src/client/pdf2md.ts +747 -0
- package/src/daemon/catalog-source.ts +9 -0
- package/src/daemon/daemon.ts +70 -18
- package/src/daemon/keypool.ts +12 -7
- package/src/daemon/main.ts +7 -1
- package/src/daemon/probe-all.ts +212 -0
- package/src/daemon/probe.ts +1 -1
- package/src/daemon/router.ts +215 -17
- package/src/daemon/server.ts +33 -1
- package/src/daemon/stats.ts +13 -1
- package/src/daemon/upstream.ts +14 -3
- package/src/daemon/vpn/fortinet.ts +42 -29
- package/src/daemon/vpn/manager.ts +50 -16
- package/src/daemon/vpn/proxy.ts +3 -1
- package/src/plugin/connect.ts +4 -4
- package/src/plugin/ensure-daemon.ts +48 -19
- package/src/plugin/index.ts +121 -46
- package/src/plugin/tui.tsx +77 -0
- package/src/shared/config.ts +53 -4
- package/src/shared/connect.ts +3 -4
- package/src/shared/daemon-client.ts +29 -0
- package/src/shared/errors.ts +1 -1
- package/src/shared/fs.ts +8 -3
- package/src/shared/keys.ts +65 -0
- package/src/shared/models-file.ts +1 -1
- package/src/shared/secrets.ts +2 -6
- package/bin/ufr.ts +0 -4
- package/src/cli/catalog.ts +0 -44
- package/src/cli/connect.ts +0 -146
- package/src/cli/daemon-client.ts +0 -19
- package/src/cli/index.ts +0 -97
- package/src/cli/io.ts +0 -98
- package/src/cli/keys.ts +0 -130
- package/src/cli/stats.ts +0 -37
- package/src/cli/status.ts +0 -48
- package/src/cli/stop.ts +0 -8
- package/src/cli/ufr-check.ts +0 -43
package/src/cli/io.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises"
|
|
2
|
-
import type { Readable, Writable } from "node:stream"
|
|
3
|
-
|
|
4
|
-
export type Io = {
|
|
5
|
-
out(s: string): void
|
|
6
|
-
err(s: string): void
|
|
7
|
-
isTTY: boolean
|
|
8
|
-
prompt(question: string, o?: { secret?: boolean }): Promise<string>
|
|
9
|
-
confirm(question: string): Promise<boolean>
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function terminalIo(): Io {
|
|
13
|
-
return {
|
|
14
|
-
out: (s) => void process.stdout.write(s),
|
|
15
|
-
err: (s) => void process.stderr.write(s),
|
|
16
|
-
isTTY: process.stdin.isTTY === true,
|
|
17
|
-
prompt: (q, o) => (o?.secret ? (process.stdin.isTTY ? readSecret(q) : readPipedSecret(q)) : readLine(q)),
|
|
18
|
-
// Non-interactive sessions (scripts, pipes) can never answer a question:
|
|
19
|
-
// treat a confirm as "no" immediately instead of waiting on stdin forever.
|
|
20
|
-
confirm: async (q) =>
|
|
21
|
-
process.stdin.isTTY === true && /^(y|yes|j|ja)$/i.test((await readLine(`${q} [y/N] `)).trim()),
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Testable variant: answers one prompt from a closed/ended stream with "" instead of hanging. */
|
|
26
|
-
export function readLineFrom(q: string, input: Readable, output: Writable): Promise<string> {
|
|
27
|
-
return readLine(q, input, output)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function readLine(
|
|
31
|
-
q: string,
|
|
32
|
-
input: Readable = process.stdin,
|
|
33
|
-
output: Writable = process.stdout,
|
|
34
|
-
): Promise<string> {
|
|
35
|
-
const rl = createInterface({ input, output })
|
|
36
|
-
try {
|
|
37
|
-
// Resolve on close as well: with stdin already at EOF (piped input exhausted,
|
|
38
|
-
// no terminal) question() would otherwise never settle and hang the CLI.
|
|
39
|
-
return await new Promise<string>((resolve) => {
|
|
40
|
-
rl.once("close", () => resolve(""))
|
|
41
|
-
rl.question(q).then(resolve, () => resolve(""))
|
|
42
|
-
})
|
|
43
|
-
} finally {
|
|
44
|
-
rl.close()
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* A secret piped in (stdin not a TTY): read one line with readline in non-terminal
|
|
50
|
-
* mode, which never writes the line back. The question goes to stderr, not to the
|
|
51
|
-
* output that readLine's terminal mode would echo the key into.
|
|
52
|
-
*/
|
|
53
|
-
export function readPipedSecret(q: string, input: Readable = process.stdin, err: Writable = process.stderr): Promise<string> {
|
|
54
|
-
err.write(q)
|
|
55
|
-
const rl = createInterface({ input, terminal: false })
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
let done = false
|
|
58
|
-
const finish = (line: string) => {
|
|
59
|
-
if (done) return
|
|
60
|
-
done = true
|
|
61
|
-
rl.close()
|
|
62
|
-
err.write("\n")
|
|
63
|
-
resolve(line)
|
|
64
|
-
}
|
|
65
|
-
rl.once("line", finish)
|
|
66
|
-
rl.once("close", () => finish(""))
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Read one line without echo — for API keys. */
|
|
71
|
-
function readSecret(q: string): Promise<string> {
|
|
72
|
-
process.stdout.write(q)
|
|
73
|
-
const stdin = process.stdin
|
|
74
|
-
return new Promise((resolve) => {
|
|
75
|
-
let buf = ""
|
|
76
|
-
const finish = () => {
|
|
77
|
-
stdin.setRawMode(false)
|
|
78
|
-
stdin.pause()
|
|
79
|
-
stdin.off("data", onData)
|
|
80
|
-
process.stdout.write("\n")
|
|
81
|
-
resolve(buf)
|
|
82
|
-
}
|
|
83
|
-
const onData = (d: Buffer) => {
|
|
84
|
-
for (const ch of d.toString("utf8")) {
|
|
85
|
-
if (ch === "\r" || ch === "\n") return finish()
|
|
86
|
-
if (ch === "\u0003") {
|
|
87
|
-
stdin.setRawMode(false)
|
|
88
|
-
process.exit(130)
|
|
89
|
-
}
|
|
90
|
-
if (ch === "\u007f" || ch === "\b") buf = buf.slice(0, -1)
|
|
91
|
-
else buf += ch
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
stdin.setRawMode(true)
|
|
95
|
-
stdin.resume()
|
|
96
|
-
stdin.on("data", onData)
|
|
97
|
-
})
|
|
98
|
-
}
|
package/src/cli/keys.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { loadConfig, saveConfig } from "../shared/config"
|
|
2
|
-
import type { Paths } from "../shared/paths"
|
|
3
|
-
import { VPN_PASS, VPN_USER, type SecretStore } from "../shared/secrets"
|
|
4
|
-
import { daemonRequest } from "./daemon-client"
|
|
5
|
-
import type { FetchLike } from "../daemon/catalog-source"
|
|
6
|
-
import type { CliDeps } from "./index"
|
|
7
|
-
import { type Check, checkKey } from "./ufr-check"
|
|
8
|
-
|
|
9
|
-
const LABEL: Record<Check | "missing", string> = {
|
|
10
|
-
ok: "ok",
|
|
11
|
-
invalid: "INVALID (UFR says 401)",
|
|
12
|
-
vpn: "cannot check — not on the uni VPN",
|
|
13
|
-
unreachable: "cannot check — UFR unreachable",
|
|
14
|
-
rate_limited: "ok (currently rate-limited)",
|
|
15
|
-
missing: "MISSING in keyring",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** A running gateway read its keys at start: stop it while idle so opencode starts it with the new set. */
|
|
19
|
-
export async function applyKeyChange(d: { paths: Paths; fetch: FetchLike; log: (m: string) => void }): Promise<void> {
|
|
20
|
-
const res = await daemonRequest(d.paths, d.fetch, "/v1/_status")
|
|
21
|
-
if (!res?.ok) return // not running — it reads the keys when opencode next starts it
|
|
22
|
-
const { inFlight } = (await res.json()) as { inFlight?: number }
|
|
23
|
-
if (inFlight !== 0) {
|
|
24
|
-
d.log("the gateway is answering a request right now — run `ufr stop` later so it picks up the new keys")
|
|
25
|
-
return
|
|
26
|
-
}
|
|
27
|
-
const stopped = await daemonRequest(d.paths, d.fetch, "/v1/_shutdown", "POST")
|
|
28
|
-
d.log(stopped?.ok ? "gateway stopped — it restarts with the new keys when opencode next needs it"
|
|
29
|
-
: "could not stop the gateway — run `ufr stop` so it picks up the new keys")
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type DisconnectResult = { removed: string[]; loginRemoved: boolean }
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Provider-removal cleanup: delete every stored key alias and the uni login
|
|
36
|
-
* from the keyring, clear config.keys, and stop an idle gateway (it reads its
|
|
37
|
-
* keys at start, so a later start serves nothing). Idempotent.
|
|
38
|
-
*/
|
|
39
|
-
export async function disconnectAll(d: {
|
|
40
|
-
paths: Paths
|
|
41
|
-
secrets: SecretStore
|
|
42
|
-
fetch: FetchLike
|
|
43
|
-
log: (m: string) => void
|
|
44
|
-
}): Promise<DisconnectResult> {
|
|
45
|
-
const cfg = await loadConfig(d.paths.configFile)
|
|
46
|
-
const removed: string[] = []
|
|
47
|
-
for (const alias of cfg.keys) {
|
|
48
|
-
if (await d.secrets.delete(alias)) removed.push(alias)
|
|
49
|
-
}
|
|
50
|
-
const userRemoved = await d.secrets.delete(VPN_USER)
|
|
51
|
-
const passRemoved = await d.secrets.delete(VPN_PASS)
|
|
52
|
-
const loginRemoved = userRemoved || passRemoved
|
|
53
|
-
cfg.keys = []
|
|
54
|
-
await saveConfig(d.paths.configFile, cfg)
|
|
55
|
-
await applyKeyChange(d)
|
|
56
|
-
return { removed, loginRemoved }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function cmdDisconnect(d: CliDeps): Promise<number> {
|
|
60
|
-
const r = await disconnectAll({ paths: d.paths, secrets: d.secrets, fetch: d.fetch, log: (m) => d.io.out(m + "\n") })
|
|
61
|
-
d.io.out(r.removed.length > 0 ? `removed from keyring: ${r.removed.join(", ")}\n` : "nothing was stored\n")
|
|
62
|
-
if (r.loginRemoved) d.io.out("removed the uni login\n")
|
|
63
|
-
d.io.out("done — reconnect any time with `ufr connect` or /connect in opencode\n")
|
|
64
|
-
return 0
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function cmdKeys(d: CliDeps, args: string[]): Promise<number> {
|
|
68
|
-
const [sub, ...rest] = args
|
|
69
|
-
const cfg = await loadConfig(d.paths.configFile)
|
|
70
|
-
switch (sub) {
|
|
71
|
-
case "list": {
|
|
72
|
-
if (cfg.keys.length === 0) {
|
|
73
|
-
d.io.out("no keys — add them with `ufr connect`\n")
|
|
74
|
-
return 0
|
|
75
|
-
}
|
|
76
|
-
for (const a of cfg.keys) d.io.out(`${a}\t${(await d.secrets.get(a)) ? "stored" : LABEL.missing}\n`)
|
|
77
|
-
return 0
|
|
78
|
-
}
|
|
79
|
-
case "remove": {
|
|
80
|
-
const alias = rest[0]
|
|
81
|
-
if (!alias) {
|
|
82
|
-
d.io.err("usage: ufr keys remove <alias>\n")
|
|
83
|
-
return 2
|
|
84
|
-
}
|
|
85
|
-
await d.secrets.delete(alias)
|
|
86
|
-
const had = cfg.keys.includes(alias)
|
|
87
|
-
cfg.keys = cfg.keys.filter((k) => k !== alias)
|
|
88
|
-
await saveConfig(d.paths.configFile, cfg)
|
|
89
|
-
d.io.out(`removed "${alias}"\n`)
|
|
90
|
-
if (had) await applyKeyChange({ paths: d.paths, fetch: d.fetch, log: (m) => d.io.out(m + "\n") })
|
|
91
|
-
return 0
|
|
92
|
-
}
|
|
93
|
-
case "test": {
|
|
94
|
-
let bad = 0
|
|
95
|
-
for (const a of rest[0] ? [rest[0]] : cfg.keys) {
|
|
96
|
-
const key = await d.secrets.get(a)
|
|
97
|
-
const r: Check | "missing" = key ? await checkKey(d.fetch, cfg.upstream.baseUrl, key) : "missing"
|
|
98
|
-
if (r !== "ok" && r !== "rate_limited") bad++
|
|
99
|
-
d.io.out(`${a}\t${LABEL[r]}\n`)
|
|
100
|
-
}
|
|
101
|
-
return bad ? 1 : 0
|
|
102
|
-
}
|
|
103
|
-
default:
|
|
104
|
-
d.io.err("usage: ufr keys list | remove <alias> | test [alias]\n")
|
|
105
|
-
return 2
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** ufr login — inspect or remove the stored uni login (set it with `ufr connect`). */
|
|
110
|
-
export async function cmdLogin(d: CliDeps, args: string[]): Promise<number> {
|
|
111
|
-
const [sub] = args
|
|
112
|
-
if (sub === "remove") {
|
|
113
|
-
await d.secrets.delete(VPN_USER)
|
|
114
|
-
await d.secrets.delete(VPN_PASS)
|
|
115
|
-
d.io.out("removed the stored uni login\n")
|
|
116
|
-
return 0
|
|
117
|
-
}
|
|
118
|
-
if (sub === "show" || sub === undefined) {
|
|
119
|
-
const u = await d.secrets.get(VPN_USER)
|
|
120
|
-
const p = await d.secrets.get(VPN_PASS)
|
|
121
|
-
if (!u) {
|
|
122
|
-
d.io.out("no uni login stored — set it with `ufr connect --login <user> --password <pass> --keys …`\n")
|
|
123
|
-
return 1
|
|
124
|
-
}
|
|
125
|
-
d.io.out(`${u}\tpassword ${p ? "stored" : "MISSING in keyring"}\n`)
|
|
126
|
-
return p ? 0 : 1
|
|
127
|
-
}
|
|
128
|
-
d.io.err("usage: ufr login show | remove\n")
|
|
129
|
-
return 2
|
|
130
|
-
}
|
package/src/cli/stats.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
2
|
-
import { Stats, type SummaryRow } from "../daemon/stats"
|
|
3
|
-
import { loadConfig } from "../shared/config"
|
|
4
|
-
import { startOfLocalDay } from "../shared/time"
|
|
5
|
-
import type { CliDeps } from "./index"
|
|
6
|
-
|
|
7
|
-
function table(label: string, rows: SummaryRow[], budget: number, showPct: boolean): string {
|
|
8
|
-
if (rows.length === 0) return `${label}: no requests\n`
|
|
9
|
-
const lines = rows.map((r) => {
|
|
10
|
-
const cost = `$${r.costUsd.toFixed(4)}${r.unpriced ? ` +${r.unpriced} unpriced` : ""}`
|
|
11
|
-
const pct = showPct && budget > 0 ? ` ${((r.costUsd / budget) * 100).toFixed(2)}% of $${budget} (est.)` : ""
|
|
12
|
-
return ` ${r.name.padEnd(36)} ${String(r.requests).padStart(6)} req ${String(r.errors).padStart(5)} err ` +
|
|
13
|
-
`${String(r.promptTokens).padStart(11)} in ${String(r.completionTokens).padStart(9)} out ${cost}${pct}`
|
|
14
|
-
})
|
|
15
|
-
return `${label}\n${lines.join("\n")}\n`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function cmdStats(d: CliDeps, days: number): Promise<number> {
|
|
19
|
-
if (!existsSync(d.paths.statsDb)) {
|
|
20
|
-
d.io.out("no stats yet — the gateway records requests once opencode uses it\n")
|
|
21
|
-
return 0
|
|
22
|
-
}
|
|
23
|
-
const cfg = await loadConfig(d.paths.configFile)
|
|
24
|
-
const stats = new Stats(d.paths.statsDb)
|
|
25
|
-
try {
|
|
26
|
-
const today = days === 1
|
|
27
|
-
const since = today ? startOfLocalDay(d.now()) : d.now() - days * 86_400_000
|
|
28
|
-
const { byModel, byKey } = stats.summary(since)
|
|
29
|
-
d.io.out(`${today ? "today (since local midnight)" : `last ${days} days`}\n\n`)
|
|
30
|
-
d.io.out(table("per model", byModel, cfg.dailyBudgetUsd, false))
|
|
31
|
-
d.io.out("\n")
|
|
32
|
-
d.io.out(table("per key", byKey, cfg.dailyBudgetUsd, today))
|
|
33
|
-
return 0
|
|
34
|
-
} finally {
|
|
35
|
-
stats.close()
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/cli/status.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { StatusJson } from "../daemon/daemon"
|
|
2
|
-
import { loadConfig } from "../shared/config"
|
|
3
|
-
import { VPN_PASS, VPN_USER } from "../shared/secrets"
|
|
4
|
-
import { daemonRequest } from "./daemon-client"
|
|
5
|
-
import type { CliDeps } from "./index"
|
|
6
|
-
|
|
7
|
-
const ago = (ms: number) =>
|
|
8
|
-
ms < 60_000 ? `${Math.round(ms / 1000)}s` : ms < 3_600_000 ? `${Math.round(ms / 60_000)}m` : `${Math.round(ms / 3_600_000)}h`
|
|
9
|
-
|
|
10
|
-
export function formatStatus(s: StatusJson): string {
|
|
11
|
-
const L: string[] = []
|
|
12
|
-
L.push(`gateway v${s.version} pid ${s.pid} 127.0.0.1:${s.port} up ${ago(s.uptimeMs)} in flight ${s.inFlight}`)
|
|
13
|
-
if (s.upstream.ok === false) L.push(`UFR NOT reachable — ${s.upstream.message}`)
|
|
14
|
-
else if (s.upstream.ok) L.push(`UFR reachable (checked ${ago(s.now - s.upstream.at)} ago)`)
|
|
15
|
-
else L.push("UFR not checked yet")
|
|
16
|
-
if (s.keys.length === 0) L.push("keys none — run `ufr connect`")
|
|
17
|
-
s.keys.forEach((k, i) => {
|
|
18
|
-
const spend = s.spendToday[k.alias] ?? 0
|
|
19
|
-
const pct = s.dailyBudgetUsd > 0 ? ` (${((spend / s.dailyBudgetUsd) * 100).toFixed(2)}% of $${s.dailyBudgetUsd}, est.)` : ""
|
|
20
|
-
const state = k.invalid ? " INVALID" : k.blockedForMs > 0 ? ` cooling down ${ago(k.blockedForMs)}` : ""
|
|
21
|
-
L.push(`${i === 0 ? "keys " : " "}${k.alias} ${k.used}/${k.cap} per 60s today $${spend.toFixed(4)}${pct}${state}`)
|
|
22
|
-
})
|
|
23
|
-
L.push(`pool ${s.pool.enabled ? `${s.pool.inWindow}/${s.pool.cap} per hour` : "limiter off"}`)
|
|
24
|
-
const open = Object.entries(s.breakers).filter(([, b]) => b.state !== "closed")
|
|
25
|
-
L.push(open.length === 0 ? "breakers all closed"
|
|
26
|
-
: `breakers ${open.map(([g, b]) => `${g} ${b.state} (rung ${b.level + 1}, ${ago(b.retryAfterMs)} left)`).join(", ")}`)
|
|
27
|
-
L.push(`catalog ${s.catalog.models} models; models.json from ${s.catalog.source}, UFR list from ${s.catalog.ufrSource}` +
|
|
28
|
-
(s.catalog.loadedAt ? ` (loaded ${ago(s.now - s.catalog.loadedAt)} ago)` : ""))
|
|
29
|
-
if (s.vpn) L.push(`vpn ${s.vpn.mode} — ${s.vpn.detail}`)
|
|
30
|
-
for (const w of s.catalog.warnings) L.push(`warning ${w}`)
|
|
31
|
-
return L.join("\n") + "\n"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function cmdStatus(d: CliDeps): Promise<number> {
|
|
35
|
-
const user = await d.secrets.get(VPN_USER)
|
|
36
|
-
const pass = await d.secrets.get(VPN_PASS)
|
|
37
|
-
const vpnLine = user
|
|
38
|
-
? `vpn login ${user} (password ${pass ? "stored" : "MISSING in keyring"})\n`
|
|
39
|
-
: "vpn login not stored — off campus the built-in VPN needs one (ufr login add <user>)\n"
|
|
40
|
-
const res = await daemonRequest(d.paths, d.fetch, "/v1/_status")
|
|
41
|
-
if (!res || !res.ok) {
|
|
42
|
-
const cfg = await loadConfig(d.paths.configFile)
|
|
43
|
-
d.io.out(`gateway not running (opencode starts it)\nkeys ${cfg.keys.join(", ") || "none — run `ufr connect`"}\n${vpnLine}`)
|
|
44
|
-
return 0
|
|
45
|
-
}
|
|
46
|
-
d.io.out(formatStatus((await res.json()) as StatusJson) + vpnLine)
|
|
47
|
-
return 0
|
|
48
|
-
}
|
package/src/cli/stop.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { daemonRequest } from "./daemon-client"
|
|
2
|
-
import type { CliDeps } from "./index"
|
|
3
|
-
|
|
4
|
-
export async function cmdStop(d: CliDeps): Promise<number> {
|
|
5
|
-
const res = await daemonRequest(d.paths, d.fetch, "/v1/_shutdown", "POST")
|
|
6
|
-
d.io.out(res?.ok ? "gateway stopping — opencode starts it again when needed\n" : "gateway not running\n")
|
|
7
|
-
return 0
|
|
8
|
-
}
|
package/src/cli/ufr-check.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { FetchLike } from "../daemon/catalog-source"
|
|
2
|
-
import { isVpnPage } from "../shared/vpn"
|
|
3
|
-
|
|
4
|
-
export type Check = "ok" | "invalid" | "vpn" | "unreachable" | "rate_limited"
|
|
5
|
-
|
|
6
|
-
async function classify(res: Response): Promise<Check> {
|
|
7
|
-
const type = res.headers.get("content-type") ?? ""
|
|
8
|
-
if (type.includes("text/html")) {
|
|
9
|
-
const html = await res.text().catch(() => "")
|
|
10
|
-
return isVpnPage(html) ? "vpn" : "unreachable"
|
|
11
|
-
}
|
|
12
|
-
if (res.status >= 300 && res.status < 400) return "unreachable"
|
|
13
|
-
if (res.status === 401 || res.status === 403) return "invalid"
|
|
14
|
-
if (res.status === 429) return "rate_limited"
|
|
15
|
-
return res.ok ? "ok" : "unreachable"
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function checkKey(f: FetchLike, baseUrl: string, key: string): Promise<Check> {
|
|
19
|
-
try {
|
|
20
|
-
return await classify(await f(`${baseUrl}/models`, {
|
|
21
|
-
headers: { Authorization: `Bearer ${key}` },
|
|
22
|
-
redirect: "manual",
|
|
23
|
-
signal: AbortSignal.timeout(15_000),
|
|
24
|
-
}))
|
|
25
|
-
} catch {
|
|
26
|
-
return "unreachable"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** One minimal chat call (max_tokens 1) — proves the whole path incl. the VPN. */
|
|
31
|
-
export async function checkChat(f: FetchLike, baseUrl: string, key: string, model: string): Promise<Check> {
|
|
32
|
-
try {
|
|
33
|
-
return await classify(await f(`${baseUrl}/chat/completions`, {
|
|
34
|
-
method: "POST",
|
|
35
|
-
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
36
|
-
body: JSON.stringify({ model, max_tokens: 1, messages: [{ role: "user", content: "Hi" }] }),
|
|
37
|
-
redirect: "manual",
|
|
38
|
-
signal: AbortSignal.timeout(60_000),
|
|
39
|
-
}))
|
|
40
|
-
} catch {
|
|
41
|
-
return "unreachable"
|
|
42
|
-
}
|
|
43
|
-
}
|