From db3cd3c8b8b1ce09a8b0f48d83499b53fc32e8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 29 Apr 2026 20:31:48 -0400 Subject: [PATCH] fix(budget): add Fireworks model pricing and auto-unpause after limit increase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes for the non-functional budget system: 1. Fireworks models hardcoded cost {input: 0, output: 0}, making all Fireworks/Fire Pass runs report $0 and budget enforcement useless. Added per-model pricing to FIREWORKS_CURATED_MODELS with a fallback for unknown models. Pi-ai known providers (Anthropic, OpenAI, etc.) already had correct per-model pricing via getModels(). 2. saveAgent IPC handler passed the form's submitted status through without checking whether the agent was budget-paused and the new budget exceeds spent. Now auto-resolves paused → idle when the user raises budget_monthly_usd above spent_monthly_usd. 3. checkAutoResolve in budget-enforcement-service resolved policy incidents but never touched agents.status. Added unpauseBudgetBlockedAgents() that scans for paused agents whose legacy budget now has headroom and flips them to idle. --- src/main/budget-enforcement-service.ts | 17 ++++++++++-- src/main/ipc-core-agent-graph-handlers.ts | 13 ++++++--- src/main/model-resolver.ts | 34 ++++++++++++++++------- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/main/budget-enforcement-service.ts b/src/main/budget-enforcement-service.ts index df934143..6c4bac83 100644 --- a/src/main/budget-enforcement-service.ts +++ b/src/main/budget-enforcement-service.ts @@ -157,13 +157,11 @@ export function createBudgetEnforcementService({ const spent = db.core.budget.getSpentInWindow(policy); let resolved = false; - // If spend is below hard-stop threshold, resolve all open incidents if (spent < policy.amount) { const count = db.core.budget.resolveIncidentsForPolicy(policyId, "auto_resolved_limit_increase"); if (count > 0) resolved = true; } - // If spend is below warn threshold, resolve warn incidents specifically const warnAmount = Math.floor((policy.amount * policy.warnPercent) / 100); if (spent < warnAmount) { const count = db.core.budget.resolveWarnIncidentsForPolicy(policyId, "auto_resolved_limit_increase"); @@ -171,11 +169,26 @@ export function createBudgetEnforcementService({ } if (resolved) { + unpauseBudgetBlockedAgents(companyId); logger.info(`Budget auto-resolve: policy ${policyId} incidents resolved (spent=${spent}, limit=${policy.amount})`); publishDomainChanged(); } } + function unpauseBudgetBlockedAgents(companyId: string): void { + const paused = db.queryAll<{ id: string; budgetMonthlyUsd: number; spentMonthlyUsd: number }>( + `SELECT id, budget_monthly_usd as budgetMonthlyUsd, spent_monthly_usd as spentMonthlyUsd + FROM agents WHERE company_id = ? AND status = 'paused' AND budget_monthly_usd > 0`, + companyId, + ); + for (const agent of paused) { + if (agent.budgetMonthlyUsd > agent.spentMonthlyUsd) { + db.queryAll("UPDATE agents SET status = 'idle', updated_at = ? WHERE id = ?", new Date().toISOString(), agent.id); + logger.info(`[budget] Auto-unpaused agent ${agent.id}: budget $${agent.budgetMonthlyUsd} > spent $${agent.spentMonthlyUsd}`); + } + } + } + /** * Closes incidents whose calendar month window has ended. * Per D-06: window rollover cleanup. diff --git a/src/main/ipc-core-agent-graph-handlers.ts b/src/main/ipc-core-agent-graph-handlers.ts index d900283c..cd811822 100644 --- a/src/main/ipc-core-agent-graph-handlers.ts +++ b/src/main/ipc-core-agent-graph-handlers.ts @@ -99,6 +99,14 @@ export function registerCoreAgentGraphHandlers({ if (!modelSelection.ok) { return fail("CONNECTOR_MODEL_NOT_ALLOWED", modelSelection.error); } + let resolvedStatus = parsed.status; + if (resolvedStatus === "paused" && existingAgent.status === "paused" && parsed.budgetMonthlyUsd > 0) { + const spent = existingAgent.spentMonthlyUsd ?? 0; + if (parsed.budgetMonthlyUsd > spent) { + resolvedStatus = "idle"; + } + } + let id: string; try { id = db.core.agents.saveAgent({ @@ -108,7 +116,7 @@ export function registerCoreAgentGraphHandlers({ role: parsed.role, title: parsed.title, department: parsed.department ?? null, - status: parsed.status, + status: resolvedStatus, reportsTo: parsed.reportsTo ?? null, connectorId: parsed.connectorId, workspaceId: parsed.workspaceId ?? null, @@ -116,9 +124,6 @@ export function registerCoreAgentGraphHandlers({ metadataJson: parsed.metadataJson, capabilities: parsed.capabilities, budgetMonthlyUsd: parsed.budgetMonthlyUsd, - // User edited this agent through the form and picked a specific - // (connector, model). Respect their intent: pin the strategy to - // `explicit` so the dispatcher never overrides their choice. modelStrategy: "explicit", }); } catch (error) { diff --git a/src/main/model-resolver.ts b/src/main/model-resolver.ts index 7d1c1464..be873cdb 100644 --- a/src/main/model-resolver.ts +++ b/src/main/model-resolver.ts @@ -37,15 +37,29 @@ const FIREWORKS_DEFAULT_MAX_TOKENS = 8192; export const FIREWORKS_FIREPASS_MODEL_ID = "accounts/fireworks/routers/kimi-k2p5-turbo"; export const FIREWORKS_FIREPASS_MODEL_NAME = "Kimi K2.5 Turbo (Fire Pass)"; -const FIREWORKS_CURATED_MODELS: Array<{ id: string; name: string; contextWindow?: number }> = [ - { id: "accounts/fireworks/models/kimi-k2-instruct", name: "Kimi K2 Instruct", contextWindow: 131_072 }, - { id: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", name: "Qwen3 Coder 480B A35B Instruct", contextWindow: 262_144 }, - { id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1", contextWindow: 163_840 }, - { id: "accounts/fireworks/models/glm-4p5", name: "GLM 4.5", contextWindow: 131_072 }, - { id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B Instruct", contextWindow: 131_072 }, +const FIREWORKS_CURATED_MODELS: Array<{ + id: string; + name: string; + contextWindow?: number; + cost: { input: number; output: number }; +}> = [ + { id: "accounts/fireworks/models/kimi-k2-instruct", name: "Kimi K2 Instruct", contextWindow: 131_072, cost: { input: 0.50, output: 0.50 } }, + { id: "accounts/fireworks/models/qwen3-coder-480b-a35b-instruct", name: "Qwen3 Coder 480B A35B Instruct", contextWindow: 262_144, cost: { input: 0.50, output: 0.50 } }, + { id: "accounts/fireworks/models/deepseek-v3p1", name: "DeepSeek V3.1", contextWindow: 163_840, cost: { input: 0.20, output: 0.20 } }, + { id: "accounts/fireworks/models/glm-4p5", name: "GLM 4.5", contextWindow: 131_072, cost: { input: 0.30, output: 0.30 } }, + { id: "accounts/fireworks/models/llama-v3p3-70b-instruct", name: "Llama 3.3 70B Instruct", contextWindow: 131_072, cost: { input: 0.20, output: 0.20 } }, ]; -function buildFireworksModel(modelId: string, name?: string, contextWindow?: number): Model<"openai-completions"> { +const FIREWORKS_FIREPASS_COST = { input: 0.50, output: 0.50 }; +const FIREWORKS_FALLBACK_COST = { input: 0.50, output: 0.50 }; + +function buildFireworksModel( + modelId: string, + name?: string, + contextWindow?: number, + cost?: { input: number; output: number }, +): Model<"openai-completions"> { + const modelCost = cost ?? FIREWORKS_FALLBACK_COST; return { id: modelId, name: name ?? modelId, @@ -54,7 +68,7 @@ function buildFireworksModel(modelId: string, name?: string, contextWindow?: num baseUrl: FIREWORKS_OPENAI_BASE_URL, reasoning: false, input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + cost: { input: modelCost.input, output: modelCost.output, cacheRead: 0, cacheWrite: 0 }, contextWindow: contextWindow ?? FIREWORKS_DEFAULT_CONTEXT, maxTokens: FIREWORKS_DEFAULT_MAX_TOKENS, }; @@ -62,11 +76,11 @@ function buildFireworksModel(modelId: string, name?: string, contextWindow?: num export function resolveModel(provider: string, modelId: string): Model | null { if (provider === "fireworks-firepass") { - return buildFireworksModel(FIREWORKS_FIREPASS_MODEL_ID, FIREWORKS_FIREPASS_MODEL_NAME, 262_144); + return buildFireworksModel(FIREWORKS_FIREPASS_MODEL_ID, FIREWORKS_FIREPASS_MODEL_NAME, 262_144, FIREWORKS_FIREPASS_COST); } if (provider === "fireworks") { const curated = FIREWORKS_CURATED_MODELS.find((entry) => entry.id === modelId); - return buildFireworksModel(modelId, curated?.name, curated?.contextWindow); + return buildFireworksModel(modelId, curated?.name, curated?.contextWindow, curated?.cost); } if (!isPiKnownProvider(provider)) { log.warn(`[model-resolver] Unknown pi provider: ${provider}`);