Compare commits

..

1 Commits

Author SHA1 Message Date
93deab68a7 Add Ollama and LiteLLM backend support (v0.2.0)
All checks were successful
Build App / build-macos (push) Successful in 2m25s
Build App / build-windows (push) Successful in 3m27s
Build App / build-linux (push) Successful in 4m31s
Build App / sync-to-github (push) Successful in 9s
Add two new auth modes for projects alongside Anthropic and Bedrock:
- Ollama: connect to local or remote Ollama servers via ANTHROPIC_BASE_URL
- LiteLLM: connect through a LiteLLM proxy gateway to 100+ model providers

Both modes inject ANTHROPIC_BASE_URL and ANTHROPIC_AUTH_TOKEN env vars into
the container, with optional model override via ANTHROPIC_MODEL. LiteLLM
API keys are stored securely in the OS keychain. Config changes trigger
automatic container recreation via fingerprinting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:05:52 -07:00
10 changed files with 337 additions and 7 deletions

View File

@@ -72,7 +72,7 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
- `container.rs` — Container lifecycle (create, start, stop, remove, inspect) - `container.rs` — Container lifecycle (create, start, stop, remove, inspect)
- `exec.rs` — PTY exec sessions with bidirectional stdin/stdout streaming - `exec.rs` — PTY exec sessions with bidirectional stdin/stdout streaming
- `image.rs` — Image build/pull with progress streaming - `image.rs` — Image build/pull with progress streaming
- **`models/`** — Serde structs (`Project`, `AuthMode`, `BedrockConfig`, `ContainerInfo`, `AppSettings`). These define the IPC contract with the frontend. - **`models/`** — Serde structs (`Project`, `AuthMode`, `BedrockConfig`, `OllamaConfig`, `LiteLlmConfig`, `ContainerInfo`, `AppSettings`). These define the IPC contract with the frontend.
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs` - **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
### Container (`container/`) ### Container (`container/`)
@@ -90,6 +90,8 @@ Containers use a **stop/start** model (not create/destroy). Installed packages p
Per-project, independently configured: Per-project, independently configured:
- **Anthropic (OAuth)** — `claude login` in terminal, token persists in config volume - **Anthropic (OAuth)** — `claude login` in terminal, token persists in config volume
- **AWS Bedrock** — Static keys, profile, or bearer token injected as env vars - **AWS Bedrock** — Static keys, profile, or bearer token injected as env vars
- **Ollama** — Connect to a local or remote Ollama server via `ANTHROPIC_BASE_URL` (e.g., `http://host.docker.internal:11434`)
- **LiteLLM** — Connect through a LiteLLM proxy gateway via `ANTHROPIC_BASE_URL` + `ANTHROPIC_AUTH_TOKEN` to access 100+ model providers
## Styling ## Styling

View File

@@ -1,7 +1,7 @@
{ {
"name": "triple-c", "name": "triple-c",
"private": true, "private": true,
"version": "0.1.0", "version": "0.2.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -4668,7 +4668,7 @@ dependencies = [
[[package]] [[package]]
name = "triple-c" name = "triple-c"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"bollard", "bollard",
"chrono", "chrono",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "triple-c" name = "triple-c"
version = "0.1.0" version = "0.2.0"
edition = "2021" edition = "2021"
[lib] [lib]

View File

@@ -34,6 +34,11 @@ fn store_secrets_for_project(project: &Project) -> Result<(), String> {
secure::store_project_secret(&project.id, "aws-bearer-token", v)?; 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)?;
}
}
Ok(()) Ok(())
} }
@@ -51,6 +56,10 @@ fn load_secrets_for_project(project: &mut Project) {
bedrock.aws_bearer_token = secure::get_project_secret(&project.id, "aws-bearer-token") bedrock.aws_bearer_token = secure::get_project_secret(&project.id, "aws-bearer-token")
.unwrap_or(None); .unwrap_or(None);
} }
if let Some(ref mut litellm) = project.litellm_config {
litellm.api_key = secure::get_project_secret(&project.id, "litellm-api-key")
.unwrap_or(None);
}
} }
/// Resolve enabled MCP servers and filter to Docker-only ones. /// Resolve enabled MCP servers and filter to Docker-only ones.
@@ -180,6 +189,22 @@ pub async fn start_project_container(
} }
} }
if project.auth_mode == AuthMode::Ollama {
let ollama = project.ollama_config.as_ref()
.ok_or_else(|| "Ollama auth mode selected but no Ollama configuration found.".to_string())?;
if ollama.base_url.is_empty() {
return Err("Ollama base URL is required.".to_string());
}
}
if project.auth_mode == AuthMode::LiteLlm {
let litellm = project.litellm_config.as_ref()
.ok_or_else(|| "LiteLLM auth mode selected but no LiteLLM configuration found.".to_string())?;
if litellm.base_url.is_empty() {
return Err("LiteLLM base URL is required.".to_string());
}
}
// Update status to starting // Update status to starting
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?; state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;

