Files
lib-llm-providers-ui/README.md
jules 7ff0ccb160 Initial commit
Multi-provider LLM picker for Crema apps. Sits on top of @crema/llm-ui
and adds:

- A named provider catalog (OpenAI, Anthropic, DeepSeek, Qwen, LM Studio)
  with default base URLs and suggested models.
- buildAdapter(): async factory that resolves the API key from a
  caller-injected resolveSecret() (direct mode) or assembles an
  ArcadiaProxyAdapter pointing at /api/v1/ai/llm/chat (proxy mode).
- LLMProvidersSettingsCard: provider/model picker, vault-key reference
  field with "looks like an API key" warning, direct/proxy toggle,
  context/response budgets, system prompt, optional Test connection.
- Persisted settings store at crema.llm-providers.settings, reactive
  via useSyncExternalStore.

The lib is provider-agnostic about how keys are stored; the consuming
app injects a resolveSecret() that hits whatever vault it owns.
arcadia-admin and vibespace both wire this to /api/v1/secrets/:name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 22:50:59 +10:00

101 lines
3.8 KiB
Markdown

# lib-llm-providers-ui
Multi-provider LLM picker + settings card for Crema apps. Sits on top of `@crema/llm-ui` and adds:
- A **named provider catalog** (OpenAI, Anthropic, DeepSeek, Qwen, LM Studio) with sensible default base URLs and model lists.
- An **arcadia-proxy adapter** that routes chat completions through `POST /api/v1/ai/llm/chat`, so API keys never leave the server.
- A **direct-mode adapter** that resolves the API key from arcadia's Secrets Manager (`GET /api/v1/secrets/:name`) and calls the provider directly from the browser.
- A **settings card** (`LLMProvidersSettingsCard`) that ties it all together — provider, model, base-URL override, arcadia-secret name, transport toggle, context/response budgets, system prompt.
- A **persistent settings store** with `useSettings()` / `saveSettings()` / `resetSettings()` and a `crema:llm-providers-change` event for reactive updates across tabs.
## Quick start
```tsx
import {
LLMProvidersSettingsCard,
buildAdapter,
useSettings,
} from "@crema/llm-providers-ui"
import { LLMProvider, useChat } from "@crema/llm-ui"
import { useArcadiaClient } from "@crema/arcadia-client"
import { useEffect, useState } from "react"
function App() {
const arcadia = useArcadiaClient()
const settings = useSettings()
const [adapter, setAdapter] = useState(null)
useEffect(() => {
buildAdapter({
settings,
// Direct mode — fetch the API key from arcadia's vault.
resolveSecret: async (name) => {
const res = await arcadia.GET<{ data: { value: string } }>(`/api/v1/secrets/${name}`)
return res.data.value
},
// Or proxy mode (when the backend endpoint exists):
arcadiaBaseURL: import.meta.env.VITE_ARCADIA_URL,
arcadiaAuthToken: sessionStorage.getItem("arcadia_access_token") ?? undefined,
arcadiaTenantId: import.meta.env.VITE_ARCADIA_TENANT,
}).then(setAdapter)
}, [settings, arcadia])
if (!adapter) return null
return (
<LLMProvider adapter={adapter} model={settings.model}>
<Chat />
</LLMProvider>
)
}
```
For the settings page:
```tsx
<LLMProvidersSettingsCard
hideTransportToggle={!proxyAvailable}
onTest={async (s) => {
// Wire to a one-off completion through buildAdapter.
// Return { ok: true, message: "Connected." } or { ok: false, message: "..." }.
}}
/>
```
## Direct vs proxy
| Mode | Where the key lives at call time | When to use |
|---|---|---|
| `direct` | Briefly in the browser (fetched from arcadia per-call) | When the backend proxy isn't deployed yet, or for local development. |
| `proxy` | Server-side only (read by arcadia, never sent to the client) | Production. Requires `POST /api/v1/ai/llm/chat` on arcadia — see `LLM_PROXY_CONTRACT.md` in the consuming app. |
The settings card lets the user toggle between them. `buildAdapter()` returns the right adapter automatically.
## Provider catalog
Edit `PROVIDERS` in `src/index.tsx` to add more. Each entry is:
```ts
{
id: "deepseek",
label: "DeepSeek",
baseURL: "https://api.deepseek.com/v1",
transport: "openai-compatible", // or "anthropic"
requiresKey: true,
defaultModels: ["deepseek-chat", "deepseek-reasoner"],
hint: "OpenAI-compatible. Create a key at platform.deepseek.com.",
}
```
Adding a provider with a different shape (e.g. Google AI) means picking the closest existing transport, or extending `@crema/llm-ui` with a new adapter and adding a transport string here.
## Wiring into a Crema app
1. Clone this lib as a sibling.
2. Add `@source "../../lib-llm-providers-ui/src";` under `/* CREMA:SOURCES */` in `app/app.css`.
3. Add the path alias under `// CREMA:PATHS` in `tsconfig.json`:
```json
"@crema/llm-providers-ui": ["../lib-llm-providers-ui/src/index.tsx"],
"@crema/llm-providers-ui/*": ["../lib-llm-providers-ui/src/*"],
```
4. `crema add` does this automatically once the manifest knows about the lib.