Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ We are passionate about privacy and open source, aiming to build tools that empo
We are building Chathouse to make interacting with Large Language Models simple, secure, and respectful of your privacy.

- **Privacy-first and Self-hosted**: Run it on your own server (you own the data).
- **Bring Your Own Key (BYOK)**: Connect directly to your favorite AI providers (OpenAI, Anthropic, Google) - no need to pay for subscriptions.
- **Bring Your Own Provider**: Connect directly to OpenAI, Anthropic, Google, or local Ollama/OpenWebUI endpoints.
- **File Uploads**: Attach files & media to your messages and have them processed by the AI.
- **Temporary Chats**
- **Sharable Chats**
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/content/docs/selfhosting/configuring.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ Add API keys for the AI providers you want to use. Keys are encrypted at rest us
| OpenAI | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
| Anthropic | [platform.claude.com/settings/keys](https://platform.claude.com/settings/keys) |
| Google Gemini | [aistudio.google.com/app/api-keys](https://aistudio.google.com/app/api-keys) |
| Ollama | API key optional. Use an Ollama or OpenWebUI base URL instead. |

For Ollama, the base URL must be reachable from the Chathouse server and worker. Use `http://localhost:11434/v1` when Ollama runs beside the app, `http://host.docker.internal:11434/v1` when Chathouse runs in Docker and Ollama runs on the host, `http://localhost:3000/api` for an OpenWebUI gateway on the same host, or `http://host.docker.internal:3000/api` when Chathouse runs in Docker and OpenWebUI runs on the host.

### Models (Settings > Models)

Expand Down
25 changes: 17 additions & 8 deletions apps/docs/content/docs/usage/connections.mdx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
---
title: Connecting AI providers
description: Bring your own API keys to connect OpenAI, Anthropic, and Google Gemini.
description: Bring your own API keys and local endpoints to connect OpenAI, Anthropic, Google Gemini, and Ollama.
---

Chathouse uses a Bring Your Own Key (BYOK) approach — you connect directly to AI providers using your own API keys. There's no subscription, no middleman, and no markup. You pay only for what you use at each provider's standard API rates.
Chathouse uses a bring-your-own-provider approach. You can connect cloud providers with your own API keys, or point Chathouse at a local Ollama/OpenWebUI endpoint.

## Supported providers

| Provider | Models | Get an API key |
| ------------- | ------------------------------------ | ---------------------------------------------------------------------------------- |
| **OpenAI** | GPT-4o, GPT-4, o1, o3, and more | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
| **Anthropic** | Claude Sonnet, Opus, Haiku, and more | [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) |
| **Google** | Gemini 2.5 Pro, Flash, and more | [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey) |
| Provider | Models | Get an API key |
| ------------- | ----------------------------------------- | ---------------------------------------------------------------------------------- |
| **OpenAI** | GPT-4o, GPT-4, o1, o3, and more | [platform.openai.com/api-keys](https://platform.openai.com/api-keys) |
| **Anthropic** | Claude Sonnet, Opus, Haiku, and more | [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) |
| **Google** | Gemini 2.5 Pro, Flash, and more | [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey) |
| **Ollama** | Local models and OpenWebUI gateway models | Optional, only needed if your gateway requires one |

## Adding an API key

Expand All @@ -31,12 +32,20 @@ Click the **Get API key** link on the provider card to open the provider's key m

### Paste and save

Back in Chathouse, paste your API key into the input field and click **Save**. The key format varies by provider:
Back in Chathouse, paste your API key into the input field and click **Save**. The key format varies by cloud provider:

- **OpenAI** — starts with `sk-...`
- **Anthropic** — starts with `sk-ant-...`
- **Google** — starts with `AIza...`

For Ollama, enter the base URL that the Chathouse server or worker can reach:

- **Local Ollama** — `http://localhost:11434/v1`
- **Docker host Ollama** — `http://host.docker.internal:11434/v1`
- **OpenWebUI gateway** — `http://localhost:3000/api` when Chathouse and OpenWebUI run on the same host, or `http://host.docker.internal:3000/api` when Chathouse runs in Docker and OpenWebUI runs on the host

If your OpenWebUI instance requires authentication, paste an OpenWebUI API key in the optional API key field.

</Step>
<Step>

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/docs/usage/conversations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Type your message and press **Enter** (or click the send button) to start a conv
Before sending a message, click the **model selector** at the bottom-left of the chat input. This opens a popup with two sections:

- **Favorites** — models you've starred for quick access
- **Provider tabs** — browse all available models grouped by provider (OpenAI, Anthropic, Google)
- **Provider tabs** — browse all available models grouped by provider (OpenAI, Anthropic, Google, Ollama)

<img alt="Model selector" src="/docs/img/usage/model-selector.png" />

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/content/docs/usage/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ You can also star/unstar models directly from the chat model selector without vi

Use the **search bar** at the top to filter models by name or ID.

You can also filter by provider using the tab bar (All, OpenAI, Anthropic, Google) or toggle the **Show only favorites** switch to see just your starred models.
You can also filter by provider using the tab bar (All, OpenAI, Anthropic, Google, Ollama) or toggle the **Show only favorites** switch to see just your starred models.

## Refreshing models

When a provider releases new models, click the **Refresh** button to re-fetch the latest model list from all connected providers. The button will spin while the refresh is in progress.

For Ollama, pull the model in Ollama first, then click **Refresh** in Chathouse.

## Context window

Each model displays its context window size as a badge (e.g., **128K**, **200K**). This tells you how much conversation history and content the model can process in a single request.
23 changes: 18 additions & 5 deletions apps/web/app/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { Form, Link, useFetcher, useNavigation } from 'react-router'
import type { Provider, ModelFeature, ReasoningLevel } from '~/lib/models'
import type { EnrichedModel } from '~/lib/models.server'

import { ALL_PROVIDERS } from '~/lib/providers'
import { cn, formatFileSize } from '~/lib/utils'
import { Input, Menu, MenuItem, MenuIcons, Modal, ProviderLogo, Text, PROVIDER_NAMES } from '~/ui'

Expand Down Expand Up @@ -53,7 +54,6 @@ interface FavoriteActionData {
}

type SelectorTab = 'favorites' | Provider
const ALL_PROVIDERS: Provider[] = ['openai', 'anthropic', 'google']

export function ChatInput({
models,
Expand Down Expand Up @@ -677,7 +677,9 @@ export function ChatInput({
{PROVIDER_NAMES[activeTab]}
</Text>
<Text as="p" size="sm" colour="muted">
Connect your API key to use these models
{activeTab === 'ollama'
? 'Configure an Ollama endpoint to use local or remote models'
: 'Connect your API key to use these models'}
</Text>
</div>
<Link
Expand Down Expand Up @@ -706,9 +708,20 @@ export function ChatInput({
) : (
<>
<LightningIcon className="text-surface-400 mx-auto mb-2 h-5 w-5" />
<Text as="p" size="sm" colour="muted">
All models disabled
</Text>
{activeTab === 'ollama' ? (
<>
<Text as="p" size="sm" colour="muted">
No Ollama models available
</Text>
<Text as="p" size="xs" colour="muted" className="mt-1">
Pull a model, then refresh models
</Text>
</>
) : (
<Text as="p" size="sm" colour="muted">
All models disabled
</Text>
)}
<Link
to="/settings/models"
className="text-primary-600 hover:text-primary-700 mt-1.5 inline-flex items-center gap-1 text-sm font-medium hover:underline"
Expand Down
127 changes: 127 additions & 0 deletions apps/web/app/lib/ollama.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { OLLAMA_DEFAULT_BASE_URL, OLLAMA_PRESETS } from './providers'

const OLLAMA_MODEL_FETCH_TIMEOUT_MS = 5_000

function getTrustedOllamaOrigins(): Set<string> {
const origins = new Set(OLLAMA_PRESETS.map((preset) => new URL(preset.value).origin))

for (const entry of (process.env.TRUSTED_OLLAMA_HOSTS || '').split(',')) {
const value = entry.trim()
if (!value) continue

let url: URL
try {
url = new URL(value)
} catch {
throw new Error('TRUSTED_OLLAMA_HOSTS entries must be valid http:// or https:// origins')
}

if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('TRUSTED_OLLAMA_HOSTS entries must use http:// or https://')
}

origins.add(url.origin)
}

return origins
}

function assertTrustedOllamaOrigin(url: URL): void {
if (process.env.ALLOW_UNSAFE_OLLAMA_HOSTS === 'true') return
if (getTrustedOllamaOrigins().has(url.origin)) return

throw new Error(
`Ollama host ${url.origin} is not trusted. ` +
'Add it to TRUSTED_OLLAMA_HOSTS or set ALLOW_UNSAFE_OLLAMA_HOSTS=true to allow custom hosts.',
)
}

function getOllamaBaseUrlCandidates(input: string): string[] {
const value = input.trim() || OLLAMA_DEFAULT_BASE_URL
let url: URL

try {
url = new URL(value)
} catch {
throw new Error('Enter a valid Ollama or OpenWebUI URL')
}

if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('Ollama URLs must start with http:// or https://')
}

assertTrustedOllamaOrigin(url)

url.search = ''
url.hash = ''
url.pathname = url.pathname.replace(/\/+$/, '')

const path = url.pathname
if (!path || path === '/') {
return [`${url.origin}/v1`, `${url.origin}/api`]
}

if (path.endsWith('/v1') || path.endsWith('/api')) {
return [url.toString()]
}

throw new Error('Ollama base URL must be an origin, or end with /v1 or /api')
}

function parseOpenAICompatibleModelIds(value: unknown): string[] | null {
if (!value || typeof value !== 'object' || !Array.isArray((value as { data?: unknown }).data)) {
return null
}

const ids = new Set<string>()
for (const item of (value as { data: unknown[] }).data) {
if (!item || typeof item !== 'object') continue
const id = (item as { id?: unknown }).id
if (typeof id === 'string' && id.trim()) ids.add(id.trim())
}

return [...ids]
}

export async function validateOpenAICompatibleModelEndpoint(
baseUrlInput: string,
apiKey?: string,
): Promise<{ baseUrl: string; modelIds: string[] }> {
const candidates = getOllamaBaseUrlCandidates(baseUrlInput)
const errors: string[] = []

for (const baseUrl of candidates) {
try {
const trimmedApiKey = apiKey?.trim()
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), OLLAMA_MODEL_FETCH_TIMEOUT_MS)
const response = await fetch(`${baseUrl}/models`, {
signal: controller.signal,
...(trimmedApiKey ? { headers: { Authorization: `Bearer ${trimmedApiKey}` } } : {}),
}).finally(() => clearTimeout(timeout))

if (!response.ok) {
errors.push(`${baseUrl} returned ${response.status}`)
continue
}

const modelIds = parseOpenAICompatibleModelIds(await response.json())
if (!modelIds) {
errors.push(`${baseUrl} returned an invalid model list`)
continue
}

return { baseUrl, modelIds }
} catch (reason) {
const message =
reason instanceof Error && reason.name === 'AbortError'
? 'request timed out'
: reason instanceof Error
? reason.message
: 'request failed'
errors.push(`${baseUrl}: ${message}`)
}
}

throw new Error(errors[0] || 'Could not reach the Ollama model endpoint')
}
27 changes: 27 additions & 0 deletions apps/web/app/lib/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Provider } from './models'

export const ALL_PROVIDERS = [
'openai',
'anthropic',
'google',
'ollama',
] as const satisfies readonly Provider[]

export const PROVIDER_NAMES: Record<Provider, string> = {
anthropic: 'Anthropic',
openai: 'OpenAI',
google: 'Google',
ollama: 'Ollama',
}

export const OLLAMA_DEFAULT_BASE_URL = 'http://localhost:11434/v1'

export const OLLAMA_PRESETS = [
{ label: 'Local', value: OLLAMA_DEFAULT_BASE_URL },
{ label: 'Docker host', value: 'http://host.docker.internal:11434/v1' },
{ label: 'OpenWebUI', value: 'http://localhost:3000/api' },
] as const

export function isProvider(value: unknown): value is Provider {
return typeof value === 'string' && ALL_PROVIDERS.includes(value as Provider)
}
2 changes: 1 addition & 1 deletion apps/web/app/routes/settings.about.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default function AboutSettingsPage() {
<div className="text-surface-900 mt-8 space-y-4 text-sm leading-relaxed">
<p>
Chathouse is an open-source, self-hosted AI chat application. Connect your own API keys
and use models from OpenAI, Anthropic, Google, and others from a single interface.
and use models from OpenAI, Anthropic, Google, Ollama, and others from a single interface.
</p>

<p>
Expand Down
Loading
Loading