Rename LiteLLM backend to OpenAI Compatible
All checks were successful
Build App / compute-version (push) Successful in 8s
Build App / build-macos (push) Successful in 2m25s
Build App / build-windows (push) Successful in 4m0s
Build App / build-linux (push) Successful in 4m47s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 12s

Reflects that this backend works with any OpenAI API-compatible endpoint
(LiteLLM, OpenRouter, vLLM, text-generation-inference, LocalAI, etc.),
not just LiteLLM. Includes serde aliases for backward compatibility with
existing projects.json files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 06:16:05 -07:00
parent 879322bc9a
commit d7d7a83aec
8 changed files with 100 additions and 98 deletions

View File

@@ -34,9 +34,9 @@ fn store_secrets_for_project(project: &Project) -> Result<(), String> {
secure::store_project_secret(&project.id, "aws-bearer-token", v)?;
}
}
if let Some(ref litellm) = project.litellm_config {
if let Some(ref v) = litellm.api_key {
secure::store_project_secret(&project.id, "litellm-api-key", v)?;
if let Some(ref oai_config) = project.openai_compatible_config {
if let Some(ref v) = oai_config.api_key {
secure::store_project_secret(&project.id, "openai-compatible-api-key", v)?;
}
}
Ok(())
@@ -56,8 +56,8 @@ fn load_secrets_for_project(project: &mut Project) {
bedrock.aws_bearer_token = secure::get_project_secret(&project.id, "aws-bearer-token")
.unwrap_or(None);
}
if let Some(ref mut litellm) = project.litellm_config {
litellm.api_key = secure::get_project_secret(&project.id, "litellm-api-key")
if let Some(ref mut oai_config) = project.openai_compatible_config {
oai_config.api_key = secure::get_project_secret(&project.id, "openai-compatible-api-key")
.unwrap_or(None);
}
}
@@ -197,11 +197,11 @@ pub async fn start_project_container(
}
}
if project.backend == Backend::LiteLlm {
let litellm = project.litellm_config.as_ref()
.ok_or_else(|| "LiteLLM backend selected but no LiteLLM configuration found.".to_string())?;
if litellm.base_url.is_empty() {
return Err("LiteLLM base URL is required.".to_string());
if project.backend == Backend::OpenAiCompatible {
let oai_config = project.openai_compatible_config.as_ref()
.ok_or_else(|| "OpenAI Compatible backend selected but no configuration found.".to_string())?;
if oai_config.base_url.is_empty() {
return Err("OpenAI Compatible base URL is required.".to_string());
}
}

View File

@@ -244,13 +244,13 @@ fn compute_ollama_fingerprint(project: &Project) -> String {
}
}
/// Compute a fingerprint for the LiteLLM configuration so we can detect changes.
fn compute_litellm_fingerprint(project: &Project) -> String {
if let Some(ref litellm) = project.litellm_config {
/// Compute a fingerprint for the OpenAI Compatible configuration so we can detect changes.
fn compute_openai_compatible_fingerprint(project: &Project) -> String {
if let Some(ref config) = project.openai_compatible_config {
let parts = vec![
litellm.base_url.clone(),
litellm.api_key.as_deref().unwrap_or("").to_string(),
litellm.model_id.as_deref().unwrap_or("").to_string(),
config.base_url.clone(),
config.api_key.as_deref().unwrap_or("").to_string(),
config.model_id.as_deref().unwrap_or("").to_string(),
];
sha256_hex(&parts.join("|"))
} else {
@@ -516,14 +516,14 @@ pub async fn create_container(
}
}
// LiteLLM configuration
if project.backend == Backend::LiteLlm {
if let Some(ref litellm) = project.litellm_config {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", litellm.base_url));
if let Some(ref key) = litellm.api_key {
// OpenAI Compatible configuration
if project.backend == Backend::OpenAiCompatible {
if let Some(ref config) = project.openai_compatible_config {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", config.base_url));
if let Some(ref key) = config.api_key {
env_vars.push(format!("ANTHROPIC_AUTH_TOKEN={}", key));
}
if let Some(ref model) = litellm.model_id {
if let Some(ref model) = config.model_id {
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
}
}
@@ -698,7 +698,7 @@ pub async fn create_container(
labels.insert("triple-c.paths-fingerprint".to_string(), compute_paths_fingerprint(&project.paths));
labels.insert("triple-c.bedrock-fingerprint".to_string(), compute_bedrock_fingerprint(project));
labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project));
labels.insert("triple-c.litellm-fingerprint".to_string(), compute_litellm_fingerprint(project));
labels.insert("triple-c.openai-compatible-fingerprint".to_string(), compute_openai_compatible_fingerprint(project));
labels.insert("triple-c.ports-fingerprint".to_string(), compute_ports_fingerprint(&project.port_mappings));
labels.insert("triple-c.image".to_string(), image_name.to_string());
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
@@ -948,11 +948,11 @@ pub async fn container_needs_recreation(
return Ok(true);
}
// ── LiteLLM config fingerprint ───────────────────────────────────────
let expected_litellm_fp = compute_litellm_fingerprint(project);
let container_litellm_fp = get_label("triple-c.litellm-fingerprint").unwrap_or_default();
if container_litellm_fp != expected_litellm_fp {
log::info!("LiteLLM config mismatch");
// ── OpenAI Compatible config fingerprint ────────────────────────────
let expected_oai_fp = compute_openai_compatible_fingerprint(project);
let container_oai_fp = get_label("triple-c.openai-compatible-fingerprint").unwrap_or_default();
if container_oai_fp != expected_oai_fp {
log::info!("OpenAI Compatible config mismatch");
return Ok(true);
}

View File

@@ -35,7 +35,8 @@ pub struct Project {
pub backend: Backend,
pub bedrock_config: Option<BedrockConfig>,
pub ollama_config: Option<OllamaConfig>,
pub litellm_config: Option<LiteLlmConfig>,
#[serde(alias = "litellm_config")]
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
pub allow_docker_access: bool,
#[serde(default)]
pub mission_control_enabled: bool,
@@ -70,7 +71,7 @@ pub enum ProjectStatus {
/// - `Anthropic`: Direct Anthropic API (user runs `claude login` inside the container)
/// - `Bedrock`: AWS Bedrock with per-project AWS credentials
/// - `Ollama`: Local or remote Ollama server
/// - `LiteLlm`: LiteLLM proxy gateway for 100+ model providers
/// - `OpenAiCompatible`: Any OpenAI API-compatible endpoint (e.g., LiteLLM, vLLM, etc.)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum Backend {
@@ -79,8 +80,8 @@ pub enum Backend {
Anthropic,
Bedrock,
Ollama,
#[serde(alias = "litellm")]
LiteLlm,
#[serde(alias = "lite_llm", alias = "litellm")]
OpenAiCompatible,
}
impl Default for Backend {
@@ -132,13 +133,14 @@ pub struct OllamaConfig {
pub model_id: Option<String>,
}
/// LiteLLM gateway configuration for a project.
/// LiteLLM translates Anthropic API calls to 100+ model providers.
/// OpenAI Compatible endpoint configuration for a project.
/// Routes Anthropic API calls through any OpenAI API-compatible endpoint
/// (e.g., LiteLLM, vLLM, or other compatible gateways).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LiteLlmConfig {
/// The base URL of the LiteLLM proxy (e.g., "http://host.docker.internal:4000" or "https://litellm.example.com")
pub struct OpenAiCompatibleConfig {
/// The base URL of the OpenAI-compatible endpoint (e.g., "http://host.docker.internal:4000" or "https://api.example.com")
pub base_url: String,
/// API key for the LiteLLM proxy
/// API key for the OpenAI-compatible endpoint
#[serde(skip_serializing, default)]
pub api_key: Option<String>,
/// Optional model override
@@ -157,7 +159,7 @@ impl Project {
backend: Backend::default(),
bedrock_config: None,
ollama_config: None,
litellm_config: None,
openai_compatible_config: None,
allow_docker_access: false,
mission_control_enabled: false,
ssh_key_path: None,

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react";
import { open } from "@tauri-apps/plugin-dialog";
import { listen } from "@tauri-apps/api/event";
import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, LiteLlmConfig } from "../../lib/types";
import type { Project, ProjectPath, Backend, BedrockConfig, BedrockAuthMethod, OllamaConfig, OpenAiCompatibleConfig } from "../../lib/types";
import { useProjects } from "../../hooks/useProjects";
import { useMcpServers } from "../../hooks/useMcpServers";
import { useTerminal } from "../../hooks/useTerminal";
@@ -63,10 +63,10 @@ export default function ProjectCard({ project }: Props) {
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
const [ollamaModelId, setOllamaModelId] = useState(project.ollama_config?.model_id ?? "");
// LiteLLM local state
const [litellmBaseUrl, setLitellmBaseUrl] = useState(project.litellm_config?.base_url ?? "http://host.docker.internal:4000");
const [litellmApiKey, setLitellmApiKey] = useState(project.litellm_config?.api_key ?? "");
const [litellmModelId, setLitellmModelId] = useState(project.litellm_config?.model_id ?? "");
// OpenAI Compatible local state
const [openaiCompatibleBaseUrl, setOpenaiCompatibleBaseUrl] = useState(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
const [openaiCompatibleApiKey, setOpenaiCompatibleApiKey] = useState(project.openai_compatible_config?.api_key ?? "");
const [openaiCompatibleModelId, setOpenaiCompatibleModelId] = useState(project.openai_compatible_config?.model_id ?? "");
// Sync local state when project prop changes (e.g., after save or external update)
useEffect(() => {
@@ -88,9 +88,9 @@ export default function ProjectCard({ project }: Props) {
setBedrockModelId(project.bedrock_config?.model_id ?? "");
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
setOllamaModelId(project.ollama_config?.model_id ?? "");
setLitellmBaseUrl(project.litellm_config?.base_url ?? "http://host.docker.internal:4000");
setLitellmApiKey(project.litellm_config?.api_key ?? "");
setLitellmModelId(project.litellm_config?.model_id ?? "");
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
setOpenaiCompatibleApiKey(project.openai_compatible_config?.api_key ?? "");
setOpenaiCompatibleModelId(project.openai_compatible_config?.model_id ?? "");
}, [project]);
// Listen for container progress events
@@ -197,7 +197,7 @@ export default function ProjectCard({ project }: Props) {
model_id: null,
};
const defaultLiteLlmConfig: LiteLlmConfig = {
const defaultOpenAiCompatibleConfig: OpenAiCompatibleConfig = {
base_url: "http://host.docker.internal:4000",
api_key: null,
model_id: null,
@@ -212,8 +212,8 @@ export default function ProjectCard({ project }: Props) {
if (mode === "ollama" && !project.ollama_config) {
updates.ollama_config = defaultOllamaConfig;
}
if (mode === "lite_llm" && !project.litellm_config) {
updates.litellm_config = defaultLiteLlmConfig;
if (mode === "open_ai_compatible" && !project.openai_compatible_config) {
updates.openai_compatible_config = defaultOpenAiCompatibleConfig;
}
await update({ ...project, ...updates });
} catch (e) {
@@ -355,30 +355,30 @@ export default function ProjectCard({ project }: Props) {
}
};
const handleLitellmBaseUrlBlur = async () => {
const handleOpenaiCompatibleBaseUrlBlur = async () => {
try {
const current = project.litellm_config ?? defaultLiteLlmConfig;
await update({ ...project, litellm_config: { ...current, base_url: litellmBaseUrl } });
const current = project.openai_compatible_config ?? defaultOpenAiCompatibleConfig;
await update({ ...project, openai_compatible_config: { ...current, base_url: openaiCompatibleBaseUrl } });
} catch (err) {
console.error("Failed to update LiteLLM base URL:", err);
console.error("Failed to update OpenAI Compatible base URL:", err);
}
};
const handleLitellmApiKeyBlur = async () => {
const handleOpenaiCompatibleApiKeyBlur = async () => {
try {
const current = project.litellm_config ?? defaultLiteLlmConfig;
await update({ ...project, litellm_config: { ...current, api_key: litellmApiKey || null } });
const current = project.openai_compatible_config ?? defaultOpenAiCompatibleConfig;
await update({ ...project, openai_compatible_config: { ...current, api_key: openaiCompatibleApiKey || null } });
} catch (err) {
console.error("Failed to update LiteLLM API key:", err);
console.error("Failed to update OpenAI Compatible API key:", err);
}
};
const handleLitellmModelIdBlur = async () => {
const handleOpenaiCompatibleModelIdBlur = async () => {
try {
const current = project.litellm_config ?? defaultLiteLlmConfig;
await update({ ...project, litellm_config: { ...current, model_id: litellmModelId || null } });
const current = project.openai_compatible_config ?? defaultOpenAiCompatibleConfig;
await update({ ...project, openai_compatible_config: { ...current, model_id: openaiCompatibleModelId || null } });
} catch (err) {
console.error("Failed to update LiteLLM model ID:", err);
console.error("Failed to update OpenAI Compatible model ID:", err);
}
};
@@ -449,7 +449,7 @@ export default function ProjectCard({ project }: Props) {
<div className="mt-2 ml-4 space-y-2 min-w-0 overflow-hidden">
{/* Backend selector */}
<div className="flex items-center gap-1 text-xs">
<span className="text-[var(--text-secondary)] mr-1">Backend:<Tooltip text="Choose the AI model provider for this project. Anthropic: Connect directly to Claude via OAuth login (run 'claude login' in terminal). Bedrock: Route through AWS Bedrock using your AWS credentials. Ollama: Use locally-hosted open-source models (Llama, Mistral, etc.) via an Ollama server. LiteLLM: Connect through a LiteLLM proxy gateway to access 100+ model providers (OpenAI, Azure, Gemini, etc.)." /></span>
<span className="text-[var(--text-secondary)] mr-1">Backend:<Tooltip text="Choose the AI model provider for this project. Anthropic: Connect directly to Claude via OAuth login (run 'claude login' in terminal). Bedrock: Route through AWS Bedrock using your AWS credentials. Ollama: Use locally-hosted open-source models (Llama, Mistral, etc.) via an Ollama server. OpenAI Compatible: Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.) to access 100+ model providers." /></span>
<select
value={project.backend}
onChange={(e) => { e.stopPropagation(); handleBackendChange(e.target.value as Backend); }}
@@ -460,7 +460,7 @@ export default function ProjectCard({ project }: Props) {
<option value="anthropic">Anthropic</option>
<option value="bedrock">Bedrock</option>
<option value="ollama">Ollama</option>
<option value="lite_llm">LiteLLM</option>
<option value="open_ai_compatible">OpenAI Compatible</option>
</select>
</div>
@@ -956,38 +956,38 @@ export default function ProjectCard({ project }: Props) {
);
})()}
{/* LiteLLM config */}
{project.backend === "lite_llm" && (() => {
{/* OpenAI Compatible config */}
{project.backend === "open_ai_compatible" && (() => {
const inputCls = "w-full px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-xs text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50";
return (
<div className="space-y-2 pt-1 border-t border-[var(--border-color)]">
<label className="block text-xs font-medium text-[var(--text-primary)]">LiteLLM Gateway</label>
<label className="block text-xs font-medium text-[var(--text-primary)]">OpenAI Compatible Endpoint</label>
<p className="text-xs text-[var(--text-secondary)]">
Connect through a LiteLLM proxy to use 100+ model providers.
Connect through any OpenAI API-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.).
</p>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL<Tooltip text="URL of your LiteLLM proxy server. Use host.docker.internal for a locally running proxy." /></label>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL<Tooltip text="URL of your OpenAI API-compatible server. Use host.docker.internal for a locally running service." /></label>
<input
value={litellmBaseUrl}
onChange={(e) => setLitellmBaseUrl(e.target.value)}
onBlur={handleLitellmBaseUrlBlur}
value={openaiCompatibleBaseUrl}
onChange={(e) => setOpenaiCompatibleBaseUrl(e.target.value)}
onBlur={handleOpenaiCompatibleBaseUrlBlur}
placeholder="http://host.docker.internal:4000"
disabled={!isStopped}
className={inputCls}
/>
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-70">
Use host.docker.internal for local, or a URL for remote/containerized LiteLLM.
Use host.docker.internal for local, or a URL for a remote OpenAI-compatible service.
</p>
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">API Key<Tooltip text="Authentication key for your LiteLLM proxy, if required." /></label>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">API Key<Tooltip text="Authentication key for your OpenAI-compatible endpoint, if required." /></label>
<input
type="password"
value={litellmApiKey}
onChange={(e) => setLitellmApiKey(e.target.value)}
onBlur={handleLitellmApiKeyBlur}
value={openaiCompatibleApiKey}
onChange={(e) => setOpenaiCompatibleApiKey(e.target.value)}
onBlur={handleOpenaiCompatibleApiKeyBlur}
placeholder="sk-..."
disabled={!isStopped}
className={inputCls}
@@ -995,11 +995,11 @@ export default function ProjectCard({ project }: Props) {
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)<Tooltip text="Model identifier as configured in your LiteLLM proxy (e.g. gpt-4o, gemini-pro)." /></label>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)<Tooltip text="Model identifier as configured in your provider (e.g. gpt-4o, gemini-pro)." /></label>
<input
value={litellmModelId}
onChange={(e) => setLitellmModelId(e.target.value)}
onBlur={handleLitellmModelIdBlur}
value={openaiCompatibleModelId}
onChange={(e) => setOpenaiCompatibleModelId(e.target.value)}
onBlur={handleOpenaiCompatibleModelIdBlur}
placeholder="gpt-4o / gemini-pro / etc."
disabled={!isStopped}
className={inputCls}

View File

@@ -23,7 +23,7 @@ export interface Project {
backend: Backend;
bedrock_config: BedrockConfig | null;
ollama_config: OllamaConfig | null;
litellm_config: LiteLlmConfig | null;
openai_compatible_config: OpenAiCompatibleConfig | null;
allow_docker_access: boolean;
mission_control_enabled: boolean;
ssh_key_path: string | null;
@@ -45,7 +45,7 @@ export type ProjectStatus =
| "stopping"
| "error";
export type Backend = "anthropic" | "bedrock" | "ollama" | "lite_llm";
export type Backend = "anthropic" | "bedrock" | "ollama" | "open_ai_compatible";
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
@@ -66,7 +66,7 @@ export interface OllamaConfig {
model_id: string | null;
}
export interface LiteLlmConfig {
export interface OpenAiCompatibleConfig {
base_url: string;
api_key: string | null;
model_id: string | null;