mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-18 15:48:04 +02:00
[eric] [eric] add PostHog analytics, 9Router subscription proxy, unified usage tracking
- PostHog integration: collector, analytics subapp, opt-in UI, Analytics page - 9Router: auto-start, OAuth subscription flow, /v1/messages Anthropic format support - Settings overhaul: multi-provider API keys, subscription connect UI, onboarding modal - Unified usage: merge 9Router cost/token data into Settings Usage tab - Provider system: providers/, agent_loop, tools/ (unused, for future non-Anthropic support) - Agent SDK: restored as primary with 9Router ANTHROPIC_BASE_URL fallback - Updated system prompt, credential resolution, dashboard analytics
This commit is contained in:
@@ -3,3 +3,4 @@ const host = window.location.hostname || 'localhost';
|
||||
|
||||
export const API_BASE = `http://${host}:${port}/api`;
|
||||
export const WS_BASE = `ws://${host}:${port}`;
|
||||
export const OPENSWARM_DEFAULT_PROXY_URL = 'https://api.openswarm.ai';
|
||||
|
||||
@@ -50,6 +50,7 @@ export interface AgentSession {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'draft' | 'running' | 'waiting_approval' | 'completed' | 'error' | 'stopped';
|
||||
provider: string;
|
||||
model: string;
|
||||
mode: string;
|
||||
worktree_path: string | null;
|
||||
@@ -75,6 +76,7 @@ export interface AgentSession {
|
||||
|
||||
export interface AgentConfig {
|
||||
name?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
mode?: string;
|
||||
system_prompt?: string;
|
||||
@@ -151,6 +153,7 @@ export interface SendMessagePayload {
|
||||
prompt: string;
|
||||
mode?: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
images?: Array<{ data: string; media_type: string }>;
|
||||
contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>;
|
||||
forcedTools?: string[];
|
||||
@@ -161,11 +164,11 @@ export interface SendMessagePayload {
|
||||
|
||||
export const sendMessage = createAsyncThunk(
|
||||
'agents/sendMessage',
|
||||
async ({ sessionId, prompt, mode, model, images, contextPaths, forcedTools, attachedSkills, hidden, selectedBrowserIds }: SendMessagePayload) => {
|
||||
async ({ sessionId, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, hidden, selectedBrowserIds }: SendMessagePayload) => {
|
||||
await fetch(`${AGENTS_API}/sessions/${sessionId}/message`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt, mode, model, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, hidden, selected_browser_ids: selectedBrowserIds }),
|
||||
body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, hidden, selected_browser_ids: selectedBrowserIds }),
|
||||
});
|
||||
return { sessionId, prompt };
|
||||
}
|
||||
@@ -213,6 +216,7 @@ export interface LaunchAndSendPayload {
|
||||
prompt: string;
|
||||
mode: string;
|
||||
model: string;
|
||||
provider?: string;
|
||||
images?: Array<{ data: string; media_type: string }>;
|
||||
contextPaths?: Array<{ path: string; type: 'file' | 'directory' }>;
|
||||
forcedTools?: string[];
|
||||
@@ -232,7 +236,7 @@ export const fetchSession = createAsyncThunk(
|
||||
|
||||
export const launchAndSendFirstMessage = createAsyncThunk(
|
||||
'agents/launchAndSendFirstMessage',
|
||||
async ({ draftId, config, prompt, mode, model, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds }: LaunchAndSendPayload) => {
|
||||
async ({ draftId, config, prompt, mode, model, provider, images, contextPaths, forcedTools, attachedSkills, selectedBrowserIds }: LaunchAndSendPayload) => {
|
||||
const launchRes = await fetch(`${AGENTS_API}/launch`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -244,7 +248,7 @@ export const launchAndSendFirstMessage = createAsyncThunk(
|
||||
await fetch(`${AGENTS_API}/sessions/${session.id}/message`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt, mode, model, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, selected_browser_ids: selectedBrowserIds }),
|
||||
body: JSON.stringify({ prompt, mode, model, provider, images, context_paths: contextPaths, forced_tools: forcedTools, attached_skills: attachedSkills, selected_browser_ids: selectedBrowserIds }),
|
||||
});
|
||||
|
||||
const refreshRes = await fetch(`${AGENTS_API}/sessions/${session.id}`);
|
||||
@@ -424,6 +428,7 @@ const agentsSlice = createSlice({
|
||||
id: draftId,
|
||||
name: 'New chat',
|
||||
status: 'draft',
|
||||
provider: 'anthropic',
|
||||
model: 'sonnet',
|
||||
mode,
|
||||
worktree_path: null,
|
||||
@@ -664,6 +669,13 @@ const agentsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
|
||||
updateSessionProvider(state, action: PayloadAction<{ sessionId: string; provider: string }>) {
|
||||
const session = state.sessions[action.payload.sessionId];
|
||||
if (session) {
|
||||
session.provider = action.payload.provider;
|
||||
}
|
||||
},
|
||||
|
||||
updateSessionModel(state, action: PayloadAction<{ sessionId: string; model: string }>) {
|
||||
const session = state.sessions[action.payload.sessionId];
|
||||
if (session) {
|
||||
@@ -1004,6 +1016,7 @@ export const {
|
||||
updateSessionCost,
|
||||
addBranch,
|
||||
setActiveBranch,
|
||||
updateSessionProvider,
|
||||
updateSessionModel,
|
||||
updateSessionMode,
|
||||
closeSessionFromWs,
|
||||
|
||||
@@ -3,208 +3,64 @@ import { API_BASE } from '@/shared/config';
|
||||
|
||||
const ANALYTICS_API = `${API_BASE}/analytics`;
|
||||
|
||||
export interface AnalyticsSummary {
|
||||
export interface UsageSummary {
|
||||
total_sessions: number;
|
||||
total_cost_usd: number;
|
||||
total_messages: number;
|
||||
total_tool_calls: number;
|
||||
avg_session_duration_seconds: number;
|
||||
session_completion_rate: number;
|
||||
approval_rate: number;
|
||||
models_used: Record<string, number>;
|
||||
modes_used: Record<string, number>;
|
||||
top_tools: [string, number][];
|
||||
}
|
||||
|
||||
export interface UsagePoint {
|
||||
date: string;
|
||||
sessions: number;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export interface CostPoint {
|
||||
date: string;
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export interface ToolRank {
|
||||
tool: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ApprovalStats {
|
||||
allow: number;
|
||||
deny: number;
|
||||
total: number;
|
||||
rate: number;
|
||||
avg_latency_ms: number;
|
||||
}
|
||||
|
||||
export interface SessionStats {
|
||||
completed: number;
|
||||
stopped: number;
|
||||
error: number;
|
||||
total: number;
|
||||
completion_rate: number;
|
||||
avg_duration_seconds: number;
|
||||
avg_cost_per_session: number;
|
||||
completion_rate: number;
|
||||
models_used: Record<string, number>;
|
||||
providers_used: Record<string, number>;
|
||||
top_tools: Record<string, number>;
|
||||
status_breakdown: Record<string, number>;
|
||||
// 9Router enrichment
|
||||
total_prompt_tokens: number;
|
||||
total_completion_tokens: number;
|
||||
cost_by_model: Record<string, { cost: number; requests: number; prompt_tokens: number; completion_tokens: number }>;
|
||||
cost_by_provider: Record<string, { cost: number; requests: number }>;
|
||||
cost_source: ' 9router' | 'sdk' | 'none';
|
||||
nine_router_available: boolean;
|
||||
total_requests: number;
|
||||
}
|
||||
|
||||
export interface HourlyPoint {
|
||||
hour: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface DurationBucket {
|
||||
label: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface CostByModel {
|
||||
model: string;
|
||||
cost: number;
|
||||
sessions: number;
|
||||
}
|
||||
|
||||
export interface CumulativeCostPoint {
|
||||
date: string;
|
||||
cumulative: number;
|
||||
daily: number;
|
||||
}
|
||||
|
||||
export interface ToolDuration {
|
||||
tool: string;
|
||||
calls: number;
|
||||
avg_ms: number;
|
||||
max_ms: number;
|
||||
}
|
||||
|
||||
export interface SessionCost {
|
||||
timestamp: string;
|
||||
model: string;
|
||||
cost: number;
|
||||
duration: number;
|
||||
messages: number;
|
||||
export interface CostBreakdown {
|
||||
available: boolean;
|
||||
period: string;
|
||||
total_cost: number;
|
||||
total_requests: number;
|
||||
total_prompt_tokens: number;
|
||||
total_completion_tokens: number;
|
||||
by_model: Record<string, any>;
|
||||
by_provider: Record<string, any>;
|
||||
}
|
||||
|
||||
interface AnalyticsState {
|
||||
summary: AnalyticsSummary | null;
|
||||
usage: UsagePoint[];
|
||||
cost: CostPoint[];
|
||||
tools: ToolRank[];
|
||||
approvals: ApprovalStats | null;
|
||||
sessionStats: SessionStats | null;
|
||||
hourly: HourlyPoint[];
|
||||
durationDist: DurationBucket[];
|
||||
costByModel: CostByModel[];
|
||||
cumulativeCost: CumulativeCostPoint[];
|
||||
toolDurations: ToolDuration[];
|
||||
sessionCosts: SessionCost[];
|
||||
exportPreview: any | null;
|
||||
summary: UsageSummary | null;
|
||||
costBreakdown: CostBreakdown | null;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const initialState: AnalyticsState = {
|
||||
summary: null,
|
||||
usage: [],
|
||||
cost: [],
|
||||
tools: [],
|
||||
approvals: null,
|
||||
sessionStats: null,
|
||||
hourly: [],
|
||||
durationDist: [],
|
||||
costByModel: [],
|
||||
cumulativeCost: [],
|
||||
toolDurations: [],
|
||||
sessionCosts: [],
|
||||
exportPreview: null,
|
||||
costBreakdown: null,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
export const fetchAnalyticsSummary = createAsyncThunk('analytics/fetchSummary', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/summary`);
|
||||
return (await res.json()) as AnalyticsSummary;
|
||||
const res = await fetch(`${ANALYTICS_API}/usage-summary`);
|
||||
return (await res.json()) as UsageSummary;
|
||||
});
|
||||
|
||||
export const fetchUsage = createAsyncThunk(
|
||||
'analytics/fetchUsage',
|
||||
async ({ period, range }: { period: string; range: number }) => {
|
||||
const res = await fetch(`${ANALYTICS_API}/usage?period=${period}&range=${range}`);
|
||||
const data = await res.json();
|
||||
return data.data as UsagePoint[];
|
||||
export const fetchCostBreakdown = createAsyncThunk(
|
||||
'analytics/fetchCostBreakdown',
|
||||
async (period: string = '7d') => {
|
||||
const res = await fetch(`${ANALYTICS_API}/cost-breakdown?period=${period}`);
|
||||
return (await res.json()) as CostBreakdown;
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchCost = createAsyncThunk(
|
||||
'analytics/fetchCost',
|
||||
async ({ period, range }: { period: string; range: number }) => {
|
||||
const res = await fetch(`${ANALYTICS_API}/cost?period=${period}&range=${range}`);
|
||||
const data = await res.json();
|
||||
return data.data as CostPoint[];
|
||||
},
|
||||
);
|
||||
|
||||
export const fetchTools = createAsyncThunk('analytics/fetchTools', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/tools?limit=20`);
|
||||
const data = await res.json();
|
||||
return data.data as ToolRank[];
|
||||
});
|
||||
|
||||
export const fetchApprovals = createAsyncThunk('analytics/fetchApprovals', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/approvals`);
|
||||
return (await res.json()) as ApprovalStats;
|
||||
});
|
||||
|
||||
export const fetchSessionStats = createAsyncThunk('analytics/fetchSessionStats', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/sessions-stats`);
|
||||
return (await res.json()) as SessionStats;
|
||||
});
|
||||
|
||||
export const fetchHourlyActivity = createAsyncThunk('analytics/fetchHourly', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/hourly-activity`);
|
||||
const data = await res.json();
|
||||
return data.data as HourlyPoint[];
|
||||
});
|
||||
|
||||
export const fetchDurationDistribution = createAsyncThunk('analytics/fetchDurationDist', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/duration-distribution`);
|
||||
const data = await res.json();
|
||||
return data.data as DurationBucket[];
|
||||
});
|
||||
|
||||
export const fetchCostByModel = createAsyncThunk('analytics/fetchCostByModel', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/cost-by-model`);
|
||||
const data = await res.json();
|
||||
return data.data as CostByModel[];
|
||||
});
|
||||
|
||||
export const fetchCumulativeCost = createAsyncThunk('analytics/fetchCumulativeCost', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/cumulative-cost?range=90`);
|
||||
const data = await res.json();
|
||||
return data.data as CumulativeCostPoint[];
|
||||
});
|
||||
|
||||
export const fetchToolDurations = createAsyncThunk('analytics/fetchToolDurations', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/tool-durations`);
|
||||
const data = await res.json();
|
||||
return data.data as ToolDuration[];
|
||||
});
|
||||
|
||||
export const fetchSessionCosts = createAsyncThunk('analytics/fetchSessionCosts', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/cost-per-session?limit=50`);
|
||||
const data = await res.json();
|
||||
return data.data as SessionCost[];
|
||||
});
|
||||
|
||||
export const fetchExportPreview = createAsyncThunk('analytics/fetchExportPreview', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/export/preview`);
|
||||
return await res.json();
|
||||
});
|
||||
|
||||
export const doExport = createAsyncThunk('analytics/doExport', async () => {
|
||||
const res = await fetch(`${ANALYTICS_API}/export`, { method: 'POST' });
|
||||
return await res.json();
|
||||
});
|
||||
|
||||
const analyticsSlice = createSlice({
|
||||
name: 'analytics',
|
||||
initialState,
|
||||
@@ -217,18 +73,9 @@ const analyticsSlice = createSlice({
|
||||
state.summary = action.payload;
|
||||
})
|
||||
.addCase(fetchAnalyticsSummary.rejected, (state) => { state.loading = false; })
|
||||
.addCase(fetchUsage.fulfilled, (state, action) => { state.usage = action.payload; })
|
||||
.addCase(fetchCost.fulfilled, (state, action) => { state.cost = action.payload; })
|
||||
.addCase(fetchTools.fulfilled, (state, action) => { state.tools = action.payload; })
|
||||
.addCase(fetchApprovals.fulfilled, (state, action) => { state.approvals = action.payload; })
|
||||
.addCase(fetchSessionStats.fulfilled, (state, action) => { state.sessionStats = action.payload; })
|
||||
.addCase(fetchHourlyActivity.fulfilled, (state, action) => { state.hourly = action.payload; })
|
||||
.addCase(fetchDurationDistribution.fulfilled, (state, action) => { state.durationDist = action.payload; })
|
||||
.addCase(fetchCostByModel.fulfilled, (state, action) => { state.costByModel = action.payload; })
|
||||
.addCase(fetchCumulativeCost.fulfilled, (state, action) => { state.cumulativeCost = action.payload; })
|
||||
.addCase(fetchToolDurations.fulfilled, (state, action) => { state.toolDurations = action.payload; })
|
||||
.addCase(fetchSessionCosts.fulfilled, (state, action) => { state.sessionCosts = action.payload; })
|
||||
.addCase(fetchExportPreview.fulfilled, (state, action) => { state.exportPreview = action.payload; });
|
||||
.addCase(fetchCostBreakdown.fulfilled, (state, action) => {
|
||||
state.costBreakdown = action.payload;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { API_BASE } from '@/shared/config';
|
||||
|
||||
const AGENTS_API = `${API_BASE}/agents`;
|
||||
|
||||
export interface ModelOption {
|
||||
value: string;
|
||||
label: string;
|
||||
version?: string;
|
||||
context_window: number;
|
||||
}
|
||||
|
||||
interface ModelsState {
|
||||
byProvider: Record<string, ModelOption[]>;
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const initialState: ModelsState = {
|
||||
byProvider: {},
|
||||
loaded: false,
|
||||
};
|
||||
|
||||
export const fetchModels = createAsyncThunk('models/fetchModels', async () => {
|
||||
const res = await fetch(`${AGENTS_API}/models`);
|
||||
if (!res.ok) throw new Error('Failed to fetch models');
|
||||
const data = await res.json();
|
||||
// API returns { models: { provider: [...] } }
|
||||
const models = data.models || data;
|
||||
return models as Record<string, ModelOption[]>;
|
||||
});
|
||||
|
||||
const modelsSlice = createSlice({
|
||||
name: 'models',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(fetchModels.fulfilled, (state, action) => {
|
||||
state.byProvider = action.payload;
|
||||
state.loaded = true;
|
||||
})
|
||||
.addCase(fetchModels.rejected, (state) => {
|
||||
// Mark as loaded even on failure so we fall back to hardcoded options
|
||||
state.loaded = true;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export default modelsSlice.reducer;
|
||||
@@ -11,6 +11,13 @@ export const DEFAULT_SYSTEM_PROMPT =
|
||||
`If a Browser is selected, prioritize this over other tools when it makes sense (so the user also has observability).\n\n` +
|
||||
`If multiple Browsers are selected, parallelize the tasks across them.`;
|
||||
|
||||
export interface CustomProvider {
|
||||
name: string;
|
||||
base_url: string;
|
||||
api_key: string;
|
||||
models: Array<{ value: string; label: string; context_window?: number }>;
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
default_system_prompt: string | null;
|
||||
default_folder: string | null;
|
||||
@@ -21,6 +28,10 @@ export interface AppSettings {
|
||||
theme: 'light' | 'dark';
|
||||
new_agent_shortcut: string;
|
||||
anthropic_api_key: string | null;
|
||||
openai_api_key?: string | null;
|
||||
google_api_key?: string | null;
|
||||
openrouter_api_key?: string | null;
|
||||
custom_providers?: CustomProvider[];
|
||||
browser_homepage: string;
|
||||
auto_select_mode_on_new_agent: boolean;
|
||||
expand_new_chats_in_dashboard: boolean;
|
||||
|
||||
@@ -13,6 +13,7 @@ import dashboardLayoutReducer from './dashboardLayoutSlice';
|
||||
import dashboardsReducer from './dashboardsSlice';
|
||||
import updateReducer from './updateSlice';
|
||||
import analyticsReducer from './analyticsSlice';
|
||||
import modelsReducer from './modelsSlice';
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
@@ -30,6 +31,7 @@ export const store = configureStore({
|
||||
dashboards: dashboardsReducer,
|
||||
update: updateReducer,
|
||||
analytics: analyticsReducer,
|
||||
models: modelsReducer,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ class WebSocketManager {
|
||||
sendMessage(
|
||||
sessionId: string,
|
||||
prompt: string,
|
||||
opts?: { mode?: string; model?: string; images?: Array<{ data: string; media_type: string }> },
|
||||
opts?: { mode?: string; model?: string; provider?: string; images?: Array<{ data: string; media_type: string }> },
|
||||
) {
|
||||
this.send('agent:send_message', {
|
||||
session_id: sessionId,
|
||||
|
||||
Reference in New Issue
Block a user