@legioncodeinc/rflectr 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.markdown-link-check.json +7 -0
  2. package/AGENTS.md +169 -0
  3. package/LICENSE +661 -0
  4. package/README.md +612 -0
  5. package/assets/733630021_1421561133353555_3999689754075308337_n.jpg +0 -0
  6. package/assets/github-home-image.png +0 -0
  7. package/assets/og-image.jpg +0 -0
  8. package/assets/og-image.png +0 -0
  9. package/assets/og-image.psd +0 -0
  10. package/assets/rflectr-no-bg.png +0 -0
  11. package/assets/vertex-models.example.json +14 -0
  12. package/dist/cli.js +15708 -0
  13. package/library/README.md +39 -0
  14. package/library/issues/README.md +46 -0
  15. package/library/issues/backlog/README.md +26 -0
  16. package/library/issues/completed/README.md +13 -0
  17. package/library/issues/in-work/README.md +13 -0
  18. package/library/knowledge/README.md +34 -0
  19. package/library/knowledge/private/README.md +40 -0
  20. package/library/knowledge/private/ai/README.md +8 -0
  21. package/library/knowledge/private/ai/model-discovery-classification.md +81 -0
  22. package/library/knowledge/private/ai/translation-layer.md +88 -0
  23. package/library/knowledge/private/architecture/README.md +10 -0
  24. package/library/knowledge/private/architecture/launch-flow-claude.md +93 -0
  25. package/library/knowledge/private/architecture/system-overview.md +108 -0
  26. package/library/knowledge/private/auth/README.md +9 -0
  27. package/library/knowledge/private/auth/oauth-device-flows.md +95 -0
  28. package/library/knowledge/private/data/README.md +8 -0
  29. package/library/knowledge/private/data/preferences-config.md +87 -0
  30. package/library/knowledge/private/data/provider-registry.md +126 -0
  31. package/library/knowledge/private/infrastructure/README.md +7 -0
  32. package/library/knowledge/private/infrastructure/server-gateway.md +87 -0
  33. package/library/knowledge/private/integrations/README.md +8 -0
  34. package/library/knowledge/private/integrations/harnesses.md +87 -0
  35. package/library/knowledge/private/integrations/local-proxy.md +82 -0
  36. package/library/knowledge/private/security/README.md +9 -0
  37. package/library/knowledge/private/security/credential-storage.md +129 -0
  38. package/library/knowledge/private/standards/documentation-framework.md +154 -0
  39. package/library/knowledge/public/README.md +49 -0
  40. package/library/knowledge/public/faqs/README.md +7 -0
  41. package/library/knowledge/public/faqs/troubleshooting.md +92 -0
  42. package/library/knowledge/public/guides/README.md +13 -0
  43. package/library/knowledge/public/guides/ai-agents.md +273 -0
  44. package/library/knowledge/public/guides/api-server.md +108 -0
  45. package/library/knowledge/public/guides/claude-desktop.md +382 -0
  46. package/library/knowledge/public/guides/codex.md +296 -0
  47. package/library/knowledge/public/guides/gemini-cli.md +105 -0
  48. package/library/knowledge/public/guides/model-compatibility.md +80 -0
  49. package/library/knowledge/public/guides/providers.md +90 -0
  50. package/library/knowledge/public/overview/README.md +7 -0
  51. package/library/knowledge/public/overview/what-is-rflectr.md +71 -0
  52. package/library/notes/README.md +21 -0
  53. package/library/requirements/README.md +51 -0
  54. package/library/requirements/backlog/README.md +30 -0
  55. package/library/requirements/completed/README.md +14 -0
  56. package/library/requirements/completed/prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md +205 -0
  57. package/library/requirements/completed/prd-001-cli-core-launch-orchestration/qa/.gitkeep +0 -0
  58. package/library/requirements/completed/prd-002-provider-registry/qa/.gitkeep +0 -0
  59. package/library/requirements/completed/prd-003-model-discovery-classification/qa/.gitkeep +0 -0
  60. package/library/requirements/completed/prd-004-translation-layer/qa/.gitkeep +0 -0
  61. package/library/requirements/completed/prd-005-local-proxy-catalog-routing/qa/.gitkeep +0 -0
  62. package/library/requirements/completed/prd-007-oauth-device-flows/qa/.gitkeep +0 -0
  63. package/library/requirements/completed/prd-011-claude-desktop-integration/qa/.gitkeep +0 -0
  64. package/library/requirements/in-work/README.md +19 -0
  65. package/library/requirements/reports/README.md +31 -0
  66. package/package.json +84 -0
  67. package/scripts/refresh-models-dev-cache.mjs +34 -0
  68. package/test-proxy.ts +19 -0
  69. package/test-split.js +1 -0
