5fb1db4de4
All 11 chunks are implemented; update status docs to reflect the honest state rather than the stale 'Not started' labels. - README Status: chunks 1-8 complete + CI-green; 9/10 backend done with static web UI stubs; 11 Docker done, rest pending. - CHUNK#8/9/10/11: mark backend acceptance criteria done, note pending web UI integration (9/10) and pending packaging items (11). - Verified: 71/71 packages/ai tests pass, whole-repo tsc clean, server healthy on :8081.
5.1 KiB
5.1 KiB
CHUNK #8 — AI Provider Layer
Duration: ~2 days · Prereqs: CHUNK #2 (storage, packages/core token budget) · Follows: CHUNK #7 · Unblocks: CHUNK #9 (roles + suggestions build on this)
Purpose: Build the provider abstraction that talks to any OpenAI-compatible endpoint (local Ollama/llama.cpp/LM Studio/vLLM) plus Anthropic, Gemini, and OpenRouter via the Vercel AI SDK v5 — with streaming, thinking/reasoning params, and the token-budget engine that keeps every prompt inside the context window.
Steps
1. Provider config (packages/ai/src/providers/)
- Provider type (
packages/ai/src/providers/types.ts):interface ProviderConfig { id: string; kind: 'openai-compatible' | 'anthropic' | 'google' | 'openrouter'; baseUrl?: string; // e.g. http://localhost:11434/v1, https://api.openai.com/v1 apiKey?: string; // optional for local model: string; temperature: number; maxTokens: number; thinking?: { enabled: boolean; budget?: number; effort?: 'low'|'medium'|'high' }; contextWindow: number; } - Discovery (
packages/ai/src/providers/discover.ts): auto-detect local providers by probing common endpoints (http://localhost:11434/v1/models,http://localhost:1234/v1/models). Show "detected local models" in settings. - Storage: providers persisted per-user (server) or per-machine (local) in the settings store.
2. Adapter layer (packages/ai/src/adapters/)
- OpenAI-compatible adapter (
openaiCompatible.ts): uses@ai-sdk/openaiwith a customapiBase+headers. Handles streaming (generateText/streamText) and non-streaming. - Anthropic adapter (
anthropic.ts):@ai-sdk/anthropic; mapsthinking.budget→ the SDK'sthinkingparam. - Google adapter (
google.ts):@ai-sdk/google(Gemini). - OpenRouter adapter (
openrouter.ts):@ai-sdk/openai-compatiblewithhttps://openrouter.ai/api/v1base + special headers (HTTP-Referer,X-Title). - Factory (
getProvider(config)): returns the right adapter; unknownkindthrows a clear error.
3. Thinking / reasoning (packages/ai/src/thinking.ts)
- Normalize
thinkingacross providers:- o-series (OpenAI) →
reasoningEffort. - Anthropic →
thinkingbudget object. - Local (e.g. Qwen3 via Ollama) → extra body params /
chat_template_kwargs.
- o-series (OpenAI) →
- Parse streamed reasoning tokens when present (strip from final content, expose separately for debugging).
4. Token budget (packages/ai/src/budget.ts)
- Count (
countTokens(text)): wrapgpt-tokenizer(o200k_base), lazy-loaded. Already stubbed in Chunk 2 — flesh out with real tokenizer. - Allocate (
allocateBudget({ system, context, history, output, total })): returns per-slot token caps (defaults: system+role 10%, context pack 40%, history 30%, output 20%). Adjustable per provider config. - Guard (
enforceBudget(prompt, budget)): throws or truncates if the assembled prompt exceeds the context window; logs which slot overflowed.
5. Streaming client (packages/ai/src/client.ts)
streamCompletion(provider, messages, opts)→ async iterator of{ type: 'text'|'reasoning'|'tool-call'|'done', text? }.complete(provider, messages, opts)→ full text (for non-streaming tool use).- Error handling: 429 → retry with backoff; auth errors → clear message; timeout → abort.
- Usage tracking: capture
promptTokens/completionTokensper call → stored inAISession(Chunk 9) + shown in settings.
6. Tests
- Unit:
countTokenson known strings (compare to gpt-tokenizer reference). - Unit:
allocateBudget+enforceBudgetedge cases (empty context, overflow). - Unit: thinking-param normalization for each provider.
- Integration (mock): a fake OpenAI-compatible server returns a streamed response; adapter yields correct chunks. Use
mswor a local Express stub.
Acceptance criteria (all must pass)
- Configure an OpenAI-compatible provider (local Ollama URL) → streaming works.
- Configure OpenAI/Anthropic/Gemini/OpenRouter with API keys → streaming works (mock keys in tests).
- Auto-detect a local model running on
:11434. thinkingenabled → correct provider param sent; reasoning tokens parsed when present.countTokensmatches gpt-tokenizer within tolerance.- Prompt exceeding context window → clear error, no crash.
- Streaming chunks arrive in order; usage tracked per call.
- CI green (unit + mocked integration).
Definition of done
Every provider type is reachable through one streaming client, the token budget keeps prompts in-window, and local models are auto-discovered. Chunk 9 wires roles and suggestions on top.
Risks & fallbacks
- AI SDK v5 API churn → pin exact versions; wrap all SDK calls in
packages/aiso the app never imports@vercel/aidirectly. - Local model quirks (non-standard
/v1paths, missing streaming) → tolerate missingstreamby falling back to non-streaming; document required Ollama flags (--jinja, etc.). - Tokenizer drift → gpt-tokenizer is the source of truth; re-run token tests when the model family changes.