@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.
Files changed (84) hide show
  1. package/dist/cli.js +4 -1
  2. package/dist/cli.js.map +1 -0
  3. package/package.json +4 -1
  4. package/.markdown-link-check.json +0 -7
  5. package/AGENTS.md +0 -169
  6. package/assets/733630021_1421561133353555_3999689754075308337_n.jpg +0 -0
  7. package/assets/github-home-image.png +0 -0
  8. package/assets/og-image.jpg +0 -0
  9. package/assets/og-image.png +0 -0
  10. package/assets/og-image.psd +0 -0
  11. package/assets/rflectr-no-bg.png +0 -0
  12. package/assets/vertex-models.example.json +0 -14
  13. package/library/README.md +0 -39
  14. package/library/issues/README.md +0 -46
  15. package/library/issues/backlog/README.md +0 -26
  16. package/library/issues/completed/README.md +0 -13
  17. package/library/issues/in-work/README.md +0 -13
  18. package/library/knowledge/README.md +0 -34
  19. package/library/knowledge/private/README.md +0 -40
  20. package/library/knowledge/private/ai/README.md +0 -8
  21. package/library/knowledge/private/ai/model-discovery-classification.md +0 -81
  22. package/library/knowledge/private/ai/translation-layer.md +0 -88
  23. package/library/knowledge/private/architecture/README.md +0 -10
  24. package/library/knowledge/private/architecture/launch-flow-claude.md +0 -93
  25. package/library/knowledge/private/architecture/system-overview.md +0 -108
  26. package/library/knowledge/private/auth/README.md +0 -9
  27. package/library/knowledge/private/auth/oauth-device-flows.md +0 -95
  28. package/library/knowledge/private/data/README.md +0 -8
  29. package/library/knowledge/private/data/preferences-config.md +0 -87
  30. package/library/knowledge/private/data/provider-registry.md +0 -126
  31. package/library/knowledge/private/infrastructure/README.md +0 -7
  32. package/library/knowledge/private/infrastructure/server-gateway.md +0 -87
  33. package/library/knowledge/private/integrations/README.md +0 -8
  34. package/library/knowledge/private/integrations/harnesses.md +0 -87
  35. package/library/knowledge/private/integrations/local-proxy.md +0 -82
  36. package/library/knowledge/private/security/README.md +0 -9
  37. package/library/knowledge/private/security/credential-storage.md +0 -129
  38. package/library/knowledge/private/standards/documentation-framework.md +0 -154
  39. package/library/knowledge/public/README.md +0 -49
  40. package/library/knowledge/public/faqs/README.md +0 -7
  41. package/library/knowledge/public/faqs/troubleshooting.md +0 -92
  42. package/library/knowledge/public/guides/README.md +0 -13
  43. package/library/knowledge/public/guides/ai-agents.md +0 -273
  44. package/library/knowledge/public/guides/api-server.md +0 -108
  45. package/library/knowledge/public/guides/claude-desktop.md +0 -382
  46. package/library/knowledge/public/guides/codex.md +0 -296
  47. package/library/knowledge/public/guides/gemini-cli.md +0 -105
  48. package/library/knowledge/public/guides/model-compatibility.md +0 -80
  49. package/library/knowledge/public/guides/providers.md +0 -90
  50. package/library/knowledge/public/overview/README.md +0 -7
  51. package/library/knowledge/public/overview/what-is-rflectr.md +0 -71
  52. package/library/notes/README.md +0 -21
  53. package/library/requirements/README.md +0 -51
  54. package/library/requirements/backlog/README.md +0 -30
  55. package/library/requirements/completed/README.md +0 -14
  56. package/library/requirements/completed/prd-001-cli-core-launch-orchestration/prd-001-cli-core-launch-orchestration-index.md +0 -205
  57. package/library/requirements/completed/prd-001-cli-core-launch-orchestration/qa/.gitkeep +0 -0
  58. package/library/requirements/completed/prd-002-provider-registry/prd-002-provider-registry-index.md +0 -263
  59. package/library/requirements/completed/prd-002-provider-registry/qa/.gitkeep +0 -0
  60. package/library/requirements/completed/prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md +0 -260
  61. package/library/requirements/completed/prd-003-model-discovery-classification/qa/.gitkeep +0 -0
  62. package/library/requirements/completed/prd-004-translation-layer/prd-004-translation-layer-index.md +0 -196
  63. package/library/requirements/completed/prd-004-translation-layer/qa/.gitkeep +0 -0
  64. package/library/requirements/completed/prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md +0 -176
  65. package/library/requirements/completed/prd-005-local-proxy-catalog-routing/qa/.gitkeep +0 -0
  66. package/library/requirements/completed/prd-006-credential-storage/prd-006-credential-storage-index.md +0 -190
  67. package/library/requirements/completed/prd-006-credential-storage/qa/.gitkeep +0 -0
  68. package/library/requirements/completed/prd-007-oauth-device-flows/prd-007-oauth-device-flows-index.md +0 -208
  69. package/library/requirements/completed/prd-007-oauth-device-flows/qa/.gitkeep +0 -0
  70. package/library/requirements/completed/prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md +0 -249
  71. package/library/requirements/completed/prd-008-preferences-tiers-favorites/qa/.gitkeep +0 -0
  72. package/library/requirements/completed/prd-009-codex-integration/prd-009-codex-integration-index.md +0 -212
  73. package/library/requirements/completed/prd-009-codex-integration/qa/.gitkeep +0 -0
  74. package/library/requirements/completed/prd-010-gemini-cli-integration/prd-010-gemini-cli-integration-index.md +0 -211
  75. package/library/requirements/completed/prd-010-gemini-cli-integration/qa/.gitkeep +0 -0
  76. package/library/requirements/completed/prd-011-claude-desktop-integration/prd-011-claude-desktop-integration-index.md +0 -228
  77. package/library/requirements/completed/prd-011-claude-desktop-integration/qa/.gitkeep +0 -0
  78. package/library/requirements/completed/prd-012-server-gateway/prd-012-server-gateway-index.md +0 -356
  79. package/library/requirements/completed/prd-012-server-gateway/qa/.gitkeep +0 -0
  80. package/library/requirements/in-work/README.md +0 -19
  81. package/library/requirements/reports/README.md +0 -31
  82. package/scripts/refresh-models-dev-cache.mjs +0 -34
  83. package/test-proxy.ts +0 -19
  84. package/test-split.js +0 -1
