opencode-ufr 0.2.0 → 0.2.1

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.
Files changed (2) hide show
  1. package/README.md +82 -59
  2. package/package.json +37 -9
package/README.md CHANGED
@@ -1,71 +1,100 @@
1
1
  # opencode-ufr
2
2
 
3
- An [opencode](https://opencode.ai) provider for Uni Freiburg's Open WebUI
4
- models. A small local gateway runs on your machine, pools your UFR API
3
+ An [opencode](https://opencode.ai) provider for **Uni Freiburg's Open WebUI
4
+ models**. A small local gateway runs on your machine, pools your UFR API
5
5
  key(s), and handles UFR's rate limits and outages so opencode doesn't have to.
6
+ Off campus, it connects through the uni's Fortinet VPN **by itself** — with no
7
+ TUN device, no admin rights and no external tools.
6
8
 
7
- ## Requirements
8
-
9
- - opencode ≥ 2.0
10
- - A UFR account and API key (Open WebUI → Settings → Account → API keys)
11
- - [Bun](https://bun.sh) on your `PATH` for the `ufr` CLI. The plugin itself
12
- needs no Bun — it runs inside opencode.
13
- - Off campus, the **built-in VPN** connects to UFR on its own: store your uni
14
- login once (`ufr login add <user>`) and the plugin tunnels its UFR calls
15
- through `fortivpn.uni-freiburg.de` — with no TUN device, no admin rights, no
16
- openconnect and no changes to your routing. Your own VPNs keep working
17
- untouched in parallel. See the RZ guide if you prefer a system VPN:
18
- https://wiki.uni-freiburg.de/rz/doku.php?id=vpn
9
+ ```
10
+ opencode ⇄ gateway (127.0.0.1) ⇄ [Fortinet tunnel] ⇄ openwebui.uni-freiburg.de
11
+ │
12
+ ├─ key pool (1…n UFR accounts)
13
+ ├─ rate-limit pacing, circuit breakers, fallbacks
14
+ └─ VPN when off campus (userspace TCP/IP stack)
15
+ ```
19
16
 
20
17
  ## Install
21
18
 
22
- Not on npm yet — install straight from GitHub:
23
-
24
19
  ```bash
25
- bun add -g github:FinleyLaempe/opencode-ufr # puts `ufr` on PATH
26
- ufr connect
20
+ bun add -g opencode-ufr # puts `ufr` on PATH
21
+ opencode plugin add opencode-ufr # registers the plugin
27
22
  ```
28
23
 
29
- `ufr connect` asks for your UFR API key(s) — comma-separated, whitespace
30
- around keys is filtered, aliases (key1, key2, …) are assigned automatically —
31
- and optionally your uni login for the built-in VPN. Everything is verified
32
- against UFR and stored in the OS keyring (never on disk).
24
+ (or add `"plugins": ["opencode-ufr"]` to your `opencode.json` manually.)
33
25
 
34
- Inside opencode you never need the CLI: the plugin takes over the built-in
35
- `/connect` panel's **Uni Freiburg** entry. It shows the API-key(s) field plus
36
- **Uni login (optional)** and **Uni password (optional)** — both only needed on
37
- a machine outside the uni network, where the built-in VPN uses them. Submitted
38
- credentials land in the OS keyring automatically and the gateway restarts with
39
- them.
26
+ ## Setup
40
27
 
41
- Manual alternative — add to `opencode.json`:
28
+ **Inside opencode — the normal way:** open `/connect`, pick **Uni Freiburg**,
29
+ and fill in:
42
30
 
43
- ```json
44
- { "plugins": ["github:FinleyLaempe/opencode-ufr"] }
45
- ```
31
+ | Field | Needed? |
32
+ |---|---|
33
+ | **API keys** | always — paste one key per UFR account, comma-separated (whitespace is filtered) |
34
+ | **Uni login** | only off campus — e.g. `fl240@uni-freiburg.de`, empty on the uni network |
35
+ | **Uni password** | only with a login above |
36
+
37
+ Submitted credentials land in your OS keyring (never on disk), the gateway
38
+ restarts with them, and the `unifreiburg/…` models appear in the model picker.
39
+
40
+ **In a terminal — the fallback:** works before opencode ever starts.
46
41
 
47
- or run `opencode plugin add github:FinleyLaempe/opencode-ufr` yourself, then
48
- `ufr connect --keys "<key1>,<key2>"`.
42
+ ```bash
43
+ ufr connect --keys "<key1>,<key2>" # keys only
44
+ ufr connect --login fl240@uni-freiburg.de --password … --keys "…" # + VPN login
45
+ ```
49
46
 
50
- npm publication is planned; once it lands, `bunx opencode-ufr …` /
51
- `npx opencode-ufr …` will work without a global install.
47
+ `ufr connect` verifies every key against UFR, assigns aliases (`key1`, `key2`,
48
+ …) automatically and accepts keys comma- or line-separated.
52
49
 
53
50
  ## Use
54
51
 
55
52
  Models show up in opencode as `unifreiburg/<model>`, e.g.
56
- `unifreiburg/ufr/coding-complex`. Everything else is the CLI:
53
+ `unifreiburg/ufr/coding-complex` or `unifreiburg/glm-5.3-flash-llmlb`. The
54
+ gateway starts itself the first time opencode needs it (the plugin waits up to
55
+ 40 s) and shuts down after 5 minutes idle. You never manage it directly.
57
56
 
58
57
  ```bash
59
- ufr status # gateway, keys, limits, breakers, spend today
58
+ ufr status # gateway, keys, limits, breakers, vpn, spend today
60
59
  ufr stats # requests, tokens and cost
61
- ufr keys test # check your stored keys against UFR
62
- ufr keys list # aliases
60
+ ufr keys list # stored aliases
61
+ ufr keys test # check your keys against UFR
63
62
  ufr login show # the stored uni login
64
63
  ```
65
64
 
66
- The gateway starts itself the first time opencode needs it (the plugin waits
67
- up to 40 s for it to come up) and shuts down again after 5 minutes idle. You
68
- don't run or manage it directly.
65
+ ## The built-in VPN
66
+
67
+ On the campus network (or the VPN you already run) UFR is reached directly and
68
+ no tunnel opens. Off campus, the gateway:
69
+
70
+ 1. logs in to `fortivpn.uni-freiburg.de` with your stored uni login (TLS),
71
+ 2. negotiates PPP/IPCP over the Fortinet SSL-VPN channel (this is the same
72
+ protocol `openconnect --protocol=fortinet` speaks, implemented in pure
73
+ TypeScript — see `docs/fortinet-protocol.md`),
74
+ 3. runs a **userspace TCP/IP stack inside the gateway process** and routes its
75
+ UFR calls through it via a localhost CONNECT proxy.
76
+
77
+ What that means in practice:
78
+
79
+ - **No TUN device, no routing table changes, no admin rights** — identical
80
+ code on Linux, macOS and Windows.
81
+ - **Your own VPNs keep working untouched in parallel** — the plugin's tunnel
82
+ exists only inside its own process and rides on whatever network the OS
83
+ provides.
84
+ - **Everything is encrypted twice**: the tunnel itself is TLS 1.3, and your
85
+ API calls are HTTPS end-to-end to `openwebui.uni-freiburg.de` inside it.
86
+ - `vpn.mode: "auto"` (default) only tunnels when UFR is unreachable directly;
87
+ `"always"` forces the tunnel (useful behind firewalls that block the campus
88
+ route).
89
+ - If the tunnel dies, it reconnects with backoff — no session is lost, the
90
+ gateway just waits until the path is back.
91
+
92
+ ## More than one key
93
+
94
+ UFR allows one API key per account, so one key is one account's worth of
95
+ throughput. The key pool round-robins across all stored keys and keeps each
96
+ under UFR's per-key limits; adding more keys only helps if they belong to
97
+ different UFR accounts.
69
98
 
70
99
  ## What the gateway does
71
100
 
@@ -74,12 +103,6 @@ don't run or manage it directly.
74
103
  - Enforces a pool-wide cap of 800 requests/hour across all keys and models —
75
104
  UFR walls a model group for hours once it sees sustained traffic above
76
105
  roughly 900/hour.
77
- - **Connects to UFR on its own when you are off campus.** It logs in to the
78
- uni's Fortinet gateway with your stored uni login, negotiates PPP/IPCP and
79
- runs a userspace TCP/IP stack inside the gateway process: no TUN device, no
80
- routes, no admin rights, identical on Linux, macOS and Windows. On campus it
81
- talks to UFR directly and never opens the tunnel; `vpn.mode: "always"`
82
- forces the tunnel.
83
106
  - Runs a circuit breaker per model: after repeated failures it backs off from
84
107
  30 s up to 60 minutes before probing again, instead of hammering a walled
85
108
  model.
@@ -97,15 +120,10 @@ don't run or manage it directly.
97
120
  unclean shutdown, the next start detects it's stale and takes it over
98
121
  rather than refusing to start.
99
122
 
100
- ## More than one key
101
-
102
- UFR allows one API key per account, so one key is one account's worth of
103
- throughput. Adding more keys only helps if they belong to different UFR
104
- accounts — it does not raise any one account's limit.
105
-
106
123
  ## Configuration
107
124
 
108
- Non-secret settings live in a JSON file; keys always stay in the OS keyring.
125
+ Non-secret settings live in a JSON file; keys and the uni login always stay in
126
+ the OS keyring.
109
127
 
110
128
  - **Linux / macOS:** `~/.config/opencode-ufr/config.json` (state in
111
129
  `~/.local/state/opencode-ufr`, cache in `~/.cache/opencode-ufr`, data in
@@ -152,9 +170,14 @@ except `port`, which is written on first start):
152
170
 
153
171
  `port` is chosen once (preferred `47300`, next free port if taken) and then
154
172
  stays fixed across restarts. `poolPerHour: 0` disables the pool limiter.
155
- `transport.type: "direct"` never opens the tunnel; `"auto"` (the default)
156
- tunnels only when UFR is not reachable directly. `vpn.mode: "always"` tunnels
157
- every UFR call, useful behind firewalls that block the campus route.
173
+ `transport.type: "direct"` never opens the tunnel.
174
+
175
+ ## Releases
176
+
177
+ Releases are tag-driven: `scripts/release.sh 0.2.1` bumps, commits, tags and
178
+ pushes; the workflow then verifies on Ubuntu, macOS and Windows, stages the
179
+ package on npm (trusted publishing via OIDC — no stored tokens), and creates
180
+ the GitHub release after a maintainer approves the staged version with 2FA.
158
181
 
159
182
  ## Model data
160
183
 
package/package.json CHANGED
@@ -1,16 +1,44 @@
1
1
  {
2
2
  "name": "opencode-ufr",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Uni Freiburg (UFR) Open WebUI models in opencode — local gateway with key pool, UFR-aware rate limiting, circuit breaker and fallbacks",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "main": "./src/plugin/index.ts",
8
- "exports": { ".": "./src/plugin/index.ts" },
9
- "bin": { "ufr": "bin/ufr.ts", "opencode-ufr": "bin/ufr.ts" },
10
- "files": ["bin", "src", "models.json", "README.md", "LICENSE"],
11
- "engines": { "bun": ">=1.3.0" },
12
- "repository": { "type": "git", "url": "git+https://github.com/FinleyLaempe/opencode-ufr.git" },
13
- "keywords": ["opencode", "opencode-plugin", "uni-freiburg", "open-webui"],
14
- "scripts": { "test": "bun test", "typecheck": "tsc --noEmit", "prepublishOnly": "bun run typecheck && bun test" },
15
- "devDependencies": { "@types/bun": "^1.3.0", "typescript": "^5.9.0" }
8
+ "exports": {
9
+ ".": "./src/plugin/index.ts"
10
+ },
11
+ "bin": {
12
+ "ufr": "bin/ufr.ts",
13
+ "opencode-ufr": "bin/ufr.ts"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src",
18
+ "models.json",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "engines": {
23
+ "bun": ">=1.3.0"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/FinleyLaempe/opencode-ufr.git"
28
+ },
29
+ "keywords": [
30
+ "opencode",
31
+ "opencode-plugin",
32
+ "uni-freiburg",
33
+ "open-webui"
34
+ ],
35
+ "scripts": {
36
+ "test": "bun test",
37
+ "typecheck": "tsc --noEmit",
38
+ "prepublishOnly": "bun run typecheck && bun test"
39
+ },
40
+ "devDependencies": {
41
+ "@types/bun": "^1.3.0",
42
+ "typescript": "^5.9.0"
43
+ }
16
44
  }