feat(ai): add OpenAIProvider behind the registry - #384
Draft
dobrinyonkov wants to merge 6 commits into
Draft
Conversation
Implements Slice 3 of multi-provider-support. - OpenAIProvider: fetch + SSE streaming, Bearer auth, [DONE] sentinel, cross-read line buffering, cancellation via AbortSignal, API-error surfacing without leaking apiKey. checkAvailability returns 'ready' only when baseUrl, apiKey, and model are all set (no network ping). - Registered under 'openai' with displayName 'OpenAI-compatible' and a configSchema for baseUrl / apiKey / model. Gemini Nano stays the default; OpenAI is registered but not user-selectable until the settings UI lands in a later slice. - Spec covers SSE split-chunk buffering, [DONE] sentinel, 401/404/429 error surfacing, apiKey redaction, cancellation, and checkAvailability config-presence logic. Fake fetch injected at the constructor seam. - Add AbortSignal to jshintrc globals (Chrome-supported alongside the already-present AbortController).
Implements Slice 3.5 of multi-provider-support.
The panel's CSP (default-src 'self') blocks cross-origin fetch, so all
network I/O for the OpenAI-compatible provider now runs in the background
service worker. Panel-side OpenAIProvider becomes a thin port-protocol
client, symmetric to GeminiNanoProvider.
- OpenAIProvider (panel): connects lazily to a chrome.runtime port named
'openai-api', posts {type:send, config, messages}, routes chunk /
complete / error frames. Cancellation posts {type:cancel}. destroy()
posts cancel and disconnects. No fetch or SSE parsing in the panel.
- openaiHandler (background): new module, one AbortController per port.
On send, fetches ${baseUrl}/chat/completions, parses the SSE stream,
posts chunk / complete / error frames back. Redacts config.apiKey out
of any error message it echoes back over the wire. Aborts on cancel
or port disconnect.
- main.js dispatches port.name === 'openai-api' to attachOpenAIHandler
alongside the existing 'prompt-api' branch.
- OpenAIProvider.spec.js rewritten in the fake-port style used by
GeminiNanoProvider.spec.js: request-message shape, chunk/complete/error
handling, cancellation posts {type:cancel}, disconnect surfaces an
error, destroy disconnects.
- New openaiHandler.spec.js covers SSE split-chunk buffering, [DONE]
sentinel, 401/404/429 error surfacing, apiKey redaction (asserted as
a full-message substring check), fetch rejection, cancel aborts the
in-flight fetch, and port disconnect aborts the fetch.
Manifest CSP and host_permissions were already in the clean state on
this branch — no revert needed.
(cherry picked from commit 0e79d64537cf8073ea6f64ea66aef8f4cf4db5c6)
Slice 3.5 moved OpenAIProvider's HTTP I/O into the background service
worker on the theory that host_permissions alone would cover the
cross-origin fetch. That theory is wrong on current Chrome — the
extension_pages CSP applies to the service worker too, and its
default-src 'self' falls back for connect-src, blocking any endpoint.
Add an explicit connect-src using CSP scheme-source expressions:
connect-src 'self' http: https:
The scheme-source form ("http:", "https:") is what Chrome's CSP
parser accepts for allow-any-host-over-scheme. Host-source with a bare
wildcard ("http://*") parses without error but does not match any
host — a subtle CSP gotcha that cost us a debugging round.
No new attack surface: the extension already declares access to every
http(s) origin via host_permissions. Slice 4's settings UI can tighten
this to user-specified origins if that becomes worthwhile.
(cherry picked from commit 830f4ad01d547e6394e80046309cc3259135c34c)
AssistantController had three hard-coded 'Gemini Nano is ready' strings
that clobbered the banner whenever setUrl, clearConversation, or a
post-streaming-failure recovery ran. With OpenAI configured, opening
DevTools showed the correct provider banner momentarily, then setUrl
fired and the banner reverted to 'Gemini Nano is ready'.
Cache the last known ready message from the provider's own
checkAvailability, and re-emit that on every subsequent ready
transition. Also give OpenAIProvider a self-identifying ready message
('OpenAI-compatible (<model>) ready') so the banner names the active
provider instead of a generic 'Ready'.
(cherry picked from commit 2b8673871ac4cc32ecd047a930f7d8ef8a2db696)
Collapse the identical 401/404/429 error tests into one table-driven loop, and export parseSseEvent so the SSE edge cases become direct unit tests instead of full fetch+stream+port harness setups. Adds coverage for keep-alive lines, non-JSON payloads, and empty choices.
OpenAIProvider was missing getUsageInfo, which AssistantController calls unconditionally after every stream-complete. With OpenAI selected the first reply threw a synchronous TypeError that escaped the panel's promise chain. Add a stub returning null; the token pill already handles null by leaving its last state in place. destroy() also cleared _disconnectHandler without invoking it, so a clear/setUrl during an active stream left the sendMessage promise permanently pending, leaking the port and abort listener. Reject the pending promise before tearing down the port.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds a second provider —
OpenAIProvider— behind the Provider interface and registry that PR1 introduced. This is a foundation slice: the provider is registered and fully unit-tested, but Gemini Nano stays the default and there is no UI to switch to OpenAI yet. The settings modal and storage-driven provider selection follow in a later PR.Four commits:
OpenAIProviderbehind the registry. fetch + SSE streaming, Bearer auth,[DONE]sentinel, cross-read line buffering, cancellation viaAbortSignal, and API-error surfacing that never leaks the apiKey.checkAvailabilityreturnsreadyonly whenbaseUrl,apiKey, andmodelare all set (no network ping). Registered under'openai'with aconfigSchema.background/openaiHandler.jsport protocol.AssistantControllerhad three'Gemini Nano is ready'strings that clobbered the banner onsetUrl,clearConversation, and post-streaming recovery. Cache the active provider's own ready message and re-emit that instead. Invisible until a non-Gemini provider is actually selected.No user-visible change
AIChat.jsis byte-identical to PR1 —providerNamestays'gemini-nano',providerConfigstays{}. An end user sees exactly the PR1 behavior. OpenAI is reachable only by constructing the controller withproviderName: 'openai'and a config, which nothing in the shipped UI does yet.What is deliberately NOT here
setProviderhot-swap.not-configuredbanner action or token-counter/feature-detection UI work.All of that is the next slice.
Testing
npx grunt karma:CI— 614 tests pass.eslintandjshintclean.Manual (pending a live gateway): construct
AIChatwithproviderName: 'openai'+ gateway config, confirmcheckAvailabilityreports ready and a send round-trips through the service worker (fetch appears under the extension's service worker in the Network tab, not the panel). Confirm the default Gemini path is untouched.Review path
Read
OpenAIProvider.jsandbackground/openaiHandler.js(the port protocol between them).providers/index.jsis the one-line registry entry. TheAssistantController.jschange is the ready-message cache. Everything else is tests.