Skip to content

feat(chat): native multi-provider backend (Gemini / Anthropic / OpenAI) - #260

Merged
robdmac merged 5 commits into
mainfrom
feat/chat-multi-provider
Jul 12, 2026
Merged

robdmac merged 5 commits into
mainfrom
feat/chat-multi-provider

Conversation

@robdmac

@robdmac robdmac commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Makes Orcabot chat work with any of Gemini / Anthropic / OpenAI, and turns the
no-key state into a guided setup instead of a dead-end error. Primary motivation:
desktop chat, where no system Gemini key ships and a user must bring their own.

robdmac and others added 5 commits July 12, 2026 16:11
…one set

Orcabot chat hard-required GEMINI_ORCABOT_KEY and 500'd if it was missing —
which is always the case on desktop (no system key ships, and baking one in
would be a leaked key). The handler already had a user-GEMINI_API_KEY fallback,
but the top-of-function guard bailed before ever reaching it, so even a desktop
user who had set their own key got a dead "Something went wrong".

Backend (chat/handler.ts):
- Drop the premature GEMINI_ORCABOT_KEY guard. Resolve the key as system key OR
  the user's decrypted GEMINI_API_KEY (the existing fallback), and only fail when
  NEITHER exists — with a distinct `E79230 / CHAT_NO_KEY` (400) the client can act
  on. Cloud behavior is unchanged (system key always present).

Frontend (ChatPanel.tsx):
- On the E79230 error, replace the generic notice with a clear message plus the
  inline AiProviderSetupCard (saves GEMINI_API_KEY to _global, exactly where the
  handler reads it), so the user can add a key and send again. A dismissal flag
  hides the card after setup; it re-shows if a fresh no-key error occurs.

Net: on desktop, chat now works once the user adds their own Gemini key, and the
no-key state guides them there instead of a dead-end error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
Chat was hardwired to Gemini's wire format. Introduce a provider abstraction so
the same agentic tool loop runs against whichever provider key the user has.

New src/chat/providers/:
- types.ts — canonical, provider-neutral ChatMsg / ChatToolDef / ChatChunk +
  ChatProvider interface (streamTurn(system, messages, tools)).
- gemini.ts — wraps the existing (tested) gemini/client; system as priming pair;
  thoughtSignature round-tripped via CanonToolCall.meta; drops history calls
  lacking a signature (Gemini 3 requirement) plus their paired results.
- anthropic.ts — Messages API: system param, tool_use/tool_result blocks,
  input_json_delta streaming accumulation.
- openai.ts — Chat Completions: system message, tool_calls delta accumulation.
- select.ts — decrypts the user's stored keys and picks a provider.

handler.ts: build a canonical conversation + system prompt once, select a
provider, run the loop against provider.streamTurn (append canonical
assistant/tool turns between iterations). Tool-call IDs are carried canonically
(real for Anthropic/OpenAI, synthesized for Gemini).

Selection keeps cloud unchanged: with GEMINI_ORCABOT_KEY set, chat stays on
Gemini (user's key if present, else system) — we do NOT silently spend a user's
paid Anthropic/OpenAI key when free Gemini is available. Only on desktop (no
system key) does it route to the user's Gemini → Anthropic → OpenAI key.

Frontend: the no-key card copy now says "add a provider key (Claude, Gemini, or
OpenAI)"; AiProviderSetupCard already collects all three.

Models (cheap/fast, easily changed): gemini-3-flash, claude-haiku-4-5,
gpt-4o-mini. Both sides typecheck clean. The Anthropic/OpenAI streaming + tool
loops are NOT yet exercised against live APIs — need a real key to validate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
When chat hit the CHAT_NO_KEY error, the user's message stayed as a shown-but-
unanswered bubble; after adding a key they had to retype it. Now the setup
card's onDone re-sends the last user message. sendMessage gains a
{ skipUserEcho } option so the retry reuses the already-shown bubble instead of
duplicating it. Only fires when keys were actually saved (not on skip).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…went wrong"

An out-of-quota / bad-key / rate-limit provider error showed the same generic
"Something went wrong — please try again", which for a BYO-key desktop user is a
dead end (the OpenAI quota case looked like a bug until the logs were read).

Classify provider errors in the handler (friendlyProviderError) into short,
self-serviceable messages — "OpenAI: quota exceeded — check your plan and
billing.", "Anthropic: API key rejected — check the key.", etc. — without leaking
raw provider JSON, and render that message in ChatPanel instead of the hardcoded
text. Unknown errors still fall back to the generic notice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
…kers

- P1: OpenAI streams send BOTH a finish_reason chunk and a [DONE] sentinel, and
  flushToolCalls() yielded from `pending` without clearing it — so every tool
  call fired twice (mutating tools run twice; duplicate call ids corrupt the next
  turn). Delete each entry as it's yielded so a second flush is a no-op.
- P2: bump the required revision markers in ChatPanel.tsx (chat-v34) and
  useChat.ts (chat-v4) to match their changes, per frontend/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBxVYcEidYfRm5Gf4j7nPL
@robdmac
robdmac merged commit 9ee86c4 into main Jul 12, 2026
1 check passed
@robdmac
robdmac deleted the feat/chat-multi-provider branch July 12, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant