Skip to content

feat(providers): support OpenAI-compatible / Azure / Google endpoints#17

Open
pftom wants to merge 2 commits intomainfrom
feat/multi-model-endpoints
Open

feat(providers): support OpenAI-compatible / Azure / Google endpoints#17
pftom wants to merge 2 commits intomainfrom
feat/multi-model-endpoints

Conversation

@pftom
Copy link
Copy Markdown
Contributor

@pftom pftom commented Apr 28, 2026

Summary

Closes #16. The hosted-API BYOK fallback was hard-wired to api.anthropic.com. This PR turns that single client into a four-provider router so the same browser-side BYOK path works against Anthropic, any OpenAI-compatible endpoint (OpenRouter / LiteLLM / DeepSeek / Groq / Together / Mistral / OpenAI itself), Azure OpenAI, and Google Gemini direct.

  • Adds provider: 'anthropic' | 'openai' | 'azure' | 'google' to AppConfig (defaults to anthropic so existing localStorage configs migrate without touching anything).
  • New src/providers/model.ts router; new clients openai.ts (SSE /chat/completions, shared pump exported to azure.ts), azure.ts (deployment URL + api-version + api-key header), google.ts (:streamGenerateContent?alt=sse).
  • src/providers/presets.ts carries per-provider defaults (baseUrl, model suggestions, api-key placeholder, Azure api-version flag) so the SettingsDialog stays declarative.
  • SettingsDialog gains a provider segment-control on the Hosted-API tab and renders an Azure-only api-version field. Switching providers preserves any non-empty user values.
  • EntryView / AvatarMenu env meta line shows the active provider label.
  • en + zh-CN locales updated; README + README.zh-CN document the four providers and the recommended LiteLLM-proxy path for AWS Bedrock / GCP Vertex Anthropic models (SigV4 / GCP JWT signing belongs on the server, not the browser — same shape lobe-chat uses).

Issue

Resolves #16 — the asker wanted to point Open Design at a Microsoft OpenAI endpoint running Anthropic-style models. With this PR they pick Azure OpenAI in Settings, paste their resource endpoint as Base URL, the deployment id as Model, the key, and (optionally) an api-version. For Anthropic-on-Bedrock / Anthropic-on-Vertex they front them with a LiteLLM (or equivalent) proxy and select the Anthropic provider — proxy URL goes in Base URL.