View File

@@ -231,6 +231,33 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
} }
} }
/// Compute a fingerprint for the Ollama configuration so we can detect changes.
fn compute_ollama_fingerprint(project: &Project) -> String {
if let Some(ref ollama) = project.ollama_config {
let parts = vec![
ollama.base_url.clone(),
ollama.model_id.as_deref().unwrap_or("").to_string(),
];
sha256_hex(&parts.join("|"))
} else {
String::new()
}
}
/// 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 {
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(),
];
sha256_hex(&parts.join("|"))
} else {
String::new()
}
}
/// Compute a fingerprint for the project paths so we can detect changes. /// Compute a fingerprint for the project paths so we can detect changes.
/// Sorted by mount_name so order changes don't cause spurious recreation. /// Sorted by mount_name so order changes don't cause spurious recreation.
fn compute_paths_fingerprint(paths: &[ProjectPath]) -> String { fn compute_paths_fingerprint(paths: &[ProjectPath]) -> String {
@@ -478,6 +505,30 @@ pub async fn create_container(
} }
} }
// Ollama configuration
if project.auth_mode == AuthMode::Ollama {
if let Some(ref ollama) = project.ollama_config {
env_vars.push(format!("ANTHROPIC_BASE_URL={}", ollama.base_url));
env_vars.push("ANTHROPIC_AUTH_TOKEN=ollama".to_string());
if let Some(ref model) = ollama.model_id {
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
}
}
}
// LiteLLM configuration
if project.auth_mode == AuthMode::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 {
env_vars.push(format!("ANTHROPIC_AUTH_TOKEN={}", key));
}
if let Some(ref model) = litellm.model_id {
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
}
}
}
// Custom environment variables (global + per-project, project overrides global for same key) // Custom environment variables (global + per-project, project overrides global for same key)
let merged_env = merge_custom_env_vars(global_custom_env_vars, &project.custom_env_vars); let merged_env = merge_custom_env_vars(global_custom_env_vars, &project.custom_env_vars);
let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "CLAUDE_", "TRIPLE_C_"]; let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "CLAUDE_", "TRIPLE_C_"];
@@ -646,6 +697,8 @@ pub async fn create_container(
labels.insert("triple-c.auth-mode".to_string(), format!("{:?}", project.auth_mode)); labels.insert("triple-c.auth-mode".to_string(), format!("{:?}", project.auth_mode));
labels.insert("triple-c.paths-fingerprint".to_string(), compute_paths_fingerprint(&project.paths)); 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.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.ports-fingerprint".to_string(), compute_ports_fingerprint(&project.port_mappings)); 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.image".to_string(), image_name.to_string());
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string()); labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
@@ -885,6 +938,22 @@ pub async fn container_needs_recreation(
return Ok(true); return Ok(true);
} }
// ── Ollama config fingerprint ────────────────────────────────────────
let expected_ollama_fp = compute_ollama_fingerprint(project);
let container_ollama_fp = get_label("triple-c.ollama-fingerprint").unwrap_or_default();
if container_ollama_fp != expected_ollama_fp {
log::info!("Ollama config mismatch");
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");
return Ok(true);
}
// ── Image ──────────────────────────────────────────────────────────── // ── Image ────────────────────────────────────────────────────────────
// The image label is set at creation time; if the user changed the // The image label is set at creation time; if the user changed the
// configured image we need to recreate. We only compare when the // configured image we need to recreate. We only compare when the

