@legioncodeinc/rflectr 0.1.0 → 0.1.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.
- package/README.md +1 -5
- package/dist/cli.js +1 -1
- package/library/README.md +39 -39
- package/library/issues/README.md +46 -46
- package/library/issues/backlog/README.md +26 -26
- package/library/issues/completed/README.md +13 -13
- package/library/issues/in-work/README.md +13 -13
- package/library/knowledge/README.md +34 -34
- package/library/knowledge/private/README.md +40 -40
- package/library/knowledge/private/standards/documentation-framework.md +154 -154
- package/library/knowledge/public/README.md +49 -49
- package/library/notes/README.md +21 -21
- package/library/requirements/README.md +51 -51
- package/library/requirements/backlog/README.md +30 -30
- package/library/requirements/completed/README.md +14 -14
- package/library/requirements/completed/prd-002-provider-registry/prd-002-provider-registry-index.md +263 -0
- package/library/requirements/completed/prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md +260 -0
- package/library/requirements/completed/prd-004-translation-layer/prd-004-translation-layer-index.md +196 -0
- package/library/requirements/completed/prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md +176 -0
- package/library/requirements/completed/prd-006-credential-storage/prd-006-credential-storage-index.md +190 -0
- package/library/requirements/completed/prd-006-credential-storage/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-007-oauth-device-flows/prd-007-oauth-device-flows-index.md +208 -0
- package/library/requirements/completed/prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md +249 -0
- package/library/requirements/completed/prd-008-preferences-tiers-favorites/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-009-codex-integration/prd-009-codex-integration-index.md +212 -0
- package/library/requirements/completed/prd-009-codex-integration/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-010-gemini-cli-integration/prd-010-gemini-cli-integration-index.md +211 -0
- package/library/requirements/completed/prd-010-gemini-cli-integration/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md +228 -0
- package/library/requirements/completed/prd-012-server-gateway/prd-012-server-gateway-index.md +356 -0
- package/library/requirements/completed/prd-012-server-gateway/qa/.gitkeep +0 -0
- package/library/requirements/in-work/README.md +19 -19
- package/library/requirements/reports/README.md +31 -31
- package/package.json +1 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# PRD-011: Claude Desktop Integration *(Retroactive)*
|
|
2
|
+
|
|
3
|
+
> **Status:** Shipped
|
|
4
|
+
> **Priority:** —
|
|
5
|
+
> **Effort:** —
|
|
6
|
+
> **Written:** June 2026
|
|
7
|
+
> **Retroactive:** Yes — written after implementation (rflectr v0.2.7).
|
|
8
|
+
> **Source:** `src/claude-desktop/*`, `src/claude-app.ts`
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
`rflectr claude-app` launches the **Claude Desktop** app in third-party-inference ("3P") mode, pointed at a local rflectr gateway instead of Anthropic's servers. The user picks a provider + model (or a favorites catalog), rflectr starts an in-process gateway server on a random local port, writes a gateway config into Claude Desktop's on-disk 3P config library, and opens (or restarts) the app. On exit, rflectr restores the original config.
|
|
15
|
+
|
|
16
|
+
The defining constraint of this surface: **a desktop app cannot inherit environment variables** the way a launched CLI can. The CLI launchers (`rflectr claude`, `codex`, `gemini`) point the host at the proxy purely through child-process env vars and never touch a config file (see [PRD-001 — CLI Core & Launch Orchestration](../prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md)). Claude Desktop has no such hook. So this is the one surface where rflectr **writes the host application's own config file** — and therefore must back it up and restore it on exit, guarded by a lock file to survive crashes. This is the deliberate exception to rflectr's env-only isolation contract.
|
|
17
|
+
|
|
18
|
+
Entry point: `runClaudeAppCommand` (`src/claude-app.ts:61`). Supported on **macOS and Windows only** (`claudeAppSupported`, `src/claude-desktop/app-launch.ts:11`).
|
|
19
|
+
|
|
20
|
+
> See also: [`harnesses.md`](../../../knowledge/private/integrations/harnesses.md) (private integration notes — the "where each host departs from the pattern" overview) and [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) (public user-facing setup guide).
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## What Was Built
|
|
25
|
+
|
|
26
|
+
- A `rflectr claude-app` command that, on macOS/Windows, drives Claude Desktop into 3P mode against a local gateway with no manual config editing.
|
|
27
|
+
- A **config-write** path: a `<uuid>.json` gateway config is written into the Claude Desktop 3P config library, and `_meta.json`'s `appliedId` is repointed at it so the app picks it up on next launch (`writeRflectrConfig`, `src/claude-desktop/app-config.ts:58`).
|
|
28
|
+
- A **backup + restore** path: `_meta.json` is copied to `_meta.json.bak` before the patch, and a `.rflectr.lock` file records the live session. On clean exit, crash recovery, or `--restore`, the backup is restored and the injected config removed (`src/claude-desktop/app-session.ts`).
|
|
29
|
+
- **Session lifecycle** handling: concurrent-session detection, stale-session recovery on startup, SIGINT/SIGTERM-driven shutdown, and a `process.on('exit')` cleanup hook.
|
|
30
|
+
- **Model selection** reusing the Codex provider/model pickers, plus a favorites-catalog mode driven by saved preferences.
|
|
31
|
+
- The gateway itself is the full `server` gateway (`startServer`, see [PRD-012 — Server Gateway](../prd-012-server-gateway/prd-012-server-gateway-index.md)), serving `/anthropic` with a synthetic model catalog — not a bespoke per-protocol proxy.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Goals
|
|
36
|
+
|
|
37
|
+
- Let a user route Claude Desktop's **Cowork** and **Code** tabs at registry providers / OpenCode Zen / Go with one command, no manual Developer-menu config.
|
|
38
|
+
- Never leave Claude Desktop's config in a broken 3P state after the rflectr session ends — restore the prior config on exit and after crashes.
|
|
39
|
+
- Keep the model catalog under the user's control (single selected model, or a favorites-only catalog).
|
|
40
|
+
- Reuse the existing `server` gateway and SDK translation layer rather than building a Claude-Desktop-specific proxy.
|
|
41
|
+
|
|
42
|
+
## Non-Goals
|
|
43
|
+
|
|
44
|
+
- **Linux support.** macOS and Windows only; `claudeAppSupported()` throws otherwise (`src/claude-desktop/app-launch.ts:11`).
|
|
45
|
+
- **Restoring Claude Desktop to first-party (Anthropic sign-in / Chat tab) mode.** rflectr restores the *pre-session* 3P config state; full 1P revert is a documented manual procedure in the user guide ([`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) → "Restore Claude Desktop to Anthropic's servers").
|
|
46
|
+
- **The Chat tab.** With 3P inference, Claude Desktop offers only Cowork and Code; Chat is an Anthropic product constraint, not an rflectr limitation.
|
|
47
|
+
- **Editing `settings.json`-style host config beyond the 3P config library.** rflectr only writes the `<uuid>.json` and `_meta.json` under `Claude-3p/configLibrary/`.
|
|
48
|
+
- **Mid-session model switching** inside the running app (the Gemini CLI surface has a `.model` switch; Claude Desktop does not).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Features
|
|
53
|
+
|
|
54
|
+
| # | Feature | Implementation |
|
|
55
|
+
|---|---------|----------------|
|
|
56
|
+
| F1 | `rflectr claude-app` command + help/restore/trace flags | `runClaudeAppCommand`, `claudeAppHelpText` (`src/claude-app.ts:27`, `src/claude-app.ts:61`) |
|
|
57
|
+
| F2 | Platform gate (macOS/Windows only) | `claudeAppSupported` (`src/claude-desktop/app-launch.ts:11`) |
|
|
58
|
+
| F3 | App discovery (Claude.app / Claude.exe / Start menu) | `findClaudeApp` (`src/claude-desktop/app-launch.ts:68`) |
|
|
59
|
+
| F4 | Launch or restart the app | `launchOrRestartClaudeApp` (`src/claude-desktop/app-launch.ts:198`) |
|
|
60
|
+
| F5 | Provider + model picker (reuses Codex pickers) | `pickCodexProvider` / `pickCodexModel` via `src/claude-app.ts:127`–`134` |
|
|
61
|
+
| F6 | Favorites-catalog mode | `favorites.length > 0` branch + `filterServerModelsByFavorites` (`src/claude-app.ts:121`, `src/claude-app.ts:153`) |
|
|
62
|
+
| F7 | Gateway config write into 3P config library | `writeRflectrConfig` (`src/claude-desktop/app-config.ts:58`) |
|
|
63
|
+
| F8 | `_meta.json` backup before patch | `backupMetaJson` (`src/claude-desktop/app-session.ts:43`) |
|
|
64
|
+
| F9 | Lock file (pid / startedAt / uuid / proxyPort) | `ClaudeSessionLock`, `writeSessionLock` (`src/claude-desktop/app-session.ts:7`, `:28`) |
|
|
65
|
+
| F10 | Concurrent + stale session detection | `isConcurrentLiveSession`, `hasStaleSession` (`src/claude-desktop/app-session.ts:76`, `:67`) |
|
|
66
|
+
| F11 | Shutdown wait (SIGINT/SIGTERM) + exit-hook cleanup | `waitForShutdown`, `setupExitCleanup` (`src/claude-desktop/app-session.ts:94`, `:119`) |
|
|
67
|
+
| F12 | Restore on exit / crash / `--restore` | `cleanupSession`, `recoverSession` (`src/claude-desktop/app-session.ts:113`, `:82`) |
|
|
68
|
+
| F13 | Local gateway (the `server` gateway, `/anthropic`) | `startServer` + `createGatewayModelCatalog` (`src/claude-app.ts:183`) |
|
|
69
|
+
| F14 | Recent-models persistence per provider | `savePreferences` recent-models update (`src/claude-app.ts:206`–`212`) |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Architecture & Implementation
|
|
74
|
+
|
|
75
|
+
### Where this surface departs from the env-only rule
|
|
76
|
+
|
|
77
|
+
CLI hosts are pointed at the proxy through child-process environment variables only (PRD-001). A desktop app has no env to inherit, so Claude Desktop reads a **config file** at launch. rflectr therefore writes that config — and to keep the rule "never permanently mutate the host" intact, it pairs every write with a backup and a guaranteed restore.
|
|
78
|
+
|
|
79
|
+
```mermaid
|
|
80
|
+
flowchart TD
|
|
81
|
+
cli["CLI hosts (claude, codex, gemini)"] -->|child-process env vars| proxy["translating proxy / gateway"]
|
|
82
|
+
app["Desktop app (claude-app)"] -->|"writes <uuid>.json + repoints _meta.json"| proxy
|
|
83
|
+
app -.->|"_meta.json.bak + .rflectr.lock"| restore["restore original config on exit / crash"]
|
|
84
|
+
proxy --> gw["server gateway /anthropic + SDK adapter"]
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Gateway config shape
|
|
88
|
+
|
|
89
|
+
`buildRflectrConfig(proxyPort)` (`src/claude-desktop/app-config.ts:48`) produces the 3P gateway profile:
|
|
90
|
+
|
|
91
|
+
```jsonc
|
|
92
|
+
{
|
|
93
|
+
"inferenceProvider": "gateway",
|
|
94
|
+
"inferenceGatewayBaseUrl": "http://127.0.0.1:<port>/anthropic",
|
|
95
|
+
"inferenceGatewayApiKey": "dummy",
|
|
96
|
+
"inferenceGatewayAuthScheme": "bearer",
|
|
97
|
+
"coworkEgressAllowedHosts": ["*"]
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- `inferenceGatewayBaseUrl` ends in `/anthropic` with **no `/v1` suffix** — Claude Desktop appends `/v1/models` and `/v1/messages` itself; a `/anthropic/v1` URL would break discovery and inference (documented in [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md)).
|
|
102
|
+
- The API key is the literal `'dummy'` because local-mode gateway has no server password; the server is started with `apiKey: 'dummy'` / `serverPassword: null` (`src/claude-app.ts:186`–`187`).
|
|
103
|
+
|
|
104
|
+
### Config write + `_meta.json` repointing
|
|
105
|
+
|
|
106
|
+
`writeRflectrConfig(proxyPort)` (`src/claude-desktop/app-config.ts:58`):
|
|
107
|
+
|
|
108
|
+
1. Generates a `randomUUID()` and writes `buildRflectrConfig(...)` to `configLibrary/<uuid>.json`.
|
|
109
|
+
2. Reads (or initializes) `_meta.json`, sets `appliedId = uuid`, and appends an `entries` row `{ id: uuid, name: 'Rflectr Gateway' }` if not already present.
|
|
110
|
+
3. Returns the uuid (used as the session/cleanup key).
|
|
111
|
+
|
|
112
|
+
Config roots, by platform (`getClaudeDesktopHome`, `src/claude-desktop/app-config.ts:8`):
|
|
113
|
+
|
|
114
|
+
| Platform | 3P config root |
|
|
115
|
+
|---|---|
|
|
116
|
+
| macOS | `~/Library/Application Support/Claude-3p/` |
|
|
117
|
+
| Windows | `%LOCALAPPDATA%\Claude-3p/` |
|
|
118
|
+
|
|
119
|
+
The config library and `_meta.json` live under `configLibrary/` within that root (`getConfigLibraryPath`, `getMetaJsonPath`, `src/claude-desktop/app-config.ts:15`, `:19`).
|
|
120
|
+
|
|
121
|
+
### Backup / restore via lock files
|
|
122
|
+
|
|
123
|
+
The lock and backup are the safety mechanism that makes config-writing reversible:
|
|
124
|
+
|
|
125
|
+
- **Backup:** `backupMetaJson()` copies `_meta.json` → `_meta.json.bak` *before* the patch (`src/claude-desktop/app-session.ts:43`). Called at `src/claude-app.ts:181`, before `startServer` and `writeRflectrConfig`.
|
|
126
|
+
- **Lock:** `writeSessionLock({ pid, startedAt, uuid, proxyPort })` writes `.rflectr.lock` in the 3P home (`src/claude-desktop/app-session.ts:28`; shape `ClaudeSessionLock`, `:7`). Written at `src/claude-app.ts:196` right after the config is applied.
|
|
127
|
+
- **Restore:** `restoreMetaJson()` copies the `.bak` back over `_meta.json` and deletes the backup (`src/claude-desktop/app-session.ts:51`). `removeRflectrConfig(uuid)` deletes the injected `<uuid>.json` (`:60`).
|
|
128
|
+
- **Cleanup entry points:** `cleanupSession(uuid)` (clean exit, `:113`) and `recoverSession()` (crash / `--restore`, `:82`) both run restore + config-removal + lock deletion. `setupExitCleanup(uuid)` registers `cleanupSession` on `process.on('exit')` as a last-resort net (`:119`).
|
|
129
|
+
|
|
130
|
+
### Session lifecycle
|
|
131
|
+
|
|
132
|
+
```mermaid
|
|
133
|
+
sequenceDiagram
|
|
134
|
+
participant U as User
|
|
135
|
+
participant R as rflectr claude-app
|
|
136
|
+
participant FS as Claude-3p config
|
|
137
|
+
participant App as Claude Desktop
|
|
138
|
+
|
|
139
|
+
U->>R: rflectr claude-app
|
|
140
|
+
R->>R: claudeAppSupported() + TTY check
|
|
141
|
+
R->>R: isConcurrentLiveSession()? -> abort if live
|
|
142
|
+
R->>R: hasStaleSession()? -> recoverSession()
|
|
143
|
+
R->>R: pick provider + model (or favorites)
|
|
144
|
+
R->>FS: backupMetaJson() (_meta.json -> .bak)
|
|
145
|
+
R->>R: startServer() on 127.0.0.1:0
|
|
146
|
+
R->>FS: writeRflectrConfig(port) (<uuid>.json + _meta.appliedId)
|
|
147
|
+
R->>FS: writeSessionLock({pid,uuid,port})
|
|
148
|
+
R->>R: setupExitCleanup(uuid)
|
|
149
|
+
R->>App: launchOrRestartClaudeApp()
|
|
150
|
+
U-->>R: Ctrl+C (SIGINT)
|
|
151
|
+
R->>R: waitForShutdown() resolves
|
|
152
|
+
R->>FS: cleanupSession(uuid) (restore .bak, rm config, rm lock)
|
|
153
|
+
R->>App: optionally quitClaudeAppGracefully()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Startup guards (`src/claude-app.ts`):
|
|
157
|
+
- **Interactive-terminal required** — non-TTY aborts (`src/claude-app.ts:84`).
|
|
158
|
+
- **Concurrent session** — `isConcurrentLiveSession()` (lock present + pid alive) aborts with a "stop it with Ctrl+C" message (`src/claude-app.ts:90`; `src/claude-desktop/app-session.ts:76`).
|
|
159
|
+
- **Stale session** — `hasStaleSession()` (lock present + pid dead) triggers `recoverSession()` to clean up a prior crash before proceeding (`src/claude-app.ts:96`).
|
|
160
|
+
|
|
161
|
+
Shutdown ordering (`src/claude-app.ts:232`–`245`): `waitForShutdown()` resolves on SIGINT/SIGTERM, then `cleanupSession(uuid)` runs **before** the optional "close Claude Desktop?" prompt — so the config is restored ASAP and a second Ctrl+C during the prompt finds nothing left to undo (per the inline comment at `src/claude-app.ts:235`).
|
|
162
|
+
|
|
163
|
+
### Model selection
|
|
164
|
+
|
|
165
|
+
- Providers come from `fetchProviderCatalog({ agent: 'codex-app' })`, filtered by `codexCompatibleProviders(..., 'codex-app')` (`src/claude-app.ts:105`, `:113`). The provider's models are narrowed with `routableModelsForProvider(provider, 'codex-app')` (`providerForClaudePicker`, `src/claude-app.ts:57`).
|
|
166
|
+
- **Single-model mode:** the picked model becomes a one-entry `ServerModelInfo[]` carrying `modelFormat`, `npm`, `apiBaseUrl`, `baseUrl`, `completionsUrl`, `upstreamModelId`, `contextWindow`, and the resolved provider `apiKey` (`src/claude-app.ts:157`–`174`). Credential resolved via `activeProvider.apiKey` or `resolveProviderCredential(id, authRef)` (`src/claude-app.ts:138`–`148`).
|
|
167
|
+
- **Favorites mode:** when `prefs.favoriteModels.length > 0`, a `__favorites__` picker option loads all server models and filters them with `filterServerModelsByFavorites(allModels, favorites)` (`src/claude-app.ts:153`–`155`).
|
|
168
|
+
- Either way, the model list is wrapped in `createGatewayModelCatalog(serverModels, { maskGatewayIds: true })` and handed to `startServer` (`src/claude-app.ts:188`). Discovery-id masking is on so Claude Desktop's competitor-name filtering doesn't hide models (rationale in [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) troubleshooting).
|
|
169
|
+
- Recent models per provider are persisted (single-mode only) via `savePreferences` (`src/claude-app.ts:206`–`212`).
|
|
170
|
+
|
|
171
|
+
### App discovery & launch (platform specifics)
|
|
172
|
+
|
|
173
|
+
`findClaudeApp()` (`src/claude-desktop/app-launch.ts:68`):
|
|
174
|
+
- **macOS:** checks `/Applications/Claude.app` and `~/Applications/Claude.app`, then falls back to `mdfind` by bundle id `com.anthropic.claudefordesktop`.
|
|
175
|
+
- **Windows:** checks `%LOCALAPPDATA%\Programs\Claude` and `%LOCALAPPDATA%\Claude` (including `app-*` subfolders) for `Claude.exe`, then falls back to `Get-StartApps` returning a `shell:AppsFolder\<AppID>` URI.
|
|
176
|
+
|
|
177
|
+
`launchOrRestartClaudeApp()` (`src/claude-desktop/app-launch.ts:198`): if the app isn't running, it just opens it; if it is running, it prompts to restart (so the new config is read), quits gracefully (`osascript` on macOS, `CloseMainWindow()` on Windows), waits up to 5s, force-quits Windows PIDs if needed, then reopens. "Is it running?" uses `osascript` (macOS) or PowerShell `Get-Process` / matching-PID checks (Windows) (`isClaudeAppRunning`, `:121`).
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Acceptance Criteria
|
|
182
|
+
|
|
183
|
+
- [x] `rflectr claude-app` launches Claude Desktop in 3P gateway mode on macOS and Windows (`runClaudeAppCommand`, `src/claude-app.ts:61`; `claudeAppSupported`, `src/claude-desktop/app-launch.ts:11`).
|
|
184
|
+
- [x] A `<uuid>.json` gateway config is written into the 3P config library with `inferenceProvider: 'gateway'`, a `/anthropic` base URL (no `/v1`), `bearer` auth, and a `'dummy'` key (`buildRflectrConfig` / `writeRflectrConfig`, `src/claude-desktop/app-config.ts:48`, `:58`).
|
|
185
|
+
- [x] `_meta.json` `appliedId` is repointed at the new uuid and an `entries` row is added (`src/claude-desktop/app-config.ts:65`–`71`).
|
|
186
|
+
- [x] `_meta.json` is backed up to `_meta.json.bak` before the patch (`backupMetaJson`, `src/claude-desktop/app-session.ts:43`; called `src/claude-app.ts:181`).
|
|
187
|
+
- [x] A `.rflectr.lock` records `pid`, `startedAt`, `uuid`, and `proxyPort` (`ClaudeSessionLock` / `writeSessionLock`, `src/claude-desktop/app-session.ts:7`, `:28`).
|
|
188
|
+
- [x] On clean exit (Ctrl+C), the original `_meta.json` is restored, the injected config removed, and the lock deleted (`cleanupSession`, `src/claude-desktop/app-session.ts:113`; invoked `src/claude-app.ts:237`).
|
|
189
|
+
- [x] After a crash, a stale session is detected and cleaned up on next run, and `--restore` performs the same recovery (`hasStaleSession` + `recoverSession`, `src/claude-desktop/app-session.ts:67`, `:82`; `--restore` at `src/claude-app.ts:67`).
|
|
190
|
+
- [x] A concurrent live session is detected and the second invocation aborts (`isConcurrentLiveSession`, `src/claude-desktop/app-session.ts:76`; `src/claude-app.ts:90`).
|
|
191
|
+
- [x] Exit-hook cleanup is registered so an abrupt `process.exit` still restores config (`setupExitCleanup`, `src/claude-desktop/app-session.ts:119`; `src/claude-app.ts:204`).
|
|
192
|
+
- [x] Both a single selected model and a favorites-only catalog can drive the gateway (`src/claude-app.ts:153`–`174`).
|
|
193
|
+
- [x] The gateway is the shared `server` gateway serving `/anthropic`, not a bespoke proxy (`startServer` + `createGatewayModelCatalog`, `src/claude-app.ts:183`–`192`).
|
|
194
|
+
- [x] Non-interactive terminals are rejected (`src/claude-app.ts:84`).
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Files
|
|
199
|
+
|
|
200
|
+
| File | Role |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `src/claude-app.ts` | Command entry: `runClaudeAppCommand`, help text, picker orchestration, server start, session wiring, shutdown |
|
|
203
|
+
| `src/claude-desktop/app-config.ts` | 3P config paths, `buildRflectrConfig`, `writeRflectrConfig`, `_meta.json` read/write |
|
|
204
|
+
| `src/claude-desktop/app-session.ts` | Lock file + backup/restore: `backupMetaJson`, `restoreMetaJson`, `removeRflectrConfig`, lock read/write, stale/concurrent detection, `cleanupSession`, `recoverSession`, `waitForShutdown`, `setupExitCleanup` |
|
|
205
|
+
| `src/claude-desktop/app-launch.ts` | App discovery + launch/restart/quit per platform: `claudeAppSupported`, `findClaudeApp`, `isClaudeAppRunning`, `launchOrRestartClaudeApp`, `quitClaudeAppGracefully` |
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Risks & Known Limitations
|
|
210
|
+
|
|
211
|
+
- **Config-editing exception to the env-only rule.** Unlike every CLI launcher, this surface writes the host application's config file. Reversibility depends entirely on the backup (`_meta.json.bak`) + lock (`.rflectr.lock`) machinery. If the backup or lock is lost, manual recovery (the documented 1P-revert procedure) is required.
|
|
212
|
+
- **Backup granularity.** Only `_meta.json` is backed up; the injected `<uuid>.json` is removed by uuid on cleanup. If `_meta.json` is mutated by Claude Desktop itself between backup and restore, restore overwrites those changes with the pre-session snapshot.
|
|
213
|
+
- **`process.on('exit')` constraints.** The exit hook runs synchronous cleanup; it cannot await async work. A hard kill (`SIGKILL`) bypasses both the SIGINT/SIGTERM handler and the exit hook, leaving a stale session for the next-run / `--restore` recovery to clean up.
|
|
214
|
+
- **Linux unsupported.** `claudeAppSupported()` throws on any non-darwin/non-win32 platform.
|
|
215
|
+
- **No mid-session model switch.** The catalog is fixed at launch; changing models means restarting the session.
|
|
216
|
+
- **Chat tab unavailable in 3P mode** (Anthropic product constraint). Claude in Chrome is also incompatible with a gateway. Both documented in [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md).
|
|
217
|
+
- **Full 1P revert is manual.** rflectr restores the pre-session 3P config but does not return Claude Desktop to Anthropic sign-in; the user guide documents the multi-step manual revert.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Related
|
|
222
|
+
|
|
223
|
+
- [`harnesses.md`](../../../knowledge/private/integrations/harnesses.md) — private integration notes (the host-departure pattern, platform-differences table).
|
|
224
|
+
- [`claude-desktop.md`](../../../knowledge/public/guides/claude-desktop.md) — public setup, gateway cheat sheet, restore-to-1P, troubleshooting.
|
|
225
|
+
- [PRD-009 — Codex Integration](../prd-009-codex-integration/prd-009-codex-integration-index.md) — sibling desktop-app surface (`codex-app`) using the same config-patch + backup/restore-via-lock pattern.
|
|
226
|
+
- [PRD-005 — Local Proxy & Catalog Routing](../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md) — the proxy/translation machinery the gateway builds on.
|
|
227
|
+
- [PRD-012 — Server Gateway](../prd-012-server-gateway/prd-012-server-gateway-index.md) — the `startServer` gateway that serves `/anthropic` for Claude Desktop.
|
|
228
|
+
- [PRD-001 — CLI Core & Launch Orchestration](../prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md) — the env-only isolation contract this surface deliberately departs from.
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# PRD-012: Server Gateway *(Retroactive)*
|
|
2
|
+
|
|
3
|
+
> **Status:** Shipped
|
|
4
|
+
> **Priority:** —
|
|
5
|
+
> **Effort:** —
|
|
6
|
+
> **Written:** June 2026
|
|
7
|
+
> **Retroactive:** Yes — written after implementation (rflectr v0.2.7).
|
|
8
|
+
> **Source:** `src/server/*`
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Every other rflectr command is short-lived: it starts a proxy, spawns a coding
|
|
15
|
+
agent as a child process, and tears everything down on exit. `rflectr server`
|
|
16
|
+
inverts that model. It runs a **long-lived, foreground HTTP gateway** that
|
|
17
|
+
exposes the same model backends — OpenCode Zen, OpenCode Go, every materialized
|
|
18
|
+
local registry provider, or Claude on Google Vertex AI — behind both an
|
|
19
|
+
**Anthropic-compatible** and an **OpenAI-compatible** endpoint on a single port
|
|
20
|
+
(default **17645**).
|
|
21
|
+
|
|
22
|
+
The gateway is the backend for [PRD-011 Claude Desktop Integration](../prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md)
|
|
23
|
+
and for any tool that can be pointed at a base URL (e.g. THE AI Counsel, OpenAI-compatible
|
|
24
|
+
editor extensions). It reuses the same translation core as the CLI proxies
|
|
25
|
+
(PRD-004 / PRD-005): anthropic-format models forward raw to the provider's
|
|
26
|
+
`/v1/messages`; openai-format models route through the shared Vercel AI SDK
|
|
27
|
+
adapter via `createLanguageModel`. There is no second translation path.
|
|
28
|
+
|
|
29
|
+
`runServerCommand(options)` (`src/server/index.ts:377`) drives an interactive
|
|
30
|
+
wizard (which providers to expose, optional favorites-only catalog, discovery-id
|
|
31
|
+
masking, local vs network bind, server password) and then `startServer()`
|
|
32
|
+
(`src/server/router.ts:76`) listens until `Ctrl+C`.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## What Was Built
|
|
37
|
+
|
|
38
|
+
- **`rflectr server`** — foreground gateway over Zen/Go cloud models plus every
|
|
39
|
+
local registry provider, served on one port as both Anthropic and OpenAI APIs
|
|
40
|
+
(`src/server/index.ts:377`, `src/server/router.ts:76`).
|
|
41
|
+
- **`rflectr server --vertex`** — Claude on Google Vertex AI using local gcloud
|
|
42
|
+
Application Default Credentials, no OpenCode key required
|
|
43
|
+
(`src/server/index.ts:290`, `src/server/vertex-config.ts`).
|
|
44
|
+
- **Unified model loading** — `loadServerModels()` merges Zen, Go, and local
|
|
45
|
+
provider models into a single `ServerModelInfo[]`, enriched with reasoning
|
|
46
|
+
metadata (`src/server/index.ts:155`).
|
|
47
|
+
- **Per-endpoint routing** — `handleAnthropicMessages` and
|
|
48
|
+
`handleOpenAIChatCompletions` dispatch by `modelFormat`: anthropic → raw
|
|
49
|
+
forward; openai → SDK adapter with a per-`(model × npm × baseURL)`
|
|
50
|
+
`LanguageModel` cache (`src/server/router.ts:155`, `:242`, `:331`).
|
|
51
|
+
- **Auth gate** — Bearer / `x-api-key` comparison against an optional server
|
|
52
|
+
password; `null` password (local mode) allows all callers
|
|
53
|
+
(`src/server/auth.ts:10`).
|
|
54
|
+
- **Discovery-id masking** — self-inverse provider/model-slug reversal so vendor
|
|
55
|
+
names never appear literally in Claude Desktop / Cowork discovery ids
|
|
56
|
+
(`src/server/vendor-mask.ts:14`).
|
|
57
|
+
- **Provider / favorites filtering** — expose a chosen subset of providers, or
|
|
58
|
+
only favorite models (`src/server/catalog-filter.ts`).
|
|
59
|
+
- **Credential hygiene** — `GET /models` strips `apiKey` from every model entry;
|
|
60
|
+
header values are CR/LF-sanitized (`src/server/router.ts:125`, `:397`).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Goals
|
|
65
|
+
|
|
66
|
+
1. Serve every configured backend (Zen, Go, local registry providers, Vertex)
|
|
67
|
+
behind **one** local HTTP port that speaks **both** Anthropic and OpenAI wire
|
|
68
|
+
formats.
|
|
69
|
+
2. Reuse the shared SDK translation core (PRD-004) and upstream-forward helpers
|
|
70
|
+
(PRD-005) — no gateway-specific translation logic.
|
|
71
|
+
3. Make the gateway safe to expose on a LAN: optional server password, network
|
|
72
|
+
bind opt-in, credential stripping in catalog responses.
|
|
73
|
+
4. Provide a discovery surface Claude Desktop / Cowork can consume, including
|
|
74
|
+
optional vendor-name masking.
|
|
75
|
+
5. Offer a zero-OpenCode-key path to Claude via Vertex AI using existing gcloud
|
|
76
|
+
ADC.
|
|
77
|
+
|
|
78
|
+
## Non-Goals
|
|
79
|
+
|
|
80
|
+
- Process management / daemonization — the server runs in the foreground and
|
|
81
|
+
exits on `Ctrl+C` (`waitForShutdown` in `src/server/index.ts:189`). No
|
|
82
|
+
systemd unit, PID file, or background mode is shipped.
|
|
83
|
+
- TLS termination — the gateway listens over plain HTTP; HTTPS is expected to be
|
|
84
|
+
handled by a front proxy if needed.
|
|
85
|
+
- Accurate cost reporting for non-Anthropic models (inherited limitation from
|
|
86
|
+
the translation layer; Claude clients apply their own pricing table).
|
|
87
|
+
- Rate limiting, request quotas, or multi-tenant key management.
|
|
88
|
+
- Live context-window updates on `/model` switch (see Risks).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
|
|
94
|
+
| # | Feature | Where |
|
|
95
|
+
|---|---------|-------|
|
|
96
|
+
| F1 | Foreground gateway on port 17645, dual Anthropic + OpenAI endpoints | `src/server/router.ts:76`, `src/server/index.ts:450` |
|
|
97
|
+
| F2 | Interactive wizard: start mode, favorites-only, exposed providers, masking | `src/server/index.ts:256`, `src/server/prompts.ts` |
|
|
98
|
+
| F3 | Unified model load (Zen + Go + local providers) with reasoning enrichment | `src/server/index.ts:155`, `:176` |
|
|
99
|
+
| F4 | Anthropic Messages relay (raw forward or SDK adapter by format) | `src/server/router.ts:155` |
|
|
100
|
+
| F5 | OpenAI Chat Completions relay (direct relay or SDK adapter) | `src/server/router.ts:242` |
|
|
101
|
+
| F6 | Gateway alias ids + bidirectional catalog lookup | `src/server/models.ts:114`, `:140` |
|
|
102
|
+
| F7 | Discovery-id masking (self-inverse) | `src/server/vendor-mask.ts:14` |
|
|
103
|
+
| F8 | Bearer / `x-api-key` auth with null-password local mode | `src/server/auth.ts:10` |
|
|
104
|
+
| F9 | `apiKey` stripped from `GET /models` | `src/server/router.ts:125` |
|
|
105
|
+
| F10 | Provider-subset and favorites-only filtering | `src/server/catalog-filter.ts:6`, `:15` |
|
|
106
|
+
| F11 | Vertex AI mode via gcloud ADC | `src/server/index.ts:290`, `src/server/vertex-config.ts` |
|
|
107
|
+
| F12 | Server-password save/reuse, network-bind opt-in | `src/server/index.ts:205`, `src/server/prompts.ts:51` |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Architecture & Implementation
|
|
112
|
+
|
|
113
|
+
### Request flow
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
flowchart TD
|
|
117
|
+
client["Claude Desktop / any tool"] --> ep{"method + path"}
|
|
118
|
+
ep -->|"GET /health"| health["{ ok: true }"]
|
|
119
|
+
ep -->|other| auth{"isAuthorized?"}
|
|
120
|
+
auth -->|no| u401["401 Unauthorized"]
|
|
121
|
+
auth -->|yes| route{"path"}
|
|
122
|
+
route -->|"GET /models"| list["catalog.list() — apiKey stripped"]
|
|
123
|
+
route -->|"GET /anthropic/v1/models"| amodels["formatGatewayAnthropicModels (optional mask)"]
|
|
124
|
+
route -->|"GET /openai/v1/models"| omodels["formatOpenAIModels"]
|
|
125
|
+
route -->|"POST /anthropic/v1/messages"| anth["handleAnthropicMessages"]
|
|
126
|
+
route -->|"POST /openai/v1/chat/completions"| oai["handleOpenAIChatCompletions"]
|
|
127
|
+
anth --> fmt{"model.modelFormat"}
|
|
128
|
+
oai --> fmt
|
|
129
|
+
fmt -->|anthropic| raw["raw forward → {baseUrl}/v1/messages"]
|
|
130
|
+
fmt -->|openai| sdk["createLanguageModel + SDK adapter"]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`routeRequest` (`src/server/router.ts:109`) handles `/health` **before** the auth
|
|
134
|
+
check, then gates everything else through `isAuthorized` before dispatching by
|
|
135
|
+
method + path.
|
|
136
|
+
|
|
137
|
+
### Server model loading
|
|
138
|
+
|
|
139
|
+
`loadServerModels()` (`src/server/index.ts:155`) calls
|
|
140
|
+
`fetchProviderCatalog({ agent: 'server' })` and assembles one `ServerModelInfo[]`:
|
|
141
|
+
|
|
142
|
+
- **Zen** models, filtered by the registry's `subscriptionFilter` (free-only when
|
|
143
|
+
configured) via `filterZenModelsForServer` (`src/server/index.ts:115`), then
|
|
144
|
+
mapped by `zenGoModelsToServerModels` (`src/provider-catalog.ts:259`).
|
|
145
|
+
- **Go** models, filtered to drop `modelFormat === 'unsupported'` by
|
|
146
|
+
`usableGoModels` (`src/server/index.ts:123`), same mapper.
|
|
147
|
+
- **Local registry providers**, mapped by `localProvidersToServerModels`
|
|
148
|
+
(`src/provider-catalog.ts:228`), each carrying `npm`, `apiBaseUrl`, `baseUrl`,
|
|
149
|
+
`completionsUrl`, `apiKey`, `authType`, and `oauthAccountId`.
|
|
150
|
+
|
|
151
|
+
For Zen/Go, openai-format models get `npm = '@ai-sdk/openai-compatible'` and
|
|
152
|
+
`apiBaseUrl = ${backend.baseUrl}/v1`; anthropic-format models stay raw
|
|
153
|
+
passthrough (no `npm`) — matching the CLI catalog's `zenGoModelToRoute`
|
|
154
|
+
(`src/provider-catalog.ts:273`).
|
|
155
|
+
|
|
156
|
+
Every model is then passed through `enrichServerModelReasoning`
|
|
157
|
+
(`src/server/index.ts:176`), which calls `getReasoningCapabilities` and stamps a
|
|
158
|
+
`defaultEffort` fallback for openai-format models that declare one.
|
|
159
|
+
|
|
160
|
+
### Catalog & gateway aliases (`src/server/models.ts`)
|
|
161
|
+
|
|
162
|
+
`createGatewayModelCatalog(models, opts?)` (`:140`) builds a bidirectional
|
|
163
|
+
lookup keyed by `model.id` **and** by the exposed gateway alias, so Claude
|
|
164
|
+
clients (which only surface `claude-*` / `anthropic-*` ids) can address a model
|
|
165
|
+
by either form.
|
|
166
|
+
|
|
167
|
+
- `gatewayAliasId(model)` (`:114`) → `anthropic-{provider}__{model}` via
|
|
168
|
+
`aliasModelId` (from `src/proxy.ts`).
|
|
169
|
+
- `exposedGatewayAliasId(model, opts?)` (`:118`) → masked alias when
|
|
170
|
+
`opts.maskGatewayIds`.
|
|
171
|
+
- `gatewayDisplayName(model, opts?)` (`:124`) → `"Model Name"`, or
|
|
172
|
+
`"Model Name (Provider Label)"` when masking is on.
|
|
173
|
+
- `upstreamModelId(model)` (`:159`) → strips a trailing `[1m]` context suffix
|
|
174
|
+
for the wire call.
|
|
175
|
+
- `formatGatewayAnthropicModels` / `formatOpenAIModels` (`:129`, `:174`) build
|
|
176
|
+
the endpoint payloads; `formatAnthropicModelEntry` (`:58`) attaches
|
|
177
|
+
`context_window` / `max_input_tokens` via `resolveContextWindow`.
|
|
178
|
+
|
|
179
|
+
### Routing — Anthropic messages (`src/server/router.ts:155`)
|
|
180
|
+
|
|
181
|
+
1. Parse JSON body; look up the model in the catalog (`lookupModel`, `:307`).
|
|
182
|
+
2. **anthropic format** (`:176`): validate `baseUrl` is `http(s)://`, compute
|
|
183
|
+
`{baseUrl}/v1/messages` (or the cloud backend's URL via `backendFor`, `:322`),
|
|
184
|
+
forward the body verbatim — swapping in `upstreamModelId(model)` and relaying
|
|
185
|
+
the inbound `anthropic-beta` header — through `postJsonUpstream`
|
|
186
|
+
(shared with PRD-005's `upstream-forward.ts`).
|
|
187
|
+
3. **openai format** (`:192`): guard with `isSdkMigratedNpm(model.npm)`; init or
|
|
188
|
+
reuse a cached `LanguageModel` (`getOrInitLanguageModel`, `:331`);
|
|
189
|
+
`sdkTranslateRequest` → `streamAnthropicResponse` (SSE) or
|
|
190
|
+
`generateAnthropicResponse`. The response `model` field is set to the masked
|
|
191
|
+
display name when masking is on (`getResponseModelId`, `:363`) so Claude
|
|
192
|
+
Desktop's status chip shows a human-readable name.
|
|
193
|
+
|
|
194
|
+
### Routing — OpenAI chat completions (`src/server/router.ts:242`)
|
|
195
|
+
|
|
196
|
+
- `supportsDirectOpenAIChatCompletions(model)` (`src/server/models.ts:165`) is
|
|
197
|
+
true for openai-format models with a `completionsUrl` or a Zen/Go backend — those
|
|
198
|
+
relay raw through `relayAnthropicMessages` to `{completionsUrl|backend}/v1/chat/completions`.
|
|
199
|
+
- Otherwise the request goes through the SDK adapter: `translateOpenAiRequest` →
|
|
200
|
+
`streamOpenAiResponse` / `generateOpenAiResponse` (`src/openai-adapter.ts`).
|
|
201
|
+
|
|
202
|
+
### LanguageModel cache (`src/server/router.ts:331`)
|
|
203
|
+
|
|
204
|
+
`getOrInitLanguageModel` keys the cache on
|
|
205
|
+
`providerId/sourceBackend ∣ id ∣ upstreamModelId ∣ npm ∣ baseURL` (joined with
|
|
206
|
+
`\x1f`) so a given model is instantiated once per process and reused across
|
|
207
|
+
requests.
|
|
208
|
+
|
|
209
|
+
### Auth (`src/server/auth.ts`)
|
|
210
|
+
|
|
211
|
+
`isAuthorized(request, serverPassword)` (`:10`) returns `true` immediately when
|
|
212
|
+
`serverPassword === null` (local mode). Otherwise it accepts a `Bearer` token
|
|
213
|
+
(`extractBearerToken`, `:19`) **or** an `x-api-key` header, each passed through
|
|
214
|
+
`sanitizeCredential` (first non-empty line only, `:4`). In **network mode** the
|
|
215
|
+
wizard requires a server password; it is the only gate once the port is reachable
|
|
216
|
+
beyond localhost, so it must be treated as a real secret. Incoming header values
|
|
217
|
+
are CR/LF-stripped in `sanitizeIncomingHeaderValue` (`src/server/router.ts:397`).
|
|
218
|
+
|
|
219
|
+
### Vendor masking (`src/server/vendor-mask.ts`)
|
|
220
|
+
|
|
221
|
+
`maskGatewayModelId(aliasId)` (`:14`) reverses the provider-slug and
|
|
222
|
+
model-suffix segments of `anthropic-{provider}__{model}`. It is **self-inverse**
|
|
223
|
+
— `unmaskGatewayModelId` (`:24`) calls the same function. The masked catalog
|
|
224
|
+
registers all of `model.id`, the masked alias, and the raw alias so chat
|
|
225
|
+
requests resolve regardless of which id the client sends
|
|
226
|
+
(`createGatewayModelCatalog`, `src/server/models.ts:146`).
|
|
227
|
+
|
|
228
|
+
### Vertex mode (`src/server/index.ts:290`, `src/server/vertex-config.ts`)
|
|
229
|
+
|
|
230
|
+
`runVertexServerCommand` exposes **Claude on Vertex AI** without an OpenCode key:
|
|
231
|
+
|
|
232
|
+
- `buildVertexRuntimeConfig(env?)` (`vertex-config.ts:107`) resolves project
|
|
233
|
+
(`ANTHROPIC_VERTEX_PROJECT_ID` → `GOOGLE_CLOUD_PROJECT` → `GOOGLE_VERTEX_PROJECT`)
|
|
234
|
+
and location (`GOOGLE_CLOUD_LOCATION` → `CLOUD_ML_REGION` → `GOOGLE_VERTEX_LOCATION`
|
|
235
|
+
→ `global`); returns `null` if no project is set.
|
|
236
|
+
- `hasApplicationDefaultCredentials()` (`:66`) checks
|
|
237
|
+
`GOOGLE_APPLICATION_CREDENTIALS` or `~/.config/gcloud/application_default_credentials.json`.
|
|
238
|
+
- `vertexModelsToServerModels(config)` (`:118`) builds `ServerModelInfo[]` routed
|
|
239
|
+
through `@ai-sdk/google-vertex/anthropic` (`VERTEX_ANTHROPIC_NPM`,
|
|
240
|
+
`modelFormat: 'openai'`, `sourceBackend: 'vertex'`).
|
|
241
|
+
- `createVertexModelCatalog(models)` (`:159`) adds short aliases (`sonnet` /
|
|
242
|
+
`haiku` / `opus`) and `[1m]` context variants, resolving client lookups via
|
|
243
|
+
`vertexClientModelLookupCandidates` (`:139`). Defaults: `claude-sonnet-4-6`,
|
|
244
|
+
`claude-opus-4-6`, `claude-haiku-4-5`; overridable at
|
|
245
|
+
`~/.rflectr/vertex-models.json`.
|
|
246
|
+
|
|
247
|
+
The Vertex server starts with `apiKey: 'vertex-local'` and passes a `vertex`
|
|
248
|
+
config (`{ project, location }`) to `startServer`, which threads it into
|
|
249
|
+
`createLanguageModel` (`src/server/router.ts:331`, `:348`).
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## API Surface
|
|
254
|
+
|
|
255
|
+
Base URLs for clients:
|
|
256
|
+
|
|
257
|
+
- Anthropic: `http://127.0.0.1:17645/anthropic`
|
|
258
|
+
- OpenAI: `http://127.0.0.1:17645/openai/v1`
|
|
259
|
+
|
|
260
|
+
> Do **not** append `/v1` to the Anthropic base URL — the Anthropic SDK adds API
|
|
261
|
+
> paths itself.
|
|
262
|
+
|
|
263
|
+
| Method + path | Purpose | Source |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| `GET /health` | Liveness `{ ok: true }` (pre-auth) | `src/server/router.ts:114` |
|
|
266
|
+
| `GET /models` | Raw catalog, `apiKey` stripped | `src/server/router.ts:124` |
|
|
267
|
+
| `GET /anthropic/v1/models` | Anthropic-format list (optionally masked) | `src/server/router.ts:129` |
|
|
268
|
+
| `GET /openai/v1/models` | OpenAI-format list | `src/server/router.ts:134` |
|
|
269
|
+
| `POST /anthropic/v1/messages` | Anthropic Messages relay | `src/server/router.ts:139` |
|
|
270
|
+
| `POST /openai/v1/chat/completions` | OpenAI Chat Completions relay | `src/server/router.ts:144` |
|
|
271
|
+
|
|
272
|
+
`POST /anthropic/v1/messages` honors `stream` (SSE when true), supports both
|
|
273
|
+
streaming and non-streaming for anthropic-format (raw forward) and openai-format
|
|
274
|
+
(SDK adapter) models, and relays the inbound `anthropic-beta` header on raw
|
|
275
|
+
forwards. Unknown / unsupported models return `400`; upstream/SDK errors surface
|
|
276
|
+
as `502`.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Acceptance Criteria
|
|
281
|
+
|
|
282
|
+
- [x] `rflectr server` starts a foreground HTTP gateway on port 17645 serving
|
|
283
|
+
both `/anthropic` and `/openai/v1` endpoints (`src/server/index.ts:450`,
|
|
284
|
+
`src/server/router.ts:76`).
|
|
285
|
+
- [x] `loadServerModels()` merges Zen, Go, and local registry provider models
|
|
286
|
+
into one `ServerModelInfo[]` (`src/server/index.ts:155`).
|
|
287
|
+
- [x] Local providers are appended carrying `npm` / `apiBaseUrl` / `baseUrl` /
|
|
288
|
+
`completionsUrl` / `apiKey` (`src/provider-catalog.ts:228`).
|
|
289
|
+
- [x] `handleAnthropicMessages` raw-forwards anthropic-format models to
|
|
290
|
+
`{baseUrl}/v1/messages` (`src/server/router.ts:176`).
|
|
291
|
+
- [x] openai-format models route through the `isSdkMigratedNpm` guard →
|
|
292
|
+
`createLanguageModel` + `streamAnthropicResponse` /
|
|
293
|
+
`generateAnthropicResponse` (`src/server/router.ts:192`).
|
|
294
|
+
- [x] `GET /models` strips `apiKey` from every entry (`src/server/router.ts:125`).
|
|
295
|
+
- [x] Auth accepts `Bearer` or `x-api-key`; `null` password allows all callers
|
|
296
|
+
(`src/server/auth.ts:10`).
|
|
297
|
+
- [x] Discovery-id masking reverses provider/model segments and is self-inverse
|
|
298
|
+
(`src/server/vendor-mask.ts:14`).
|
|
299
|
+
- [x] Provider-subset and favorites-only filtering are available in the wizard
|
|
300
|
+
(`src/server/catalog-filter.ts`, `src/server/index.ts:405`).
|
|
301
|
+
- [x] `rflectr server --vertex` exposes Claude on Vertex AI via gcloud ADC with
|
|
302
|
+
no OpenCode key (`src/server/index.ts:290`, `src/server/vertex-config.ts:66`).
|
|
303
|
+
- [x] Per-`(model × npm × baseURL)` `LanguageModel` cache reuses instances across
|
|
304
|
+
requests (`src/server/router.ts:331`).
|
|
305
|
+
- [x] `/health` is reachable without auth (`src/server/router.ts:114`).
|
|
306
|
+
- [x] Network mode requires a server password before binding to `0.0.0.0`
|
|
307
|
+
(`src/server/index.ts:205`, `:394`).
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Files
|
|
312
|
+
|
|
313
|
+
| File | Role |
|
|
314
|
+
|------|------|
|
|
315
|
+
| `src/server/index.ts` | Command entry, wizard, `loadServerModels`, reasoning enrichment, Vertex command, startup output |
|
|
316
|
+
| `src/server/router.ts` | HTTP server, routing, Anthropic + OpenAI handlers, LanguageModel cache, header sanitization |
|
|
317
|
+
| `src/server/models.ts` | `ServerModelInfo`, catalog builders, gateway aliases, display names, endpoint payload formatters |
|
|
318
|
+
| `src/server/auth.ts` | `isAuthorized`, `extractBearerToken`, `sanitizeCredential` |
|
|
319
|
+
| `src/server/catalog-filter.ts` | Provider-subset / favorites filtering, provider summary |
|
|
320
|
+
| `src/server/provider-select.ts` | Interactive exposed-providers picker |
|
|
321
|
+
| `src/server/vendor-mask.ts` | Self-inverse discovery-id masking |
|
|
322
|
+
| `src/server/vertex-config.ts` | Vertex runtime config, ADC detection, Vertex model catalog |
|
|
323
|
+
| `src/server/prompts.ts` | Wizard prompts (start mode, listen mode, password, masking, favorites) |
|
|
324
|
+
| `src/provider-catalog.ts` | `zenGoModelsToServerModels`, `localProvidersToServerModels` (shared) |
|
|
325
|
+
| `src/upstream-forward.ts` | `postJsonUpstream`, `relayAnthropicMessages` (shared with proxy) |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Risks & Known Limitations
|
|
330
|
+
|
|
331
|
+
- **No TLS / no daemonization.** Plain HTTP, foreground only. A LAN deployment
|
|
332
|
+
relies entirely on the server password as its sole access gate.
|
|
333
|
+
- **Server password is the only network gate.** Once bound to `0.0.0.0`, any
|
|
334
|
+
caller with the password reaches every exposed provider's upstream key. Treat
|
|
335
|
+
it as a real secret.
|
|
336
|
+
- **Cost display inaccurate for non-Anthropic models** — Claude clients apply
|
|
337
|
+
their own pricing table; the gateway cannot correct it.
|
|
338
|
+
- **Context window reflects launch state.** Discovery payloads carry a static
|
|
339
|
+
`context_window`; a live `/model` switch in a Claude client does not refresh it.
|
|
340
|
+
- **OAuth-only local providers** with no stored key are skipped upstream of this
|
|
341
|
+
gateway (PRD-002), so they never appear in the catalog.
|
|
342
|
+
- **Vertex auth beyond ADC** (impersonation, workload identity) is not handled —
|
|
343
|
+
only `GOOGLE_APPLICATION_CREDENTIALS` or the default ADC file are detected.
|
|
344
|
+
- **`::ts::` / `[1m]` string conventions** are inherited from the translation and
|
|
345
|
+
alias layers; the same edge-case caveats apply.
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Related
|
|
350
|
+
|
|
351
|
+
- [PRD-002: Provider Registry](../prd-002-provider-registry/prd-002-provider-registry-index.md) — local provider discovery feeding `localProvidersToServerModels`.
|
|
352
|
+
- [PRD-003: Model Discovery & Classification](../prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md) — `ModelInfo` source for `zenGoModelsToServerModels`.
|
|
353
|
+
- [PRD-004: Translation Layer](../prd-004-translation-layer/prd-004-translation-layer-index.md) — the shared SDK adapter (`createLanguageModel`, `streamAnthropicResponse`).
|
|
354
|
+
- [PRD-005: Local Proxy & Catalog Routing](../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md) — shared `upstream-forward.ts` and `aliasModelId`.
|
|
355
|
+
- [PRD-011: Claude Desktop Integration](../prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md) — primary consumer of this gateway.
|
|
356
|
+
- Knowledge: [Server Gateway (private)](../../../knowledge/private/infrastructure/server-gateway.md) · [API Server guide (public)](../../../knowledge/public/guides/api-server.md)
|
|
File without changes
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
---
|
|
2
|
-
ai_description: |
|
|
3
|
-
Contains PRD folders actively being implemented. A folder lives here
|
|
4
|
-
from the moment implementation begins until the work ships.
|
|
5
|
-
Structure inside is identical to backlog/: prd-<###>-<slug>/index + sub-PRDs + qa/.
|
|
6
|
-
To promote: move entire prd-<###>-<slug>/ folder to completed/.
|
|
7
|
-
Do NOT create new PRD folders here; create them in backlog/ first,
|
|
8
|
-
then move to in-work/ when implementation starts.
|
|
9
|
-
human_description: |
|
|
10
|
-
PRDs currently being implemented. Do not start new PRDs here —
|
|
11
|
-
create them in backlog/ and move the folder here when work begins.
|
|
12
|
-
When work ships, move the entire folder to completed/.
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# Requirements — In Work
|
|
16
|
-
|
|
17
|
-
PRDs currently being implemented. Folder location = lifecycle state.
|
|
18
|
-
|
|
19
|
-
Move an entire `prd-<###>-<slug>/` folder **from** `backlog/` → here when implementation starts, and **from** here → `completed/` when the work ships.
|
|
1
|
+
---
|
|
2
|
+
ai_description: |
|
|
3
|
+
Contains PRD folders actively being implemented. A folder lives here
|
|
4
|
+
from the moment implementation begins until the work ships.
|
|
5
|
+
Structure inside is identical to backlog/: prd-<###>-<slug>/index + sub-PRDs + qa/.
|
|
6
|
+
To promote: move entire prd-<###>-<slug>/ folder to completed/.
|
|
7
|
+
Do NOT create new PRD folders here; create them in backlog/ first,
|
|
8
|
+
then move to in-work/ when implementation starts.
|
|
9
|
+
human_description: |
|
|
10
|
+
PRDs currently being implemented. Do not start new PRDs here —
|
|
11
|
+
create them in backlog/ and move the folder here when work begins.
|
|
12
|
+
When work ships, move the entire folder to completed/.
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Requirements — In Work
|
|
16
|
+
|
|
17
|
+
PRDs currently being implemented. Folder location = lifecycle state.
|
|
18
|
+
|
|
19
|
+
Move an entire `prd-<###>-<slug>/` folder **from** `backlog/` → here when implementation starts, and **from** here → `completed/` when the work ships.
|