Test plan

  • npm run build (typecheck + Vite build) — green locally.
  • Switch provider in Settings → run a chat against each:
    • Anthropic (https://api.anthropic.com, claude-sonnet-4-5, sk-ant-…)
    • OpenAI-compatible via OpenRouter (https://openrouter.ai/api/v1, anthropic/claude-3.5-sonnet)
    • Azure OpenAI (https://<resource>.openai.azure.com, deployment id as model, valid api-version)
    • Google Gemini (https://generativelanguage.googleapis.com, gemini-2.0-flash, AIza key)
  • Refresh page → confirm provider/baseUrl/model/apiVersion round-trip through localStorage.
  • Existing daemon (Local CLI) path is untouched — verify a project-level chat still streams via /api/chat.

…dpoints (closes #16)

The hosted-API BYOK fallback was hard-wired to api.anthropic.com. Issue
#16 asks for Azure (Microsoft OpenAI hosting Anthropic-style models) but
the same plumbing unlocks every common BYOK target — OpenRouter,
LiteLLM, DeepSeek, Groq, Together, Mistral, plus Google Gemini direct.

Provider model:
- New `provider: 'anthropic' | 'openai' | 'azure' | 'google'` discriminator
  on AppConfig (defaults to 'anthropic' so existing localStorage
  configs migrate seamlessly).
- src/providers/model.ts routes to one of four streaming clients:
  anthropic.ts (existing SDK path), openai.ts (new SSE pump shared with
  azure.ts), azure.ts (deployment URL + api-key header), google.ts
  (Generative Language streamGenerateContent).
- src/providers/presets.ts ships per-provider defaults (baseUrl, model
  suggestions, api-key placeholder, Azure api-version flag) so the UI
  can stay declarative.

UI:
- SettingsDialog now shows a provider picker on the Hosted-API tab and
  surfaces an Azure-only api-version field. Provider switches preserve
  any non-empty user values.
- EntryView / AvatarMenu env meta line shows the active provider label.
- en + zh-CN locales updated; README + README.zh-CN document every
  provider, with explicit guidance to reach AWS Bedrock / GCP Vertex
  Anthropic models via a server-side LiteLLM proxy (signing belongs on
  the server, not the browser).

Why an OpenAI-compatible adapter rather than a native Bedrock/Vertex
client: AWS SigV4 and GCP service-account JWTs aren't safe to do from a
browser holding long-lived BYOK credentials. LiteLLM (or any
Anthropic/OpenAI-compatible proxy) sidesteps that and is the same path
lobe-chat uses for Bedrock/Vertex.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 32f5f857d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/components/SettingsDialog.tsx Outdated
Comment thread src/components/SettingsDialog.tsx Outdated
Copy link
Copy Markdown
Contributor

@lefarcen lefarcen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: Comment — solid, well-scoped router refactor that does what #16 asked, and the structure is easy to extend (the four-case switch + presets row + per-provider file pattern is the right shape).

Big-picture observations:

  1. SSE pump robustness. The OpenAI/Azure/Google parsers split frames on \n\n only. Per the SSE spec, frames may also be delimited by \r\n\r\n, and several proxies (Cloudflare-fronted endpoints, certain LiteLLM configs) emit CRLF. Worth normalizing or splitting on /\r?\n\r?\n/. Also, [DONE] is continued rather than breaked — fine in practice, but a misbehaving server can leave the stream half-open.
  2. Tool-use coverage. Only text deltas are bridged. Anthropic-side tool_use comes through the SDK, but the OpenAI/Azure/Google clients ignore delta.tool_calls / parts[*].functionCall entirely. If skills/artifact prompts in this app rely on tool blocks today, switching providers silently degrades them. At minimum the limitation should be called out in the README + Settings hint; ideally the deltas are bridged into Anthropic-shaped events.
  3. Google ?key= exposure. The API key sits in the request URL, so it shows up in DevTools Network, browser history on redirect, the HTTP referer chain, and any logging proxy in front of Google. The Generative Language API also accepts x-goog-api-key as a header — strictly safer for a browser BYOK path.
  4. Provider-switch state shape. setProvider's heuristic preserves any non-empty baseUrl/model, but it can't tell user-typed values from the previous provider's preset, so switching Anthropic → OpenAI keeps https://api.anthropic.com. See inline.

Migration path looks correct ({ ...DEFAULT_CONFIG, ...parsed } defaults missing provider to 'anthropic'). Noted that the test-plan checkboxes are still [ ] — recommend running through Anthropic + Azure + Gemini smoke tests before merge, since the wire-format details (api-version default, :streamGenerateContent framing, [DONE] handling) are easy to regress.

Comment thread src/providers/openai.ts Outdated
Comment thread src/providers/openai.ts Outdated
Comment thread src/providers/openai.ts
Comment thread src/providers/openai.ts Outdated
Comment thread src/providers/google.ts Outdated
Comment thread src/components/SettingsDialog.tsx Outdated
Comment thread src/components/SettingsDialog.tsx Outdated
Comment thread src/providers/presets.ts
Comment thread src/state/config.ts
Comment thread README.md Outdated
- Settings: provider switch now replaces baseUrl/model when the current
  value matches the previous provider's preset, fixing "pick provider
  and go" on a fresh install (anthropic→openai/azure/google no longer
  inherits anthropic defaults).
- Settings: require a non-empty Base URL for every provider, not just
  Azure — OpenAI/Google paths would otherwise fail at runtime.
- openai/google: split SSE frames on /\r?\n\r?\n/ so CRLF-emitting
  upstreams (Cloudflare-fronted proxies, certain LiteLLM configs) work.
- openai: stop reading after `data: [DONE]` so a half-open stream from
  a misbehaving proxy can't block the client.
- openai/google: classify 401/403/404/429/5xx into one-line messages a
  BYOK user can act on, instead of dumping a JSON wall.
- google: send the API key via x-goog-api-key header instead of the
  query string (was leaking into DevTools, history, referer chain).
- google: drop systemInstruction.role — Gemini's REST surface only
  expects { parts: [...] } and some endpoints reject the role field.
- azure: pin DEFAULT_API_VERSION to the GA tag 2024-10-21 (preview
  versions rotate); log the effective version once per session.
- presets: rewrite the stale `providerLabel` comment, link LiteLLM
  Bedrock / Vertex provider docs alongside the proxy guidance.
- state/config: comment that the spread-merge in loadConfig() IS the
  migration path for fields added after a localStorage entry exists.
- README + zh-CN: document that OpenAI/Azure/Google are text-only today
  (tool calls, vision, reasoning content are not bridged) and link
  LiteLLM's Bedrock / Vertex docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lefarcen
Copy link
Copy Markdown
Contributor

👋 Thanks @pftom — turning the BYOK fallback into a four-provider router is a meaningful unlock for everyone who can't (or doesn't want to) point at api.anthropic.com directly. The provider default-to-anthropic migration via { ...DEFAULT_CONFIG, ...parsed } is a clean upgrade path. 🙏

Inline concerns are mostly stream-parser robustness:

  • 🔒 Google ?key= query-string auth leaks the key into browser network logs — x-goog-api-key header is the safer move
  • ⚠️ Both OpenAI and Google splitters use buf.indexOf('\\n\\n') — won't catch CRLF-framed events that some upstreams emit
  • 💡 No tool-use bridging across providers (text-only) — fine to scope, just worth documenting

Migration / settings UX is tight. ✅

@lefarcen lefarcen added the enhancement New feature or request label Apr 29, 2026
lefarcen
lefarcen previously approved these changes May 2, 2026
Copy link
Copy Markdown
Contributor

@lefarcen lefarcen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

@lefarcen
Copy link
Copy Markdown
Contributor

lefarcen commented May 2, 2026

PR is APPROVED ✅ but shows CONFLICTING merge state — likely needs a rebase against latest main.

If you'd like me to verify the conflicts or help coordinate the rebase, ping me. Otherwise ready to merge once conflicts are resolved.

@lefarcen lefarcen dismissed their stale review May 2, 2026 03:26

Dismissed — accidental empty approval; defer to the prior COMMENTED review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Question, can this work directly against a Microsoft OpenAI endpoint running anthropic models?

2 participants