@@ -1,260 +0,0 @@
1
- # PRD-003: Model Discovery & Classification *(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/models.ts`, `src/model-compatibility.ts`, `src/context-window.ts`, `src/reasoning-capabilities.ts`
9
-
10
- ---
11
-
12
- ## Overview
13
-
14
- `rflectr` re-points Claude Code (and the other agents) at alternative model backends. Before a user can pick a model, the launcher has to build a list of models that are actually usable, decide for each one whether it can be forwarded raw to an Anthropic-compatible endpoint or must be translated, and stamp it with the metadata Claude Code needs to render an accurate status bar (context window, cost, reasoning controls).
15
-
16
- This PRD documents that subsystem: how the cloud (OpenCode Zen/Go) model list is assembled from two merged sources, how every model's `modelFormat` is classified, how context windows and reasoning capabilities are resolved, and how unusable models (incompatible, deprecated, stale-free) are filtered out.
17
-
18
- The single most load-bearing output is `modelFormat`. It is the branch point for the entire launch flow — `'anthropic'` means direct passthrough, anything else routes through the SDK adapter proxy (see [PRD-004 — Translation Layer](../prd-004-translation-layer/prd-004-translation-layer-index.md)).
19
-
20
- Knowledge doc: [`model-discovery-classification.md`](../../../knowledge/private/ai/model-discovery-classification.md).
21
-
22
- ---
23
-
24
- ## What Was Built
25
-
26
- - A **two-source merge** that builds the cloud model list from a live API call (`GET {backendUrl}/v1/models`, no auth) enriched by the local OpenCode CLI cache (`~/.cache/opencode/models.json`) — `src/models.ts:131` (`getModels`), `src/models.ts:85` (`mergeModels`).
27
- - A pure **format classifier** `classifyModelFormat(modelId, providerNpm)` returning `'anthropic' | 'openai' | 'unsupported'` from provider npm first, then ID-prefix heuristics — `src/constants.ts:58`.
28
- - A **Go-backend override** that demotes any `'anthropic'` classification to `'openai'`, because the Go gateway is OpenAI-compatible and an `@ai-sdk/anthropic` npm in the cache is a metadata error — `src/models.ts:50`, `src/models.ts:95`.
29
- - **`sourceBackend` stamping** so a combined Zen-free + Go-paid list (the `go` tier) can resolve the correct base URL per selected model — `src/models.ts:51`, `src/models.ts:96`.
30
- - **Context-window resolution** from cache `limit.context` → ID heuristics → 200K default, plus a `[1m]` suffix convention for million-token variants — `src/context-window.ts`, `src/context-model-id.ts`.
31
- - **Reasoning-capability metadata** (levels, default level, mode, wire format) resolved per provider npm + model id — `src/reasoning-capabilities.ts`, `src/provider-factory.ts:469`.
32
- - **Incompatibility filtering** via a curated JSON blacklist plus conservative models.dev capability checks — `src/model-compatibility.ts`, `src/data/model-incompatible.json`.
33
- - **Graceful degradation**: API failure falls back to the cache, then to caller-supplied fallback models, then errors — `src/models.ts:140`.
34
-
35
- ---
36
-
37
- ## Goals
38
-
39
- - Show the user only models that will actually work in a coding agent (no embedding/image/audio/video/deprecated/managed-agent models).
40
- - Decide passthrough-vs-translate deterministically and at the model level, not the provider level.
41
- - Render an accurate remaining-context status bar in Claude Code for non-Anthropic models.
42
- - Surface reasoning-effort controls where the provider supports them.
43
- - Never hard-depend on the OpenCode cache: it is enrichment, not a runtime requirement.
44
- - Degrade gracefully when the model API is unreachable.
45
-
46
- ## Non-Goals
47
-
48
- - Live `/model` switching context-window updates — fixed at launch in switch-menu mode (documented limitation).
49
- - Accurate cost display for non-Anthropic models — Claude Code owns its pricing table; this is unfixable from rflectr.
50
- - Registry-provider model materialization — owned by [PRD-002 — Provider Registry](../prd-002-provider-registry/prd-002-provider-registry-index.md). This PRD covers the cloud Zen/Go path; registry models arrive pre-stamped.
51
- - Backend selection / tier logic — owned by [PRD-008 — Preferences, Tiers & Favorites](../prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md). This PRD consumes `backend.id` / `sourceBackend`.
52
- - The translation that happens *after* a model is classified `'openai'` — owned by [PRD-004 — Translation Layer](../prd-004-translation-layer/prd-004-translation-layer-index.md).
53
-
54
- ---
55
-
56
- ## Features
57
-
58
- | # | Feature | Source |
59
- |---|---------|--------|
60
- | F1 | Two-source merge (live API ids + OpenCode cache enrichment) | `src/models.ts:85`, `src/models.ts:131` |
61
- | F2 | `classifyModelFormat` (npm → id-prefix heuristic) | `src/constants.ts:58` |
62
- | F3 | Go-backend anthropic→openai demotion | `src/models.ts:50`, `src/models.ts:95` |
63
- | F4 | `sourceBackend` per-model stamping | `src/models.ts:51`, `src/models.ts:96` |
64
- | F5 | `isFree` detection (cost.input == 0 && cost.output == 0) | `src/models.ts:44` |
65
- | F6 | Brand derivation for grouping | `src/models.ts:23` (`deriveBrand`) |
66
- | F7 | Context-window resolution (cache → heuristics → 200K) | `src/context-window.ts:131` |
67
- | F8 | `[1m]` context-suffix convention | `src/context-model-id.ts` |
68
- | F9 | Reasoning-capability metadata resolution | `src/reasoning-capabilities.ts:24`, `src/provider-factory.ts:469` |
69
- | F10 | Curated incompatibility blacklist filtering | `src/model-compatibility.ts:46`, `src/data/model-incompatible.json` |
70
- | F11 | models.dev conservative capability filtering | `src/model-compatibility.ts:60`, `src/registry/models-dev.ts:229` |
71
- | F12 | Deprecated/stale model exclusion | `src/models.ts:43` (cache `status`), `model-incompatible.json` (`stale_promotion`) |
72
- | F13 | Graceful fallback chain on API failure | `src/models.ts:140` |
73
-
74
- ---
75
-
76
- ## Architecture & Implementation
77
-
78
- ### The two-source merge
79
-
80
- The cloud (OpenCode Zen / Go) model list is built from two sources merged together in `getModels` — `src/models.ts:131`:
81
-
82
- 1. **Primary — live API.** `fetchModelsFromApi(backend)` does `GET {backend.baseUrl}/v1/models` (5s abort timeout, no real auth — sends a placeholder `Bearer test`) and returns the available model ids — `src/models.ts:69`. This is the authority on *what exists*.
83
- 2. **Enrichment — OpenCode cache.** `readModelsFromCache(backendId)` reads `~/.cache/opencode/models.json` (path `OPENCODE_CACHE_PATH`, `src/constants.ts:48`) and indexes the entries under the `opencode` (Zen) or `opencode-go` (Go) provider keys — `src/models.ts:31`. Each cache entry supplies `name`, `family`, `cost`, `provider.npm`, and `limit.context`. The cache is optional; a missing or unparseable file simply yields `null` (`loadOpencodeCache`, `src/context-window.ts:63`).
84
-
85
- `mergeModels(apiIds, cache, backendId)` — `src/models.ts:85` — drives the join:
86
-
87
- ```
88
- for each apiId:
89
- if shouldHideModel(...) → drop (incompatibility filter)
90
- if cache has apiId → spread cached ModelInfo, re-stamp sourceBackend + modelFormat
91
- else → synthesize a bare ModelInfo (name = id, brand 'Other',
92
- classifyModelFormat(id, undefined), resolveContextWindow(id))
93
- ```
94
-
95
- So the API ids are the spine; the cache only *enriches* ids that already appear in the live response. An id present only in the cache is never shown.
96
-
97
- ```mermaid
98
- flowchart TD
99
- api["GET {backendUrl}/v1/models — available ids (no auth)"]
100
- cache["~/.cache/opencode/models.json — name, family, cost, npm, limit.context"]
101
- api --> filter["shouldHideModel() — blacklist + models.dev"]
102
- filter --> merge["mergeModels()"]
103
- cache --> merge
104
- merge --> out["ModelInfo[] — modelFormat, sourceBackend, contextWindow, cost, isFree"]
105
- ```
106
-
107
- ### The classification heuristic
108
-
109
- `classifyModelFormat(modelId, providerNpm)` — `src/constants.ts:58` — is a pure function. Provider npm (from the cache) wins; if absent, an ID-prefix heuristic is applied to the lowercased id:
110
-
111
- | Order | Condition | Result | Rationale |
112
- |---|---|---|---|
113
- | 1 | `providerNpm === '@ai-sdk/anthropic'` | `anthropic` | Direct Anthropic passthrough. |
114
- | 2 | `providerNpm === '@ai-sdk/openai'` | `unsupported` | Cloud Zen/Go proxy can't do model-specific OpenAI endpoints. |
115
- | 3 | `providerNpm === '@ai-sdk/google'` | `unsupported` | Needs Gemini-specific endpoints the cloud path lacks. |
116
- | 4 | id starts with `claude-` | `anthropic` | Heuristic fallback when no cache npm. |
117
- | 5 | id starts with `gpt-` | `unsupported` | Heuristic fallback. |
118
- | 6 | id starts with `gemini-` | `unsupported` | Heuristic fallback. |
119
- | 7 | (everything else) | `openai` | Catch-all → SDK adapter via local proxy. |
120
-
121
- **Nuance:** `unsupported` is a *cloud-wizard* restriction, not a global one. The cloud OpenCode Zen/Go proxy layer can't reach GPT/Gemini directly, so those are hidden in the wizard. The same GPT/Gemini models *are* usable through the **local OpenAI / Google provider** (PRD-002), which carries the real `@ai-sdk/openai` / `@ai-sdk/google` npm and routes through the SDK adapter normally.
122
-
123
- ### The Go-backend override
124
-
125
- The Go gateway is OpenAI-compatible. If the cache labels a Go model `@ai-sdk/anthropic` (a metadata error), `mergeModels` and `readModelsFromCache` both demote `'anthropic'` → `'openai'` so it routes through the translation proxy rather than attempting a raw Anthropic passthrough that would fail — `src/models.ts:50` and `src/models.ts:95`.
126
-
127
- ### `sourceBackend` and the combined-list problem
128
-
129
- Every `ModelInfo` carries `sourceBackend: 'zen' | 'go'`, set from the backend that was queried (`src/models.ts:51`, `:96`). The `go` subscription tier shows Zen *free* models **and** Go *paid* models in one combined list; `sourceBackend` is what lets the launcher set the correct `ANTHROPIC_BASE_URL` per selected model rather than per session.
130
-
131
- ### `isFree` and brand grouping
132
-
133
- `isFree` is true when the cache cost is explicitly `input === 0 && output === 0` — `src/models.ts:44`. Bare (cache-miss) models default `isFree: false`. `deriveBrand(family)` maps a family string to a display brand via the `BRAND_MAP` prefix table (Claude, GPT, Gemini, DeepSeek, Qwen, MiniMax, Kimi, GLM, MiMo, Grok, Nemotron, else `'Other'`) — `src/models.ts:9`. `groupModels` then splits free models out and buckets the rest by brand, each sorted by id — `src/models.ts:111`.
134
-
135
- ### Context-window resolution
136
-
137
- `resolveContextWindow(modelId, explicit?)` — `src/context-window.ts:131` — resolves in priority order:
138
-
139
- 1. An explicit positive value (cache `limit.context`, or a pre-resolved number) wins.
140
- 2. Otherwise `lookupContextWindow` consults a model-id → context map built from the cache. The map prefers the `opencode` / `opencode-go` provider keys (`CACHE_PROVIDER_PRIORITY`), then falls back to the max context seen across any provider for that id — `src/context-window.ts:75`.
141
- 3. Otherwise ID-pattern heuristics (`HEURISTIC_RULES`, ordered most-specific-first) — `src/context-window.ts:30`.
142
- 4. Otherwise `DEFAULT_CONTEXT_WINDOW = 200_000` — Claude Code's own fallback for unknown models — `src/context-window.ts:12`.
143
-
144
- The resolved window is written to `CLAUDE_CODE_MAX_CONTEXT_TOKENS` (by `buildChildEnv`) and emitted in the proxy's synthetic `GET /v1/models` (`context_window` per model) so the host can render remaining context.
145
-
146
- ### The `[1m]` suffix convention
147
-
148
- Claude Code treats third-party routes as 200K unless the model id ends with `[1m]` — `src/context-model-id.ts:3`. `claudeCodeClientModelId(modelId, contextWindow?)` strips any existing suffix, resolves the real window, and re-appends `[1m]` when the window exceeds 200K so the host renders the larger window — `src/context-model-id.ts:17`. `routeLookupIds` generates the suffix/`models/`-prefix variants so inbound ids still match catalog aliases — `src/context-model-id.ts:27`.
149
-
150
- ### Reasoning-capability metadata
151
-
152
- `resolveReasoningCapabilities({ npm, modelId, ...metadata })` — `src/reasoning-capabilities.ts:24` — delegates to `getReasoningCapabilities(npm, modelId, metadata)` in `src/provider-factory.ts:469`. The result (`ReasoningCapabilities`, `src/provider-factory.ts:206`) carries effort `levels`, a `defaultLevel`, `supportsSummaries`, a `mode` (`none | internal-only | controllable`), a provenance `source`, a `confidence`, and an optional `wireFormat` discriminated union (openrouter / openai-effort / anthropic-thinking / google-thinking-config / mistral-effort / deepseek-thinking) — `src/provider-factory.ts:187`. Per-provider effort vocabularies differ (e.g. OpenAI `low/medium/high/xhigh`, xAI `none/low/medium/high`, DeepSeek `high/max/off`) — `src/provider-factory.ts:216`.
153
-
154
- ### Incompatibility filtering
155
-
156
- `shouldHideModel(ctx)` — `src/model-compatibility.ts:68` — hides a model when `hideReason` returns non-null. Two layers:
157
-
158
- 1. **Curated blacklist** (`src/data/model-incompatible.json`) keyed by `{provider, modelId, agents?}`. `provider: '*'` matches any provider; an absent/empty `agents` matches every agent. Categories include `image_generation`, `audio_only`, `video_generation`, `embedding`, `managed_agent`, `deprecated`, `gated_access`, and `stale_promotion` — `src/model-compatibility.ts:46`.
159
- 2. **models.dev conservative capabilities** — `shouldHideByModelsDevCapabilities` hides a model only when its models.dev row is explicit: non-text-only output modalities, `tool_call === false`, or `interactions === true && chat === false` — `src/registry/models-dev.ts:229`.
160
-
161
- `mergeModels` calls this with `agent: 'claude'` for the cloud path — `src/models.ts:91`. Cache entries with `status === 'deprecated'` are dropped at read time too — `src/models.ts:43`.
162
-
163
- ### Stale-free models
164
-
165
- The knowledge doc and `CLAUDE.md` reference a `STALE_FREE_MODELS` constant for models whose free promotion ended but the API still returns. In the shipped v0.2.7 tree this responsibility lives in the **incompatibility blacklist** instead: `qwen3.6-plus-free` is listed with `category: "stale_promotion"` (`src/data/model-incompatible.json`) and filtered through the same `shouldHideModel` path. There is no separate `STALE_FREE_MODELS` symbol in `src/constants.ts` in this version.
166
-
167
- ### Graceful degradation
168
-
169
- `getModels` — `src/models.ts:131` — tries the live API first. On any throw it falls back: cache (if non-empty) → caller `fallbackModels` (if any) → a thrown error instructing the user to check network / OpenCode status. The returned `fromCache` flag tells callers the list is stale.
170
-
171
- ---
172
-
173
- ## Data Shapes
174
-
175
- `ModelInfo` (cloud Zen/Go path) — `src/types.ts:22`:
176
-
177
- | Field | Type | Notes |
178
- |---|---|---|
179
- | `id` | `string` | Model id from the API. |
180
- | `name` | `string` | Display name (cache `name`, else id). |
181
- | `isFree` | `boolean` | Cache cost both zero. |
182
- | `brand` | `string` | From `deriveBrand(family)`. |
183
- | `sourceBackend` | `'zen' \| 'go'` | Backend that was queried; drives base-URL selection. |
184
- | `modelFormat` | `'anthropic' \| 'openai' \| 'unsupported'` | Launch branch point (`ModelFormat`, `src/types.ts:5`). |
185
- | `cost` | `ModelCost?` | `{ input, output, cache_read?, cache_write? }`. |
186
- | `contextWindow` | `number?` | Resolved window for the status bar. |
187
-
188
- `ModelFormat` semantics:
189
-
190
- | Value | Meaning |
191
- |---|---|
192
- | `anthropic` | Direct passthrough to the provider's Anthropic endpoint. |
193
- | `openai` | Routed through the SDK adapter via the local proxy. |
194
- | `unsupported` | Hidden in the cloud OpenCode wizard only (not a global ban). |
195
-
196
- `ReasoningCapabilities` — `src/provider-factory.ts:206`: `{ levels: string[], defaultLevel: string, supportsSummaries: boolean, mode: 'none'|'internal-only'|'controllable', source, confidence, wireFormat? }`.
197
-
198
- `IncompatibleModelEntry` — `src/model-compatibility.ts:20`: `{ provider, modelId, category, reason, agents?, sources?, verifiedAt? }`.
199
-
200
- ---
201
-
202
- ## Acceptance Criteria
203
-
204
- - [x] Cloud model list is built by merging live API ids with OpenCode cache enrichment — `src/models.ts:131`, `:85`.
205
- - [x] API ids are authoritative; cache-only ids are not shown — `src/models.ts:90`.
206
- - [x] OpenCode cache is optional — a missing/unparseable file degrades to `null` without error — `src/context-window.ts:63`.
207
- - [x] `classifyModelFormat` resolves npm first, then id-prefix heuristics, returning one of three `ModelFormat` values — `src/constants.ts:58`.
208
- - [x] Go backend demotes `anthropic` → `openai` — `src/models.ts:50`, `:95`.
209
- - [x] `sourceBackend` is stamped on every model for per-model base-URL resolution — `src/models.ts:51`, `:96`.
210
- - [x] `isFree` is true only when cache cost input and output are both 0 — `src/models.ts:44`.
211
- - [x] Context window resolves cache `limit.context` → cache index → id heuristics → 200K default — `src/context-window.ts:131`.
212
- - [x] `[1m]` suffix is appended for >200K windows so Claude Code renders the larger window — `src/context-model-id.ts:17`.
213
- - [x] Reasoning capabilities are resolved per npm + model id with a per-provider wire format — `src/reasoning-capabilities.ts:24`, `src/provider-factory.ts:469`.
214
- - [x] Incompatible models (image/audio/video/embedding/managed-agent/deprecated/gated/stale) are filtered via the curated blacklist — `src/model-compatibility.ts:46`, `src/data/model-incompatible.json`.
215
- - [x] models.dev capability checks hide non-text/no-tool/interactions-only models conservatively — `src/registry/models-dev.ts:229`.
216
- - [x] Cache entries marked `status: 'deprecated'` are dropped at read time — `src/models.ts:43`.
217
- - [x] API failure degrades to cache → fallback models → error, with a `fromCache` flag — `src/models.ts:140`.
218
-
219
- ---
220
-
221
- ## Files
222
-
223
- **Primary**
224
-
225
- - `src/models.ts` — two-source merge, `getModels`, `mergeModels`, `readModelsFromCache`, `fetchModelsFromApi`, `deriveBrand`, `groupModels`, `isFree`, `sourceBackend`, Go override.
226
- - `src/constants.ts` — `classifyModelFormat` (`:58`), `OPENCODE_CACHE_PATH` (`:48`).
227
- - `src/context-window.ts` — `resolveContextWindow`, `lookupContextWindow`, `buildContextWindowIndex`, `contextWindowFromHeuristics`, `loadOpencodeCache`, `DEFAULT_CONTEXT_WINDOW`.
228
- - `src/context-model-id.ts` — `[1m]` suffix handling, `claudeCodeClientModelId`, `routeLookupIds`.
229
- - `src/reasoning-capabilities.ts` — `resolveReasoningCapabilities`, `effortProviderOptions` (re-exports from `provider-factory`).
230
- - `src/model-compatibility.ts` — `shouldHideModel`, `hideReason`, `findBlacklistEntry`.
231
-
232
- **Supporting**
233
-
234
- - `src/data/model-incompatible.json` — curated incompatibility blacklist (image/audio/video/embedding/managed-agent/deprecated/gated/stale entries).
235
- - `src/data/models-dev-cache.json` — relayed models.dev snapshot used for conservative capability filtering and enrichment.
236
- - `src/data/pricing-cache.json` — relayed pricing snapshot.
237
- - `src/registry/models-dev.ts` — `findModelsDevModel`, `loadModelsDevCache`, `shouldHideByModelsDevCapabilities`.
238
- - `src/provider-factory.ts` — `getReasoningCapabilities`, `ReasoningCapabilities` / `ReasoningMetadata` types, effort vocabularies, wire formats.
239
- - `src/types.ts` — `ModelInfo`, `ModelFormat`, `ModelCost`.
240
-
241
- ---
242
-
243
- ## Risks & Known Limitations
244
-
245
- - **Cost display is inaccurate for non-Anthropic models.** Claude Code applies its own internal pricing table to whatever model id it sees, so the cost shown for a Groq/DeepSeek/Gemini model is wrong. Unfixable from rflectr — the host owns its pricing display.
246
- - **GPT and Gemini models are `unsupported` in the cloud wizard.** They are hidden from the OpenCode Zen/Go wizard because the cloud proxy layer can't reach those model-specific endpoints. Workaround is the local OpenAI/Google provider (PRD-002), which routes through the SDK adapter.
247
- - **Context window is fixed at launch in switch-menu mode.** Claude Code's gateway model discovery carries only id + display name (no `context_window`) and fetches `/v1/models` once at startup, so a live `/model` switch does not update the displayed window. Single-model launches show the correct window.
248
- - **OpenCode cache is enrichment, not authority.** Without it, models still appear (id-only) but lose `name`, `family`, `cost`, and accurate per-model context/format from npm — classification then relies on id-prefix heuristics, which can misclassify novel ids.
249
- - **Heuristic windows can drift.** `HEURISTIC_RULES` and the brand map are hand-maintained; a new model family with no cache entry falls to the 200K default or a coarse pattern match until the rules are updated.
250
- - **Blacklist is point-in-time.** Entries carry `verifiedAt` dates (mostly 2026-06); a model that changes capabilities upstream won't be re-evaluated until the JSON is updated.
251
- - **`STALE_FREE_MODELS` is documented but not present as a constant** in v0.2.7 — the stale-free responsibility moved into `model-incompatible.json` (`category: stale_promotion`). Docs referencing the constant are slightly ahead of/behind the code.
252
-
253
- ---
254
-
255
- ## Related
256
-
257
- - [Knowledge: Model Discovery & Classification](../../../knowledge/private/ai/model-discovery-classification.md)
258
- - [PRD-002 — Provider Registry](../prd-002-provider-registry/prd-002-provider-registry-index.md) — registry providers arrive with models pre-stamped (`modelFormat`, `npm`, `contextWindow`, `reasoning`).
259
- - [PRD-004 — Translation Layer](../prd-004-translation-layer/prd-004-translation-layer-index.md) — what happens after a model is classified `'openai'`.
260
- - [PRD-008 — Preferences, Tiers & Favorites](../prd-008-preferences-tiers-favorites/prd-008-preferences-tiers-favorites-index.md) — subscription tiers that drive which backends are queried and combined.
@@ -1,196 +0,0 @@
1
- # PRD-004: Translation Layer (Vercel AI SDK Adapter) *(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/sdk-adapter.ts`, `src/provider-factory.ts`, `src/openai-adapter.ts`, `src/gemini-parts.ts`
9
-
10
- ---
11
-
12
- ## Overview
13
-
14
- A Claude Code / Codex / Gemini host speaks one wire format (Anthropic `/v1/messages`, OpenAI Responses, or Gemini REST). The alternative model backends rflectr re-points those hosts at speak many — OpenAI Chat Completions, OpenAI Responses, Gemini `v1beta`, xAI, Mistral, DeepSeek, OpenRouter, openai-compatible endpoints, and so on. The translation layer is the single code path that bridges the host's format to whatever the selected model actually wants.
15
-
16
- The defining decision is that there is **exactly one translation path**, not N. Every non-Anthropic provider routes through the Vercel AI SDK (`ai` + `@ai-sdk/*`) — the same packages OpenCode loads — which owns wire format, endpoint selection, and provider quirks. rflectr never hand-rolls an Anthropic→OpenAI or Anthropic→Gemini translator per provider. Adding a provider once makes it usable from every host.
17
-
18
- Anthropic-native models skip the adapter entirely and are forwarded raw; `isSdkMigratedNpm(npm)` (`src/provider-factory.ts:68`) is the gate — true for any npm except `@ai-sdk/anthropic`.
19
-
20
- See the knowledge doc: [`../../../knowledge/private/ai/translation-layer.md`](../../../knowledge/private/ai/translation-layer.md).
21
-
22
- ## What Was Built
23
-
24
- - A provider factory (`createLanguageModel`) that maps OpenCode's `api.npm` package name to a Vercel AI SDK `LanguageModel` via dynamic `import(npm)` and `create*`-export discovery (`src/provider-factory.ts:102`).
25
- - A Responses-vs-Chat API selector (`modelPrefersResponsesApi`) that routes newer OpenAI/xAI reasoning models through the Responses API (`src/provider-factory.ts:34`).
26
- - The Anthropic-facing adapter: `translateRequest` (request → SDK call params), `streamAnthropicResponse` (SDK `fullStream` → Anthropic SSE), and `generateAnthropicResponse` (non-streaming) (`src/sdk-adapter.ts:222`, `:413`, `:431`).
27
- - Inline `role:'system'` folding — Claude Code's mid-conversation skills list and system-reminders are merged into the system prompt instead of being dropped (`src/sdk-adapter.ts:90`, `:232`).
28
- - The `thought_signature` round-trip — smuggled through the tool-use id so reasoning models (especially Gemini) get their signature echoed back verbatim (`src/proxy-shared.ts:93`, `:72`).
29
- - Per-provider reasoning capability + effort translation (`getReasoningCapabilities`, `effortProviderOptions`, `thinkingProviderOptions`, `deepMergeProviderOptions`) (`src/provider-factory.ts:469`, `:633`, `:741`, `:725`).
30
- - Two more host-facing directions reusing the same factory + SDK model: the OpenAI Chat Completions adapter (`src/openai-adapter.ts`), the Codex Responses adapter (`src/codex-responses-adapter.ts`), and the Gemini parts translator (`src/gemini-parts.ts`).
31
-
32
- ## Goals
33
-
34
- - One translation path for all non-Anthropic providers — eliminate the combinatorial per-provider translator mess.
35
- - Let the SDK own wire format, endpoint selection, and provider quirks (message ordering, reasoning signatures, tool-call encoding).
36
- - Preserve host behavior that the Anthropic wire format alone would lose — inline system messages and reasoning signatures.
37
- - Keep `dist/cli.js` small by loading SDK provider packages on demand from `node_modules`.
38
- - Make every host (Claude Code, Codex, Gemini, Claude Desktop gateway) converge on the same factory so a provider added once works everywhere.
39
-
40
- ## Non-Goals
41
-
42
- - Hand-rolled per-provider wire translation — explicitly rejected in favor of the SDK.
43
- - Owning the agent tool loop. The adapter is strictly **one turn per request**; Claude Code owns the loop (`src/sdk-adapter.ts:3`).
44
- - Bundling SDK provider packages into the CLI. They ship as `dependencies` / `external` and resolve at runtime (`src/provider-factory.ts:81`).
45
- - Routing Anthropic-native models — those bypass the adapter and are forwarded raw (passthrough handled by the proxy, see PRD-005).
46
- - Accurate cost reporting for non-Anthropic models (Claude Code applies its own pricing table; documented limitation).
47
-
48
- ## Features
49
-
50
- | # | Feature | Source |
51
- |---|---------|--------|
52
- | F1 | Single-path gate (`isSdkMigratedNpm`): SDK for all npm except `@ai-sdk/anthropic` | `src/provider-factory.ts:68` |
53
- | F2 | `createLanguageModel({npm,modelId,apiKey,baseURL})` factory via dynamic `import(npm)` + `create*` discovery | `src/provider-factory.ts:102`, `:72`, `:81` |
54
- | F3 | Responses-vs-Chat API selection (`modelPrefersResponsesApi`) for OpenAI/xAI | `src/provider-factory.ts:34` |
55
- | F4 | `translateRequest` — messages, tools, `tool_choice`, system | `src/sdk-adapter.ts:222` |
56
- | F5 | Inline `role:'system'` folding into the system prompt | `src/sdk-adapter.ts:90`, `:232` |
57
- | F6 | `streamAnthropicResponse` — SDK `fullStream` → Anthropic SSE | `src/sdk-adapter.ts:413`, `:273` |
58
- | F7 | `generateAnthropicResponse` — non-streaming case | `src/sdk-adapter.ts:431` |
59
- | F8 | `thought_signature` round-trip via tool-use id (encode/decode) | `src/proxy-shared.ts:93`, `:72`; `src/sdk-adapter.ts:190`, `:352` |
60
- | F9 | Per-provider reasoning effort/thinking translation | `src/provider-factory.ts:469`, `:633`, `:741` |
61
- | F10 | OpenAI Chat Completions host direction (reuses factory + SDK model) | `src/openai-adapter.ts:38` |
62
- | F11 | Gemini parts → Anthropic blocks translation | `src/gemini-parts.ts:16`, `:45` |
63
- | F12 | Codex Responses host direction (reuses factory + SDK model) | `src/codex-responses-adapter.ts:243` |
64
-
65
- ## Architecture & Implementation
66
-
67
- ### One translation path
68
-
69
- ```mermaid
70
- flowchart TD
71
- host["Host wire format<br/>(Anthropic / Responses / Gemini)"]
72
- host --> gate{"isSdkMigratedNpm(npm)?"}
73
- gate -- "no (@ai-sdk/anthropic)" --> raw["raw passthrough (PRD-005)"]
74
- gate -- "yes" --> trans["translate*Request() — host body → SdkCallParams"]
75
- trans --> factory["createLanguageModel({npm, modelId, apiKey, baseURL})"]
76
- factory --> model["Vercel AI SDK LanguageModel"]
77
- model --> run["streamText / generateText"]
78
- run --> back["map SDK fullStream → host SSE / JSON"]
79
- back --> host
80
- ```
81
-
82
- The classifier in PRD-003 supplies `npm` and `modelFormat`; the proxy in PRD-005 dispatches into this layer.
83
-
84
- ### Request translation (Anthropic → SDK)
85
-
86
- `translateRequest(body, npm, options?)` (`src/sdk-adapter.ts:222`) builds an `SdkCallParams` object:
87
-
88
- - **Messages** — `translateMessages` (`src/sdk-adapter.ts:153`) walks Anthropic blocks into SDK `ModelMessage[]`: text, images (`imagePart`, `:103`), `tool_use` → `tool-call`, `tool_result` → a `tool` role message, and `thinking` → SDK `reasoning` parts. Tool-result messages need a tool name, resolved by `annotateToolNames` (`:116`) which builds an id→name map first.
89
- - **Tools** — `translateTools` (`:204`) wraps each Anthropic tool as an SDK `tool({ description, inputSchema: jsonSchema(...) })`.
90
- - **`tool_choice`** — `translateToolChoice` (`:214`) maps `auto`→`'auto'`, `any`→`'required'`, `tool`→`{type:'tool',toolName}`.
91
- - **System folding** — `systemToString` (`:82`) flattens the top-level `system`; `inlineSystemText` (`:90`) collects mid-conversation `role:'system'` messages (Claude Code injects the skills list and system-reminders this way) and joins them into the system prompt so they are not dropped (`:232`).
92
- - **Reasoning** — effort is read from `output_config.effort` (`anthropicEffortFromRequest`, `:65`) or `options.defaultEffort` (the Claude Desktop gateway omits effort); `providerOptions` is the deep-merge of `thinkingProviderOptions(npm)` and `effortProviderOptions(...)` (`:244`).
93
- - **ChatGPT Codex OAuth branch** — when `openAiOAuth` is set, the system prompt moves into `providerOptions.openai.instructions`, `system`/`maxOutputTokens` are cleared, and a default `"You are a coding assistant."` is used if no system text exists (`:235`, `:251`, `:258`).
94
-
95
- ### Factory discovery (`createLanguageModel`)
96
-
97
- `createLanguageModel(spec)` (`src/provider-factory.ts:102`) is async and routes by `npm`:
98
-
99
- | npm | Behavior | Source |
100
- |---|---|---|
101
- | `@ai-sdk/google-vertex/anthropic` (`VERTEX_ANTHROPIC_NPM`) | Claude on Vertex AI via ADC (no apiKey) | `:105` |
102
- | `@ai-sdk/openai` | OAuth → ChatGPT Codex backend (`https://chatgpt.com/backend-api/codex`); API key → direct. `modelPrefersResponsesApi()` picks `openai.responses(id)` vs `openai.chat(id)` | `:117` |
103
- | `@ai-sdk/xai` | Direct; also consults `modelPrefersResponsesApi()` | `:134` |
104
- | `@ai-sdk/google` | Direct — ignores `baseURL`, uses native `v1beta` (passing the OpenAI-compat discovery URL would 404) | `:142` |
105
- | `@ai-sdk/anthropic` | Direct; strips a trailing `/v1` from `baseURL`, re-appends `/v1` for the SDK | `:149` |
106
- | `@ai-sdk/openai-compatible`, `@openrouter/ai-sdk-provider` | Routed via `baseURL` | `:160`, `:167` |
107
- | anything else | `loadSdkProviderFactory(npm)` → dynamic `import(npm)` → `findCreateFactory` finds the `create*()` export | `:170`, `:81`, `:72` |
108
-
109
- `findCreateFactory` (`:72`) scans the module's exports for a function whose name starts with `create`. `loadSdkProviderFactory` (`:81`) caches the promise per npm and, on `ERR_MODULE_NOT_FOUND`, raises an install hint. Reasoning models matching `/deepseek-r1|think|reasoning|qwq/` are wrapped with `extractReasoningMiddleware({ tagName: 'think' })` (`:176`). The `@ai-sdk/*` packages are npm `dependencies` marked `external` in `tsup.config.ts`, so `dist/cli.js` stays small.
110
-
111
- ### Responses-vs-Chat selection
112
-
113
- `modelPrefersResponsesApi(modelId)` (`src/provider-factory.ts:34`) returns true when a model must use `/v1/responses` rather than `/v1/chat/completions`:
114
-
115
- | Pattern | Examples | Matched by |
116
- |---|---|---|
117
- | `gpt-5.4` / `gpt-5.5` prefixes | `gpt-5.4`, `gpt-5.5-fast` | `RESPONSES_ONLY_PREFIXES` exact/`-`-prefix (`:12`, `:36`) |
118
- | `gpt-5-pro` / `gpt-5.2-pro` | `gpt-5-pro` | same |
119
- | `gpt-5-codex` and versioned codex | `gpt-5-codex`, `gpt-5.3-codex` | prefix + `gpt-*-codex` check (`:40`) |
120
- | o-series | `o3`, `o4`, `o3-mini` | prefix list (`:18`) |
121
- | xAI multi-agent | `grok-4.20-multi-agent`, `grok-4.2-multiagent` | `grok-*` + `multi-agent`/`multiagent` (`:42`) |
122
-
123
- `upstreamModelId` (from PRD-003) carries OpenCode's `api.id` because catalog ids may differ from upstream API ids (e.g. `gpt-5.5-fast` → `gpt-5.5`).
124
-
125
- ### Response translation (SDK → Anthropic SSE)
126
-
127
- `writeAnthropicStream` (`src/sdk-adapter.ts:273`) consumes the SDK `fullStream` and emits Anthropic SSE events. It tracks one open content block at a time and maps SDK parts:
128
-
129
- - `reasoning-start` / `reasoning-delta` → `thinking` block + `thinking_delta`; `reasoning-end` captures the round-trip signature emitted later as a `signature_delta` on close (`:321`, `:331`, `:302`).
130
- - `text-start` / `text-delta` → `text` block + `text_delta` (`:337`).
131
- - `tool-input-start` / `-delta` → `tool_use` block (id encoded with the thought signature) + `input_json_delta`; `tool-call` handles the non-streamed case (`:349`, `:365`).
132
- - `finish` maps `finishReason` and usage; `error` closes the open block and emits an Anthropic `error` event (`:381`, `:393`).
133
-
134
- `streamAnthropicResponse` (`:413`) wires `streamText` to `writeAnthropicStream` and swallows stream-property rejections; `generateAnthropicResponse` (`:431`) runs `generateText` and builds a single Anthropic message JSON.
135
-
136
- ### thought_signature round-trip
137
-
138
- Reasoning models (especially Gemini) require their `thought_signature` echoed back verbatim on the next turn, but the Anthropic wire format has no field for it. rflectr smuggles it through the tool-use id:
139
-
140
- - **Encode** — `encodeToolUseId(rawId, signature)` (`src/proxy-shared.ts:93`) base64url-encodes the signature and appends it after a `__ts__` separator: `{rawId}__ts__{base64url(signature)}`. When emitting blocks, the adapter calls it at `tool-input-start` / `tool-call` with the signature from `grabRoundTripSignature` (`src/sdk-adapter.ts:352`, `:371`).
141
- - **Decode** — `splitToolUseId(id)` (`src/proxy-shared.ts:72`) recovers `{ rawId, thoughtSignature }`. On the next request, `translateMessages` decodes it and, for Google, sets `providerOptions.google.thoughtSignature` on the `tool-call` part (`src/sdk-adapter.ts:190`); the SDK then handles Gemini's strict echo-back.
142
-
143
- `grabRoundTripSignature` (`src/proxy-shared.ts:22`) reads the signature from provider metadata — `google.thoughtSignature` / `thought_signature` for Gemini, `openai.reasoningEncryptedContent` for OpenAI Responses. On the Gemini host direction, `partThoughtSignature` (`src/gemini-parts.ts:8`) pulls it off the function-call part and `parseGeminiPart` re-encodes it into the tool-use id (`:30`).
144
-
145
- > **Separator note:** the live separator is `__ts__` with a base64url-encoded payload (`src/proxy-shared.ts:38`, `:93`). `::ts::` is retained only as a legacy decode fallback for sessions started before the change (`:82`). The knowledge doc describes the `::ts::` form.
146
-
147
- ### Reusing the factory for the other two host directions
148
-
149
- The same `createLanguageModel` + `streamText`/`generateText` underpins all hosts; only the host-facing translation differs:
150
-
151
- - **OpenAI Chat Completions** (`src/openai-adapter.ts`): `translateOpenAiRequest` (`:38`) builds `SdkCallParams` from an OpenAI body; `generateOpenAiResponse` (`:131`) / `streamOpenAiResponse` (`:161`) emit the OpenAI JSON / SSE shape.
152
- - **Codex Responses API** (`src/codex-responses-adapter.ts`): `translateResponsesRequest` (`:243`) / `translateResponsesInput` (`:166`) / `translateResponsesTools` (`:230`) build SDK params from a Responses body; `streamResponsesResponse` (`:575`) / `generateResponsesResponse` (`:593`) emit Responses SSE/JSON.
153
- - **Gemini REST** (`src/gemini-parts.ts`): `parseGeminiPart` (`:16`), `collectAnthropicBlocksFromGeminiParts` (`:45`), and `mapGeminiUsage` (`:76`) translate Gemini parts and usage.
154
-
155
- ## Acceptance Criteria
156
-
157
- - [x] All non-Anthropic providers route through the Vercel AI SDK; `@ai-sdk/anthropic` is the only npm that bypasses it (`isSdkMigratedNpm`, `src/provider-factory.ts:68`).
158
- - [x] `createLanguageModel` resolves an SDK `LanguageModel` from `{npm, modelId, apiKey, baseURL}` via dynamic `import(npm)` + `create*` discovery (`src/provider-factory.ts:102`, `:72`, `:81`).
159
- - [x] Special factory branches exist for Vertex Anthropic, OpenAI (OAuth Codex backend + Responses/chat), xAI, Google `v1beta`, Anthropic `/v1` normalization, openai-compatible, and OpenRouter (`src/provider-factory.ts:105`–`:174`).
160
- - [x] `modelPrefersResponsesApi` returns true for GPT-5.4+/5.5/pro, `*-codex`, the o-series, and xAI multi-agent models (`src/provider-factory.ts:34`).
161
- - [x] `translateRequest` maps messages, tools, `tool_choice`, and system into `SdkCallParams` (`src/sdk-adapter.ts:222`).
162
- - [x] Inline `role:'system'` messages are folded into the system prompt rather than dropped (`src/sdk-adapter.ts:90`, `:232`).
163
- - [x] `streamAnthropicResponse` maps the SDK `fullStream` to Anthropic SSE and `generateAnthropicResponse` handles the non-streaming case (`src/sdk-adapter.ts:413`, `:431`).
164
- - [x] `thought_signature` is encoded into the tool-use id and decoded back into `providerOptions.google.thoughtSignature` (`src/proxy-shared.ts:93`, `:72`; `src/sdk-adapter.ts:190`).
165
- - [x] Per-provider reasoning effort/thinking is translated via `getReasoningCapabilities` / `effortProviderOptions` / `thinkingProviderOptions` / `deepMergeProviderOptions` (`src/provider-factory.ts:469`, `:633`, `:741`, `:725`).
166
- - [x] The Codex Responses and OpenAI/Gemini host directions reuse the same factory + SDK model (`src/codex-responses-adapter.ts:243`, `src/openai-adapter.ts:38`, `src/gemini-parts.ts:16`).
167
- - [x] SDK provider packages load on demand and stay `external` so `dist/cli.js` remains small (`src/provider-factory.ts:81`; `tsup.config.ts`).
168
-
169
- ## Files
170
-
171
- | File | Role |
172
- |------|------|
173
- | `src/sdk-adapter.ts` | Anthropic `/v1/messages` ↔ SDK; `translateRequest`, `translateMessages`, `streamAnthropicResponse`, `generateAnthropicResponse`, inline-system folding |
174
- | `src/provider-factory.ts` | `createLanguageModel`, `isSdkMigratedNpm`, `modelPrefersResponsesApi`, reasoning capability + effort translation |
175
- | `src/openai-adapter.ts` | OpenAI Chat Completions host direction (`translateOpenAiRequest`, `generate`/`streamOpenAiResponse`) |
176
- | `src/gemini-parts.ts` | Gemini content-part → Anthropic block translation + usage mapping |
177
- | `src/codex-responses-adapter.ts` | Codex Responses API host direction (translation aspects) |
178
- | `src/proxy-shared.ts` | `encodeToolUseId` / `splitToolUseId` (thought_signature round-trip), `grabRoundTripSignature`, SSE helpers |
179
- | `tests/sdk-adapter.test.ts`, `tests/provider-factory.test.ts` | Unit coverage for the pure translation functions |
180
-
181
- ## Risks & Known Limitations
182
-
183
- - **`thought_signature` separator collision.** The signature is appended after a separator in the tool-use id. The (live) `__ts__` separator carries a base64url payload, so a collision would require the encoded payload to itself contain `__ts__` — extremely unlikely. The legacy `::ts::` form (kept as a decode fallback) would break only if a raw signature literally contained `::ts::` (`src/proxy-shared.ts:38`, `:82`).
184
- - **Gemini strict echo-back.** Gemini rejects requests that don't echo `thought_signature` verbatim. This is why the hand-rolled Gemini-native path was retired — the SDK handles the echo-back once the signature round-trips (`src/sdk-adapter.ts:190`, `src/gemini-parts.ts:8`).
185
- - **Cost display inaccuracy.** Claude Code applies its own pricing table, so non-Anthropic model cost is always wrong (documented, by design).
186
- - **One turn per request.** The adapter never loops; if a host expected the adapter to drive a tool loop it would break. Claude Code owns the loop (`src/sdk-adapter.ts:3`).
187
- - **`@ai-sdk/github-copilot` is unsupported.** OpenCode loads it from internal `@opencode-ai/core`, not a public npm factory the dynamic `import(npm)` can resolve (documented limitation).
188
- - **Provider-specific reasoning mappings are heuristic.** Effort levels are mapped per provider (e.g. xAI has no `medium`; DeepSeek `low/medium`→`high`), so a requested effort may snap to the nearest valid value (`src/provider-factory.ts:424`, `:359`).
189
-
190
- ## Related
191
-
192
- - [`../../../knowledge/private/ai/translation-layer.md`](../../../knowledge/private/ai/translation-layer.md) — the knowledge doc this PRD documents.
193
- - [`../prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md`](../prd-003-model-discovery-classification/prd-003-model-discovery-classification-index.md) — classification supplies the `npm` and `modelFormat` that drive factory routing and the `isSdkMigratedNpm` gate.
194
- - [`../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md`](../prd-005-local-proxy-catalog-routing/prd-005-local-proxy-catalog-routing-index.md) — the local proxy dispatches Anthropic-format requests into this adapter (or raw passthrough for `@ai-sdk/anthropic`).
195
- - [`../prd-009-codex-integration/prd-009-codex-integration-index.md`](../prd-009-codex-integration/prd-009-codex-integration-index.md) — Codex reuses the factory + SDK model via the Responses host direction.
196
- - [`../prd-010-gemini-cli-integration/prd-010-gemini-cli-integration-index.md`](../prd-010-gemini-cli-integration/prd-010-gemini-cli-integration-index.md) — Gemini CLI reuses the factory via the Gemini REST host direction.