Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/openai-compatible-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@roomote/web': minor
'@roomote/types': minor
'@roomote/worker': minor
---

Add an OpenAI-compatible inference provider option for any OpenAI API endpoint.
1 change: 1 addition & 0 deletions apps/docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"root": "models",
"expanded": true,
"pages": [
"providers/inference/openai-compatible",
"providers/inference/litellm",
"providers/inference/ollama",
"providers/inference/vllm"
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ as per-task auth tokens or workspace paths.
| `LITELLM_BASE_URL` | LiteLLM | LiteLLM endpoint URL, usually including its `/v1` path. |
| `LITELLM_API_KEY` | LiteLLM | LiteLLM gateway API key. Required when configuring LiteLLM. |
| `OLLAMA_BASE_URL` | Ollama | Ollama endpoint URL. Use the service root, for example `http://ollama:11434`. |
| `OPENAI_COMPATIBLE_BASE_URL` | OpenAI-compatible | Generic OpenAI API endpoint URL, usually including its `/v1` path. |
| `OPENAI_COMPATIBLE_API_KEY` | Optional | Bearer API key for an OpenAI-compatible endpoint that requires authentication. |
| `VLLM_BASE_URL` | vLLM | vLLM OpenAI-compatible endpoint URL, usually including its `/v1` path. |
| `VLLM_API_KEY` | Optional | Bearer API key for a vLLM endpoint that requires authentication. |

Expand Down
12 changes: 7 additions & 5 deletions apps/docs/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,16 @@ a provider never re-adds models you removed.

### Self-hosted and gateway providers

LiteLLM, Ollama, and vLLM let you bring an endpoint that you operate or host on
your own infrastructure. Their models are discovered from the configured
endpoint instead of Roomote's recommended catalog. Connect the provider in
**Settings > Models**, then select from the discovered models and choose the
defaults and role mappings that fit your deployment.
OpenAI-compatible endpoints let you bring any server that speaks the OpenAI
`/v1` API — including LiteLLM, Ollama, vLLM, and custom proxies. Models are
discovered from the configured endpoint instead of Roomote's recommended
catalog. Connect the provider in **Settings > Models**, then select from the
discovered models and choose the defaults and role mappings that fit your
deployment.

| Provider | Best for | Setup |
| --- | --- | --- |
| [OpenAI-compatible](/providers/inference/openai-compatible) | Any OpenAI API endpoint (LiteLLM, vLLM, custom proxies, local servers) | Endpoint URL and optional API key |
| [LiteLLM](/providers/inference/litellm) | A single gateway that routes to one or more model vendors | Endpoint URL and required API key |
| [Ollama](/providers/inference/ollama) | Local or private model serving without provider API keys | Endpoint URL |
| [vLLM](/providers/inference/vllm) | Serving OpenAI-compatible models on your own GPU infrastructure | Endpoint URL and optional API key |
Expand Down
67 changes: 67 additions & 0 deletions apps/docs/providers/inference/openai-compatible.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: OpenAI-compatible
icon: plug
description: Connect any OpenAI API endpoint as a Roomote inference provider.
---

OpenAI-compatible is the generic option for any server that implements the
OpenAI `/v1` API. Use it for LiteLLM proxies, vLLM, local OpenAI servers, or
other OpenAI-shaped gateways when you do not need a provider-specific setup page.

## Configure an OpenAI-compatible endpoint

In **Settings > Models**, add **OpenAI-compatible** and provide the endpoint URL.
An API key is optional and only needed when your endpoint requires bearer auth.
You can instead manage both values as deployment environment variables:

```sh
OPENAI_COMPATIBLE_BASE_URL=https://proxy.example.com/v1
OPENAI_COMPATIBLE_API_KEY=...
```

The endpoint must be reachable from the Roomote deployment. For a Compose
deployment, use a service DNS name such as `http://litellm:4000/v1` when the
proxy runs on the same private network. Do not use `localhost` unless the
endpoint runs in the same network namespace as the Roomote service that proxies
inference requests.

After saving the provider, Roomote discovers models from `/v1/models`. Enable
the models you want and select a default coding model and any specialized roles.
Model IDs use the `openai-compatible/<model-id>` form, where `<model-id>` is the
name returned by your endpoint.

## When to use this versus LiteLLM, Ollama, or vLLM

Choose **OpenAI-compatible** when you want one clear setup path for any OpenAI
API endpoint. Choose the named LiteLLM, Ollama, or vLLM providers when you
prefer those labels or their dedicated env var names. Runtime discovery and
chat completion behavior are the same OpenAI `/v1` protocol for all of them.

## Secure the endpoint

Keep the endpoint private to Roomote whenever possible. Put it on an internal
network or behind a private ingress, require an API key when the server supports
it, and use TLS when traffic crosses an untrusted network. Store
`OPENAI_COMPATIBLE_API_KEY` in your deployment secret manager or as an encrypted
Roomote deployment variable, not in an environment's task variables or
repository files.

Roomote proxies model traffic through its inference gateway, so task sandboxes
do not need direct network access to the endpoint or the API key.

## Verify setup

1. save the endpoint URL and optional API key
2. confirm models appear in **Settings > Models**
3. enable one model and make it the default coding model
4. start a small Roomote task and confirm it completes through the endpoint
5. check the upstream server logs for the request

## Common issues

- **No models appear.** Confirm the endpoint URL includes the OpenAI `/v1` API
path and that the API key can list models when auth is required.
- **Tasks cannot reach the endpoint.** Check DNS, container networking, firewall
rules, and whether the endpoint is reachable from the Roomote deployment.
- **Tool calling fails.** Roomote needs models that support tool calling. Confirm
the upstream model and gateway configuration expose tools over chat completions.
16 changes: 11 additions & 5 deletions apps/web/src/app/(onboarding)/setup/StepInferenceProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,19 @@ export function StepInferenceProvider({
: apiKey.trim() || undefined;

if (isEndpointProvider) {
const provider = selectedProvider as 'ollama' | 'vllm' | 'litellm';
const provider = selectedProvider as
| 'openai-compatible'
| 'ollama'
| 'vllm'
| 'litellm';
const endpointApiKeyEnvVarName = additionalEnvFields.find(
(field) => field.secret,
)?.envVarName;
const connection = {
baseUrl: submittedCredential,
apiKey:
additionalEnvValues[
`${selectedProvider.toUpperCase()}_API_KEY`
]?.trim() || undefined,
apiKey: endpointApiKeyEnvVarName
? additionalEnvValues[endpointApiKeyEnvVarName]?.trim() || undefined
: undefined,
};
const discovery = await discoverProviderModels.mutateAsync({
provider,
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/trpc/commands/task-models/index.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,13 @@ const LOCAL_ENDPOINT_PROVIDER_CATALOG = SETUP_MODEL_PROVIDER_CATALOG.filter(
);

export const LOCAL_TASK_MODEL_PROVIDER_IDS =
LOCAL_ENDPOINT_PROVIDER_CATALOG.map((provider) => provider.id);
LOCAL_ENDPOINT_PROVIDER_CATALOG.map((provider) => provider.id) as [
(typeof LOCAL_ENDPOINT_PROVIDER_CATALOG)[number]['id'],
...(typeof LOCAL_ENDPOINT_PROVIDER_CATALOG)[number]['id'][],
];

export type LocalTaskModelProviderId =
(typeof LOCAL_ENDPOINT_PROVIDER_CATALOG)[number]['id'];
(typeof LOCAL_TASK_MODEL_PROVIDER_IDS)[number];

export type LocalProviderConnectionInput = {
baseUrl?: string;
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/trpc/routers/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ import {
suggestTaskModelsCommand,
updateTaskModelSettingsCommand,
} from '../commands/task-models';
import { LOCAL_TASK_MODEL_PROVIDER_IDS } from '../commands/task-models/local-provider-discovery';
import {
disconnectChatGptSubscriptionCommand,
getChatGptSubscriptionStatusCommand,
Expand Down Expand Up @@ -1708,7 +1709,7 @@ export const appRouter = createRouter({
discoverProviderModels: protectedProcedure
.input(
z.object({
provider: z.enum(['ollama', 'vllm', 'litellm']),
provider: z.enum(LOCAL_TASK_MODEL_PROVIDER_IDS),
baseUrl: z.string().trim().optional(),
apiKey: z.string().trim().optional(),
}),
Expand All @@ -1720,7 +1721,7 @@ export const appRouter = createRouter({
qualifyProviderModel: protectedProcedure
.input(
z.object({
provider: z.enum(['ollama', 'vllm', 'litellm']),
provider: z.enum(LOCAL_TASK_MODEL_PROVIDER_IDS),
modelId: z.string().trim().min(1),
baseUrl: z.string().trim().optional(),
apiKey: z.string().trim().optional(),
Expand Down
55 changes: 55 additions & 0 deletions apps/worker/src/run-task/agent-home.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 32 additions & 14 deletions apps/worker/src/run-task/agent-home.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,26 +70,40 @@ export const OPENCODE_AUTH_FILE_NAME = 'auth.json';
const OPENROUTER_PROVIDER_ID = 'openrouter';

const OPENAI_COMPATIBLE_PROVIDER_CONFIGS = {
'openai-compatible': {
name: 'OpenAI-compatible',
baseUrlEnvVarName: 'OPENAI_COMPATIBLE_BASE_URL',
fallbackBaseUrl: 'http://127.0.0.1:4000/v1',
apiKeyEnvVarName: 'OPENAI_COMPATIBLE_API_KEY' as string | undefined,
keyless: false,
// Arbitrary endpoints must not inherit OPENAI_* credentials. When the
// optional dedicated key is blank, omit the API key entirely so keyless
// servers that reject bearer auth match setup qualification behavior.
allowOpenAiEnvFallback: false,
},
ollama: {
name: 'Ollama',
baseUrlEnvVarName: 'OLLAMA_BASE_URL',
fallbackBaseUrl: 'http://127.0.0.1:11434/v1',
apiKeyEnvVarName: undefined,
apiKeyEnvVarName: undefined as string | undefined,
keyless: true,
allowOpenAiEnvFallback: false,
},
vllm: {
name: 'vLLM',
baseUrlEnvVarName: 'VLLM_BASE_URL',
fallbackBaseUrl: 'http://127.0.0.1:8000/v1',
apiKeyEnvVarName: 'VLLM_API_KEY',
apiKeyEnvVarName: 'VLLM_API_KEY' as string | undefined,
keyless: false,
allowOpenAiEnvFallback: true,
},
litellm: {
name: 'LiteLLM',
baseUrlEnvVarName: 'LITELLM_BASE_URL',
fallbackBaseUrl: 'http://127.0.0.1:4000/v1',
apiKeyEnvVarName: 'LITELLM_API_KEY',
apiKeyEnvVarName: 'LITELLM_API_KEY' as string | undefined,
keyless: false,
allowOpenAiEnvFallback: true,
},
} as const;

Expand Down Expand Up @@ -706,6 +720,19 @@ function mergeOpenAiCompatibleProviderConfig(
const directApiKey = provider.apiKeyEnvVarName
? runtimeEnv[provider.apiKeyEnvVarName]?.trim()
: undefined;
const baseURL =
runtimeEnv[provider.baseUrlEnvVarName]?.trim() ||
(provider.allowOpenAiEnvFallback
? runtimeEnv.OPENAI_BASE_URL?.trim()
: '') ||
provider.fallbackBaseUrl;
const apiKeyOptions = directApiKey
? { apiKey: `{env:${provider.apiKeyEnvVarName}}` }
: provider.keyless
? { apiKey: 'ollama' }
: provider.allowOpenAiEnvFallback && runtimeEnv.OPENAI_API_KEY?.trim()
? { apiKey: '{env:OPENAI_API_KEY}' }
: {};

merged = {
...merged,
Expand All @@ -715,19 +742,10 @@ function mergeOpenAiCompatibleProviderConfig(
name: provider.name,
options: {
...existingOptions,
baseURL:
runtimeEnv[provider.baseUrlEnvVarName]?.trim() ||
(!provider.keyless ? runtimeEnv.OPENAI_BASE_URL?.trim() : '') ||
provider.fallbackBaseUrl,
baseURL,
// OpenAI-compatible clients require an API key even though Ollama
// itself accepts unauthenticated requests.
...(directApiKey
? { apiKey: `{env:${provider.apiKeyEnvVarName}}` }
: provider.keyless
? { apiKey: 'ollama' }
: runtimeEnv.OPENAI_API_KEY?.trim()
? { apiKey: '{env:OPENAI_API_KEY}' }
: {}),
...apiKeyOptions,
},
models: {
...existingModels,
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/__tests__/inference-gateway.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading