From c1f722f7e1d13fbbe53bc5f244b7ba3bdf09f8f4 Mon Sep 17 00:00:00 2001 From: Gabriel Bo Date: Mon, 2 Mar 2026 20:20:02 -0800 Subject: [PATCH] working chat + dashboard design sidebar + cli/dmg setup --- frontend/src/App.tsx | 12 +- frontend/src/components/Chat/ChatArea.tsx | 17 +- frontend/src/components/Chat/InputArea.tsx | 5 + .../src/components/Chat/MessageBubble.tsx | 16 +- frontend/src/components/Chat/SystemPanel.tsx | 273 +++++++++++++++ .../components/Dashboard/CostComparison.tsx | 6 +- frontend/src/components/Sidebar/Sidebar.tsx | 2 + frontend/src/lib/store.ts | 8 + frontend/src/pages/ChatPage.tsx | 13 +- frontend/src/pages/GetStartedPage.tsx | 316 ++++++++++++++++++ frontend/tsconfig.tsbuildinfo | 2 +- src/openjarvis/agents/orchestrator.py | 3 +- src/openjarvis/engine/ollama.py | 21 +- src/openjarvis/server/savings.py | 13 +- src/openjarvis/server/stream_bridge.py | 15 +- 15 files changed, 697 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/Chat/SystemPanel.tsx create mode 100644 frontend/src/pages/GetStartedPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bc2aed5e..9986a432 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,6 +4,7 @@ import { Layout } from './components/Layout'; import { ChatPage } from './pages/ChatPage'; import { DashboardPage } from './pages/DashboardPage'; import { SettingsPage } from './pages/SettingsPage'; +import { GetStartedPage } from './pages/GetStartedPage'; import { CommandPalette } from './components/CommandPalette'; import { useAppStore } from './lib/store'; import { fetchModels, fetchServerInfo, fetchSavings } from './lib/api'; @@ -51,17 +52,23 @@ export default function App() { return () => clearInterval(interval); }, []); // eslint-disable-line react-hooks/exhaustive-deps - // Global Cmd+K + const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel); + + // Global keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); setCommandPaletteOpen(!commandPaletteOpen); } + if ((e.metaKey || e.ctrlKey) && e.key === 'i') { + e.preventDefault(); + toggleSystemPanel(); + } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [commandPaletteOpen, setCommandPaletteOpen]); + }, [commandPaletteOpen, setCommandPaletteOpen, toggleSystemPanel]); return ( <> @@ -70,6 +77,7 @@ export default function App() { } /> } /> } /> + } /> {commandPaletteOpen && } diff --git a/frontend/src/components/Chat/ChatArea.tsx b/frontend/src/components/Chat/ChatArea.tsx index 9aa96489..4b6bd78c 100644 --- a/frontend/src/components/Chat/ChatArea.tsx +++ b/frontend/src/components/Chat/ChatArea.tsx @@ -3,7 +3,7 @@ import { MessageBubble } from './MessageBubble'; import { InputArea } from './InputArea'; import { StreamingDots } from './StreamingDots'; import { useAppStore } from '../../lib/store'; -import { Sparkles } from 'lucide-react'; +import { Sparkles, PanelRightOpen, PanelRightClose } from 'lucide-react'; function getGreeting(): string { const hour = new Date().getHours(); @@ -15,6 +15,8 @@ function getGreeting(): string { export function ChatArea() { const messages = useAppStore((s) => s.messages); const streamState = useAppStore((s) => s.streamState); + const systemPanelOpen = useAppStore((s) => s.systemPanelOpen); + const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel); const listRef = useRef(null); const shouldAutoScroll = useRef(true); @@ -32,8 +34,21 @@ export function ChatArea() { const isEmpty = messages.length === 0 && !streamState.isStreaming; + const PanelIcon = systemPanelOpen ? PanelRightClose : PanelRightOpen; + return (
+ {/* Toggle bar */} +
+ +
useAppStore.getState().setSavings(data)) + .catch(() => {}); } }, [ input, diff --git a/frontend/src/components/Chat/MessageBubble.tsx b/frontend/src/components/Chat/MessageBubble.tsx index cbdf2d84..1214b858 100644 --- a/frontend/src/components/Chat/MessageBubble.tsx +++ b/frontend/src/components/Chat/MessageBubble.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import remarkGfm from 'remark-gfm'; @@ -6,6 +6,12 @@ import { Copy, Check } from 'lucide-react'; import { ToolCallCard } from './ToolCallCard'; import type { ChatMessage } from '../../types'; +function stripThinkTags(text: string): string { + let cleaned = text.replace(/[\s\S]*?<\/think>\s*/gi, ''); + cleaned = cleaned.replace(/^[\s\S]*?<\/think>\s*/i, ''); + return cleaned.trim(); +} + interface Props { message: ChatMessage; } @@ -100,6 +106,8 @@ export function MessageBubble({ message }: Props) { ); } + const cleanContent = useMemo(() => stripThinkTags(message.content), [message.content]); + return (
{/* Tool calls */} @@ -112,7 +120,7 @@ export function MessageBubble({ message }: Props) { )} {/* Assistant message */} - {message.content && ( + {cleanContent && (
- {message.content} + {cleanContent}
)} {/* Footer: usage + copy */}
- + {message.usage && ( {message.usage.total_tokens} tokens diff --git a/frontend/src/components/Chat/SystemPanel.tsx b/frontend/src/components/Chat/SystemPanel.tsx new file mode 100644 index 00000000..f339dd7b --- /dev/null +++ b/frontend/src/components/Chat/SystemPanel.tsx @@ -0,0 +1,273 @@ +import { useState, useEffect, useCallback } from 'react'; +import { + Zap, + Activity, + Thermometer, + DollarSign, + TrendingDown, + Cloud, + HardDrive, + Hash, + X, +} from 'lucide-react'; +import { useAppStore } from '../../lib/store'; + +interface EnergyData { + total_energy_j?: number; + energy_per_token_j?: number; + avg_power_w?: number; +} + +interface TelemetryStats { + total_requests?: number; + total_tokens?: number; +} + +const CLOUD_PRICING = [ + { name: 'GPT-5.3', input: 2.00, output: 10.00, primary: true }, + { name: 'Claude Opus 4.6', input: 5.00, output: 25.00, primary: false }, + { name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00, primary: false }, +]; + +export function SystemPanel() { + const savings = useAppStore((s) => s.savings); + const toggleSystemPanel = useAppStore((s) => s.toggleSystemPanel); + const [energy, setEnergy] = useState(null); + const [telemetry, setTelemetry] = useState(null); + + const fetchData = useCallback(async () => { + try { + const base = import.meta.env.VITE_API_URL || ''; + const [energyRes, telRes] = await Promise.allSettled([ + fetch(`${base}/v1/telemetry/energy`).then((r) => (r.ok ? r.json() : null)), + fetch(`${base}/v1/telemetry/stats`).then((r) => (r.ok ? r.json() : null)), + ]); + if (energyRes.status === 'fulfilled' && energyRes.value) { + setEnergy(energyRes.value as EnergyData); + } + if (telRes.status === 'fulfilled' && telRes.value) { + setTelemetry(telRes.value as TelemetryStats); + } + } catch { + // best-effort + } + }, []); + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 3000); + return () => clearInterval(interval); + }, [fetchData]); + + // Re-fetch energy/telemetry when savings updates (after a chat message) + useEffect(() => { + if (savings) fetchData(); + }, [savings, fetchData]); + + const thermalStatus = + (energy?.avg_power_w ?? 0) < 50 + ? { label: 'Cool', color: 'var(--color-success)' } + : (energy?.avg_power_w ?? 0) < 150 + ? { label: 'Warm', color: 'var(--color-warning)' } + : { label: 'Hot', color: 'var(--color-error)' }; + + const promptK = (savings?.total_prompt_tokens ?? 0) / 1000; + const completionK = (savings?.total_completion_tokens ?? 0) / 1000; + + return ( +
+ {/* Header */} +
+ + System + + +
+ +
+ {/* Session Stats */} +
+

+ Session +

+
+ + +
+
+ + {/* Energy */} +
+

+ Energy +

+
+ + + +
+ + + {thermalStatus.label} + +
+
+
+ + {/* Cost Comparison */} +
+

+ Cost Comparison +

+ + {/* Local */} +
+ +
+
Local
+
+
+ ${(savings?.local_cost ?? 0).toFixed(4)} +
+
+ + {/* Cloud providers */} +
+ {CLOUD_PRICING.map((provider) => { + const cost = (promptK * provider.input) / 1000 + (completionK * provider.output) / 1000; + const saved = cost - (savings?.local_cost ?? 0); + return ( +
+ +
+
+ {provider.name} +
+
+
+
+ ${cost.toFixed(4)} +
+ {saved > 0.0001 && ( +
+ + ${saved.toFixed(4)} +
+ )} +
+
+ ); + })} +
+ + {/* Server-reported savings */} + {savings && savings.per_provider.length > 0 && ( +
+
+ Server-reported +
+ {savings.per_provider.map((p) => ( +
+ {p.label} + ${p.total_cost.toFixed(4)} +
+ ))} +
+ )} +
+
+
+ ); +} + +function MiniStat({ + icon: Icon, + label, + value, + unit, +}: { + icon: typeof Zap; + label: string; + value: string; + unit?: string; +}) { + return ( +
+
+ + + {label} + +
+
+ {value} + {unit && ( + + {unit} + + )} +
+
+ ); +} + +function formatNumber(n: number): string { + if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'; + if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'; + return String(n); +} diff --git a/frontend/src/components/Dashboard/CostComparison.tsx b/frontend/src/components/Dashboard/CostComparison.tsx index 8563008a..41858f44 100644 --- a/frontend/src/components/Dashboard/CostComparison.tsx +++ b/frontend/src/components/Dashboard/CostComparison.tsx @@ -2,9 +2,9 @@ import { DollarSign, TrendingDown, Cloud, HardDrive } from 'lucide-react'; import { useAppStore } from '../../lib/store'; const CLOUD_PRICING = [ - { name: 'GPT-4o', input: 2.50, output: 10.00 }, - { name: 'Claude 3.5', input: 3.00, output: 15.00 }, - { name: 'GPT-4o-mini', input: 0.15, output: 0.60 }, + { name: 'GPT-5.3', input: 2.00, output: 10.00 }, + { name: 'Claude Opus 4.6', input: 5.00, output: 25.00 }, + { name: 'Gemini 3.1 Pro', input: 2.00, output: 12.00 }, ]; export function CostComparison() { diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 15ca632d..572eec28 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -9,6 +9,7 @@ import { PanelLeftClose, PanelLeft, Cpu, + Rocket, } from 'lucide-react'; import { ConversationList } from './ConversationList'; import { useAppStore } from '../../lib/store'; @@ -34,6 +35,7 @@ export function Sidebar() { { path: '/', icon: MessageSquare, label: 'Chat' }, { path: '/dashboard', icon: BarChart3, label: 'Dashboard' }, { path: '/settings', icon: Settings, label: 'Settings' }, + { path: '/get-started', icon: Rocket, label: 'Get Started' }, ]; return ( diff --git a/frontend/src/lib/store.ts b/frontend/src/lib/store.ts index 8fefe5db..c73b5b15 100644 --- a/frontend/src/lib/store.ts +++ b/frontend/src/lib/store.ts @@ -109,6 +109,9 @@ interface AppState { // Sidebar sidebarOpen: boolean; + // System panel + systemPanelOpen: boolean; + // Actions: conversations loadConversations: () => void; createConversation: (model?: string) => string; @@ -139,6 +142,8 @@ interface AppState { setCommandPaletteOpen: (open: boolean) => void; toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void; + toggleSystemPanel: () => void; + setSystemPanelOpen: (open: boolean) => void; } export const useAppStore = create((set, get) => { @@ -166,6 +171,7 @@ export const useAppStore = create((set, get) => { commandPaletteOpen: false, sidebarOpen: true, + systemPanelOpen: true, // ── Conversations ─────────────────────────────────────────────── @@ -313,6 +319,8 @@ export const useAppStore = create((set, get) => { setCommandPaletteOpen: (open: boolean) => set({ commandPaletteOpen: open }), toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })), setSidebarOpen: (open: boolean) => set({ sidebarOpen: open }), + toggleSystemPanel: () => set((s) => ({ systemPanelOpen: !s.systemPanelOpen })), + setSystemPanelOpen: (open: boolean) => set({ systemPanelOpen: open }), }; }); diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx index 43ff34e0..b7742aa6 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/ChatPage.tsx @@ -1,5 +1,16 @@ import { ChatArea } from '../components/Chat/ChatArea'; +import { SystemPanel } from '../components/Chat/SystemPanel'; +import { useAppStore } from '../lib/store'; export function ChatPage() { - return ; + const systemPanelOpen = useAppStore((s) => s.systemPanelOpen); + + return ( +
+
+ +
+ {systemPanelOpen && } +
+ ); } diff --git a/frontend/src/pages/GetStartedPage.tsx b/frontend/src/pages/GetStartedPage.tsx new file mode 100644 index 00000000..a89d9027 --- /dev/null +++ b/frontend/src/pages/GetStartedPage.tsx @@ -0,0 +1,316 @@ +import { useState, useMemo } from 'react'; +import { + Sparkles, + Download, + Terminal, + Globe, + Monitor, + Apple, + ChevronDown, + ChevronRight, + Copy, + Check, + Cpu, +} from 'lucide-react'; + +const GITHUB_BASE = + 'https://github.com/hazy/OpenJarvis/releases/latest/download'; + +interface Platform { + id: string; + label: string; + shortLabel: string; + file: string; + icon: typeof Apple; +} + +const PLATFORMS: Platform[] = [ + { + id: 'mac-arm', + label: 'macOS (Apple Silicon)', + shortLabel: 'macOS (Apple Silicon)', + file: 'OpenJarvis_aarch64.dmg', + icon: Apple, + }, + { + id: 'mac-intel', + label: 'macOS (Intel)', + shortLabel: 'macOS (Intel)', + file: 'OpenJarvis_x64.dmg', + icon: Apple, + }, + { + id: 'windows', + label: 'Windows (64-bit)', + shortLabel: 'Windows (64-bit)', + file: 'OpenJarvis_x64-setup.msi', + icon: Monitor, + }, + { + id: 'linux-deb', + label: 'Linux (DEB)', + shortLabel: 'Linux (DEB)', + file: 'OpenJarvis_amd64.deb', + icon: Terminal, + }, + { + id: 'linux-rpm', + label: 'Linux (RPM)', + shortLabel: 'Linux (RPM)', + file: 'OpenJarvis_x86_64.rpm', + icon: Terminal, + }, +]; + +function detectPlatform(): string { + const ua = navigator.userAgent.toLowerCase(); + const platform = navigator.platform?.toLowerCase() || ''; + + if (platform.includes('mac') || ua.includes('macintosh')) { + // Apple Silicon detection via WebGL renderer or default to arm + try { + const canvas = document.createElement('canvas'); + const gl = canvas.getContext('webgl'); + if (gl) { + const ext = gl.getExtension('WEBGL_debug_renderer_info'); + if (ext) { + const renderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL); + if (renderer && /apple m/i.test(renderer)) return 'mac-arm'; + } + } + } catch {} + return 'mac-arm'; + } + if (platform.includes('win') || ua.includes('windows')) return 'windows'; + if (ua.includes('ubuntu') || ua.includes('debian')) return 'linux-deb'; + if (ua.includes('linux')) return 'linux-deb'; + return 'mac-arm'; +} + +function CodeBlock({ code }: { code: string }) { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
{code}
+ +
+ ); +} + +function Section({ + icon: Icon, + title, + children, + defaultOpen = false, +}: { + icon: typeof Terminal; + title: string; + children: React.ReactNode; + defaultOpen?: boolean; +}) { + const [open, setOpen] = useState(defaultOpen); + const Chevron = open ? ChevronDown : ChevronRight; + + return ( +
+ + {open && ( +
+ {children} +
+ )} +
+ ); +} + +export function GetStartedPage() { + const detectedId = useMemo(() => detectPlatform(), []); + const primary = PLATFORMS.find((p) => p.id === detectedId) || PLATFORMS[0]; + const others = PLATFORMS.filter((p) => p.id !== primary.id); + + return ( +
+
+ {/* Hero */} +
+
+ +
+

+ OpenJarvis +

+

+ Private AI that runs on your hardware. Chat, tools, agents, and + energy profiling — no cloud required. +

+ + v2.8 + +
+ + {/* Desktop Download */} + + + {/* CLI + Browser sections */} +
+
+

+ Install with pip (Python 3.10+ required): +

+ +

+ Or with uv for faster installs: +

+ +

+ Then get started: +

+ +
+ +
+

+ Launch the API server to get the full UI in your browser: +

+ +

+ Then open{' '} + + localhost:8000 + + {' '}in your browser. Chat, dashboard, energy profiling, and cost + comparison all run locally. +

+
+
+ + {/* System Requirements */} +
+
+ +

+ System Requirements +

+
+
+
+
Runtime
+ Python 3.10+ +
+
+
Inference Engine
+ Ollama, vLLM, llama.cpp, MLX, or LM Studio +
+
+
Memory
+ 8 GB+ RAM recommended +
+
+
+
+
+ ); +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index d6510fe9..72504a2b 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/App.tsx","./src/main.tsx","./src/api/client.ts","./src/api/sse.ts","./src/components/Chat/ChatArea.tsx","./src/components/Chat/CopyButton.tsx","./src/components/Chat/InputArea.tsx","./src/components/Chat/MessageBubble.tsx","./src/components/Chat/MessageList.tsx","./src/components/Chat/StreamingIndicator.tsx","./src/components/Chat/ToolCallIndicator.tsx","./src/components/Sidebar/ConversationList.tsx","./src/components/Sidebar/ModelSelector.tsx","./src/components/Sidebar/SavingsPanel.tsx","./src/components/Sidebar/Sidebar.tsx","./src/hooks/useChat.ts","./src/hooks/useConversations.ts","./src/hooks/useModels.ts","./src/hooks/useSavings.ts","./src/hooks/useServerInfo.ts","./src/storage/conversations.ts","./src/types/index.ts"],"version":"5.7.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/commandpalette.tsx","./src/components/errorboundary.tsx","./src/components/layout.tsx","./src/components/chat/chatarea.tsx","./src/components/chat/inputarea.tsx","./src/components/chat/messagebubble.tsx","./src/components/chat/streamingdots.tsx","./src/components/chat/systempanel.tsx","./src/components/chat/toolcallcard.tsx","./src/components/dashboard/costcomparison.tsx","./src/components/dashboard/energydashboard.tsx","./src/components/dashboard/tracedebugger.tsx","./src/components/sidebar/conversationlist.tsx","./src/components/sidebar/sidebar.tsx","./src/lib/api.ts","./src/lib/sse.ts","./src/lib/store.ts","./src/pages/chatpage.tsx","./src/pages/dashboardpage.tsx","./src/pages/getstartedpage.tsx","./src/pages/settingspage.tsx","./src/types/index.ts"],"version":"5.7.3"} \ No newline at end of file diff --git a/src/openjarvis/agents/orchestrator.py b/src/openjarvis/agents/orchestrator.py index 8c96c0c8..28af5140 100644 --- a/src/openjarvis/agents/orchestrator.py +++ b/src/openjarvis/agents/orchestrator.py @@ -228,6 +228,7 @@ class OrchestratorAgent(ToolUsingAgent): # No tool calls -> check continuation, then final answer if not raw_tool_calls: content = self._check_continuation(result, messages) + content = self._strip_think_tags(content) self._emit_turn_end(turns=turns, content_length=len(content)) return AgentResult( content=content, @@ -286,7 +287,7 @@ class OrchestratorAgent(ToolUsingAgent): )) # Max turns exceeded - final_content = content if content else "" + final_content = self._strip_think_tags(content) if content else "" return self._max_turns_result(all_tool_results, turns, content=final_content) diff --git a/src/openjarvis/engine/ollama.py b/src/openjarvis/engine/ollama.py index 2c823aa9..f8b41f7e 100644 --- a/src/openjarvis/engine/ollama.py +++ b/src/openjarvis/engine/ollama.py @@ -45,7 +45,11 @@ class OllamaEngine(InferenceEngine): "model": model, "messages": messages_to_dicts(messages), "stream": False, - "options": {"temperature": temperature, "num_predict": max_tokens}, + "options": { + "temperature": temperature, + "num_predict": max_tokens, + "num_ctx": kwargs.get("num_ctx", 8192), + }, } # Pass tools if provided tools = kwargs.get("tools") @@ -53,11 +57,20 @@ class OllamaEngine(InferenceEngine): payload["tools"] = tools try: resp = self._client.post("/api/chat", json=payload) + if resp.status_code == 400 and tools: + # Model may not support function calling -- retry without tools + payload.pop("tools", None) + resp = self._client.post("/api/chat", json=payload) resp.raise_for_status() except (httpx.ConnectError, httpx.TimeoutException) as exc: raise EngineConnectionError( f"Ollama not reachable at {self._host}" ) from exc + except httpx.HTTPStatusError as exc: + body = exc.response.text[:500] if exc.response else "" + raise RuntimeError( + f"Ollama returned {exc.response.status_code}: {body}" + ) from exc data = resp.json() prompt_tokens = data.get("prompt_eval_count", 0) completion_tokens = data.get("eval_count", 0) @@ -102,7 +115,11 @@ class OllamaEngine(InferenceEngine): "model": model, "messages": messages_to_dicts(messages), "stream": True, - "options": {"temperature": temperature, "num_predict": max_tokens}, + "options": { + "temperature": temperature, + "num_predict": max_tokens, + "num_ctx": kwargs.get("num_ctx", 8192), + }, } try: with self._client.stream("POST", "/api/chat", json=payload) as resp: diff --git a/src/openjarvis/server/savings.py b/src/openjarvis/server/savings.py index e273291a..af6b95cb 100644 --- a/src/openjarvis/server/savings.py +++ b/src/openjarvis/server/savings.py @@ -10,14 +10,13 @@ from typing import Any, Dict, List # --------------------------------------------------------------------------- CLOUD_PRICING: Dict[str, Dict[str, float]] = { - "gpt-5.2": { - "input_per_1m": 1.75, - "output_per_1m": 14.00, - "label": "GPT 5.2", + "gpt-5.3": { + "input_per_1m": 2.00, + "output_per_1m": 10.00, + "label": "GPT-5.3", "provider": "OpenAI", - # Rough estimates for energy/compute - "energy_wh_per_1k_tokens": 0.4, # Wh per 1K tokens (datacenter) - "flops_per_token": 3.0e12, # ~1.5T params * 2 (forward pass) + "energy_wh_per_1k_tokens": 0.4, + "flops_per_token": 3.0e12, }, "claude-opus-4.6": { "input_per_1m": 5.00, diff --git a/src/openjarvis/server/stream_bridge.py b/src/openjarvis/server/stream_bridge.py index 8b3cb4ae..aa39fc00 100644 --- a/src/openjarvis/server/stream_bridge.py +++ b/src/openjarvis/server/stream_bridge.py @@ -161,14 +161,23 @@ class AgentStreamBridge: try: agent_result = agent_task.result() except Exception as exc: - # Engine or agent error — emit a user-friendly error chunk - # instead of crashing the SSE stream. + import logging + + logger = logging.getLogger("openjarvis.server") + logger.error("Agent stream error: %s", exc, exc_info=True) + error_str = str(exc) - if "400" in error_str: + if "context length" in error_str.lower() or ( + "400" in error_str and "too long" in error_str.lower() + ): error_content = ( "The input is too long for the model's context window. " "Please try a shorter message." ) + elif "400" in error_str: + error_content = ( + f"The model returned an error: {error_str}" + ) else: error_content = f"Sorry, an error occurred: {error_str}" error_chunk = ChatCompletionChunk(