View File

@@ -33,6 +33,8 @@ pub struct Project {
pub status: ProjectStatus, pub status: ProjectStatus,
pub auth_mode: AuthMode, pub auth_mode: AuthMode,
pub bedrock_config: Option<BedrockConfig>, pub bedrock_config: Option<BedrockConfig>,
pub ollama_config: Option<OllamaConfig>,
pub litellm_config: Option<LiteLlmConfig>,
pub allow_docker_access: bool, pub allow_docker_access: bool,
#[serde(default)] #[serde(default)]
pub mission_control_enabled: bool, pub mission_control_enabled: bool,
@@ -74,6 +76,9 @@ pub enum AuthMode {
#[serde(alias = "login", alias = "api_key")] #[serde(alias = "login", alias = "api_key")]
Anthropic, Anthropic,
Bedrock, Bedrock,
Ollama,
#[serde(alias = "litellm")]
LiteLlm,
} }
impl Default for AuthMode { impl Default for AuthMode {
@@ -115,6 +120,29 @@ pub struct BedrockConfig {
pub disable_prompt_caching: bool, pub disable_prompt_caching: bool,
} }
/// Ollama configuration for a project.
/// Ollama exposes an Anthropic-compatible API endpoint.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OllamaConfig {
/// The base URL of the Ollama server (e.g., "http://host.docker.internal:11434" or "http://192.168.1.100:11434")
pub base_url: String,
/// Optional model override (e.g., "qwen3.5:27b")
pub model_id: Option<String>,
}
/// LiteLLM gateway configuration for a project.
/// LiteLLM translates Anthropic API calls to 100+ model providers.
#[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 base_url: String,
/// API key for the LiteLLM proxy
#[serde(skip_serializing, default)]
pub api_key: Option<String>,
/// Optional model override
pub model_id: Option<String>,
}
impl Project { impl Project {
pub fn new(name: String, paths: Vec<ProjectPath>) -> Self { pub fn new(name: String, paths: Vec<ProjectPath>) -> Self {
let now = chrono::Utc::now().to_rfc3339(); let now = chrono::Utc::now().to_rfc3339();
@@ -126,6 +154,8 @@ impl Project {
status: ProjectStatus::Stopped, status: ProjectStatus::Stopped,
auth_mode: AuthMode::default(), auth_mode: AuthMode::default(),
bedrock_config: None, bedrock_config: None,
ollama_config: None,
litellm_config: None,
allow_docker_access: false, allow_docker_access: false,
mission_control_enabled: false, mission_control_enabled: false,
ssh_key_path: None, ssh_key_path: None,

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json", "$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
"productName": "Triple-C", "productName": "Triple-C",
"version": "0.1.0", "version": "0.2.0",
"identifier": "com.triple-c.desktop", "identifier": "com.triple-c.desktop",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { open } from "@tauri-apps/plugin-dialog"; import { open } from "@tauri-apps/plugin-dialog";
import { listen } from "@tauri-apps/api/event"; import { listen } from "@tauri-apps/api/event";
import type { Project, ProjectPath, AuthMode, BedrockConfig, BedrockAuthMethod } from "../../lib/types"; import type { Project, ProjectPath, AuthMode, BedrockConfig, BedrockAuthMethod, OllamaConfig, LiteLlmConfig } from "../../lib/types";
import { useProjects } from "../../hooks/useProjects"; import { useProjects } from "../../hooks/useProjects";
import { useMcpServers } from "../../hooks/useMcpServers"; import { useMcpServers } from "../../hooks/useMcpServers";
import { useTerminal } from "../../hooks/useTerminal"; import { useTerminal } from "../../hooks/useTerminal";
@@ -58,6 +58,15 @@ export default function ProjectCard({ project }: Props) {
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? ""); const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? ""); const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
// Ollama local state
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 ?? "");
// Sync local state when project prop changes (e.g., after save or external update) // Sync local state when project prop changes (e.g., after save or external update)
useEffect(() => { useEffect(() => {
setEditName(project.name); setEditName(project.name);
@@ -76,6 +85,11 @@ export default function ProjectCard({ project }: Props) {
setBedrockProfile(project.bedrock_config?.aws_profile ?? ""); setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? ""); setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
setBedrockModelId(project.bedrock_config?.model_id ?? ""); 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 ?? "");
}, [project]); }, [project]);
// Listen for container progress events // Listen for container progress events
@@ -177,12 +191,29 @@ export default function ProjectCard({ project }: Props) {
disable_prompt_caching: false, disable_prompt_caching: false,
}; };
const defaultOllamaConfig: OllamaConfig = {
base_url: "http://host.docker.internal:11434",
model_id: null,
};
const defaultLiteLlmConfig: LiteLlmConfig = {
base_url: "http://host.docker.internal:4000",
api_key: null,
model_id: null,
};
const handleAuthModeChange = async (mode: AuthMode) => { const handleAuthModeChange = async (mode: AuthMode) => {
try { try {
const updates: Partial<Project> = { auth_mode: mode }; const updates: Partial<Project> = { auth_mode: mode };
if (mode === "bedrock" && !project.bedrock_config) { if (mode === "bedrock" && !project.bedrock_config) {
updates.bedrock_config = defaultBedrockConfig; updates.bedrock_config = defaultBedrockConfig;
} }
if (mode === "ollama" && !project.ollama_config) {
updates.ollama_config = defaultOllamaConfig;
}
if (mode === "lit_llm" && !project.litellm_config) {
updates.litellm_config = defaultLiteLlmConfig;
}
await update({ ...project, ...updates }); await update({ ...project, ...updates });
} catch (e) { } catch (e) {
setError(String(e)); setError(String(e));
@@ -305,6 +336,51 @@ export default function ProjectCard({ project }: Props) {
} }
}; };
const handleOllamaBaseUrlBlur = async () => {
try {
const current = project.ollama_config ?? defaultOllamaConfig;
await update({ ...project, ollama_config: { ...current, base_url: ollamaBaseUrl } });
} catch (err) {
console.error("Failed to update Ollama base URL:", err);
}
};
const handleOllamaModelIdBlur = async () => {
try {
const current = project.ollama_config ?? defaultOllamaConfig;
await update({ ...project, ollama_config: { ...current, model_id: ollamaModelId || null } });
} catch (err) {
console.error("Failed to update Ollama model ID:", err);
}
};
const handleLitellmBaseUrlBlur = async () => {
try {
const current = project.litellm_config ?? defaultLiteLlmConfig;
await update({ ...project, litellm_config: { ...current, base_url: litellmBaseUrl } });
} catch (err) {
console.error("Failed to update LiteLLM base URL:", err);
}
};
const handleLitellmApiKeyBlur = async () => {
try {
const current = project.litellm_config ?? defaultLiteLlmConfig;
await update({ ...project, litellm_config: { ...current, api_key: litellmApiKey || null } });
} catch (err) {
console.error("Failed to update LiteLLM API key:", err);
}
};
const handleLitellmModelIdBlur = async () => {
try {
const current = project.litellm_config ?? defaultLiteLlmConfig;
await update({ ...project, litellm_config: { ...current, model_id: litellmModelId || null } });
} catch (err) {
console.error("Failed to update LiteLLM model ID:", err);
}
};
const statusColor = { const statusColor = {
stopped: "bg-[var(--text-secondary)]", stopped: "bg-[var(--text-secondary)]",
starting: "bg-[var(--warning)]", starting: "bg-[var(--warning)]",
@@ -395,6 +471,28 @@ export default function ProjectCard({ project }: Props) {
> >
Bedrock Bedrock
</button> </button>
<button
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("ollama"); }}
disabled={!isStopped}
className={`px-2 py-0.5 rounded transition-colors ${
project.auth_mode === "ollama"
? "bg-[var(--accent)] text-white"
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
} disabled:opacity-50`}
>
Ollama
</button>
<button
onClick={(e) => { e.stopPropagation(); handleAuthModeChange("lit_llm"); }}
disabled={!isStopped}
className={`px-2 py-0.5 rounded transition-colors ${
project.auth_mode === "lit_llm"
? "bg-[var(--accent)] text-white"
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]"
} disabled:opacity-50`}
>
LiteLLM
</button>
</div> </div>
{/* Action buttons */} {/* Action buttons */}
@@ -851,6 +949,99 @@ export default function ProjectCard({ project }: Props) {
</div> </div>
); );
})()} })()}
{/* Ollama config */}
{project.auth_mode === "ollama" && (() => {
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)]">Ollama</label>
<p className="text-xs text-[var(--text-secondary)]">
Connect to an Ollama server running locally or on a remote host.
</p>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL</label>
<input
value={ollamaBaseUrl}
onChange={(e) => setOllamaBaseUrl(e.target.value)}
onBlur={handleOllamaBaseUrlBlur}
placeholder="http://host.docker.internal:11434"
disabled={!isStopped}
className={inputCls}
/>
<p className="text-xs text-[var(--text-secondary)] mt-0.5 opacity-70">
Use host.docker.internal for the host machine, or an IP/hostname for remote.
</p>
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)</label>
<input
value={ollamaModelId}
onChange={(e) => setOllamaModelId(e.target.value)}
onBlur={handleOllamaModelIdBlur}
placeholder="qwen3.5:27b"
disabled={!isStopped}
className={inputCls}
/>
</div>
</div>
);
})()}
{/* LiteLLM config */}
{project.auth_mode === "lit_llm" && (() => {
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>
<p className="text-xs text-[var(--text-secondary)]">
Connect through a LiteLLM proxy to use 100+ model providers.
</p>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Base URL</label>
<input
value={litellmBaseUrl}
onChange={(e) => setLitellmBaseUrl(e.target.value)}
onBlur={handleLitellmBaseUrlBlur}
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.
</p>
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">API Key</label>
<input
type="password"
value={litellmApiKey}
onChange={(e) => setLitellmApiKey(e.target.value)}
onBlur={handleLitellmApiKeyBlur}
placeholder="sk-..."
disabled={!isStopped}
className={inputCls}
/>
</div>
<div>
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Model (optional)</label>
<input
value={litellmModelId}
onChange={(e) => setLitellmModelId(e.target.value)}
onBlur={handleLitellmModelIdBlur}
placeholder="gpt-4o / gemini-pro / etc."
disabled={!isStopped}
className={inputCls}
/>
</div>
</div>
);
})()}
</div> </div>
)} )}
</div> </div>