@@ -0,0 +1,39 @@
1
+ ---
2
+ ai_description: |
3
+ This is the root of the repository's documentation library (schema v2).
4
+ You own everything under library/ except notes/, which is human-only.
5
+ Sub-trees: knowledge/ (public and private docs), requirements/ (product
6
+ work: PRDs), issues/ (reactive bug/incident work: IRDs), notes/ (junk
7
+ drawer, read-only to agents).
8
+ Schema reference: legion-shared/standards/library-schema-v2.md.
9
+ Standardize script: pnpm standardize-library --repository <name>.
10
+ human_description: |
11
+ Root of this repository's documentation library.
12
+ - knowledge/: reference documentation split by audience (public vs private)
13
+ - requirements/: planned product work (PRDs) with backlog/in-work/completed lifecycle
14
+ - issues/: reactive bug and incident work (IRDs) with same lifecycle
15
+ - notes/: unstructured scratch space — only humans write here
16
+ Run `pnpm standardize-library --repository <name>` to scaffold any missing structure.
17
+ ---
18
+
19
+ # Library
20
+
21
+ Documentation root for this repository. Schema version: **v2**.
22
+
23
+ See [`legion-shared/standards/library-schema-v2.md`](../../legion-shared/standards/library-schema-v2.md) for the full specification.
24
+
25
+ ## Top-level layout
26
+
27
+ | Folder | What goes here |
28
+ |---|---|
29
+ | `knowledge/public/` | End-user / customer-facing docs: overviews, guides, FAQs |
30
+ | `knowledge/private/` | Internal engineering and business docs: ADRs, standards, domain knowledge |
31
+ | `requirements/` | Product and feature work: PRDs in backlog/in-work/completed |
32
+ | `issues/` | Reactive bug and incident work: IRDs in backlog/in-work/completed |
33
+ | `notes/` | Human-only scratch space |
34
+
35
+ ## What does NOT belong here
36
+
37
+ - Brand assets → `legion-shared/brands/`
38
+ - Wiki entity pages → `legion-wiki/<repo>/wiki/` (derived, never edit)
39
+ - Library mirrors → `legion-wiki/<repo>/library/` (derived, never edit)
@@ -0,0 +1,46 @@
1
+ ---
2
+ ai_description: |
3
+ This folder contains all reactive bug and incident work (IRDs).
4
+ It is a PEER of requirements/, not nested under it.
5
+ Sub-folders: backlog/, in-work/, completed/ — same lifecycle as requirements/.
6
+ IRD folder naming: ird-<###>-<kebab-slug>/
7
+ IRD numbers match the GitHub issue number for this repo.
8
+ Never invent IRD numbers — a GitHub issue must exist first.
9
+ IRDs are single-scope: one issue per IRD, no sub-IRDs.
10
+ Do NOT put PRDs here — those go in requirements/.
11
+ human_description: |
12
+ Reactive bug and incident work (IRDs), organized by lifecycle stage.
13
+ - backlog/: tracked issues with a fix plan, not yet started
14
+ - in-work/: issues currently being fixed
15
+ - completed/: resolved issues (move entire folder)
16
+ IRD numbers match GitHub issue numbers. Create an IRD only after the
17
+ GitHub issue exists.
18
+ ---
19
+
20
+ # Issues
21
+
22
+ Reactive bug and incident work (IRDs), organized by lifecycle state.
23
+
24
+ ## Sub-folders
25
+
26
+ | Folder | State | Description |
27
+ |---|---|---|
28
+ | `backlog/` | Tracked | IRDs with a fix plan, not yet in progress |
29
+ | `in-work/` | Active | Issues currently being resolved |
30
+ | `completed/` | Resolved | Entire IRD folder moves here when the issue closes |
31
+
32
+ ## IRD folder structure
33
+
34
+ ```
35
+ ird-042-stale-cache/
36
+ ird-042-stale-cache-index.md single-scope fix plan
37
+ qa/
38
+ ird-042-stale-cache-qa.md QA audit (written by quality-guardian)
39
+ ```
40
+
41
+ ## Naming rules
42
+
43
+ - Folder: `ird-<###>-<kebab-slug>/`
44
+ - Index: `ird-<###>-<kebab-slug>-index.md`
45
+ - IRD number = GitHub issue number (never invented)
46
+ - No sub-IRDs (scope one issue per IRD)
@@ -0,0 +1,26 @@
1
+ ---
2
+ ai_description: |
3
+ Contains IRD folders for tracked issues not yet in active fix work.
4
+ Create a new IRD here only AFTER the GitHub issue exists for this repo.
5
+ IRD folder: ird-<###>-<slug>/ where ### = GitHub issue number.
6
+ Must contain: ird-<###>-<slug>-index.md (the fix plan) and qa/ folder.
7
+ IRDs are single-scope: do not add sub-IRDs.
8
+ human_description: |
9
+ IRDs planned but not yet in active fix work. Create IRDs here.
10
+ - Naming: ird-042-stale-cache/ with ird-042-stale-cache-index.md inside
11
+ - IRD number must match the GitHub issue number
12
+ - Create only after the GitHub issue exists
13
+ Move to in-work/ when fix work begins.
14
+ ---
15
+
16
+ # Issues — Backlog
17
+
18
+ Tracked issues with a fix plan, not yet in active resolution.
19
+
20
+ ## Creating a new IRD
21
+
22
+ 1. Confirm the GitHub issue number (e.g., #42).
23
+ 2. Create `ird-042-<kebab-slug>/`.
24
+ 3. Create `ird-042-<slug>-index.md` — the single-scope fix plan.
25
+ 4. Create `qa/` subfolder (empty; `quality-guardian` writes here).
26
+ 5. No sub-IRDs — keep scope to one issue.
@@ -0,0 +1,13 @@
1
+ ---
2
+ ai_description: |
3
+ Resolved IRD folders. Entire ird-<###>-<slug>/ folders move here from
4
+ in-work/ when the corresponding GitHub issue is closed and verified.
5
+ Read-only after landing — do NOT edit or re-open IRDs here.
6
+ human_description: |
7
+ Resolved issue folders. Move entire ird-NNN-slug/ here from in-work/
8
+ when the GitHub issue is closed and the fix is confirmed. Read-only.
9
+ ---
10
+
11
+ # Issues — Completed
12
+
13
+ Resolved IRD folders. Entire `ird-<###>-<slug>/` folders land here when the GitHub issue closes and the fix is confirmed. Do not edit files here after landing.
@@ -0,0 +1,13 @@
1
+ ---
2
+ ai_description: |
3
+ IRD folders actively being resolved. Mirror of requirements/in-work/
4
+ but for issues. Move entire ird-<###>-<slug>/ folder from backlog/
5
+ here when fix work begins, then to completed/ when the issue closes.
6
+ human_description: |
7
+ IRDs currently being fixed. Move folder from backlog/ here when work
8
+ starts, and to completed/ when the GitHub issue is closed.
9
+ ---
10
+
11
+ # Issues — In Work
12
+
13
+ IRDs currently being resolved. Move from `backlog/` → here when fix work starts, then `completed/` when the GitHub issue closes.
@@ -0,0 +1,34 @@
1
+ ---
2
+ ai_description: |
3
+ This folder contains all reference documentation for this repository,
4
+ split by intended audience: public/ for end-users, private/ for internal
5
+ team and AI agents. When filing a new doc, default to private/. Promote
6
+ to public/ only when the content is intentionally customer-facing.
7
+ Allowed writes: knowledge/public/<domain>/<slug>.md and
8
+ knowledge/private/<domain>/<slug>.md. ADRs always go in
9
+ knowledge/private/architecture/ADR-<n>-<slug>.md.
10
+ Never write to knowledge/ itself (write to the sub-folders).
11
+ human_description: |
12
+ Reference documentation split by audience.
13
+ - public/: docs that will eventually be surfaced to customers or published
14
+ - private/: internal engineering, architecture, business, and strategy docs
15
+ When adding a new doc, pick the right subdomain folder inside public/ or
16
+ private/. If the domain doesn't exist yet, create it.
17
+ ---
18
+
19
+ # Knowledge
20
+
21
+ Reference documentation for this repository, organized by audience.
22
+
23
+ ## Sub-folders
24
+
25
+ | Folder | Audience | Typical content |
26
+ |---|---|---|
27
+ | `public/` | End-users, customers, external | Overviews, user guides, FAQs |
28
+ | `private/` | Internal team + AI agents | ADRs, standards, architecture, domain engineering docs |
29
+
30
+ ## Decision rule: public vs private
31
+
32
+ > "Would I publish this on a help center or product docs site?"
33
+
34
+ Yes → `public/`. No → `private/`. When in doubt, `private/`.
@@ -0,0 +1,40 @@
1
+ ---
2
+ ai_description: |
3
+ This folder contains internal engineering and business documentation.
4
+ ADRs MUST live in architecture/ADR-<n>-<kebab-slug>.md.
5
+ Engineering standards MUST live in standards/documentation-framework.md.
6
+ Other domain folders (<domain>/) are repo-specific and may be created as
7
+ needed (ai/, auth/, data/, frontend/, infrastructure/, integrations/,
8
+ marketing/, operations/, personas/, reporting/, roadmap/, scanners/,
9
+ security/, strategy/, etc.).
10
+ Do NOT file customer-facing content here (that goes in knowledge/public/).
11
+ Write path: library/knowledge/private/<domain>/<kebab-slug>.md.
12
+ human_description: |
13
+ Internal engineering and business documentation.
14
+ - architecture/: Architecture Decision Records (ADRs)
15
+ - standards/: Documentation framework and coding standards
16
+ - <domain>/: Any repo-specific knowledge domain (ai/, auth/, data/, etc.)
17
+ Default landing zone for any doc that does not need to be customer-facing.
18
+ When creating a new domain folder, add a README.md explaining what belongs.
19
+ ---
20
+
21
+ # Knowledge — Private
22
+
23
+ Internal documentation for engineers, product, and AI agents.
24
+
25
+ ## Required sub-folders (always present)
26
+
27
+ | Folder | Contents |
28
+ |---|---|
29
+ | `architecture/` | ADRs: `ADR-<n>-<kebab-slug>.md`. Locked decisions with context, alternatives, consequences. |
30
+ | `standards/` | `documentation-framework.md` and any repo-specific writing rules. |
31
+
32
+ ## Optional domain folders
33
+
34
+ Create any of these as needed: `ai/`, `auth/`, `data/`, `frontend/`, `infrastructure/`, `integrations/`, `marketing/`, `operations/`, `personas/`, `reporting/`, `roadmap/`, `scanners/`, `security/`, `strategy/`, `reference/`, `<product>-ux-ui/`.
35
+
36
+ ## What does NOT belong here
37
+
38
+ - Customer-facing content (put in `knowledge/public/`)
39
+ - PRDs or IRDs (put in `requirements/` or `issues/`)
40
+ - Brand assets (put in `legion-shared/brands/`)
@@ -0,0 +1,8 @@
1
+ # AI
2
+
3
+ The model-routing brain: wire-format translation and model discovery/classification.
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`translation-layer.md`](translation-layer.md) | The Vercel AI SDK adapter + provider factory — the single translation path; Responses-API selection; thought_signature round-trip. |
8
+ | [`model-discovery-classification.md`](model-discovery-classification.md) | `classifyModelFormat`, the two-source merge, context-window resolution, cost-display limitation. |
@@ -0,0 +1,81 @@
1
+ # Model Discovery & Classification
2
+
3
+ > Category: Ai | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ How `rflectr` builds the model list a user picks from, and how it decides whether each model is forwarded raw or translated. Read [`translation-layer.md`](translation-layer.md) for what happens *after* a model is classified.
6
+
7
+ **Related:**
8
+ - [`translation-layer.md`](translation-layer.md)
9
+ - [`../data/provider-registry.md`](../data/provider-registry.md)
10
+ - Source: `src/constants.ts` (`classifyModelFormat`), `src/models.ts`, `src/context-window.ts`, `src/context-model-id.ts`, `src/registry/materialize.ts`
11
+
12
+ ---
13
+
14
+ ## The format decision
15
+
16
+ Every model carries a `modelFormat` that drives the launch branch (`'anthropic'` = direct passthrough, anything else = SDK adapter proxy). It is computed by `classifyModelFormat(modelId, providerNpm)` in `src/constants.ts`:
17
+
18
+ ```ts
19
+ if (providerNpm === '@ai-sdk/anthropic') return 'anthropic';
20
+ if (providerNpm === '@ai-sdk/openai') return 'unsupported';
21
+ if (providerNpm === '@ai-sdk/google') return 'unsupported';
22
+ // Fallback: ID-prefix heuristics when no cache npm is known
23
+ if (id.startsWith('claude-')) return 'anthropic';
24
+ if (id.startsWith('gpt-')) return 'unsupported';
25
+ if (id.startsWith('gemini-')) return 'unsupported';
26
+ return 'openai';
27
+ ```
28
+
29
+ The four values mean:
30
+
31
+ | `modelFormat` | Meaning |
32
+ |---|---|
33
+ | `anthropic` | Direct passthrough to the provider's Anthropic endpoint. `isAnthropicNative` is true. |
34
+ | `openai` | Routed through the SDK adapter via the local proxy. The catch-all for everything that isn't natively Anthropic. |
35
+ | `unsupported` | Hidden in the **cloud OpenCode wizard** only. GPT/Gemini through OpenCode Zen/Go's proxy layer needs model-specific endpoints that the cloud path can't provide. |
36
+
37
+ > **Important nuance:** `unsupported` is a *cloud-wizard* restriction, not a global one. To use GPT or Gemini models, configure the **local OpenAI / Google provider** (which carries the real `@ai-sdk/openai` / `@ai-sdk/google` npm) — those route through the SDK adapter normally. The `unsupported` classification only blocks the OpenCode Zen/Go proxy layer where direct OpenAI/Google access isn't available.
38
+
39
+ ---
40
+
41
+ ## The two-source merge
42
+
43
+ The cloud (OpenCode Zen/Go) model list is built from two sources merged together:
44
+
45
+ ```mermaid
46
+ flowchart TD
47
+ api["GET {backendUrl}/v1/models — available ids (no auth)"]
48
+ cache["~/.cache/opencode/models.json — name, family, cost, provider.npm"]
49
+ api --> merge["mergeModels()"]
50
+ cache --> merge
51
+ merge --> enrich["enriched ModelInfo with modelFormat, sourceBackend, contextWindow"]
52
+ ```
53
+
54
+ - **Primary:** `GET {backendUrl}/v1/models` returns the available model ids (no auth required).
55
+ - **Enrichment:** `~/.cache/opencode/models.json` (written by the OpenCode CLI, path in `OPENCODE_CACHE_PATH`) supplies `name`, `family`, `cost`, and `provider.npm`. It is optional enrichment, never a runtime dependency.
56
+
57
+ `sourceBackend` is set from the backend that was queried. This matters for the `go` subscription tier, which shows Zen free models *and* Go paid models in one combined list — `sourceBackend` lets the launcher set the correct `ANTHROPIC_BASE_URL` per selected model.
58
+
59
+ ### Stale free models
60
+
61
+ `STALE_FREE_MODELS` in `src/constants.ts` lists models whose free promotion ended but the API still returns. They are filtered out in `mergeModels()`. (Historically this held `qwen3.6-plus-free`; treat the constant as the source of truth.)
62
+
63
+ ---
64
+
65
+ ## Registry models
66
+
67
+ Registry providers (`~/.rflectr/providers.json`) carry their own `CachedModel[]`, each already stamped with `modelFormat`, `npm`, `upstreamModelId`, `contextWindow`, `cost`, `supportedParameters`, and `reasoning`. `materializeRegistry` (`src/registry/materialize.ts`) converts those into runtime `LocalProviderModel`s and applies per-agent hiding via `shouldHideModel()` — e.g. Zen/Go favorites are hidden from Codex, which has no gateway path for them. See [`../data/provider-registry.md`](../data/provider-registry.md).
68
+
69
+ ---
70
+
71
+ ## Context window resolution
72
+
73
+ The status bar in Claude Code shows remaining context, which depends on a correct window. `resolveContextWindow(modelId, contextWindow?)` (`src/context-window.ts`) picks the window; `buildChildEnv` writes it to `CLAUDE_CODE_MAX_CONTEXT_TOKENS`. The proxy's synthetic `GET /v1/models` includes `context_window` per model (`formatAnthropicModelEntry`) so the host can render it.
74
+
75
+ A model id may carry a `[1m]` suffix to denote a 1-million-token context variant; `stripOneMContextSuffix` / `claudeCodeClientModelId` (`src/context-model-id.ts`) separate the wire id from the display id and the context hint. In switch-menu mode the window is fixed at launch and does not change on live `/model` switch (see [`../architecture/launch-flow-claude.md`](../architecture/launch-flow-claude.md#the-context-window-caveat)).
76
+
77
+ ---
78
+
79
+ ## Cost display is inaccurate for non-Anthropic models
80
+
81
+ Claude Code applies its own internal pricing table to whatever model id it sees, so the cost it shows for a Groq/DeepSeek/Gemini model is wrong. This is a documented, unfixable-from-here limitation — the host owns its pricing display.
@@ -0,0 +1,88 @@
1
+ # The Translation Layer
2
+
3
+ > Category: Ai | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ The single path that lets a Claude Code / Codex / Gemini host talk to any non-Anthropic model. This doc explains the Vercel AI SDK adapter (`src/sdk-adapter.ts`) and the provider factory (`src/provider-factory.ts`) that feeds it. Read [`../architecture/system-overview.md`](../architecture/system-overview.md) first.
6
+
7
+ **Related:**
8
+ - [`model-discovery-classification.md`](model-discovery-classification.md)
9
+ - [`../integrations/local-proxy.md`](../integrations/local-proxy.md)
10
+ - [`../integrations/harnesses.md`](../integrations/harnesses.md)
11
+ - Source: `src/sdk-adapter.ts`, `src/provider-factory.ts`, `src/proxy-shared.ts`, `src/codex-responses-adapter.ts`, `src/gemini-proxy.ts`
12
+
13
+ ---
14
+
15
+ ## Why there is exactly one translation path
16
+
17
+ A naive launcher would hand-roll a translator per provider: Anthropic→OpenAI, Anthropic→Gemini, and so on. That is a combinatorial mess and every provider has quirks (message ordering, reasoning signatures, tool-call encoding). `rflectr` instead routes **all** non-Anthropic providers through the Vercel AI SDK (`ai` + `@ai-sdk/*`) — the same packages OpenCode loads. The SDK owns wire format, endpoint selection, and provider quirks, so there is one translation path, not N.
18
+
19
+ ```mermaid
20
+ flowchart TD
21
+ host["Host wire format (Anthropic / Responses / Gemini)"]
22
+ host --> trans["translate*Request() — host body → SDK call params"]
23
+ trans --> factory["createLanguageModel({npm, modelId, apiKey, baseURL})"]
24
+ factory --> model["Vercel AI SDK LanguageModel"]
25
+ model --> stream["streamText / generateText"]
26
+ stream --> back["map SDK fullStream → host SSE/response"]
27
+ back --> host
28
+ ```
29
+
30
+ The rule that decides whether to translate at all: `isSdkMigratedNpm(npm)` (`src/provider-factory.ts`) is true for **any** npm except `@ai-sdk/anthropic`. Anthropic-format models skip the adapter and are forwarded raw.
31
+
32
+ ---
33
+
34
+ ## provider-factory.ts — npm → LanguageModel
35
+
36
+ `createLanguageModel(spec)` (async) is the factory. `spec` carries `{ npm, modelId, apiKey, baseURL?, providerId?, authType?, oauthAccountId?, vertex? }`. It dynamically `import(npm)`s the SDK package and discovers its `create*` factory. The router has special branches:
37
+
38
+ | npm | Behaviour |
39
+ |---|---|
40
+ | `@ai-sdk/google-vertex/anthropic` (`VERTEX_ANTHROPIC_NPM`) | Claude on Google Vertex AI via gcloud Application Default Credentials (no apiKey). |
41
+ | `@ai-sdk/openai` | OAuth → ChatGPT Codex backend (`https://chatgpt.com/backend-api/codex`); API key → direct OpenAI. `modelPrefersResponsesApi()` picks `openai.responses(id)` vs `openai.chat(id)`. |
42
+ | `@ai-sdk/xai` | Direct; also consults `modelPrefersResponsesApi()`. |
43
+ | `@ai-sdk/google` | Direct — ignores `baseURL`, uses the native `/v1beta` endpoint. |
44
+ | `@ai-sdk/anthropic` | Direct; strips a trailing `/v1` from `baseURL` if present. |
45
+ | `@ai-sdk/openai-compatible`, `@openrouter/ai-sdk-provider` | Routed via `baseURL`. |
46
+ | anything else | `loadSdkProviderFactory(npm)` finds the `create*()` export dynamically. |
47
+
48
+ The `@ai-sdk/*` provider packages ship as npm **`dependencies`** and are marked `external` in `tsup.config.ts`, so they resolve from `node_modules` at runtime and keep `dist/cli.js` small.
49
+
50
+ ### The Responses API selector
51
+
52
+ `modelPrefersResponsesApi(modelId)` returns true for OpenAI/xAI models that require the Responses API rather than Chat Completions: GPT-5.4+, GPT-5.5, `gpt-5-pro` / `gpt-5.2-pro`, `*-codex`, the o-series (`o3`, `o4`, …), and xAI `grok-*-multi-agent`. Newer OpenAI reasoning models only round-trip correctly through Responses, so this selection is load-bearing — not cosmetic.
53
+
54
+ OpenCode catalog ids may differ from upstream API ids (e.g. `gpt-5.5-fast` → `gpt-5.5`); `upstreamModelId` carries OpenCode's `api.id` for the actual upstream call.
55
+
56
+ ### Reasoning capabilities
57
+
58
+ `getReasoningCapabilities(npm, modelId, metadata)` returns a `ReasoningCapabilities` describing whether a model exposes controllable effort/thinking, internal-only reasoning, or none — covering Claude 4.6+, Gemini 2.5+/3, Mistral reasoners, xAI reasoners, DeepSeek V4, Kimi, and OpenRouter. The adapter uses this (with `effortProviderOptions` / `thinkingProviderOptions` / `deepMergeProviderOptions`) to translate a host's thinking/effort request into the right provider option block.
59
+
60
+ ---
61
+
62
+ ## sdk-adapter.ts — Anthropic ↔ SDK
63
+
64
+ The adapter handles the Claude-Code-facing direction. Its contract: **one turn per request.** Claude Code owns the tool loop, so the adapter never loops; it translates a single request and streams a single response.
65
+
66
+ - `translateRequest(body, npm, options?)` builds the SDK call params from an Anthropic request — messages, tools, `tool_choice`, system. Critically, it **folds inline `role:'system'` messages into the system prompt**: Claude Code injects the skills list and system-reminders as system-role messages mid-conversation, and dropping them would break behaviour. `TranslateRequestOptions` carries `defaultEffort` (fallback when the client omits effort, e.g. the Claude Desktop gateway), `reasoningMetadata`, and `openAiOAuth` (ChatGPT Codex OAuth manages its own output limit and requires `instructions`).
67
+ - `streamAnthropicResponse` maps the SDK `fullStream` to Anthropic SSE.
68
+ - `generateAnthropicResponse` handles the non-streaming case.
69
+
70
+ ### The thought_signature round-trip
71
+
72
+ Reasoning models (especially Gemini) require their `thought_signature` to be echoed back verbatim on the next turn. Anthropic's wire format has no field for it, so `rflectr` smuggles it through the tool-use id:
73
+
74
+ - **Encode:** `encodeToolUseId(rawId, signature)` (`src/proxy-shared.ts`) produces `{id}::ts::{signature}`.
75
+ - **Decode:** `splitToolUseId(id)` recovers `{ rawId, thoughtSignature }`, which is fed back into `providerOptions.google.thoughtSignature`.
76
+
77
+ Gemini puts the signature on tool-call parts (captured at `tool-input-start`); the SDK then handles Gemini's strict echo-back. This is the reason the old hand-rolled Gemini-native path was retired. The `::ts::` separator would break only if a signature literally contained `::ts::` — extremely unlikely, and a documented edge.
78
+
79
+ ---
80
+
81
+ ## The other two host directions
82
+
83
+ The same factory + SDK model is reused; only the host-facing translation differs:
84
+
85
+ - **Codex Responses API** (`src/codex-responses-adapter.ts`): `translateResponsesRequest` / `translateResponsesInput` / `translateResponsesTools` build SDK params from a Responses body; `streamResponsesResponse` / `generateResponsesResponse` emit the Responses SSE/JSON shape.
86
+ - **Gemini REST** (`src/gemini-proxy.ts` + `src/gemini-parts.ts`): `translateGeminiRequest` extracts system instruction, contents, tools, and generation config; `parseGeminiPart` / `collectAnthropicBlocksFromGeminiParts` / `mapGeminiUsage` translate parts and usage.
87
+
88
+ All three converge on `createLanguageModel` + `streamText`/`generateText`. That convergence is the whole point: add a provider once and every host can use it.
@@ -0,0 +1,10 @@
1
+ # Architecture
2
+
3
+ System-level design docs and Architecture Decision Records (ADRs).
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`system-overview.md`](system-overview.md) | What rflectr is, its surfaces, the shared translation core, env isolation. **Start here.** |
8
+ | [`launch-flow-claude.md`](launch-flow-claude.md) | The `rflectr claude` flow: single-model vs switch-menu launch paths. |
9
+
10
+ ADRs (when added) live here as `ADR-<n>-<kebab-slug>.md`.
@@ -0,0 +1,93 @@
1
+ # Claude Code Launch Flow
2
+
3
+ > Category: Architecture | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ How `rflectr claude` goes from a command line to a running Claude Code process pointed at the chosen model. Read [`system-overview.md`](system-overview.md) first. This doc traces `runClaudeCommand` in `src/cli.ts` and its two launch paths.
6
+
7
+ **Related:**
8
+ - [`system-overview.md`](system-overview.md)
9
+ - [`../integrations/local-proxy.md`](../integrations/local-proxy.md)
10
+ - [`../ai/translation-layer.md`](../ai/translation-layer.md)
11
+ - [`../data/preferences-config.md`](../data/preferences-config.md)
12
+ - Source: `src/cli.ts` (`runClaudeCommand`, `runModelsCommand`, `launchClaudeViaCatalog`), `src/env.ts` (`buildChildEnv`), `src/catalog.ts`
13
+
14
+ ---
15
+
16
+ ## The two modes
17
+
18
+ The launch has two shapes, decided by one line in `runClaudeCommand`:
19
+
20
+ ```ts
21
+ const switchMenuActive = favorites.length > 0 && !launchPlan.skip;
22
+ ```
23
+
24
+ - **Single-model mode** (no favorites saved): one model, one route. Launch and exit.
25
+ - **Switch-menu mode** (`rflectr models` has saved at least one favorite): a multi-route catalog proxy is started and Claude Code's gateway model discovery is enabled, so the in-session `/model` command lists the starting model plus every favorite for live switching.
26
+
27
+ Favorites are managed by `runModelsCommand` (also in `src/cli.ts`) and persisted to `~/.rflectr/config.json`. The cap is `MAX_MODEL_CATALOG = 20` (`src/constants.ts`).
28
+
29
+ ---
30
+
31
+ ## Single-model flow
32
+
33
+ ```mermaid
34
+ flowchart TD
35
+ start["rflectr claude"] --> bin["findClaudeBinary()"]
36
+ bin --> firstrun["needsFirstRunSetup? → runFirstRunWizard()"]
37
+ firstrun --> catalog["fetchProviderCatalog()"]
38
+ catalog --> pickP["p.select: which provider?"]
39
+ pickP --> pickM["pickLocalModel(provider)"]
40
+ pickM --> fmt{"selectedModel.modelFormat"}
41
+ fmt -->|anthropic| direct["buildChildEnv(model.baseUrl, ...) — no proxy"]
42
+ fmt -->|openai| proxy["startProxy(...) → buildChildEnv(127.0.0.1:port, ...)"]
43
+ direct --> launch["launchClaude(env, model, args)"]
44
+ proxy --> launch
45
+ launch --> wait["Claude Code runs (stdio inherited)"]
46
+ wait --> close["proxyHandle.close() on exit"]
47
+ ```
48
+
49
+ The format branch is the heart of it (`src/cli.ts`):
50
+
51
+ - `modelFormat === 'anthropic'` → **direct passthrough.** `buildChildEnv(selectedModel.baseUrl!, selectedModel.id, launchApiKey, undefined, contextWindow)`. No proxy; Claude Code talks straight to the provider's Anthropic-compatible endpoint. `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` is set so beta headers are stripped on the direct hop.
52
+ - otherwise → **SDK adapter proxy.** `startProxy(completionsUrl, modelId, trace, contextWindow, { npm, baseURL, upstreamModelId, providerId, authType, oauthAccountId, supportedParameters, reasoning, interleavedReasoningField }, apiKey)` returns a `ProxyHandle`; the child env points `ANTHROPIC_BASE_URL` at `http://127.0.0.1:<port>`.
53
+
54
+ The provider API key is resolved by `resolveLocalProviderApiKey(activeProvider)` (`src/provider-catalog.ts`). If none is found, launch aborts with a message pointing at `rflectr providers`.
55
+
56
+ ---
57
+
58
+ ## Switch-menu (catalog) flow
59
+
60
+ When favorites exist, `runClaudeCommand` delegates to `launchClaudeViaCatalog`:
61
+
62
+ 1. `makeRouteResolver(localProviders, zenModels, goModels, zenGoApiKey)` (`src/catalog.ts`) builds a function that maps a `(providerId, modelId)` pair to a `ProxyRoute`.
63
+ 2. `buildCatalogRoutes(startingRoute, favorites, resolveRoute)` builds the route list — **starting model + favorites only**, never the full catalog — and reports `droppedFavorites` (favorites whose provider/model is no longer available, silently skipped).
64
+ 3. `startProxyCatalog(catalogRoutes, startingRoute.aliasId, trace)` starts one proxy serving all routes.
65
+ 4. `buildChildEnv(..., enableGatewayDiscovery = true)` sets `CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1` so Claude Code fetches `/v1/models` from the proxy and populates `/model`.
66
+
67
+ Each route's id is rewritten by `aliasModelId()` (`src/proxy.ts`) so Claude Code sees unique, gateway-safe names (e.g. `anthropic-opencode-go__deepseek-v4-flash`) in the picker. See [`../integrations/local-proxy.md`](../integrations/local-proxy.md).
68
+
69
+ A `__favorites__` pseudo-provider is unshifted onto the provider picker in switch-menu mode; selecting it loads the Favorites Catalog and uses `resolveFirstAvailableFavorite` as the starting model.
70
+
71
+ ---
72
+
73
+ ## The context-window caveat
74
+
75
+ In switch-menu mode the displayed context window 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, so `CLAUDE_CODE_MAX_CONTEXT_TOKENS` — fixed at launch by `buildChildEnv` — is the only lever. Single-model launches show the correct window. This is a documented, by-design limitation.
76
+
77
+ ---
78
+
79
+ ## Flags and boot shortcuts
80
+
81
+ `parseArgs` (`src/cli.ts`) recognises starter flags (`--dry-run`, `--setup`, `--trace`, `--help`, `--version`) and relay launch flags (`--provider`, `--model`). Everything after `--`, and any unrecognised flag, is forwarded verbatim to Claude Code (`claudeArgs`).
82
+
83
+ - `--provider X --model Y` (or print mode `-p`) skips the wizard entirely via `planLaunchWizard` / `findProviderAndModel` (`src/launch-target.ts`).
84
+ - `--dry-run` runs the whole wizard but prints a preview (`printDryRun`) and writes nothing — it ignores saved env keys, keyring, tier, and preferences, simulating a fresh first run.
85
+ - `--trace` writes a debug log under `~/.rflectr/logs/` and prints errors on exit (`prepareClaudeTraceLog` / `printTraceLog`).
86
+
87
+ Clean-stdout agent mode (`wantsCleanAgentStdout`, `setAgentStdoutMode`) suppresses the interactive intro/spinners when Claude Code is run in print/pipe mode.
88
+
89
+ ---
90
+
91
+ ## What the child process receives
92
+
93
+ For the exact env contract (`ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`, `CLAUDE_CODE_MAX_CONTEXT_TOKENS`, the tool-search / system-prompt compat vars, and the removed conflict vars), see [`../security/credential-storage.md`](../security/credential-storage.md#child-process-environment).
@@ -0,0 +1,108 @@
1
+ # System Overview
2
+
3
+ > Category: Architecture | Version: 1.0 | Date: June 2026 | Status: Active
4
+
5
+ Read this first. It explains what `rflectr` is, the surfaces it exposes, and the single translation core that every surface shares. New engineers should read this before diving into any domain doc.
6
+
7
+ **Related:**
8
+ - [`launch-flow-claude.md`](launch-flow-claude.md)
9
+ - [`../ai/translation-layer.md`](../ai/translation-layer.md)
10
+ - [`../ai/model-discovery-classification.md`](../ai/model-discovery-classification.md)
11
+ - [`../integrations/local-proxy.md`](../integrations/local-proxy.md)
12
+ - [`../integrations/harnesses.md`](../integrations/harnesses.md)
13
+ - [`../data/provider-registry.md`](../data/provider-registry.md)
14
+ - Source: `src/cli.ts`, `src/constants.ts`
15
+
16
+ ---
17
+
18
+ ## Why this exists
19
+
20
+ The major agentic coding tools — Claude Code, OpenAI Codex (CLI and desktop), Google Gemini CLI, and Claude Desktop — each speak only to their vendor's own API by default. `rflectr` is a launcher that re-points each of those tools at a *different* model backend without the tool noticing. You can run Claude Code against a Groq Llama model, Codex against DeepSeek, or Gemini CLI against a local Ollama endpoint — picking the model from an interactive wizard, then handing the host tool an environment that makes it believe it is talking to its native API.
21
+
22
+ The published npm package and CLI binary are both named `rflectr` (`package.json` is the single source of truth for the version; see [`CLAUDE.md`](../../../../CLAUDE.md) for the release workflow). The repository directory is `rflectr`.
23
+
24
+ The hard problem `rflectr` solves is **wire-format translation**: Claude Code emits Anthropic `/v1/messages`, Codex emits the OpenAI Responses API, Gemini CLI emits the Gemini REST protocol — but the chosen model may speak any of those formats (or none of them directly). A local HTTP proxy sits between the host tool and the upstream model and translates in both directions. All non-Anthropic translation flows through one path: the **Vercel AI SDK adapter** (see [`../ai/translation-layer.md`](../ai/translation-layer.md)).
25
+
26
+ ---
27
+
28
+ ## The surfaces
29
+
30
+ Every surface is a subcommand of the `rflectr` CLI, dispatched from `parseArgs` / `main` in `src/cli.ts`.
31
+
32
+ | Command | What it launches | Host wire format | Doc |
33
+ |---|---|---|---|
34
+ | `rflectr claude` | Claude Code CLI | Anthropic `/v1/messages` | [`launch-flow-claude.md`](launch-flow-claude.md) |
35
+ | `rflectr codex` | OpenAI Codex CLI | OpenAI Responses (`/v1/responses`) | [`../integrations/harnesses.md`](../integrations/harnesses.md) |
36
+ | `rflectr codex-app` | Codex desktop app | OpenAI Responses | [`../integrations/harnesses.md`](../integrations/harnesses.md) |
37
+ | `rflectr gemini` | Gemini CLI | Gemini REST (`:generateContent`) | [`../integrations/harnesses.md`](../integrations/harnesses.md) |
38
+ | `rflectr claude-app` | Claude Desktop app | Anthropic (gateway config) | [`../integrations/harnesses.md`](../integrations/harnesses.md) |
39
+ | `rflectr server` | Foreground API gateway | Anthropic + OpenAI compatible | [`../infrastructure/server-gateway.md`](../infrastructure/server-gateway.md) |
40
+ | `rflectr providers` | Provider registry manager | — | [`../data/provider-registry.md`](../data/provider-registry.md) |
41
+ | `rflectr models` / `favorites` | Favorite-model manager | — | [`launch-flow-claude.md`](launch-flow-claude.md) |
42
+
43
+ ```mermaid
44
+ flowchart TD
45
+ subgraph hosts["Host tools (unmodified)"]
46
+ cc["Claude Code"]
47
+ cx["Codex CLI / app"]
48
+ gm["Gemini CLI"]
49
+ cd["Claude Desktop"]
50
+ end
51
+ subgraph relay["rflectr"]
52
+ proxy["Local proxy 127.0.0.1:random"]
53
+ adapter["Vercel AI SDK adapter"]
54
+ registry["Provider registry ~/.rflectr/providers.json"]
55
+ end
56
+ subgraph upstream["Upstream models"]
57
+ anth["Anthropic-format endpoints"]
58
+ sdk["Any @ai-sdk provider (OpenAI, Groq, Gemini, xAI, ...)"]
59
+ end
60
+ cc -->|ANTHROPIC_BASE_URL| proxy
61
+ cx -->|OPENAI base_url| proxy
62
+ gm -->|GOOGLE_GEMINI_BASE_URL| proxy
63
+ cd -->|gateway config| proxy
64
+ proxy -->|"anthropic format"| anth
65
+ proxy -->|"everything else"| adapter
66
+ adapter --> sdk
67
+ registry --> proxy
68
+ ```
69
+
70
+ ---
71
+
72
+ ## The shared core
73
+
74
+ Three subsystems are reused by every surface:
75
+
76
+ **1. The provider registry** (`src/registry/`, `src/provider-catalog.ts`). The list of providers and their models lives in `~/.rflectr/providers.json`. It is the single source of truth for what shows up in every wizard. Built-in templates (Groq, Mistral, OpenAI, Ollama, …) are defined in `src/provider-templates.ts`; OpenCode Zen / Go are always available even with no registry. See [`../data/provider-registry.md`](../data/provider-registry.md).
77
+
78
+ **2. The translation layer** (`src/sdk-adapter.ts`, `src/provider-factory.ts`). `createLanguageModel({ npm, modelId, apiKey, baseURL })` turns whatever npm package OpenCode/the registry assigned the provider into a Vercel AI SDK `LanguageModel`. The adapter then maps the host's wire format to and from that model, one turn per request — the host tool always owns its own tool loop. See [`../ai/translation-layer.md`](../ai/translation-layer.md).
79
+
80
+ **3. The local proxy** (`src/proxy.ts` and the per-protocol variants `src/codex-proxy.ts`, `src/gemini-proxy.ts`). A throwaway HTTP server on `127.0.0.1:<random port>` that the host tool is pointed at. Anthropic-format models are forwarded raw; everything else goes through the SDK adapter. `aliasModelId()` rewrites non-`claude-*` ids to a gateway-safe form. See [`../integrations/local-proxy.md`](../integrations/local-proxy.md).
81
+
82
+ ---
83
+
84
+ ## Environment isolation, not config editing
85
+
86
+ `rflectr` never edits the host tool's settings file. It launches the child process with a purpose-built environment (`buildChildEnv` in `src/env.ts`) that:
87
+
88
+ - **Removes** the 17 conflicting Anthropic/Vertex/Bedrock/AWS/Foundry env vars listed in `CONFLICTING_ENV_VARS` (`src/constants.ts`), so stale cloud config can't leak in.
89
+ - **Sets** `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`, and `CLAUDE_CODE_MAX_CONTEXT_TOKENS`.
90
+
91
+ This avoids the backup/restore problem that settings-file rewriters have. The one caveat: Claude Code itself persists the launched model to `~/.claude/settings.json`, so a later bare `claude` may still show a relay alias. See [`../security/credential-storage.md`](../security/credential-storage.md) for the full env contract.
92
+
93
+ The two desktop apps (`claude-app`, `codex-app`) are the exception — they *do* write config files, because the apps have no env to inherit. Those writes are backed up and restored on exit via lock files. See [`../integrations/harnesses.md`](../integrations/harnesses.md).
94
+
95
+ ---
96
+
97
+ ## A critical URL constraint
98
+
99
+ `BACKENDS.baseUrl` in `src/constants.ts` must **not** include `/v1`. The Anthropic SDK appends `/v1/messages` automatically, so `https://opencode.ai/zen/v1` would produce requests to `/zen/v1/v1/messages` → 404. The same rule applies anywhere an Anthropic-format `baseUrl` is built. This is the single most common configuration footgun in the codebase.
100
+
101
+ ---
102
+
103
+ ## Where to go next
104
+
105
+ - To trace a launch end to end: [`launch-flow-claude.md`](launch-flow-claude.md).
106
+ - To understand how a Groq or DeepSeek model gets spoken to as if it were Anthropic: [`../ai/translation-layer.md`](../ai/translation-layer.md).
107
+ - To understand how the model list is built and classified: [`../ai/model-discovery-classification.md`](../ai/model-discovery-classification.md).
108
+ - To understand credential handling: [`../security/credential-storage.md`](../security/credential-storage.md) and [`../auth/oauth-device-flows.md`](../auth/oauth-device-flows.md).
@@ -0,0 +1,9 @@
1
+ # Auth
2
+
3
+ Provider sign-in flows (distinct from credential storage, which is under `security/`).
4
+
5
+ | Doc | Covers |
6
+ |---|---|
7
+ | [`oauth-device-flows.md`](oauth-device-flows.md) | RFC 8628 device flows for OpenAI/ChatGPT, xAI/Grok, GitHub Copilot; token storage and refresh; PKCE. |
8
+
9
+ For where keys/tokens are stored and the env contract, see [`../security/credential-storage.md`](../security/credential-storage.md).