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
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,29 @@ CLOUDFLARE_AI_GATEWAY_API_KEY=your-cloudflare-ai-gateway-key-here
# CUSTOM_OPENAI_API_KEY=...
# CUSTOM_OPENAI_MODEL=gpt-4o-mini

# --- Mittwald AI Hosting (OpenAI-compatible) ---
# MITTWALD_AI_BASE_URL=https://llm.aihosting.mittwald.de/v1
# MITTWALD_AI_KEY=...
# MITTWALD_AI_MODEL=Mistral-Medium-3.5-128B

# --- Optional Mullvad egress for the sandbox only ---
# Enable with docker-compose.mullvad.yml. This does NOT route the web UI,
# LiteLLM, Postgres, Neo4j, or Tailscale through Mullvad; only the sandbox
# shares the VPN gateway network namespace.
#
# Values come from a Mullvad WireGuard configuration. The private key is the
# `PrivateKey` from the downloaded WireGuard config JSON/conf, not the public
# "WireGuard key" displayed on the device page.
# MULLVAD_WIREGUARD_PRIVATE_KEY=
# MULLVAD_WIREGUARD_ADDRESSES=10.x.y.z/32
# MULLVAD_SERVER_COUNTRIES=Germany
# MULLVAD_SERVER_CITIES=
# MULLVAD_SERVER_HOSTNAMES=
# Keep this broad default for Docker Desktop control-plane reachability. Set
# it to the exact sandbox-net subnet if your in-scope target is on a private
# RFC1918 network and you need that target traffic to go through the VPN.
# MULLVAD_LOCAL_SUBNETS=172.16.0.0/12,10.0.0.0/8,192.168.0.0/16

# --- Local LLM (Ollama) ---
# Set both to run Decepticon against a local Ollama model. The
# launcher's onboard wizard writes these when you pick "Local LLM".
Expand Down
7 changes: 5 additions & 2 deletions clients/cli/src/commands/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const SUPPORTED_MODELS: Record<string, string[]> = {
"openrouter/anthropic/claude-opus-4-7",
"openrouter/anthropic/claude-sonnet-4-6",
"openrouter/anthropic/claude-haiku-4-5",
"openrouter/moonshotai/kimi-k2",
],
"NVIDIA NIM": [
"nvidia_nim/meta/llama-3.3-70b-instruct",
Expand Down Expand Up @@ -115,6 +116,7 @@ const SUPPORTED_MODELS: Record<string, string[]> = {
"cohere_chat/command-r",
],
"Moonshot Kimi K2": [
"openrouter/moonshotai/kimi-k2",
"moonshot/kimi-k2-instruct",
"moonshot/moonshot-v1-128k",
"moonshot/moonshot-v1-8k",
Expand Down Expand Up @@ -173,9 +175,10 @@ const SUPPORTED_MODELS: Record<string, string[]> = {
"cfgateway/anthropic/claude-sonnet-4-6",
"cfgateway/anthropic/claude-haiku-4-5",
],
"Mittwald": ["mittwald/Mistral-Medium-3.5-128B"],
// Local
"Ollama (local)": ["ollama_chat/qwen3-coder:30b (or your OLLAMA_MODEL)"],
"Ollama Cloud": ["ollama_chat/<your OLLAMA_CLOUD_MODEL>"],
"Ollama (local)": ["ollama_chat/qwen2.5-coder:7b", "ollama_chat/qwen3-coder:30b"],
"Ollama Cloud": ["ollama_cloud/kimi-k2.6"],
"LM Studio (local)": ["lm_studio/<your LMSTUDIO_MODEL>"],
"Custom OpenAI Endpoint": ["custom/<your CUSTOM_OPENAI_MODEL>"],
};
Expand Down
21 changes: 20 additions & 1 deletion clients/cli/src/commands/modelOverride.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,22 @@
* Empty string == no override.
*/

let _override = "";
let _override = (process.env.DECEPTICON_MODEL_OVERRIDE ?? "").trim();
let _roleOverrides: Record<string, string> = {};

try {
const raw = (process.env.DECEPTICON_MODEL_OVERRIDES ?? "").trim();
const parsed = raw ? JSON.parse(raw) : {};
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
_roleOverrides = Object.fromEntries(
Object.entries(parsed as Record<string, unknown>)
.filter(([, model]) => typeof model === "string" && model.trim())
.map(([role, model]) => [role, (model as string).trim()]),
);
}
} catch {
_roleOverrides = {};
}

