Skip to content

Commit 784e1b0

Browse files
feat: phases 2 and 3 of openrouter migration plan. (#148)
1 parent 813f15b commit 784e1b0

19 files changed

Lines changed: 902 additions & 260 deletions

docs/openrouter-migration-plan.md

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,45 @@ Chat no longer depends on a selected repository. Local-mode users can chat to up
4343
editor tab with no GitHub connection at all. A selected repository remains useful context
4444
when one is connected, but it is never a precondition.
4545

46+
## Implementation status (updated 2026-09-06)
47+
48+
### Done
49+
50+
- Phase 1 completed: chat extracted to `src/modules/chat` and decoupled from
51+
`src/modules/github` imports.
52+
- Phase 2 completed for core runtime path:
53+
- OpenRouter chat completions endpoint is live.
54+
- OpenRouter header and error handling is implemented.
55+
- Streaming and fallback request paths are both wired.
56+
- Live verification confirmed SSE keepalive comment handling and `[DONE]` sentinel flow.
57+
- Phase 3 completed:
58+
- Chat toggle remains visible regardless of GitHub PAT state.
59+
- Chat works with no repository selected (local mode).
60+
- In-drawer OpenRouter key controls are implemented with independent storage.
61+
- PR visibility logic was split away from chat visibility behavior.
62+
- Security and UX hardening completed after initial migration:
63+
- Proposal/apply actions are intent-gated (explicit edit intent required).
64+
- Read-only prompts do not surface apply actions from markdown fallback.
65+
- Unmatched proposal targets show guidance instead of a misleading apply prompt.
66+
- OpenRouter key controls now reuse the GitHub PAT-style control pattern and trash icon.
67+
- Tests and checks completed for the implemented behaviors:
68+
- Focused Playwright coverage added for intent gating, tab-context sending, and apply behavior.
69+
- Lint checks are passing.
70+
71+
### Remaining
72+
73+
- Phase 4 model catalog work is not yet implemented in runtime code:
74+
- No live `/api/v1/models` fetch integration yet.
75+
- Free vs paid grouping in the model picker is still pending.
76+
- Tool-support filtering from live model metadata is still pending.
77+
- Phase 5 remains partial:
78+
- Chat tests still live inside `playwright/github-byot-ai.spec.ts` rather than a split chat spec path.
79+
- Dedicated OpenRouter usage docs listed below are not fully completed.
80+
- Live production verification still pending for exhaustion states:
81+
- 402 out-of-credits behavior.
82+
- 429 rate-limit behavior.
83+
- Optional one-time migration notice behavior is still pending.
84+
4685
### Correction to a common assumption
4786

4887
OpenRouter's free models are **not** keyless. Every request to the OpenRouter API requires
@@ -71,8 +110,18 @@ plan's assumptions are measured rather than inferred.
71110
| Exposed response headers | Only `content-type` and `cf-ray` |
72111
| Catalog size | 430 models, 21 free, 18 free with `tools` support |
73112

74-
Still unverified, because both require a funded key: SSE keepalive handling, and the
75-
402 / 404 / 429 error mappings. Both are Phase 2 opening tasks.
113+
Still unverified, because both require exhausting an account: the 402 (out of credits) and
114+
429 (rate limited) mappings.
115+
116+
### Verified live with a funded key
117+
118+
| Check | Result |
119+
| ---------------------------- | -------------------------------------------------------------------------- |
120+
| SSE keepalive comments | `: OPENROUTER PROCESSING` lines do appear; `parseSseDataLine` ignores them |
121+
| `data: [DONE]` sentinel | Present and handled |
122+
| Invalid model slug | Returns **400**, not 404 — `"... is not a valid model ID"` |
123+
| Tool calling on a free model | `openrouter/free` emits a real `propose_editor_update` call |
124+
| Apply + undo round trip | Proposal applies to the editor tab and the undo action appears |
76125

77126
## Current state
78127

@@ -238,15 +287,15 @@ still opens and renders.
238287

239288
| Status | Meaning | Drawer message |
240289
| ------ | ---------------------- | ---------------------------------------------------- |
290+
| 400 | Unknown model slug | Model unavailable; pick another |
241291
| 401 | Invalid or revoked key | Key rejected; re-enter or create a new one |
242292
| 402 | Out of credits | Out of credits; add credits or pick a free model |
243-
| 404 | Unknown model slug | Model unavailable; pick another |
244293
| 429 | Rate limited | Free-model daily limit reached, or too many requests |
245294

246295
The 402 and 429 cases are the ones users on free models will actually hit, so their copy
247296
should name the free-model limits explicitly and point at the free-model filter in the
248-
picker. Only the 401 mapping is verified; OpenRouter checks auth before model validity,
249-
so 402/404/429 could not be provoked with an invalid key. Confirm each during Phase 2.
297+
picker. 400 and 401 are verified live. 402 and 429 remain unverified, since provoking
298+
them means exhausting an account.
250299

251300
8. **Rate metadata.** Delete header-based rate parsing entirely rather than porting it.
252301
Verified: OpenRouter exposes only `content-type` and `cf-ray` to browser JavaScript via
@@ -412,10 +461,10 @@ those 21 advertise tool support.
412461
| Browser CORS on `/api/v1/models` | Resolved | Verified: accessible cross-origin, no key required |
413462
| Free models lack tool support | Resolved | Verified: 18 of 21 free models advertise `tools` in `supported_parameters` |
414463
| Rate-limit headers unreadable in browser | Resolved | Verified: only `content-type` and `cf-ray` exposed. Drop header parsing; use `/api/v1/key` if needed |
415-
| SSE keepalive comments break the stream reader | Open | Needs a funded key. First task of Phase 2 |
416-
| 402/404/429 mappings unconfirmed | Open | Auth is checked first, so these need a valid key to provoke. Confirm during Phase 2 |
417-
| `syncAiChatTokenVisibility` split leaves gaps | Open | Enumerated above; explicit matrix coverage in Playwright |
418-
| Repository-independent chat hits untested paths | Open | Audit list above; local-mode specs with no PAT |
464+
| SSE keepalive comments break the stream reader | Resolved | Verified live with a funded key; keepalive comments are ignored and stream completion is handled correctly |
465+
| 402/404/429 mappings unconfirmed | Partial | 400 invalid model behavior is verified; 402 out-of-credits and 429 rate-limit remain to be validated against exhausted-account conditions |
466+
| `syncAiChatTokenVisibility` split leaves gaps | Resolved | Chat visibility is decoupled from PAT gating; PR surface visibility remains PAT-scoped |
467+
| Repository-independent chat hits untested paths | Partial | Core no-repository behavior is implemented and covered by focused tests; broader cross-browser matrix coverage remains |
419468
| Pinned default free slug goes away | Open | 404 on default falls back to the picker; periodic sanity check |
420469
| 50 req/day free limit feels broken to users | Open | Explicit 429 copy naming the limit and the credits threshold |
421470
| Key in `localStorage` is XSS-exposed | Accepted | Same threat model as the existing PAT; document it, and note the OpenRouter key is scoped to inference spend only, unlike the PAT which can write repositories |

0 commit comments

Comments
 (0)