@legioncodeinc/rflectr 0.1.1 → 0.1.2
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/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -0
- package/package.json +4 -1
- package/.markdown-link-check.json +0 -7
- package/AGENTS.md +0 -169
- package/assets/733630021_1421561133353555_3999689754075308337_n.jpg +0 -0
- package/assets/github-home-image.png +0 -0
- package/assets/og-image.jpg +0 -0
- package/assets/og-image.png +0 -0
- package/assets/og-image.psd +0 -0
- package/assets/rflectr-no-bg.png +0 -0
- package/assets/vertex-models.example.json +0 -14
- package/library/README.md +0 -39
- package/library/issues/README.md +0 -46
- package/library/issues/backlog/README.md +0 -26
- package/library/issues/completed/README.md +0 -13
- package/library/issues/in-work/README.md +0 -13
- package/library/knowledge/README.md +0 -34
- package/library/knowledge/private/README.md +0 -40
- package/library/knowledge/private/ai/README.md +0 -8
- package/library/knowledge/private/ai/model-discovery-classification.md +0 -81
- package/library/knowledge/private/ai/translation-layer.md +0 -88
- package/library/knowledge/private/architecture/README.md +0 -10
- package/library/knowledge/private/architecture/launch-flow-claude.md +0 -93
- package/library/knowledge/private/architecture/system-overview.md +0 -108
- package/library/knowledge/private/auth/README.md +0 -9
- package/library/knowledge/private/auth/oauth-device-flows.md +0 -95
- package/library/knowledge/private/data/README.md +0 -8
- package/library/knowledge/private/data/preferences-config.md +0 -87
- package/library/knowledge/private/data/provider-registry.md +0 -126
- package/library/knowledge/private/infrastructure/README.md +0 -7
- package/library/knowledge/private/infrastructure/server-gateway.md +0 -87
- package/library/knowledge/private/integrations/README.md +0 -8
- package/library/knowledge/private/integrations/harnesses.md +0 -87
- package/library/knowledge/private/integrations/local-proxy.md +0 -82
- package/library/knowledge/private/security/README.md +0 -9
- package/library/knowledge/private/security/credential-storage.md +0 -129
- package/library/knowledge/private/standards/documentation-framework.md +0 -154
- package/library/knowledge/public/README.md +0 -49
- package/library/knowledge/public/faqs/README.md +0 -7
- package/library/knowledge/public/faqs/troubleshooting.md +0 -92
- package/library/knowledge/public/guides/README.md +0 -13
- package/library/knowledge/public/guides/ai-agents.md +0 -273
- package/library/knowledge/public/guides/api-server.md +0 -108
- package/library/knowledge/public/guides/claude-desktop.md +0 -382
- package/library/knowledge/public/guides/codex.md +0 -296
- package/library/knowledge/public/guides/gemini-cli.md +0 -105
- package/library/knowledge/public/guides/model-compatibility.md +0 -80
- package/library/knowledge/public/guides/providers.md +0 -90
- package/library/knowledge/public/overview/README.md +0 -7
- package/library/knowledge/public/overview/what-is-rflectr.md +0 -71
- package/library/notes/README.md +0 -21
- package/library/requirements/README.md +0 -51
- package/library/requirements/backlog/README.md +0 -30
- package/library/requirements/completed/README.md +0 -14
- package/library/requirements/completed/prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md +0 -205
- package/library/requirements/completed/prd-001-cli-core-launch-orchestration/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-002-provider-registry/prd-002-provider-registry-index.md +0 -263
- package/library/requirements/completed/prd-002-provider-registry/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md +0 -260
- package/library/requirements/completed/prd-003-model-discovery-classification/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-004-translation-layer/prd-004-translation-layer-index.md +0 -196
- package/library/requirements/completed/prd-004-translation-layer/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md +0 -176
- package/library/requirements/completed/prd-005-local-proxy-catalog-routing/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-006-credential-storage/prd-006-credential-storage-index.md +0 -190
- 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 +0 -208
- package/library/requirements/completed/prd-007-oauth-device-flows/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md +0 -249
- 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 +0 -212
- 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 +0 -211
- 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 +0 -228
- package/library/requirements/completed/prd-011-claude-desktop-integration/qa/.gitkeep +0 -0
- package/library/requirements/completed/prd-012-server-gateway/prd-012-server-gateway-index.md +0 -356
- package/library/requirements/completed/prd-012-server-gateway/qa/.gitkeep +0 -0
- package/library/requirements/in-work/README.md +0 -19
- package/library/requirements/reports/README.md +0 -31
- package/scripts/refresh-models-dev-cache.mjs +0 -34
- package/test-proxy.ts +0 -19
- package/test-split.js +0 -1
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
# PRD-001: CLI Core & Launch Orchestration *(Retroactive)*
|
|
2
|
-
|
|
3
|
-
> **Status:** Shipped
|
|
4
|
-
> **Priority:** — *(retroactive — work is done)*
|
|
5
|
-
> **Effort:** —
|
|
6
|
-
> **Written:** June 2026
|
|
7
|
-
> **Retroactive:** Yes — this PRD was written after implementation, documenting shipped behavior in rflectr v0.2.7.
|
|
8
|
-
> **Source:** `src/cli.ts`, `src/env.ts`, `src/launch.ts`, `src/launch-target.ts`, `src/first-run.ts`, `src/constants.ts`, `src/ui.ts`, `src/trace-log.ts`
|
|
9
|
-
|
|
10
|
-
## Overview
|
|
11
|
-
|
|
12
|
-
`rflectr` is a CLI launcher that re-points unmodified AI coding tools (Claude Code, Codex, Gemini, Claude Desktop) at alternative model backends without the host tool noticing. This PRD covers the **core CLI surface** — argument parsing and subcommand dispatch — and the **`rflectr claude` launch orchestration**: the end-to-end flow that goes from a command line to a running Claude Code process pointed at the chosen model, then cleans up after the process exits.
|
|
13
|
-
|
|
14
|
-
The central mechanism is **environment isolation, not config editing**: `rflectr` never writes to the host tool's settings file. Instead it spawns the child process with a purpose-built environment that removes conflicting cloud/Anthropic env vars and sets `ANTHROPIC_*` to target either a provider's Anthropic-compatible endpoint directly or a local translation proxy (`src/env.ts:40`, `src/env.ts:48-51`). This avoids the backup/restore problem that settings-file rewriters face.
|
|
15
|
-
|
|
16
|
-
The orchestration has two shapes, decided by whether the user has saved favorites: **single-model mode** (one model, one route) and **switch-menu mode** (a multi-route catalog proxy plus Claude Code gateway model discovery, enabling live `/model` switching).
|
|
17
|
-
|
|
18
|
-
## What Was Built
|
|
19
|
-
|
|
20
|
-
`src/cli.ts` is the entry point that orchestrates the full flow. `main()` (`src/cli.ts:1033`) calls `parseArgs()` (`src/cli.ts:108`) to dispatch a subcommand, then routes to the matching command handler. The `claude` path is handled by `runClaudeCommand()` (`src/cli.ts:743`).
|
|
21
|
-
|
|
22
|
-
The shipped `runClaudeCommand` flow:
|
|
23
|
-
|
|
24
|
-
1. **Normalize agent args & detect clean-stdout mode** — `normalizeClaudeAgentArgs()` and `wantsCleanAgentStdout()` (`src/cli.ts:745-747`) suppress the interactive intro/spinners when Claude Code runs in print/pipe machine-readable mode.
|
|
25
|
-
2. **Locate the binary** — `findClaudeBinary()` (`src/launch.ts:24`) resolves `claude` via `which`/`where.exe` with platform fallback paths; aborts with an install hint if missing (`src/cli.ts:750-756`).
|
|
26
|
-
3. **Load preferences & detect conflicts** — `loadPreferences()` and `detectConflicts()` (`src/cli.ts:758-759`). Under `--dry-run`, prefs are an empty object so saved state is ignored.
|
|
27
|
-
4. **Plan the wizard** — `planLaunchWizard()` (`src/launch-target.ts:150`) decides whether to skip the interactive wizard based on `--provider`/`--model` flags or print-mode + saved preferences.
|
|
28
|
-
5. **First-run setup** — when no providers and no Zen/Go key exist, `needsFirstRunSetup()` → `runFirstRunWizard()` (`src/first-run.ts:21`, `src/first-run.ts:36`) runs an inline welcome wizard that never dead-ends.
|
|
29
|
-
6. **Build the provider catalog** — `fetchProviderCatalog()` + `providersForPicker()` (`src/cli.ts:797`, `src/cli.ts:806`).
|
|
30
|
-
7. **Provider/model selection** — either resolved directly from the launch plan (`findProviderAndModel`, `src/cli.ts:832`) or via the interactive `p.select` provider picker + `pickLocalModel()` (`src/cli.ts:847-884`). In switch-menu mode a `__favorites__` pseudo-provider is unshifted onto the picker (`src/cli.ts:815-821`).
|
|
31
|
-
8. **Branch on mode** — switch-menu (catalog) vs single-model.
|
|
32
|
-
9. **Build child env & launch** — `buildChildEnv()` (`src/env.ts:40`) then `launchClaude()` (`src/launch.ts:63`) with `stdio: 'inherit'`.
|
|
33
|
-
10. **Cleanup** — `proxyHandle.close()` after Claude Code exits, plus `printTraceLog()` when `--trace` is set (`src/cli.ts:1028-1029`).
|
|
34
|
-
|
|
35
|
-
The format branch is the heart of the single-model path (`src/cli.ts:968-1013`): `modelFormat === 'anthropic'` → direct passthrough (no proxy, also sets `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`); otherwise → SDK adapter proxy via `startProxy()` with `ANTHROPIC_BASE_URL` pointed at `http://127.0.0.1:<port>`.
|
|
36
|
-
|
|
37
|
-
## Goals
|
|
38
|
-
|
|
39
|
-
- Dispatch every `rflectr` subcommand (`claude`, `models`/`favorites`, `providers`, `server`, `codex`, `codex-app`, `gemini`, `claude-app`, `--ai`, `--help`, `--version`) from a single pure `parseArgs` function (`src/cli.ts:108`).
|
|
40
|
-
- Launch Claude Code against any selected provider/model with zero edits to `~/.claude/settings.json`.
|
|
41
|
-
- Isolate the child environment so stale Vertex/Bedrock/AWS/Foundry/Anthropic config cannot leak into the launched process.
|
|
42
|
-
- Support both single-model launches and multi-model switch-menu sessions from the same command.
|
|
43
|
-
- Forward unrecognized flags and everything after `--` verbatim to Claude Code so the host tool's own flags keep working.
|
|
44
|
-
- Provide `--dry-run` (simulate a fresh first run, write nothing), `--setup`, and `--trace` (redacted debug logging) developer affordances.
|
|
45
|
-
- Skip the interactive wizard for scripted / agent use via `--provider`/`--model` or print mode + saved preferences.
|
|
46
|
-
|
|
47
|
-
## Non-Goals
|
|
48
|
-
|
|
49
|
-
- Editing or backing up the host tool's settings file. Launch config is env-var-only (plus `--model`). *(The two desktop apps are the exception and are covered by PRD-009 / PRD-011, not here.)*
|
|
50
|
-
- Owning wire-format translation — that is the SDK adapter (PRD-004) and local proxy (PRD-005).
|
|
51
|
-
- Owning provider discovery and the registry (PRD-002) or model classification (PRD-003).
|
|
52
|
-
- Owning credential storage internals (PRD-006) or OAuth device flows (PRD-007).
|
|
53
|
-
- Owning favorites/tier semantics beyond reading them to drive launch mode (PRD-008).
|
|
54
|
-
- Guaranteeing Claude Code does not later persist the launched model to its own settings — that is outside rflectr's control.
|
|
55
|
-
|
|
56
|
-
## Features
|
|
57
|
-
|
|
58
|
-
| Feature | Description | Status |
|
|
59
|
-
|---|---|---|
|
|
60
|
-
| Subcommand dispatch | Pure `parseArgs` → `main` routing for all 8 subcommands plus root flags (`src/cli.ts:108`, `src/cli.ts:1033`) | Shipped |
|
|
61
|
-
| Claude binary discovery | Cross-platform `which`/`where.exe` resolution with fallback paths; `.cmd` preference on Windows (`src/launch.ts:24`) | Shipped |
|
|
62
|
-
| Single-model launch | Provider/model pick → format branch → `buildChildEnv` → `launchClaude` → proxy cleanup (`src/cli.ts:934-1031`) | Shipped |
|
|
63
|
-
| Switch-menu (catalog) launch | Multi-route proxy + gateway discovery for live `/model` switching when favorites exist (`src/cli.ts:889-932`, `launchClaudeViaCatalog` `src/cli.ts:440`) | Shipped |
|
|
64
|
-
| Environment isolation | Remove 17 conflicting vars, set `ANTHROPIC_*` + context tokens + tool-search compat (`src/env.ts:40`, `src/constants.ts:25`) | Shipped |
|
|
65
|
-
| Format-aware routing | `anthropic` → direct passthrough; otherwise → SDK adapter proxy (`src/cli.ts:968`) | Shipped |
|
|
66
|
-
| `--dry-run` | Run the wizard, print a preview, write nothing, ignore all saved state (`src/cli.ts:482`, `src/cli.ts:909`, `src/cli.ts:936`) | Shipped |
|
|
67
|
-
| `--trace` | Redacted debug log under `~/.rflectr/logs/`, errors printed on exit (`src/trace-log.ts:41`, `src/trace-log.ts:122`) | Shipped |
|
|
68
|
-
| `--provider` / `--model` boot | Skip the wizard for scripted/agent launches (`src/launch-target.ts:150`, `src/cli.ts:831`) | Shipped |
|
|
69
|
-
| Clean agent stdout | Suppress intro/spinners in Claude print/JSON mode so stdout stays machine-readable (`src/launch-target.ts:46`, `src/launch-target.ts:73`) | Shipped |
|
|
70
|
-
| First-run wizard | Inline welcome that never dead-ends (Zen quick start / import / providers) (`src/first-run.ts:36`) | Shipped |
|
|
71
|
-
| Favorites manager | `rflectr models` interactive add/remove, capped at 20, saved once on Done (`runModelsCommand` `src/cli.ts:534`) | Shipped |
|
|
72
|
-
| Signal forwarding | SIGINT/SIGTERM forwarded to the child; stdout/stderr restored on exit (`src/launch.ts:108-124`) | Shipped |
|
|
73
|
-
|
|
74
|
-
## Architecture & Implementation
|
|
75
|
-
|
|
76
|
-
### Argument parsing & dispatch
|
|
77
|
-
|
|
78
|
-
`parseArgs(args)` (`src/cli.ts:108`) is a pure function returning a `ParsedArgs` object. It handles `--ai` first, then bare-no-args → help, then root flags, then matches the first token against each subcommand. Starter flags for `claude` are `--dry-run`, `--setup`, `--trace`, `--help`, `--version` (`STARTER_CLAUDE_FLAGS`, `src/cli.ts:51`); relay launch flags are `--provider` / `--model` (`RELAY_LAUNCH_FLAGS`, `src/cli.ts:52`), parsed by `tryConsumeRelayLaunchFlag` (`src/cli.ts:81`) supporting both `--flag value` and `--flag=value` forms. For `claude`, everything after `--` and any unrecognized flag is pushed to `claudeArgs` and forwarded verbatim (`src/cli.ts:251-266`). `main()` (`src/cli.ts:1033`) short-circuits on `parsed.error`, fires an async models.dev cache refresh, then dispatches per `parsed.command`.
|
|
79
|
-
|
|
80
|
-
### The two launch modes
|
|
81
|
-
|
|
82
|
-
The mode is chosen by one line (`src/cli.ts:772`):
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
```mermaid
|
|
89
|
-
flowchart TD
|
|
90
|
-
start["rflectr claude"] --> parse["parseArgs()"]
|
|
91
|
-
parse --> bin["findClaudeBinary()"]
|
|
92
|
-
bin --> first["needsFirstRunSetup? → runFirstRunWizard()"]
|
|
93
|
-
first --> catalog["fetchProviderCatalog() → providersForPicker()"]
|
|
94
|
-
catalog --> plan["planLaunchWizard()"]
|
|
95
|
-
plan --> pick["provider + model selection<br/>(wizard or --provider/--model)"]
|
|
96
|
-
pick --> mode{"favorites.length > 0<br/>&& !skip ?"}
|
|
97
|
-
mode -->|yes| cat["buildCatalogRoutes()<br/>startProxyCatalog()<br/>buildChildEnv(gatewayDiscovery=true)"]
|
|
98
|
-
mode -->|no| fmt{"selectedModel.modelFormat"}
|
|
99
|
-
fmt -->|anthropic| direct["buildChildEnv(model.baseUrl)<br/>+ DISABLE_EXPERIMENTAL_BETAS=1"]
|
|
100
|
-
fmt -->|openai/other| proxy["startProxy() → buildChildEnv(127.0.0.1:port)"]
|
|
101
|
-
cat --> launch["launchClaude(env, model, args)"]
|
|
102
|
-
direct --> launch
|
|
103
|
-
proxy --> launch
|
|
104
|
-
launch --> wait["Claude Code runs (stdio inherited)"]
|
|
105
|
-
wait --> close["proxyHandle.close() + printTraceLog()"]
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
**Single-model path** (`src/cli.ts:934-1031`): resolves the provider API key via `resolveLocalProviderApiKey()` (aborts if none), then branches on `selectedModel.modelFormat`. Anthropic models call `buildChildEnv(selectedModel.baseUrl!, selectedModel.id, launchApiKey, undefined, selectedModel.contextWindow)` with no proxy and add `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` (`src/cli.ts:1015-1017`). All other formats start the SDK adapter proxy (`startProxy`, `src/cli.ts:978`) and point the child at `http://127.0.0.1:<port>`.
|
|
109
|
-
|
|
110
|
-
**Switch-menu path** (`launchClaudeViaCatalog`, `src/cli.ts:440`): `makeRouteResolver` + `buildCatalogRoutes` build the route list (starting model + favorites only, never the full catalog), `droppedFavorites` are silently skipped with a warning, `startProxyCatalog` serves all routes on one port, and `buildChildEnv(..., enableGatewayDiscovery = true)` sets `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` so Claude Code fetches `/v1/models` from the proxy and populates `/model` (`src/cli.ts:889-931`, `src/env.ts:65-67`).
|
|
111
|
-
|
|
112
|
-
### Environment isolation contract (`buildChildEnv`, `src/env.ts:40`)
|
|
113
|
-
|
|
114
|
-
`buildChildEnv(baseUrl, model, apiKey, proxyPort?, contextWindow?, enableGatewayDiscovery?)` copies `process.env`, then:
|
|
115
|
-
|
|
116
|
-
1. **Removes** every name in `CONFLICTING_ENV_VARS` (`src/env.ts:49-51`) — the 17 vars listed in `src/constants.ts:25-43`.
|
|
117
|
-
2. **Sets** `ANTHROPIC_BASE_URL` — `http://127.0.0.1:{proxyPort}` when `proxyPort` is provided, otherwise the passed `baseUrl` (`src/env.ts:52-54`). When a `proxyPort` is set, the base URL is *always* the local proxy regardless of `baseUrl`.
|
|
118
|
-
3. **Sets** `ANTHROPIC_API_KEY` to the resolved key (for proxy launches this is the proxy's local token, `src/cli.ts:1009`).
|
|
119
|
-
4. **Sets** `ANTHROPIC_MODEL` to `claudeCodeClientModelId(model, contextWindow)` and `CLAUDE_CODE_MAX_CONTEXT_TOKENS` to the resolved real window (`src/env.ts:57-64`).
|
|
120
|
-
5. **Optionally sets** `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` (`src/env.ts:65-67`).
|
|
121
|
-
6. **Applies** `applyClaudeCodeThirdPartyCompat` (`src/env.ts:30`): `ENABLE_TOOL_SEARCH=true` (defer MCP tools like native Claude Code) and `CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=0` (keep the full system prompt on proxy routes).
|
|
122
|
-
|
|
123
|
-
Isolation applies to the child process only — the parent shell is not mutated.
|
|
124
|
-
|
|
125
|
-
### Launch & process lifecycle (`src/launch.ts`)
|
|
126
|
-
|
|
127
|
-
`launchClaude(env, model, extraArgs)` (`src/launch.ts:63`) spawns the resolved binary with `buildClaudeArgs` (`--model {model}` + extra args), `stdio: 'inherit'`, and `shell: isWindows`. It temporarily mutes the parent's `stdout`/`stderr` writes (appending them to the `--debug-file` when tracing) so rflectr does not interleave with the child, restoring them on exit. SIGINT/SIGTERM are forwarded to the child (`src/launch.ts:108-114`). The returned promise resolves to the child exit code (or `1` on spawn error), which propagates to `process.exit` in the CLI entry guard (`src/cli.ts:1169-1179`).
|
|
128
|
-
|
|
129
|
-
### Tracing (`src/trace-log.ts`)
|
|
130
|
-
|
|
131
|
-
`prepareClaudeTraceLog()` (`src/trace-log.ts:41`) resets and returns `~/.rflectr/logs/claude-debug.log`. When `--trace` is set the CLI passes `--debug-file <path>` to Claude Code and calls `printTraceLog()` on exit (`src/cli.ts:1019-1029`). Log dirs/files are created `0o700`/`0o600` and all lines pass through `redactTraceLine` (`src/trace-log.ts:99`), which scrubs Bearer/Authorization/x-api-key headers and `sk-`, `sk-ant-`, `AIza`, `gsk_` key prefixes.
|
|
132
|
-
|
|
133
|
-
## Configuration & Environment
|
|
134
|
-
|
|
135
|
-
**Env vars SET on the child** (`src/env.ts`):
|
|
136
|
-
- `ANTHROPIC_BASE_URL` — provider Anthropic endpoint (direct) or `http://127.0.0.1:<port>` (proxy)
|
|
137
|
-
- `ANTHROPIC_API_KEY` — provider key (direct) or local proxy token (proxy)
|
|
138
|
-
- `ANTHROPIC_MODEL` — client model id (with `[1m]` suffix for 1M+ windows)
|
|
139
|
-
- `CLAUDE_CODE_MAX_CONTEXT_TOKENS` — resolved real context window
|
|
140
|
-
- `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` — switch-menu mode only
|
|
141
|
-
- `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` — anthropic-format direct launches only (`src/cli.ts:1016`)
|
|
142
|
-
- `ENABLE_TOOL_SEARCH=true`, `CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT=0` — third-party compat (`src/env.ts:34-37`)
|
|
143
|
-
|
|
144
|
-
**Env vars REMOVED** — `CONFLICTING_ENV_VARS` (`src/constants.ts:25-43`), the 17:
|
|
145
|
-
`CLAUDE_CODE_USE_VERTEX`, `ANTHROPIC_VERTEX_PROJECT_ID`, `ANTHROPIC_VERTEX_BASE_URL`, `CLOUD_ML_REGION`, `ANTHROPIC_BEDROCK_BASE_URL`, `ANTHROPIC_AWS_BASE_URL`, `ANTHROPIC_AWS_API_KEY`, `ANTHROPIC_AWS_WORKSPACE_ID`, `ANTHROPIC_FOUNDRY_API_KEY`, `ANTHROPIC_FOUNDRY_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_MODEL`, `ANTHROPIC_DEFAULT_OPUS_MODEL`, `ANTHROPIC_DEFAULT_SONNET_MODEL`, `ANTHROPIC_DEFAULT_HAIKU_MODEL`.
|
|
146
|
-
|
|
147
|
-
**Flags** (`src/cli.ts`):
|
|
148
|
-
- `--dry-run` — wizard runs, preview printed, nothing written, saved state ignored
|
|
149
|
-
- `--setup` — informational hint pointing at `rflectr providers`
|
|
150
|
-
- `--trace` — write `~/.rflectr/logs/claude-debug.log`, print redacted errors on exit
|
|
151
|
-
- `--provider X` / `--model Y` (or `provider__model` slug) — skip the wizard
|
|
152
|
-
- `--help` / `-h`, `--version` / `-v`
|
|
153
|
-
- `--` and any unrecognized flag — forwarded verbatim to Claude Code
|
|
154
|
-
|
|
155
|
-
**Constants**: `MAX_MODEL_CATALOG = 20` (favorites cap / max catalog routes, `src/constants.ts:51`); `BACKENDS` Zen/Go base URLs (no `/v1` suffix, `src/constants.ts:9`); `VERSION` derived from `package.json` (`src/constants.ts:75`).
|
|
156
|
-
|
|
157
|
-
**Critical URL constraint**: `BACKENDS.baseUrl` must NOT include `/v1` — the Anthropic SDK appends `/v1/messages` automatically (`src/constants.ts:13`). The same rule applies to any anthropic-format `baseUrl` passed to `buildChildEnv`.
|
|
158
|
-
|
|
159
|
-
## Acceptance Criteria (verification checklist — satisfied by shipped code)
|
|
160
|
-
|
|
161
|
-
- [x] AC-1.1 Given no args, when `rflectr` runs, then root help is printed and no launch occurs (`src/cli.ts:118`, `src/cli.ts:1046-1058`).
|
|
162
|
-
- [x] AC-1.2 `parseArgs` dispatches each of `claude`, `models`/`favorites`, `providers`, `server`, `codex`, `codex-app`, `gemini`, `claude-app` to its handler; an unknown first token yields a `Unknown command` error and exit 1 (`src/cli.ts:241-244`, `src/cli.ts:1036-1040`).
|
|
163
|
-
- [x] AC-1.3 For `rflectr claude`, everything after `--` and any flag not in `STARTER_CLAUDE_FLAGS`/`RELAY_LAUNCH_FLAGS` is forwarded verbatim to Claude Code (`src/cli.ts:251-266`).
|
|
164
|
-
- [x] AC-1.4 When the `claude` binary is not found, launch aborts with an install hint and exit 1 (`src/cli.ts:750-756`).
|
|
165
|
-
- [x] AC-1.5 `buildChildEnv` removes all 17 `CONFLICTING_ENV_VARS` from the child env before setting `ANTHROPIC_*` (`src/env.ts:49-51`, `src/constants.ts:25`).
|
|
166
|
-
- [x] AC-1.6 When `proxyPort` is provided, `ANTHROPIC_BASE_URL` is `http://127.0.0.1:{proxyPort}` regardless of the `baseUrl` argument (`src/env.ts:52-54`).
|
|
167
|
-
- [x] AC-1.7 An anthropic-format model launches with no proxy, direct to `selectedModel.baseUrl`, and sets `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` (`src/cli.ts:968-975`, `src/cli.ts:1015-1017`).
|
|
168
|
-
- [x] AC-1.8 A non-anthropic model starts the SDK adapter proxy and points the child at the local proxy port (`src/cli.ts:976-1013`).
|
|
169
|
-
- [x] AC-1.9 Given at least one saved favorite and no wizard-skip, launch enters switch-menu mode: a multi-route catalog proxy starts and `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` is set (`src/cli.ts:772`, `src/cli.ts:889-931`, `src/env.ts:65-67`).
|
|
170
|
-
- [x] AC-1.10 Catalog routes contain only the starting model plus favorites; favorites whose provider/model are unavailable are dropped with a warning (`src/cli.ts:901-907`).
|
|
171
|
-
- [x] AC-1.11 `--dry-run` runs the wizard, prints a preview, and writes nothing (no `recordLaunchSelection`, prefs treated as empty) (`src/cli.ts:758`, `src/cli.ts:761`, `src/cli.ts:909-922`, `src/cli.ts:936-954`).
|
|
172
|
-
- [x] AC-1.12 `--provider X --model Y` (or `provider__model` slug) skips the interactive wizard and resolves the target directly; an incomplete pair yields an error (`src/launch-target.ts:150-197`, `src/cli.ts:831-844`).
|
|
173
|
-
- [x] AC-1.13 Claude print/JSON machine-readable mode suppresses the interactive intro and spinners so stdout stays clean (`src/launch-target.ts:46-52`, `src/cli.ts:746-747`, `src/cli.ts:774`).
|
|
174
|
-
- [x] AC-1.14 On first run with no providers and no Zen/Go key, the inline welcome wizard runs and only `cancel` aborts the launch (`src/cli.ts:780-783`, `src/first-run.ts:21-36`).
|
|
175
|
-
- [x] AC-1.15 After Claude Code exits, any started proxy is closed and (when tracing) the trace log is printed (`src/cli.ts:1028-1029`, `src/cli.ts:477-478`).
|
|
176
|
-
- [x] AC-1.16 The child exit code propagates to `process.exit`, and SIGINT/SIGTERM are forwarded to the child (`src/launch.ts:108-117`, `src/cli.ts:1170-1171`).
|
|
177
|
-
- [x] AC-1.17 `--trace` writes a redacted debug log under `~/.rflectr/logs/` (dir `0o700`, file `0o600`) with secrets scrubbed (`src/trace-log.ts:26-34`, `src/trace-log.ts:99-120`).
|
|
178
|
-
- [x] AC-1.18 `rflectr models` adds/removes favorites interactively, enforces the `MAX_MODEL_CATALOG` cap, and persists once on Done (`src/cli.ts:534-741`, `src/cli.ts:579-587`, `src/cli.ts:728-730`).
|
|
179
|
-
- [x] AC-1.19 `--version` prints the `package.json` version for the root and every subcommand (`src/constants.ts:75`, `src/cli.ts:1054`, `src/cli.ts:1063`).
|
|
180
|
-
|
|
181
|
-
## Files
|
|
182
|
-
|
|
183
|
-
### Primary
|
|
184
|
-
- `src/cli.ts` — Entry point: `parseArgs`, `main`, `runClaudeCommand`, `runModelsCommand`, `launchClaudeViaCatalog`, dry-run printers, help text.
|
|
185
|
-
- `src/env.ts` — `buildChildEnv` (env isolation contract), `detectConflicts`, `applyClaudeCodeThirdPartyCompat`, `resolveApiKey`.
|
|
186
|
-
- `src/launch.ts` — `findClaudeBinary`, `buildClaudeArgs`, `launchClaude` (spawn with stdio inherited, signal forwarding, exit-code resolution).
|
|
187
|
-
- `src/launch-target.ts` — `planLaunchWizard`, `findProviderAndModel`, `parseModelSlug`, print/non-interactive detection, `wantsCleanAgentStdout`, `normalizeClaudeAgentArgs`.
|
|
188
|
-
|
|
189
|
-
### Supporting
|
|
190
|
-
- `src/first-run.ts` — `needsFirstRunSetup`, `runFirstRunWizard` (inline never-dead-end welcome).
|
|
191
|
-
- `src/constants.ts` — `CONFLICTING_ENV_VARS` (the 17), `BACKENDS`, `MAX_MODEL_CATALOG`, `VERSION`, `classifyModelFormat`.
|
|
192
|
-
- `src/ui.ts` — shared @clack/picocolors styling (`relayIntro`, `providerSelectOption`, `printPanel`, env-conflict panel, dry-run panel).
|
|
193
|
-
- `src/trace-log.ts` — `prepareClaudeTraceLog`, `printTraceLog`, secret redaction, secure log file modes.
|
|
194
|
-
|
|
195
|
-
## Risks & Known Limitations
|
|
196
|
-
|
|
197
|
-
- **Claude Code persists the launched model.** rflectr never touches `settings.json`, but Claude Code itself writes the launched model to `~/.claude/settings.json`, so a later bare `claude` may still show a relay alias (e.g. `anthropic-opencode-go__deepseek-v4-flash`). Gateway discovery caches at `~/.claude/cache/gateway-models.json`. Reset with `claude --model sonnet` or by editing/removing those files (`src/cli.ts:364-366`, knowledge `system-overview.md`).
|
|
198
|
-
- **Context window is fixed at launch in switch-menu mode.** `CLAUDE_CODE_MAX_CONTEXT_TOKENS` reflects the *launch* model and does NOT update on a live `/model` switch — Claude Code's gateway model discovery only carries `id` + `display_name` (no `context_window`) and fetches `/v1/models` once at startup. Single-model launches show the correct window. This is by design (`src/env.ts:58-64`, knowledge `launch-flow-claude.md`).
|
|
199
|
-
- **`/v1` URL footgun.** Any anthropic-format `baseUrl` that includes `/v1` produces `/v1/v1/messages` → 404. `BACKENDS.baseUrl` and provider Anthropic endpoints must omit `/v1` (`src/constants.ts:13`).
|
|
200
|
-
- **Favorites cap.** Hard cap of 20 (`MAX_MODEL_CATALOG`); excess additions are rejected in `runModelsCommand` (`src/cli.ts:579-587`, `src/cli.ts:714-716`).
|
|
201
|
-
- **Stale favorites silently skipped.** Favorites pointing at a now-unavailable provider/model are dropped from the catalog with a warning rather than failing the launch (`src/cli.ts:901-907`).
|
|
202
|
-
|
|
203
|
-
## Related
|
|
204
|
-
- Knowledge: [system-overview](../../../knowledge/private/architecture/system-overview.md), [launch-flow-claude](../../../knowledge/private/architecture/launch-flow-claude.md)
|
|
205
|
-
- Sibling PRDs: [PRD-002 Provider Registry](../prd-002-provider-registry/prd-002-provider-registry-index.md), [PRD-003 Model Discovery & Classification](../prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md), [PRD-004 Translation Layer](../prd-004-translation-layer/prd-004-translation-layer-index.md), [PRD-005 Local Proxy & Catalog Routing](../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md), [PRD-008 Preferences, Tiers & Favorites](../prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md)
|
|
File without changes
|
package/library/requirements/completed/prd-002-provider-registry/prd-002-provider-registry-index.md
DELETED
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
# PRD-002: Provider Registry *(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/registry/*`, `src/provider-templates.ts`, `src/provider-catalog.ts`, `src/providers-command.ts`
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Overview
|
|
13
|
-
|
|
14
|
-
The Provider Registry is the on-disk catalog of AI providers and their cached models — the single source of truth that feeds every launch wizard in rflectr (`claude`, `codex`, `gemini`, `server`). It is persisted to `~/.rflectr/providers.json`, a JSON document that holds provider metadata and model caches but **never** holds secrets: each entry carries an `authRef` pointer into the OS keyring rather than a key.
|
|
15
|
-
|
|
16
|
-
Earlier versions of rflectr discovered providers by spawning OpenCode's `opencode serve` and reading `/config/providers` on *every* launch — slow, and a hard runtime coupling to a running OpenCode process. The registry replaces that model: providers are imported or added **once**, persisted, and read directly on every launch. OpenCode import (`rflectr providers import`) becomes a one-time operation rather than a per-launch dependency, while OpenCode Zen / Go remain available even against an empty registry (via a live OpenCode API key).
|
|
17
|
-
|
|
18
|
-
See the authoritative knowledge doc: [`../../../knowledge/private/data/provider-registry.md`](../../../knowledge/private/data/provider-registry.md).
|
|
19
|
-
|
|
20
|
-
## What Was Built
|
|
21
|
-
|
|
22
|
-
- A typed, versioned on-disk schema (`ProviderRegistry` / `RegistryProvider` / `CachedModel`) persisted to `~/.rflectr/providers.json` with secure permissions (`0o600` file in a `0o700` dir), atomic writes, and a `.bak` backup.
|
|
23
|
-
- A library of **27 built-in provider templates** (Groq, Mistral, OpenAI, Google, Ollama, LM Studio, OpenRouter, Anthropic, Zen/Go stubs, etc.) so a user can add a provider without hand-entering base URLs.
|
|
24
|
-
- A full CRUD surface via the `rflectr providers` command: hub, `add`, `import`, `list`, `remove`, `refresh-models`, `auth`.
|
|
25
|
-
- One-time **OpenCode import** that merges API-key and OAuth providers from `opencode serve` + `auth.json`, with duplicate-provider migration and interactive conflict resolution.
|
|
26
|
-
- **Materialization**: registry entries → runtime `LocalProvider[]` consumed by every wizard, with credential resolution, per-agent model hiding, and Google id normalization.
|
|
27
|
-
- **models.dev capability enrichment** (bundled snapshot + optional live refresh) for reasoning/tool-call metadata.
|
|
28
|
-
- An **SSRF URL-security guard** for custom-endpoint base URLs (DNS resolution + private-range blocking + metadata-host blocklist).
|
|
29
|
-
- **Schema migrations** for legacy cloud ids (`opencode`→`zen`, `opencode-go`→`go`) and OAuth provider id splits (`openai`→`openai-oauth`, `xai`→`xai-oauth`).
|
|
30
|
-
|
|
31
|
-
## Goals
|
|
32
|
-
|
|
33
|
-
1. Make provider discovery a one-time persisted operation, decoupling launch from a running OpenCode process.
|
|
34
|
-
2. Store provider/model metadata locally with secure file permissions and **no secrets on disk**.
|
|
35
|
-
3. Offer a curated template catalog so common providers can be added with a key alone.
|
|
36
|
-
4. Provide a single materialization path that every wizard (Claude/Codex/Gemini/Server) consumes identically.
|
|
37
|
-
5. Keep OpenCode as the source of truth for *which* models a provider exposes; rflectr maintains no per-package allowlist beyond the templates.
|
|
38
|
-
6. Guard custom-endpoint URLs against SSRF.
|
|
39
|
-
|
|
40
|
-
## Non-Goals
|
|
41
|
-
|
|
42
|
-
- Storing API keys or OAuth tokens in `providers.json` (credentials live in the keyring — see [PRD-006](../prd-006-credential-storage/) / [PRD-007](../prd-007-oauth-device-flows/)).
|
|
43
|
-
- Maintaining a per-npm-package model allowlist beyond the template catalog (OpenCode/provider API is authoritative).
|
|
44
|
-
- Supporting providers whose auth cannot be reduced to a single forwarded API key or OAuth token (Bedrock/Azure/Vertex are reference-only templates; Vertex is served only through the dedicated `server --vertex` path).
|
|
45
|
-
- Model classification/format heuristics themselves (owned by [PRD-003](../prd-003-model-discovery-classification/)).
|
|
46
|
-
|
|
47
|
-
## Features
|
|
48
|
-
|
|
49
|
-
| # | Feature | Source | Acceptance |
|
|
50
|
-
|---|---------|--------|------------|
|
|
51
|
-
| 1 | On-disk schema + secure persistence | `src/registry/types.ts`, `src/registry/io.ts` | [AC-1](#acceptance-criteria) |
|
|
52
|
-
| 2 | Built-in template catalog (27 templates) | `src/provider-templates.ts` | [AC-2](#acceptance-criteria) |
|
|
53
|
-
| 3 | `rflectr providers` CRUD command | `src/providers-command.ts`, `src/registry/crud.ts` | [AC-3](#acceptance-criteria) |
|
|
54
|
-
| 4 | Template add (key test + model fetch) | `src/registry/add-template.ts` | [AC-4](#acceptance-criteria) |
|
|
55
|
-
| 5 | Custom-endpoint add (OpenAI/Anthropic) | `src/registry/custom-endpoint.ts` | [AC-5](#acceptance-criteria) |
|
|
56
|
-
| 6 | OpenCode one-time import + conflict resolution | `src/registry/import-opencode.ts`, `src/registry/import-build.ts` | [AC-6](#acceptance-criteria) |
|
|
57
|
-
| 7 | Materialization → `LocalProvider[]` | `src/registry/materialize.ts`, `src/registry/load.ts` | [AC-7](#acceptance-criteria) |
|
|
58
|
-
| 8 | Schema migrations | `src/registry/migrate.ts` | [AC-8](#acceptance-criteria) |
|
|
59
|
-
| 9 | models.dev capability enrichment | `src/registry/models-dev.ts` | [AC-9](#acceptance-criteria) |
|
|
60
|
-
| 10 | URL-security SSRF guard | `src/registry/url-security.ts` | [AC-10](#acceptance-criteria) |
|
|
61
|
-
| 11 | Model-list refresh per `modelSource` | `src/registry/refresh-models.ts` | [AC-11](#acceptance-criteria) |
|
|
62
|
-
| 12 | Catalog adapters for pickers/server | `src/provider-catalog.ts` | [AC-12](#acceptance-criteria) |
|
|
63
|
-
|
|
64
|
-
## Architecture & Implementation
|
|
65
|
-
|
|
66
|
-
### Data flow: registry entry → runtime provider
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
~/.rflectr/providers.json
|
|
70
|
-
→ loadRegistry() [io.ts:116 — parse, validate, apply migrations]
|
|
71
|
-
→ loadRegistryProviders() [load.ts:12 — resolve each authRef → credential]
|
|
72
|
-
resolveProviderCredential() [env.ts — env → keyring → OAuth refresh]
|
|
73
|
-
→ materializeRegistry() [materialize.ts:84 — CachedModel → LocalProviderModel]
|
|
74
|
-
skip disabled / no-models / no-credential
|
|
75
|
-
shouldHideModel() per agent [model-compatibility.ts]
|
|
76
|
-
→ LocalProvider[] (the wizard list)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### On-disk schema & persistence
|
|
80
|
-
|
|
81
|
-
`ProviderRegistry`, `RegistryProvider`, and `CachedModel` are defined in `src/registry/types.ts:9-57`; `REGISTRY_SCHEMA_VERSION = 1` at `types.ts:5`. The provider id pattern `PROVIDER_ID_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/` lives in `src/registry/validate.ts:6`, with `slugifyProviderId` / `customProviderId` helpers at `validate.ts:12-26`.
|
|
82
|
-
|
|
83
|
-
`loadRegistry(path?)` (`src/registry/io.ts:116`) returns an empty registry when the file is missing or unparseable, runs the three migrations, and persists if any migration changed the data. `parseRegistry`/`parseProvider` (`io.ts:52-114`) defensively validate every field and drop malformed providers. `saveRegistry` (`io.ts:139`) writes a `.bak` copy, writes to a `.tmp` file via `writeSecureFile` (`io.ts:36` — `openSync` with `0o600`, then `chmodSync`), then `renameSync`s atomically. `ensureSecureAppHome` (`io.ts:26`) creates `~/.rflectr` at `0o700`. The JSON is 2-space indented with a trailing newline (`io.ts:140`). **No secrets** are written: `authRef` is a pointer string only.
|
|
84
|
-
|
|
85
|
-
### Built-in templates
|
|
86
|
-
|
|
87
|
-
`src/provider-templates.ts:26` defines `PROVIDER_TEMPLATES` (27 entries). Each `ProviderTemplate` (`provider-templates.ts:8-23`) carries `id`, `name`, `authType` (`api`/`oauth`/`none`), `npm` (SDK package), optional `defaultBaseUrl`/`signupUrl`/`urlPrompt`/`apiKeyOptional`, `modelSource` (`api-list` | `static-seed` | `manual-only` | `zen-go-api`), and `supported`/`addable`/`unsupportedReason` flags. Query helpers: `listSupportedTemplates` (`:310`, filters to `supported && authType==='api' && addable!==false`), `listAddableTemplates` (`:317`, removes already-configured ids and collapses Zen/Go under `opencode-cloud`), `getTemplateById` (`:327`), `filterTemplates` (`:331`).
|
|
88
|
-
|
|
89
|
-
Reference-only (not addable) templates carry `supported:false` + `unsupportedReason`: `bedrock` (`:242`), `azure` (`:250`), `vertex` (`:259`). Zen/Go stubs (`zen`/`go`) are `addable:false` (`:285`/`:295`); `opencode-cloud` (`:269`) is the addable proxy that routes to them. `github-copilot` is an `oauth` template (`:299`).
|
|
90
|
-
|
|
91
|
-
### The `providers` command
|
|
92
|
-
|
|
93
|
-
`src/providers-command.ts` parses args (`parseProvidersArgs`, `:51`) and dispatches (`runProvidersCommand`, `:798`):
|
|
94
|
-
|
|
95
|
-
| Command | Function | Notes |
|
|
96
|
-
|---|---|---|
|
|
97
|
-
| `rflectr providers` | `runProvidersHub` (`:727`) | Interactive hub loop |
|
|
98
|
-
| `… add` | `runProvidersAdd` (`:564`) | Import / template / custom endpoint |
|
|
99
|
-
| `… import` | `runProvidersImport` (`:131`) | One-time OpenCode import w/ conflict panel |
|
|
100
|
-
| `… list` | `runProvidersList` (`:304`) | Tabular, via `resolveProvidersForDisplay` |
|
|
101
|
-
| `… remove <id>` | `runProvidersRemove` (`:607`) → `removeProviderFromRegistry` | Entry + keyring cleanup |
|
|
102
|
-
| `… refresh-models [id]` | `runProvidersRefreshModels` (`:236`) | Re-fetch model cache |
|
|
103
|
-
| `… auth <id> [--native\|--broker]` | `runProvidersAuth` (`:221`) | OAuth sign-in — see [PRD-007](../prd-007-oauth-device-flows/) |
|
|
104
|
-
|
|
105
|
-
CRUD primitives live in `src/registry/crud.ts`: `removeProviderFromRegistry` (`:23`, with safe keyring deletion gated by `credentialStillReferenced`, `:18`), `toggleProviderEnabled` (`:87`), `addZenRegistryStub`/`addGoRegistryStub` (`:54`/`:66`), `setRegistrySubscriptionFilter` (`:76`).
|
|
106
|
-
|
|
107
|
-
### Template add
|
|
108
|
-
|
|
109
|
-
`addProviderFromTemplate` (`src/registry/add-template.ts:42`): probes the SDK package is importable (`probeTemplatePackage`, `:27`, guarded by `isSdkMigratedNpm`), rejects an existing provider unless `replaceExisting`, fetches the live model list (`fetchTemplateModels`), saves the credential to `keyring:provider:<id>`, enriches with pricing (`enrichModelsWithPricing`), writes the entry, and kicks off async pricing enrichment (`enrichPricingAsync`).
|
|
110
|
-
|
|
111
|
-
### Custom endpoints
|
|
112
|
-
|
|
113
|
-
`addCustomEndpointProvider` (`src/registry/custom-endpoint.ts:136`) validates the base URL through the SSRF guard, allocates a unique `custom-…` id (`uniqueProviderId`, `:121`), fetches models via the Anthropic path (`fetchAnthropicModels`, `:41`) or the OpenAI-compatible template path, saves the key (or the placeholder `'local'` for keyless local servers), and writes a `custom-anthropic`/`custom-openai` entry.
|
|
114
|
-
|
|
115
|
-
### OpenCode import
|
|
116
|
-
|
|
117
|
-
`importFromOpencode` (`src/registry/import-opencode.ts:102`) fetches raw providers from `opencode serve`, reads `auth.json`, and merges API-key + OAuth providers via `buildImportProviderList` (`src/registry/import-build.ts:40`). It runs the legacy-cloud migration, validates each key (`validateImportKey`), resolves conflicts through an injected `resolveConflict` callback (the hub renders a panel and prompts keep/import/skip), saves credentials (API key → `keyring:provider:<id>`; OAuth → `keyring:oauth:provider:<id>`), and records skip reasons. OAuth provider ids are split via `toOAuthRegistryId` (`import-build.ts:23` — `openai`→`openai-oauth`, `xai`→`xai-oauth`). `listCredentialSkippedProviders` (`import-build.ts:112`) surfaces only actionable gaps (OAuth sign-in needed, or a provider already in the registry). `localProviderToRegistry` (`src/registry/convert.ts:28`) performs the structural conversion (no secret write).
|
|
118
|
-
|
|
119
|
-
### Materialization
|
|
120
|
-
|
|
121
|
-
`materializeRegistry` (`src/registry/materialize.ts:84`) iterates providers and calls `materializeOne` (`:54`): skips disabled / invalid-id providers, converts each `CachedModel` via `cachedModelToLocal` (`:21`), drops models whose endpoint can't be resolved (`resolveEndpoint`, `src/providers.ts:29`) and models hidden by `shouldHideModel`, and finally drops the whole provider when it has **no models** or **no credential** (`:69-72`). Google ids/display names are normalized (`normalizeGoogleModelId`/`normalizeGoogleDisplayName`); context windows and reasoning fall back to `resolveContextWindow` and the models.dev row (`findModelsDevModel`). `loadRegistryProviders` (`src/registry/load.ts:12`) resolves credentials + OAuth account ids before materializing.
|
|
122
|
-
|
|
123
|
-
### Schema migrations
|
|
124
|
-
|
|
125
|
-
`src/registry/migrate.ts`, applied inside `loadRegistry`:
|
|
126
|
-
- `migrateLegacyCloudProviders` (`:10`) — `opencode`→`zen`, `opencode-go`→`go` (collapsing a duplicate, otherwise rewriting id/templateId/name and clearing `api`).
|
|
127
|
-
- `migrateOAuthOpenAiProvider` (`:37`) — `{id:'openai', authType:'oauth'}`→`openai-oauth` so it can coexist with the API-key `openai`, preserving the original `authRef`.
|
|
128
|
-
- `migrateOAuthXaiProvider` (`:56`) — `{id:'xai', authType:'oauth'}`→`xai-oauth`.
|
|
129
|
-
|
|
130
|
-
### models.dev enrichment
|
|
131
|
-
|
|
132
|
-
`src/registry/models-dev.ts` ships a bundled snapshot (`loadBundledModelsDevCache`, `:92`, from `src/data/models-dev-cache.json`) and supports an optional live refresh (`fetchModelsDevCache`, `:179`, 15s timeout, written `0o600` to `~/.rflectr/models-dev-cache.json`). `findModelsDevModel` (`:212`) resolves the provider slug via `REGISTRY_TO_MODELS_DEV` (`:60`) and matches normalized model-id candidates; `shouldHideByModelsDevCapabilities` (`:229`) is the conservative auto-hide rule (non-text output, `tool_call===false`, interaction-only models). `refreshModelsDevCacheAsync` (`:205`) refreshes in the background.
|
|
133
|
-
|
|
134
|
-
### URL-security (SSRF guard)
|
|
135
|
-
|
|
136
|
-
`validateCustomEndpointUrl` (`src/registry/url-security.ts:65`): requires HTTPS (HTTP only when `allowInsecureLocal` and the host resolves to loopback), blocks a metadata-host blocklist (`169.254.169.254`, `metadata.google.internal`, ECS task metadata — `:20`), DNS-resolves the hostname, and blocks any address in loopback/private/link-local/unique-local/CGNAT ranges via `isBlockedIp` (`:27`, using `ipaddr.js`). Unparseable inputs fail closed. Returns a normalized, trailing-slash-stripped URL on success.
|
|
137
|
-
|
|
138
|
-
### Model-list refresh
|
|
139
|
-
|
|
140
|
-
`refreshProviderModels` (`src/registry/refresh-models.ts:321`) branches on `resolveModelSource`: `manual-only` is skipped with a hint; `zen-go-api` re-fetches via `getModels(BACKENDS[...])` (`refreshZenGoProvider`, `:76`); OAuth providers use the live-or-seed strategy (`refreshOAuthProvider`, `:97`, with the OpenAI 3-tier ChatGPT-backend strategy at `:182` and the xAI strategy at `:223`); everything else hits the API list (`refreshApiListProvider`, `:248`) with placeholder-key detection and a "keep cached models" fallback on auth rejection. `refreshAllProviderModels` (`:445`) auto-seeds Zen/Go stubs when a global OpenCode key exists, then refreshes every enabled provider.
|
|
141
|
-
|
|
142
|
-
### Catalog adapters
|
|
143
|
-
|
|
144
|
-
`src/provider-catalog.ts` adapts materialized providers for consumers: `resolveLocalProviders`/`fetchProviderCatalog` (`:38`/`:44`), `providersForPicker` (`:85`, merges live Zen/Go when not already in the registry, sorts), `resolveLocalProviderApiKey` (`:109`), `formatRegistryAuthLabel` (`:120`), `resolveProvidersForDisplay` (`:161`, the `providers list`/hub row builder), `localProvidersToServerModels`/`zenGoModelsToServerModels` (`:228`/`:259`, used by the server gateway — see [PRD-012](../prd-012-server-gateway/)).
|
|
145
|
-
|
|
146
|
-
## Configuration & Data Shapes
|
|
147
|
-
|
|
148
|
-
Path: `getProvidersPath()` → `~/.rflectr/providers.json` (override the home with `RFLECTR_HOME`).
|
|
149
|
-
|
|
150
|
-
```ts
|
|
151
|
-
// src/registry/types.ts
|
|
152
|
-
ProviderRegistry {
|
|
153
|
-
schemaVersion: number // currently 1 (REGISTRY_SCHEMA_VERSION)
|
|
154
|
-
providers: RegistryProvider[]
|
|
155
|
-
importedAt?: string // last OpenCode import (ISO)
|
|
156
|
-
pricingCacheAt?: string
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
RegistryProvider {
|
|
160
|
-
id: string // PROVIDER_ID_PATTERN: /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/
|
|
161
|
-
templateId: string // origin template, e.g. 'groq', 'custom-openai'
|
|
162
|
-
name: string // display name
|
|
163
|
-
enabled: boolean
|
|
164
|
-
authRef: string // pointer ONLY: 'keyring:provider:groq' |
|
|
165
|
-
// 'keyring:global:opencode' | 'keyring:oauth:provider:xai-oauth' | 'env:…'
|
|
166
|
-
authType?: 'api' | 'oauth' | 'none'
|
|
167
|
-
subscriptionFilter?: 'free' | 'zen' | 'go'
|
|
168
|
-
api: { npm?: string; url?: string; id?: string }
|
|
169
|
-
modelsCache?: { fetchedAt: string; models: CachedModel[] }
|
|
170
|
-
addedAt: string // ISO
|
|
171
|
-
refreshedAt?: string
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
CachedModel {
|
|
175
|
-
id: string
|
|
176
|
-
name: string
|
|
177
|
-
upstreamModelId: string // provider's native id for the wire call
|
|
178
|
-
family?: string; brand?: string
|
|
179
|
-
contextWindow?: number
|
|
180
|
-
cost?: { input: number; output: number }
|
|
181
|
-
modelFormat: 'anthropic' | 'openai'
|
|
182
|
-
npm?: string // per-model override of provider.api.npm
|
|
183
|
-
apiUrl?: string // per-model override of provider.api.url
|
|
184
|
-
sourceBackend?: string
|
|
185
|
-
supportedParameters?: string[]
|
|
186
|
-
reasoning?: boolean
|
|
187
|
-
interleavedReasoningField?: string
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
**`authRef` forms** (resolved by `resolveProviderCredential`): `keyring:provider:<id>` (per-provider key), `keyring:global:opencode` (shared OpenCode key, used by Zen/Go), `keyring:oauth:provider:<id>` (OAuth credential JSON), `env:VARNAME` (env-var fallback).
|
|
192
|
-
|
|
193
|
-
## Acceptance Criteria
|
|
194
|
-
|
|
195
|
-
- [x] **AC-1 — Schema & secure persistence.** `providers.json` round-trips through `loadRegistry`/`saveRegistry`; the file is written `0o600` inside a `0o700` dir via atomic tmp+rename with a `.bak` backup; malformed/missing files yield an empty registry; **no secrets** are stored (`authRef` is a pointer). `src/registry/io.ts:26,36,116,139`; covered by `tests/registry.test.ts`.
|
|
196
|
-
- [x] **AC-2 — Template catalog.** 27 built-in templates exist; `listSupportedTemplates`/`listAddableTemplates`/`getTemplateById`/`filterTemplates` filter by support, configured-state, and query; reference-only templates (Bedrock/Azure/Vertex) carry `unsupportedReason`. `src/provider-templates.ts:26,310-340`; covered by `tests/provider-templates.test.ts`.
|
|
197
|
-
- [x] **AC-3 — CRUD command.** `parseProvidersArgs` resolves `hub`/`add`/`import`/`list`/`remove`/`refresh-models`/`auth` with argument validation and help text. `src/providers-command.ts:51,104,798`; covered by `tests/providers-command.test.ts`.
|
|
198
|
-
- [x] **AC-4 — Template add.** `addProviderFromTemplate` probes the SDK package, tests the key by fetching models, rejects empty keys and existing providers (unless replacing), persists credential + entry, and enriches pricing. `src/registry/add-template.ts:42`; covered by `tests/registry-add-template.test.ts`.
|
|
199
|
-
- [x] **AC-5 — Custom endpoints.** `addCustomEndpointProvider` validates the URL, allocates a unique `custom-…` id, supports OpenAI-compatible and Anthropic-style servers, and persists a keyless `'local'` placeholder for local servers. `src/registry/custom-endpoint.ts:121,136`.
|
|
200
|
-
- [x] **AC-6 — OpenCode import.** `importFromOpencode` merges API-key + OAuth providers, validates keys, resolves duplicate-provider conflicts (keep/import/skip), saves credentials to the keyring, and reports skip reasons; OAuth ids split to `<id>-oauth`. `src/registry/import-opencode.ts:102`, `src/registry/import-build.ts:23,40,112`; covered by `tests/import-opencode.test.ts`.
|
|
201
|
-
- [x] **AC-7 — Materialization.** `materializeRegistry` converts enabled entries to `LocalProvider[]`, dropping providers with no credential or no cached models and applying per-agent `shouldHideModel`. `src/registry/materialize.ts:54,84`, `src/registry/load.ts:12`.
|
|
202
|
-
- [x] **AC-8 — Migrations.** Legacy `opencode`/`opencode-go` ids migrate to `zen`/`go`; OAuth `openai`/`xai` split to `openai-oauth`/`xai-oauth`, preserving `authRef`. `src/registry/migrate.ts:10,37,56`; run inside `loadRegistry` (`io.ts:123`).
|
|
203
|
-
- [x] **AC-9 — models.dev enrichment.** A bundled snapshot ships in the binary; an optional live refresh writes `0o600` to `~/.rflectr/models-dev-cache.json`; `findModelsDevModel` supplies reasoning/interleaved metadata during materialization. `src/registry/models-dev.ts:92,179,212`.
|
|
204
|
-
- [x] **AC-10 — SSRF guard.** `validateCustomEndpointUrl` blocks metadata hosts and private/loopback/link-local/CGNAT addresses after DNS resolution, allows loopback HTTP only with `allowInsecureLocal`, and fails closed on parse errors. `src/registry/url-security.ts:20,27,65`; covered by `tests/url-security.test.ts`.
|
|
205
|
-
- [x] **AC-11 — Model refresh.** `refreshProviderModels` re-fetches per `modelSource` (zen-go / OAuth live-or-seed / api-list), detects placeholder keys, and keeps cached models on auth rejection. `src/registry/refresh-models.ts:248,321,445`; covered by `tests/registry-refresh-models.test.ts`.
|
|
206
|
-
- [x] **AC-12 — Catalog adapters.** `providersForPicker` / `resolveProvidersForDisplay` / `localProvidersToServerModels` / `zenGoModelsToServerModels` adapt materialized providers for the CLI pickers, `providers list`, and the server gateway. `src/provider-catalog.ts:85,161,228,259`; covered by `tests/provider-catalog-display.test.ts`.
|
|
207
|
-
|
|
208
|
-
## Files
|
|
209
|
-
|
|
210
|
-
### Primary
|
|
211
|
-
- `src/registry/types.ts` — schema (`ProviderRegistry`, `RegistryProvider`, `CachedModel`, `REGISTRY_SCHEMA_VERSION`).
|
|
212
|
-
- `src/registry/io.ts` — load/save with secure perms, parse/validate, migration trigger.
|
|
213
|
-
- `src/registry/load.ts` — credential resolution + materialization entry (`loadRegistryProviders`).
|
|
214
|
-
- `src/registry/materialize.ts` — registry entries → `LocalProvider[]`.
|
|
215
|
-
- `src/registry/crud.ts` — add/remove/toggle, Zen/Go stubs, subscription filter.
|
|
216
|
-
- `src/registry/validate.ts` — provider-id pattern + slugify/custom-id helpers.
|
|
217
|
-
- `src/registry/builtins.ts` — Zen/Go registry stub entries.
|
|
218
|
-
- `src/registry/migrate.ts` — legacy-cloud + OAuth-id migrations.
|
|
219
|
-
- `src/registry/import-opencode.ts` + `src/registry/import-build.ts` — one-time OpenCode import + merge logic.
|
|
220
|
-
- `src/registry/convert.ts` — `LocalProvider` ↔ `RegistryProvider`.
|
|
221
|
-
- `src/registry/add-template.ts` — template add flow.
|
|
222
|
-
- `src/registry/custom-endpoint.ts` — custom OpenAI/Anthropic endpoint add + `fetchAnthropicModels`.
|
|
223
|
-
- `src/registry/resolve-template.ts` — imported-id → template + default base URL resolution.
|
|
224
|
-
- `src/registry/refresh-models.ts` — model-list refresh per `modelSource`.
|
|
225
|
-
- `src/registry/models-dev.ts` — models.dev capability cache (bundled + live).
|
|
226
|
-
- `src/registry/url-security.ts` — SSRF guard for custom URLs.
|
|
227
|
-
- `src/provider-templates.ts` — the 27 built-in templates + query helpers.
|
|
228
|
-
- `src/provider-catalog.ts` — picker/display/server adapters.
|
|
229
|
-
- `src/providers-command.ts` — the `rflectr providers` command.
|
|
230
|
-
|
|
231
|
-
### Supporting
|
|
232
|
-
- `src/registry/index.ts` — public barrel re-exports.
|
|
233
|
-
- `src/registry/fetch-template-models.ts` — live model-list fetch per template.
|
|
234
|
-
- `src/registry/google-model-id.ts` — Google model-id / display-name normalization.
|
|
235
|
-
- `src/registry/model-source.ts` — `resolveModelSource(provider)`.
|
|
236
|
-
- `src/registry/pricing.ts` — pricing index + async enrichment.
|
|
237
|
-
- `src/registry/refresh-credentials.ts` — credential resolution + placeholder-key detection for refresh.
|
|
238
|
-
- `src/registry/validate-import-key.ts` — import key validation.
|
|
239
|
-
- `src/registry/opencode-auth.ts` — `auth.json` read + OAuth credential shaping.
|
|
240
|
-
- `src/registry/provider-auth.ts`, `src/registry/auth-broker.ts` — provider OAuth (see [PRD-007](../prd-007-oauth-device-flows/)).
|
|
241
|
-
- `src/providers.ts` — `resolveEndpoint` + `normalizeProviders` (shared with import).
|
|
242
|
-
- `src/paths.ts` — `getProvidersPath` / `getAppHome` / `RFLECTR_HOME`.
|
|
243
|
-
- `src/data/models-dev-cache.json` — bundled models.dev snapshot.
|
|
244
|
-
|
|
245
|
-
## Risks & Known Limitations
|
|
246
|
-
|
|
247
|
-
- **Cost display inaccuracy** — pricing enrichment is best-effort; Claude Code applies its own pricing table for non-Anthropic models, so displayed cost is always inaccurate for them (documented, by design).
|
|
248
|
-
- **OAuth-only providers without a stored token** are silently skipped at materialization (no credential → provider dropped).
|
|
249
|
-
- **`@ai-sdk/github-copilot` model factory is unavailable** — OpenCode loads it from internal `@opencode-ai/core`, not a shippable public npm factory. OAuth login works; the model provider does not.
|
|
250
|
-
- **Bedrock / Azure / Vertex are reference-only** templates (`supported:false`); they need env-based auth beyond a forwarded API key. Vertex is supported only via the dedicated `server --vertex` path — see [PRD-012](../prd-012-server-gateway/).
|
|
251
|
-
- **SSRF guard resolves DNS at validation time**, not at request time — a TOCTOU rebinding window exists between validation and use (mitigated by the request itself going through the SDK adapter / proxy).
|
|
252
|
-
- **models.dev live refresh is silent on failure** — falls back to the bundled snapshot, so capability metadata may lag the upstream catalog when offline.
|
|
253
|
-
- **Stale cached models** persist when a refresh fails on auth rejection (deliberate — keeps the user functional, surfaced with a warning).
|
|
254
|
-
|
|
255
|
-
## Related
|
|
256
|
-
|
|
257
|
-
- Knowledge: [`provider-registry.md`](../../../knowledge/private/data/provider-registry.md)
|
|
258
|
-
- [PRD-001 — CLI Core & Launch Orchestration](../prd-001-cli-core-launch-orchestration/) (consumes materialized providers at launch)
|
|
259
|
-
- [PRD-003 — Model Discovery & Classification](../prd-003-model-discovery-classification/) (`resolveEndpoint`, format classification, `getModels`)
|
|
260
|
-
- [PRD-006 — Credential Storage & API Key Management](../prd-006-credential-storage/) (`authRef` resolution, keyring)
|
|
261
|
-
- [PRD-007 — OAuth Device Flows](../prd-007-oauth-device-flows/) (`providers auth`, OAuth import, id splits)
|
|
262
|
-
- [PRD-008 — Preferences, Tiers & Favorites](../prd-008-preferences-tiers-favorites/) (`subscriptionFilter`, model hiding)
|
|
263
|
-
- [PRD-012 — Server Gateway](../prd-012-server-gateway/) (`localProvidersToServerModels`, Vertex path)
|
|
File without changes
|