export function setModelOverride(id: string): void {
_override = id.trim();
Expand All @@ -19,3 +34,7 @@ export function setModelOverride(id: string): void {
export function getModelOverride(): string {
return _override;
}

export function getModelOverrides(): Record<string, string> {
return { ..._roleOverrides };
}
6 changes: 4 additions & 2 deletions clients/cli/src/components/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ const BANNER_LOGO = extractLogo(BANNER_FULL);
const BANNER_LOGO_WIDTH = Math.max(
...BANNER_LOGO.split("\n").map((l) => l.length),
);
const COMPACT_FULL_WIDTH = Math.max(BANNER_FULL_WIDTH, 220);
const COMPACT_LOGO_WIDTH = Math.max(BANNER_LOGO_WIDTH, 110);

// ── Responsive banner component ──────────────────────────────────────

Expand All @@ -55,12 +57,12 @@ export const Banner = React.memo(function Banner() {
const cols = stdout?.columns ?? 80;

// Wide terminal — full banner with logo + text art
if (cols >= BANNER_FULL_WIDTH) {
if (cols >= COMPACT_FULL_WIDTH) {
return <Text color="red">{BANNER_FULL}</Text>;
}

// Medium terminal — braille logo + text name below
if (cols >= BANNER_LOGO_WIDTH) {
if (cols >= COMPACT_LOGO_WIDTH) {
return (
<Box flexDirection="column">
<Text color="red">{BANNER_LOGO}</Text>
Expand Down
40 changes: 23 additions & 17 deletions clients/cli/src/hooks/useAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
extractText,
stripResultTags,
} from "@decepticon/streaming";
import { getModelOverride } from "../commands/modelOverride.js";
import { getModelOverride, getModelOverrides } from "../commands/modelOverride.js";
import { getAssistantOverride } from "../commands/assistantOverride.js";

interface LangChainMessage {
Expand Down Expand Up @@ -58,6 +58,25 @@ interface PendingTool {
args: Record<string, unknown>;
}

function buildRunConfig(): { config?: { configurable: Record<string, unknown> } } {
const configurable: Record<string, unknown> = {};
const slug = process.env.DECEPTICON_ENGAGEMENT;
if (slug) {
configurable.engagement_name = slug;
configurable.workspace_path =
process.env.DECEPTICON_WORKSPACE_PATH ?? "/workspace";
}
const modelOverride = getModelOverride();
if (modelOverride) {
configurable.model_override = modelOverride;
}
const modelOverrides = getModelOverrides();
if (Object.keys(modelOverrides).length > 0) {
configurable.model_overrides = modelOverrides;
}
return Object.keys(configurable).length > 0 ? { config: { configurable } } : {};
}

export interface StreamStats {
startTime: number;
totalTokens: number;
Expand Down Expand Up @@ -765,28 +784,13 @@ export function useAgent({
messages: [{ role: "user", content: message }],
};

const configurable: Record<string, unknown> = {};
const slug = process.env.DECEPTICON_ENGAGEMENT;
if (slug) {
configurable.engagement_name = slug;
configurable.workspace_path =
process.env.DECEPTICON_WORKSPACE_PATH ?? "/workspace";
}
const modelOverride = getModelOverride();
if (modelOverride) {
configurable.model_override = modelOverride;
}

const hasConfigurable = Object.keys(configurable).length > 0;
const streamConfig = hasConfigurable ? { configurable } : undefined;

try {
const stream = client.runs.stream(
threadIdRef.current!,
getAssistantOverride() || assistantIdRef.current,
{
input,
...(streamConfig ? { config: streamConfig } : {}),
...buildRunConfig(),
...STREAM_OPTIONS,
onDisconnect: "continue",
signal: abortController.signal,
Expand Down Expand Up @@ -872,6 +876,7 @@ export function useAgent({
getAssistantOverride() || assistantIdRef.current,
{
command: { resume: value },
...buildRunConfig(),
...STREAM_OPTIONS,
onDisconnect: "continue",
signal: abortController.signal,
Expand Down Expand Up @@ -934,6 +939,7 @@ export function useAgent({
getAssistantOverride() || assistantIdRef.current,
{
command: { resume: value ?? true },
...buildRunConfig(),
...STREAM_OPTIONS,
onDisconnect: "continue",
signal: abortController.signal,
Expand Down
4 changes: 2 additions & 2 deletions clients/launcher/internal/ui/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ const bannerLogo = "" +
"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢷⡿⡯⠃\n" +
"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠁"

const bannerFullWidth = 165
const bannerLogoWidth = 60
const bannerFullWidth = 220
const bannerLogoWidth = 110

// RenderBanner returns a responsive banner based on terminal width.
func RenderBanner() string {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "Engagement" ADD COLUMN "modelOverride" TEXT;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE "Engagement" ADD COLUMN "modelOverrides" JSONB;
4 changes: 3 additions & 1 deletion clients/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ model Engagement {
userId String @default("local")

// LangGraph thread tracking
threadId String?
threadId String?
modelOverride String?
modelOverrides Json?
workspacePath String?

createdAt DateTime @default(now())
Expand Down
Loading