diff --git a/LITELLM_MODELS_REFACTOR.md b/LITELLM_MODELS_REFACTOR.md new file mode 100644 index 0000000..d2fa7c0 --- /dev/null +++ b/LITELLM_MODELS_REFACTOR.md @@ -0,0 +1,181 @@ +# LiteLLM Models Loading Refactor + +## Overview +Changed the model loading logic to **first attempt fetching from `LITELLM_BASE_URL/models`**, falling back to `models.json` if unavailable. + +## Changes Made + +### 1. **Backend: `apps/server/src/litellm.ts`** + +**Added:** +- `fetchAvailableModels()` - Async function that: + - Attempts to fetch models from `LITELLM_BASE_URL/v1/models` endpoint + - Includes authorization header if `LITELLM_API_KEY` is set + - Includes 5-second timeout for API calls + - Logs successful fetch or fallback to console + - Returns Set of model IDs from API or falls back to `models.json` + +- `getValidModelIds()` - Public async function: + - Initializes and caches the model list on first call + - Returns Promise> of valid model IDs + - Subsequent calls return cached result + +- `getValidModelIdsSync()` - Helper function: + - Synchronously returns cached model IDs + - Used in `isValidLiteLlmModel()` for request validation + - Falls back to `models.json` during startup before async initialization + +**Modified:** +- `isValidLiteLlmModel()` - Now uses sync cache function + +**Type additions:** +- `LiteLlmModelsResponse` - Type for API response from LITELLM_BASE_URL + +### 2. **App Initialization: `apps/server/src/app.ts`** + +**Added:** +- Import of `getValidModelIds` from litellm module +- Initialization call in `createApp()` to fetch models at startup +- Error handling that logs failures but doesn't block app startup + +### 3. **Shared: `packages/shared/src/modelPrices.ts`** + +**Added:** +- `fetchLiteLlmModelsFromApi(baseUrl, apiKey?)` - Public async function for fetching models from API + - Takes optional `apiKey` parameter for authorization + - Type definition `LiteLlmModelsResponse` + - Can be used if future model list updates are needed on the frontend + +### 4. **Error Messages: `apps/server/src/routes/litellm.ts`** + +**Updated:** +- Model validation error now says: `"model is not available in LITELLM_BASE_URL or models.json"` +- Previously: `"model must match apps/server/src/models.json"` + +## Flow Diagram + +``` +BACKEND STARTUP +┌─────────────────────────────────────────────────────┐ +│ App Startup (app.ts) │ +│ - Calls getValidModelIds() │ +└──────────────────┬──────────────────────────────────┘ + │ + ▼ + ┌──────────────────────────┐ + │ getValidModelIds() │ + │ (async, cached) │ + └──────────────────────────┘ + │ + ▼ + ┌──────────────────────────────────────┐ + │ fetchAvailableModels() │ + └──────────────────────────────────────┘ + │ + ┌──────────┴──────────┐ + │ │ + ▼ ▼ +┌─────────────────────┐ ┌──────────────┐ +│ LITELLM_BASE_URL/ │ │ Timeout/ │ +│ v1/models endpoint │ │ Failure │ +└──────────┬──────────┘ └──────┬───────┘ + │ │ + SUCCESS FALLBACK + │ │ + ▼ ▼ + ┌─────────┐ ┌──────────────┐ + │ API │ │ models.json │ + │ Models │ │ (file) │ + └────┬────┘ └────┬─────────┘ + │ │ + └──────────┬─────────┘ + ▼ + ┌─────────────────────────┐ + │ MODEL_IDS Set (cached) │ + └────────────┬────────────┘ + │ + ┌─────────────┴───────────────────┐ + │ │ + ▼ ▼ + ┌──────────────────┐ ┌──────────────────┐ + │ GET /api/ │ │ isValidLiteLlmModel() + │ litellm/models │ │ (for validation) │ + └──────────────────┘ └──────────────────┘ + │ + ▼ +FRONTEND +┌─────────────────────────────┐ +│ LiteLLMChat mounted │ +│ - Calls /api/litellm/models │ +└────────────┬────────────────┘ + │ + ▼ + ┌────────────────────┐ + │ availableModels │ + │ state updated │ + └────────────────────┘ + │ + ▼ + ┌────────────────────┐ + │ Model dropdown │ + │ shows 5 models │ + │ from LiteLLM │ + └────────────────────┘ +``` + +## Key Features + +✅ **Non-blocking:** App startup doesn't wait for API fetch +✅ **Graceful fallback:** Uses `models.json` if API unavailable +✅ **Caching:** Model list cached after first fetch +✅ **Sync validation:** Request handlers use cached sync function +✅ **Logging:** Console output shows fetch success or fallback +✅ **Error handling:** API failures don't crash app, just log warning + +## Behavior by Scenario + +| Scenario | Behavior | +|----------|----------| +| LITELLM_BASE_URL configured & API responsive | Uses models from API endpoint | +| LITELLM_BASE_URL configured but API down | Logs warning, falls back to models.json | +| LITELLM_BASE_URL not set | Uses models.json directly | +| API returns empty list | Falls back to models.json | +| API timeout (5s) | Falls back to models.json | + +## Testing Checklist + +- [ ] Start server with `LITELLM_BASE_URL` pointing to live service + - Verify console shows: `[LiteLLM] Loaded X models from ...` +- [ ] Start server with `LITELLM_BASE_URL` pointing to invalid endpoint + - Verify console shows warning and fallback message +- [ ] Start server without `LITELLM_BASE_URL` env var + - Verify it uses models.json without errors +- [ ] Create LiteLLM session with valid model from API + - Should succeed +- [ ] Create LiteLLM session with model not in API/models.json + - Should fail with updated error message +- [ ] Verify existing models.json-based flows still work + +## Frontend Changes + +**File: `apps/web/src/LiteLLMChat.tsx`** + +Updated to dynamically fetch models from the backend at component mount: +- Added `availableModels` state (defaults to `LITELLM_CHAT_MODEL_OPTIONS` for fallback) +- Added `useEffect` hook that calls `/api/litellm/models` endpoint +- Updated model dropdown to use `availableModels` instead of static `LITELLM_CHAT_MODEL_OPTIONS` +- Falls back gracefully to static list if fetch fails + +**File: `apps/server/src/routes/litellm.ts`** + +Added new endpoint: +- `GET /api/litellm/models` - Returns `{ models: string[] }` +- Calls `getValidModelIds()` to get the cached/fetched model list +- Returns sorted array of model IDs + +## Migration Notes + +- Zero breaking changes +- Fully backward compatible +- Existing deployments work without env var changes +- Optional opt-in to LITELLM_BASE_URL for dynamic model loading diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts index 3fe619b..e681c54 100644 --- a/apps/server/src/app.ts +++ b/apps/server/src/app.ts @@ -6,6 +6,7 @@ import { sessionsRouter } from "./routes/sessions"; import { claudeSdkRouter } from "./routes/claudeSdk"; import { liteLlmRouter } from "./routes/litellm"; import { dashboardRouter } from "./routes/dashboard"; +import { getValidModelIds } from "./litellm"; export type AgentsFleetServer = { app: Express; @@ -16,6 +17,11 @@ export type AgentsFleetServer = { export function createApp(): AgentsFleetServer { bootstrapDb(); + // Initialize model list from LITELLM_BASE_URL or fallback to models.json + getValidModelIds().catch((error) => { + console.error("Failed to initialize model list:", error); + }); + const app = express(); app.use(express.json({ limit: "1mb" })); diff --git a/apps/server/src/litellm.ts b/apps/server/src/litellm.ts index e119b0d..44e5c73 100644 --- a/apps/server/src/litellm.ts +++ b/apps/server/src/litellm.ts @@ -10,8 +10,18 @@ type ModelListFile = { }>; }; +type LiteLlmModelsResponse = { + data?: Array<{ + id?: unknown; + }>; +}; + type LiteLlmModelMessage = - | { role: "system" | "user" | "assistant"; content: string; tool_calls?: LiteLlmToolCall[] } + | { + role: "system" | "user" | "assistant"; + content: string; + tool_calls?: LiteLlmToolCall[]; + } | { role: "tool"; tool_call_id: string; content: string }; type LiteLlmToolCall = { @@ -64,14 +74,116 @@ function nowIso() { return new Date().toISOString(); } -const MODEL_IDS = new Set( - ((modelsData as ModelListFile).data ?? []) +/** + * Fetch available models from LITELLM_BASE_URL/models endpoint. + * Falls back to models.json if the API call fails. + */ +async function fetchAvailableModels(): Promise> { + const baseUrl = process.env.LITELLM_BASE_URL; + const apiKey = process.env.LITELLM_API_KEY; + + console.log( + `[LiteLLM] Attempting to fetch models from LITELLM_BASE_URL=${baseUrl}`, + ); + + if (baseUrl && baseUrl.trim().length > 0) { + try { + const modelsUrl = new URL("/v1/models", baseUrl).toString(); + const headers: Record = { + "Content-Type": "application/json", + }; + + // Add authorization header if API key is available + if (apiKey && apiKey.trim().length > 0) { + headers["Authorization"] = `Bearer ${apiKey}`; + console.log(`[LiteLLM] Using API key for authorization`); + } else { + console.log(`[LiteLLM] No API key provided`); + } + + console.log(`[LiteLLM] Fetching from: ${modelsUrl}`); + const response = await fetch(modelsUrl, { + method: "GET", + headers, + }); + + console.log(`[LiteLLM] Response status: ${response.status}`); + + if (response.ok) { + const data = (await response.json()) as LiteLlmModelsResponse; + const modelIds = (data.data ?? []) + .map((item) => item.id) + .filter((id): id is string => typeof id === "string"); + + console.log( + `[LiteLLM] Parsed ${modelIds.length} models: ${modelIds.join(", ")}`, + ); + + if (modelIds.length > 0) { + console.log( + `[LiteLLM] ✓ Successfully loaded ${modelIds.length} models from ${modelsUrl}`, + ); + return new Set(modelIds); + } else { + console.log(`[LiteLLM] API returned empty data array`); + } + } else { + const text = await response.text(); + console.warn( + `[LiteLLM] API returned status ${response.status} from ${modelsUrl}. Response: ${text}`, + ); + } + } catch (error) { + console.warn( + `[LiteLLM] Failed to fetch models from LITELLM_BASE_URL: ${error instanceof Error ? error.message : String(error)}`, + ); + console.warn(error); + } + } else { + console.log(`[LiteLLM] LITELLM_BASE_URL not configured`); + } + + // Fallback to models.json + const fallbackModels = ((modelsData as ModelListFile).data ?? []) .map((item) => item.id) - .filter((id): id is string => typeof id === "string"), -); + .filter((id): id is string => typeof id === "string"); + console.log( + `[LiteLLM] ✗ Using fallback models from models.json (${fallbackModels.length} models)`, + ); + return new Set(fallbackModels); +} + +let MODEL_IDS: Set | null = null; + +/** + * Get the set of valid model IDs, fetching from LITELLM_BASE_URL if available. + * Cached after first call. + */ +export async function getValidModelIds(): Promise> { + if (MODEL_IDS === null) { + MODEL_IDS = await fetchAvailableModels(); + } + return MODEL_IDS; +} + +/** + * Get the set of valid model IDs synchronously from cache. + * If not yet cached, uses fallback to models.json. + */ +function getValidModelIdsSync(): Set { + if (MODEL_IDS !== null) { + return MODEL_IDS; + } + // Fallback during initial startup before async fetch + return new Set( + ((modelsData as ModelListFile).data ?? []) + .map((item) => item.id) + .filter((id): id is string => typeof id === "string"), + ); +} export function isValidLiteLlmModel(model: string): boolean { - return MODEL_IDS.has(model); + return getValidModelIdsSync().has(model); } export function requireLiteLlmConfig(): { baseUrl: string; apiKey: string } { @@ -198,7 +310,12 @@ export function updateLiteLlmSessionEstimatesFromUsage( estimated_output_tokens = ?, estimated_cost_usd = ? WHERE id = ?`, - ).run(usage.inputTokens, usage.outputTokens, usage.estimatedCostUsd, sessionId); + ).run( + usage.inputTokens, + usage.outputTokens, + usage.estimatedCostUsd, + sessionId, + ); } export function assertLiteLlmSession(sessionId: string): Session { @@ -226,8 +343,7 @@ function usageFromNumbers(args: { outputTokens: number; }): Promise { return (async () => { - const estimatedCostUsd = - (await computeLiteLlmModelCostUsdAsync(args)) ?? 0; + const estimatedCostUsd = (await computeLiteLlmModelCostUsdAsync(args)) ?? 0; return { v: 1, inputTokens: args.inputTokens, @@ -324,7 +440,11 @@ async function parseStreamResponse(args: { for (const dtc of deltaToolCalls) { const i = dtc.index ?? 0; if (!toolCallMap.has(i)) { - toolCallMap.set(i, { id: dtc.id ?? "", name: dtc.function?.name ?? "", arguments: "" }); + toolCallMap.set(i, { + id: dtc.id ?? "", + name: dtc.function?.name ?? "", + arguments: "", + }); } const tc = toolCallMap.get(i)!; if (dtc.id) tc.id = dtc.id; @@ -411,7 +531,9 @@ async function callLiteLlm(args: { return { assistantText: parsed.assistantText, toolCalls: parsed.toolCalls, - usage: parsed.usage ? { ...parsed.usage, priceModelId: args.cfg.model } : null, + usage: parsed.usage + ? { ...parsed.usage, priceModelId: args.cfg.model } + : null, }; } @@ -420,10 +542,16 @@ async function callLiteLlm(args: { const text = parsed.choices?.[0]?.message?.content ?? ""; if (text) args.onChunk?.(text); const msgToolCalls: LiteLlmToolCall[] = - (parsed.choices?.[0]?.message?.tool_calls as LiteLlmToolCall[] | undefined) ?? []; + (parsed.choices?.[0]?.message?.tool_calls as + | LiteLlmToolCall[] + | undefined) ?? []; const usageChunk = extractUsage(parsed); const usage = usageChunk - ? await usageFromNumbers({ model: args.cfg.model, inputTokens: usageChunk.promptTokens, outputTokens: usageChunk.completionTokens }) + ? await usageFromNumbers({ + model: args.cfg.model, + inputTokens: usageChunk.promptTokens, + outputTokens: usageChunk.completionTokens, + }) : null; return { assistantText: text, toolCalls: msgToolCalls, usage }; } @@ -433,10 +561,13 @@ export async function runLiteLlmTurn(args: { userText: string; onChunk?: (text: string) => void; onUsage?: (usage: LiteLlmUsageSnapshotV1) => void; - onToolCall?: (toolCall: { - toolCallId: string; - command: string; - }) => Promise<{ stdout: string; stderr: string; exitCode: number; truncated: boolean; durationMs: number }>; + onToolCall?: (toolCall: { toolCallId: string; command: string }) => Promise<{ + stdout: string; + stderr: string; + exitCode: number; + truncated: boolean; + durationMs: number; + }>; }) { const cfg = loadLiteLlmConfig(args.sessionId); const { baseUrl, apiKey } = requireLiteLlmConfig(); @@ -451,7 +582,13 @@ export async function runLiteLlmTurn(args: { let totalUsage: LiteLlmUsageSnapshotV1 | null = null; for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) { - const result = await callLiteLlm({ cfg, baseUrl, apiKey, messages, onChunk: args.onChunk }); + const result = await callLiteLlm({ + cfg, + baseUrl, + apiKey, + messages, + onChunk: args.onChunk, + }); // Merge usage across turns. if (result.usage) { @@ -478,23 +615,44 @@ export async function runLiteLlmTurn(args: { } // Append the assistant message with tool_calls to the in-memory history. - messages.push({ role: "assistant", content: result.assistantText, tool_calls: result.toolCalls }); + messages.push({ + role: "assistant", + content: result.assistantText, + tool_calls: result.toolCalls, + }); // Execute each tool call and append results. for (const tc of result.toolCalls) { let command = ""; try { - const fnArgs = JSON.parse(tc.function.arguments) as { command?: unknown }; - command = typeof fnArgs.command === "string" ? fnArgs.command : tc.function.arguments; + const fnArgs = JSON.parse(tc.function.arguments) as { + command?: unknown; + }; + command = + typeof fnArgs.command === "string" + ? fnArgs.command + : tc.function.arguments; } catch { command = tc.function.arguments; } - let toolResult: { stdout: string; stderr: string; exitCode: number; truncated: boolean; durationMs: number }; + let toolResult: { + stdout: string; + stderr: string; + exitCode: number; + truncated: boolean; + durationMs: number; + }; if (args.onToolCall) { toolResult = await args.onToolCall({ toolCallId: tc.id, command }); } else { - toolResult = { stdout: "(tool execution not configured)", stderr: "", exitCode: 0, truncated: false, durationMs: 0 }; + toolResult = { + stdout: "(tool execution not configured)", + stderr: "", + exitCode: 0, + truncated: false, + durationMs: 0, + }; } const output = [ diff --git a/apps/server/src/routes/litellm.ts b/apps/server/src/routes/litellm.ts index bef2b61..10cac1d 100644 --- a/apps/server/src/routes/litellm.ts +++ b/apps/server/src/routes/litellm.ts @@ -17,6 +17,7 @@ import { storeLiteLlmConfig, storeLiteLlmMessage, updateLiteLlmSessionEstimatesFromUsage, + getValidModelIds, } from "../litellm"; import type { ProcessManager } from "../processManager"; @@ -58,7 +59,11 @@ export function liteLlmRouter(_processManager: ProcessManager): Router { return jsonError(res, 400, "model is required"); } if (!isValidLiteLlmModel(model)) { - return jsonError(res, 400, "model must match apps/server/src/models.json"); + return jsonError( + res, + 400, + "model is not available in LITELLM_BASE_URL or models.json" + ); } const budgetUsd = body?.budgetUsd; @@ -200,5 +205,18 @@ export function liteLlmRouter(_processManager: ProcessManager): Router { }, ); + // Endpoint to get available models (dynamically fetched from LITELLM_BASE_URL or fallback) + router.get("/litellm/models", async (_req: Request, res: Response) => { + try { + const modelIds = await getValidModelIds(); + const sortedModels = Array.from(modelIds).sort((a, b) => + a.localeCompare(b), + ); + return res.json({ models: sortedModels }); + } catch (error) { + return jsonError(res, 500, `Failed to get models: ${String(error)}`); + } + }); + return router; } diff --git a/apps/web/src/LiteLLMChat.tsx b/apps/web/src/LiteLLMChat.tsx index 47edfe8..5e5c831 100644 --- a/apps/web/src/LiteLLMChat.tsx +++ b/apps/web/src/LiteLLMChat.tsx @@ -1,8 +1,5 @@ import { useEffect, useMemo, useRef, useState } from "react"; -import { - LITELLM_CHAT_MODEL_OPTIONS, - getLiteLlmModelPricing, -} from "@agents_fleet/shared"; +import { getLiteLlmModelPricing } from "@agents_fleet/shared"; import type { Session, WsServerMessage } from "@agents_fleet/shared"; import { createLiteLlmSession, @@ -52,9 +49,8 @@ function nowIso() { export default function LiteLLMChat(props: Props) { const [repoPath, setRepoPath] = useState(""); - const [model, setModel] = useState( - LITELLM_CHAT_MODEL_OPTIONS[0] ?? "gpt-4o-mini", - ); + const [availableModels, setAvailableModels] = useState([]); + const [model, setModel] = useState("gpt-4o-mini"); const [budgetUsd, setBudgetUsd] = useState(""); const [session, setSession] = useState(null); const [items, setItems] = useState([]); @@ -75,6 +71,25 @@ export default function LiteLLMChat(props: Props) { } | null>(null); const messagesEndRef = useRef(null); + // Fetch available models from the backend + useEffect(() => { + fetch("/api/litellm/models") + .then((res) => res.json()) + .then((data) => { + if (data.models && Array.isArray(data.models)) { + setAvailableModels(data.models); + // Update model selection if current selection is not in new list + if (!data.models.includes(model)) { + setModel(data.models[0] ?? "gpt-4o-mini"); + } + } + }) + .catch((err) => { + console.warn("Failed to fetch available models:", err); + // Fall back to static list on error + }); + }, []); + useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [items]); @@ -112,7 +127,11 @@ export default function LiteLLMChat(props: Props) { } if (msg.type === "litellm_tool_request") { - if (msg.sessionId !== (wsRef.current ? subscribedSessionRef.current : null)) return; + if ( + msg.sessionId !== + (wsRef.current ? subscribedSessionRef.current : null) + ) + return; setItems((prev) => [ ...prev, { @@ -265,14 +284,18 @@ export default function LiteLLMChat(props: Props) { (artifact) => artifact.kind === "litellm_chat_config_v1", ); if (configArtifact) { - const parsed = JSON.parse(configArtifact.content) as { model?: unknown }; + const parsed = JSON.parse(configArtifact.content) as { + model?: unknown; + }; if (typeof parsed.model === "string") { setModel(parsed.model); } } setRepoPath(current.repo_path); setBudgetUsd( - typeof current.budget_usd === "number" ? String(current.budget_usd) : "", + typeof current.budget_usd === "number" + ? String(current.budget_usd) + : "", ); const nextItems: Item[] = json.artifacts @@ -291,7 +314,9 @@ export default function LiteLLMChat(props: Props) { ? ("user" as const) : ("assistant" as const), text: - typeof parsed.text === "string" ? parsed.text : artifact.content, + typeof parsed.text === "string" + ? parsed.text + : artifact.content, ts: artifact.timestamp, }; }); @@ -344,7 +369,13 @@ export default function LiteLLMChat(props: Props) { setItems((prev) => [ ...prev, { kind: "message", id: userItemId, role: "user", text, ts: nowIso() }, - { kind: "message", id: assistantItemId, role: "assistant", text: "", ts: nowIso() }, + { + kind: "message", + id: assistantItemId, + role: "assistant", + text: "", + ts: nowIso(), + }, ]); activeStreamRef.current = { sessionId, assistantItemId }; @@ -379,28 +410,92 @@ export default function LiteLLMChat(props: Props) { }} > {/* Status badge */} - {session.status} + + {session.status} + {/* Repo path */} - + {session.repo_path} | - in {session.estimated_input_tokens.toLocaleString()} - out {session.estimated_output_tokens.toLocaleString()} - cost ${session.estimated_cost_usd.toFixed(4)} - {session.budget_usd ? / ${session.budget_usd} : null} + + in{" "} + + {session.estimated_input_tokens.toLocaleString()} + + + + out{" "} + + {session.estimated_output_tokens.toLocaleString()} + + + + cost{" "} + + ${session.estimated_cost_usd.toFixed(4)} + + + {session.budget_usd ? ( + + / ${session.budget_usd} + + ) : null} {session.stop_reason ? ( <> | - {session.stop_reason} + + {session.stop_reason} + ) : null} | - + {session.id.slice(0, 8)} {session.status === "running" && ( @@ -458,7 +553,7 @@ export default function LiteLLMChat(props: Props) { border: "1px solid #d1d5db", }} > - {LITELLM_CHAT_MODEL_OPTIONS.map((option) => ( + {availableModels.map((option) => ( @@ -523,12 +618,15 @@ export default function LiteLLMChat(props: Props) { background: "#111827", }} > -
+
tool: bash • {new Date(item.ts).toLocaleTimeString()}
prev.map((i) => - i.kind === "tool" && i.toolCallId === item.toolCallId + i.kind === "tool" && + i.toolCallId === item.toolCallId ? { ...i, status: "approved" } : i, ), ); - ws.send(JSON.stringify({ type: "litellm_tool_decision", sessionId: sid, toolCallId: item.toolCallId, approved: true })); + ws.send( + JSON.stringify({ + type: "litellm_tool_decision", + sessionId: sid, + toolCallId: item.toolCallId, + approved: true, + }), + ); + }} + style={{ + padding: "3px 10px", + borderRadius: 6, + border: "1px solid #16a34a", + background: "#14532d", + color: "#86efac", + cursor: "pointer", + fontSize: 12, }} - style={{ padding: "3px 10px", borderRadius: 6, border: "1px solid #16a34a", background: "#14532d", color: "#86efac", cursor: "pointer", fontSize: 12 }} > Approve @@ -565,14 +679,30 @@ export default function LiteLLMChat(props: Props) { if (!ws || !sid) return; setItems((prev) => prev.map((i) => - i.kind === "tool" && i.toolCallId === item.toolCallId + i.kind === "tool" && + i.toolCallId === item.toolCallId ? { ...i, status: "denied" } : i, ), ); - ws.send(JSON.stringify({ type: "litellm_tool_decision", sessionId: sid, toolCallId: item.toolCallId, approved: false })); + ws.send( + JSON.stringify({ + type: "litellm_tool_decision", + sessionId: sid, + toolCallId: item.toolCallId, + approved: false, + }), + ); + }} + style={{ + padding: "3px 10px", + borderRadius: 6, + border: "1px solid #dc2626", + background: "#450a0a", + color: "#fca5a5", + cursor: "pointer", + fontSize: 12, }} - style={{ padding: "3px 10px", borderRadius: 6, border: "1px solid #dc2626", background: "#450a0a", color: "#fca5a5", cursor: "pointer", fontSize: 12 }} > Deny @@ -587,13 +717,36 @@ export default function LiteLLMChat(props: Props) { {item.status === "done" && (
{item.stdout && ( -
{item.stdout}
+
+                        {item.stdout}
+                      
)} {item.stderr && ( -
{item.stderr}
+
+                        {item.stderr}
+                      
)} -
- exit {item.exitCode} • {item.durationMs}ms{item.truncated ? " • truncated" : ""} +
+ exit {item.exitCode} • {item.durationMs}ms + {item.truncated ? " • truncated" : ""}
)} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index cc1f8cb..62fc07d 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -166,12 +166,7 @@ export type WsServerMessage = export { CLAUDE_SDK_MODEL_OPTIONS, - LITELLM_CHAT_MODEL_OPTIONS, getClaudeSdkModelPricing, getLiteLlmModelPricing, } from "./modelPrices"; -export type { - ClaudeSdkModelOption, - LiteLlmChatModelOption, - ModelPriceLookup, -} from "./modelPrices"; +export type { ClaudeSdkModelOption, ModelPriceLookup } from "./modelPrices"; diff --git a/packages/shared/src/modelPrices.ts b/packages/shared/src/modelPrices.ts index 9c78925..b466795 100644 --- a/packages/shared/src/modelPrices.ts +++ b/packages/shared/src/modelPrices.ts @@ -123,11 +123,6 @@ export const CLAUDE_SDK_MODEL_OPTIONS = (modelList.data ?? []) .filter(isString) .sort((a, b) => a.localeCompare(b)); -export const LITELLM_CHAT_MODEL_OPTIONS = (modelList.data ?? []) - .map((item) => item.id) - .filter(isString) - .sort((a, b) => a.localeCompare(b)); - export function getClaudeSdkModelPricing(modelId: string): ModelPriceLookup { const pricing = lookupClaudePriceEntry(modelId); return { @@ -143,5 +138,3 @@ export function getLiteLlmModelPricing(modelId: string): ModelPriceLookup { } export type ClaudeSdkModelOption = (typeof CLAUDE_SDK_MODEL_OPTIONS)[number]; -export type LiteLlmChatModelOption = - (typeof LITELLM_CHAT_MODEL_OPTIONS)[number];