View File

@@ -22,6 +22,8 @@ export interface Project {
status: ProjectStatus; status: ProjectStatus;
auth_mode: AuthMode; auth_mode: AuthMode;
bedrock_config: BedrockConfig | null; bedrock_config: BedrockConfig | null;
ollama_config: OllamaConfig | null;
litellm_config: LiteLlmConfig | null;
allow_docker_access: boolean; allow_docker_access: boolean;
mission_control_enabled: boolean; mission_control_enabled: boolean;
ssh_key_path: string | null; ssh_key_path: string | null;
@@ -43,7 +45,7 @@ export type ProjectStatus =
| "stopping" | "stopping"
| "error"; | "error";
export type AuthMode = "anthropic" | "bedrock"; export type AuthMode = "anthropic" | "bedrock" | "ollama" | "lit_llm";
export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token"; export type BedrockAuthMethod = "static_credentials" | "profile" | "bearer_token";
@@ -59,6 +61,17 @@ export interface BedrockConfig {
disable_prompt_caching: boolean; disable_prompt_caching: boolean;
} }
export interface OllamaConfig {
base_url: string;
model_id: string | null;
}
export interface LiteLlmConfig {
base_url: string;
api_key: string | null;
model_id: string | null;
}
export interface ContainerInfo { export interface ContainerInfo {
container_id: string; container_id: string;
project_id: string; project_id: string;