feat: add Venice API balance awareness

Adds balance tracking and improved error handling for Venice AI provider:

- Created src/providers/venice-balance.ts module for balance tracking
  - Extracts DIEM/USD/VCU balance from Venice response headers
  - Provides configurable warning thresholds (low: 10 DIEM, critical: 2 DIEM)
  - Includes fetch interceptor for automatic balance capture
  - Formats balance info for /status display

- Enhanced error handling for Venice billing errors
  - Added Venice-specific patterns to billing error detection
  - Improved error messages with actionable guidance
  - Links to Venice billing settings for depleted balance

- Updated /status output to show Venice balance when using Venice provider
  - Shows DIEM balance with status indicator (OK/LOW/CRITICAL)
  - Includes USD balance when available
  - Respects configurable thresholds

- Added veniceBalanceWarning config schema for models.json:
  - enabled: boolean (default true)
  - lowDiemThreshold: number (default 10)
  - criticalDiemThreshold: number (default 2)
  - showInStatus: boolean (default true)

Co-authored-by: jonisjongithub <jonisjongithub@users.noreply.github.com>
Co-authored-by: Clawdbot <bot@clawd.bot>
This commit is contained in:
jonisjongithub 2026-01-27 23:41:36 -08:00
parent 9688454a30
commit 17770fb1c3
7 changed files with 734 additions and 0 deletions

View File

@ -9,10 +9,12 @@ export {
classifyFailoverReason,
formatRawAssistantErrorForUi,
formatAssistantErrorText,
formatVeniceBillingError,
getApiErrorPayloadFingerprint,
isAuthAssistantError,
isAuthErrorMessage,
isBillingAssistantError,
isVeniceBillingError,
parseApiErrorInfo,
sanitizeUserFacingText,
isBillingErrorMessage,

View File

@ -4,6 +4,61 @@ import type { MoltbotConfig } from "../../config/config.js";
import { formatSandboxToolPolicyBlockedMessage } from "../sandbox.js";
import type { FailoverReason } from "./types.js";
/**
* Venice-specific billing/balance error patterns.
*/
const VENICE_BILLING_PATTERNS = [
"insufficient balance",
"insufficient_balance",
"spending_cap_exceeded",
"spending cap",
"balance depleted",
"diem balance",
] as const;
/**
* Check if an error message is a Venice-specific billing/balance error.
*/
export function isVeniceBillingError(errorMessage?: string): boolean {
if (!errorMessage) return false;
const lower = errorMessage.toLowerCase();
return VENICE_BILLING_PATTERNS.some((pattern) => lower.includes(pattern));
}
/**
* Format a Venice billing error with helpful guidance.
*/
export function formatVeniceBillingError(errorMessage: string): string {
const lower = errorMessage.toLowerCase();
if (lower.includes("insufficient balance") || lower.includes("insufficient_balance")) {
return (
"❌ Venice API error: Insufficient DIEM balance.\n" +
"Top up at: https://venice.ai/settings/billing"
);
}
if (lower.includes("spending_cap_exceeded") || lower.includes("spending cap")) {
return (
"❌ Venice API error: API key spending cap reached.\n" +
"Increase cap in API key settings or use a different key."
);
}
if (lower.includes("balance depleted") || lower.includes("diem balance")) {
return (
"❌ Venice API error: DIEM balance depleted.\n" +
"Top up at: https://venice.ai/settings/billing"
);
}
// Generic Venice billing error
return (
"❌ Venice API billing error.\n" +
"Check your balance at: https://venice.ai/settings/billing"
);
}
export function isContextOverflowError(errorMessage?: string): boolean {
if (!errorMessage) return false;
const lower = errorMessage.toLowerCase();
@ -299,6 +354,11 @@ export function formatAssistantErrorText(
return "The AI service is temporarily overloaded. Please try again in a moment.";
}
// Venice-specific billing/balance error handling
if (isVeniceBillingError(raw)) {
return formatVeniceBillingError(raw);
}
if (isLikelyHttpErrorText(raw) || isRawApiErrorPayload(raw)) {
return formatRawAssistantErrorForUi(raw);
}
@ -371,6 +431,14 @@ const ERROR_PATTERNS = {
"insufficient credits",
"credit balance",
"plans & billing",
// Venice-specific balance/billing errors
"insufficient balance",
"insufficient_balance",
"spending_cap_exceeded",
"spending cap",
"balance depleted",
"out of credits",
"diem balance",
],
auth: [
/invalid[_ ]?api[_ ]?key/,

View File

@ -19,6 +19,12 @@ import { formatUserTime, resolveUserTimeFormat, resolveUserTimezone } from "../d
import { normalizeGroupActivation } from "../../auto-reply/group-activation.js";
import { getFollowupQueueDepth, resolveQueueSettings } from "../../auto-reply/reply/queue.js";
import { buildStatusMessage } from "../../auto-reply/status.js";
import {
formatVeniceBalanceStatus,
getVeniceBalance,
isVeniceProvider,
resolveVeniceBalanceThresholds,
} from "../../providers/venice-balance.js";
import type { MoltbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
@ -410,6 +416,15 @@ export function createSessionStatusTool(opts?: {
typeof agentDefaults.model === "object" && agentDefaults.model
? { ...agentDefaults.model, primary: defaultLabel }
: { primary: defaultLabel };
// Generate Venice balance line if using Venice provider
let veniceBalanceLine: string | undefined;
if (isVeniceProvider(providerForCard)) {
const balance = getVeniceBalance();
const thresholds = resolveVeniceBalanceThresholds(cfg);
veniceBalanceLine = formatVeniceBalanceStatus(balance, thresholds) ?? undefined;
}
const statusText = buildStatusMessage({
config: cfg,
agent: {
@ -427,6 +442,7 @@ export function createSessionStatusTool(opts?: {
}),
usageLine,
timeLine,
veniceBalanceLine,
queue: {
mode: queueSettings.mode,
depth: queueDepth,

View File

@ -72,6 +72,8 @@ type StatusArgs = {
subagentsLine?: string;
includeTranscriptUsage?: boolean;
now?: number;
/** Venice balance status line (e.g., "🏛️ Venice: DIEM: 44.32 · Status: ✅ OK"). */
veniceBalanceLine?: string;
};
function resolveRuntimeLabel(
@ -414,6 +416,11 @@ export function buildStatusMessage(args: StatusArgs): string {
const mediaLine = formatMediaUnderstandingLine(args.mediaDecisions);
const voiceLine = formatVoiceModeLine(args.config, args.sessionEntry);
// Format Venice balance line with emoji prefix if present
const veniceBalanceLine = args.veniceBalanceLine
? `🏛️ Venice: ${args.veniceBalanceLine}`
: null;
return [
versionLine,
args.timeLine,
@ -422,6 +429,7 @@ export function buildStatusMessage(args: StatusArgs): string {
`📚 ${contextLine}`,
mediaLine,
args.usageLine,
veniceBalanceLine,
`🧵 ${sessionLine}`,
args.subagentsLine,
`⚙️ ${optionsLine}`,

View File

@ -72,11 +72,27 @@ export const BedrockDiscoverySchema = z
.strict()
.optional();
export const VeniceBalanceWarningSchema = z
.object({
/** Enable balance warnings (default: true). */
enabled: z.boolean().optional(),
/** Warn when DIEM balance falls below this threshold (default: 10). */
lowDiemThreshold: z.number().nonnegative().optional(),
/** Critical warning when DIEM balance falls below this threshold (default: 2). */
criticalDiemThreshold: z.number().nonnegative().optional(),
/** Show balance in /status output (default: true). */
showInStatus: z.boolean().optional(),
})
.strict()
.optional();
export const ModelsConfigSchema = z
.object({
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
providers: z.record(z.string(), ModelProviderSchema).optional(),
bedrockDiscovery: BedrockDiscoverySchema,
/** Venice-specific balance warning configuration. */
veniceBalanceWarning: VeniceBalanceWarningSchema,
})
.strict()
.optional();

View File

@ -0,0 +1,268 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import {
extractVeniceBalance,
updateVeniceBalance,
getVeniceBalance,
clearVeniceBalance,
evaluateBalanceStatus,
formatVeniceBalanceStatus,
generateBalanceWarning,
isVeniceBalanceError,
formatVeniceError,
isVeniceProvider,
isVeniceApiUrl,
DEFAULT_VENICE_BALANCE_THRESHOLDS,
type VeniceBalance,
} from "./venice-balance.js";
describe("venice-balance", () => {
beforeEach(() => {
clearVeniceBalance();
});
afterEach(() => {
clearVeniceBalance();
});
describe("extractVeniceBalance", () => {
it("extracts balance from Headers object", () => {
const headers = new Headers();
headers.set("x-venice-balance-diem", "44.32116826");
headers.set("x-venice-balance-usd", "10.50");
headers.set("x-venice-balance-vcu", "1000");
const balance = extractVeniceBalance(headers);
expect(balance).not.toBeNull();
expect(balance?.diem).toBeCloseTo(44.32116826);
expect(balance?.usd).toBeCloseTo(10.5);
expect(balance?.vcu).toBeCloseTo(1000);
expect(balance?.lastChecked).toBeGreaterThan(0);
});
it("extracts balance from plain object headers", () => {
const headers: Record<string, string> = {
"x-venice-balance-diem": "25.5",
};
const balance = extractVeniceBalance(headers);
expect(balance).not.toBeNull();
expect(balance?.diem).toBeCloseTo(25.5);
expect(balance?.usd).toBeUndefined();
});
it("returns null when no Venice headers present", () => {
const headers = new Headers();
headers.set("content-type", "application/json");
const balance = extractVeniceBalance(headers);
expect(balance).toBeNull();
});
it("returns null for invalid DIEM value", () => {
const headers = new Headers();
headers.set("x-venice-balance-diem", "invalid");
const balance = extractVeniceBalance(headers);
expect(balance).toBeNull();
});
});
describe("updateVeniceBalance / getVeniceBalance", () => {
it("stores and retrieves balance", () => {
const balance: VeniceBalance = {
diem: 50,
usd: 20,
lastChecked: Date.now(),
};
updateVeniceBalance(balance);
const retrieved = getVeniceBalance();
expect(retrieved).toEqual(balance);
});
it("clearVeniceBalance clears stored balance", () => {
updateVeniceBalance({ diem: 50, lastChecked: Date.now() });
clearVeniceBalance();
expect(getVeniceBalance()).toBeNull();
});
});
describe("evaluateBalanceStatus", () => {
it("returns 'ok' for balance above low threshold", () => {
const balance: VeniceBalance = { diem: 50, lastChecked: Date.now() };
expect(evaluateBalanceStatus(balance)).toBe("ok");
});
it("returns 'low' for balance below low threshold", () => {
const balance: VeniceBalance = { diem: 5, lastChecked: Date.now() };
expect(evaluateBalanceStatus(balance)).toBe("low");
});
it("returns 'critical' for balance below critical threshold", () => {
const balance: VeniceBalance = { diem: 1, lastChecked: Date.now() };
expect(evaluateBalanceStatus(balance)).toBe("critical");
});
it("returns 'depleted' for zero balance", () => {
const balance: VeniceBalance = { diem: 0, lastChecked: Date.now() };
expect(evaluateBalanceStatus(balance)).toBe("depleted");
});
it("returns 'unknown' for null balance", () => {
expect(evaluateBalanceStatus(null)).toBe("unknown");
});
it("respects custom thresholds", () => {
const balance: VeniceBalance = { diem: 15, lastChecked: Date.now() };
const customThresholds = {
...DEFAULT_VENICE_BALANCE_THRESHOLDS,
lowDiemThreshold: 20,
};
expect(evaluateBalanceStatus(balance, customThresholds)).toBe("low");
});
});
describe("formatVeniceBalanceStatus", () => {
it("returns formatted status for OK balance", () => {
const balance: VeniceBalance = { diem: 50, lastChecked: Date.now() };
const result = formatVeniceBalanceStatus(balance);
expect(result).toContain("DIEM: 50.00");
expect(result).toContain("✅ OK");
});
it("returns formatted status with USD when present", () => {
const balance: VeniceBalance = { diem: 50, usd: 10.5, lastChecked: Date.now() };
const result = formatVeniceBalanceStatus(balance);
expect(result).toContain("USD: $10.50");
});
it("returns null when showInStatus is false", () => {
const balance: VeniceBalance = { diem: 50, lastChecked: Date.now() };
const thresholds = { ...DEFAULT_VENICE_BALANCE_THRESHOLDS, showInStatus: false };
expect(formatVeniceBalanceStatus(balance, thresholds)).toBeNull();
});
it("returns null for null balance", () => {
expect(formatVeniceBalanceStatus(null)).toBeNull();
});
});
describe("generateBalanceWarning", () => {
it("returns null for OK balance", () => {
const balance: VeniceBalance = { diem: 50, lastChecked: Date.now() };
expect(generateBalanceWarning(balance)).toBeNull();
});
it("returns warning for low balance", () => {
const balance: VeniceBalance = { diem: 5, lastChecked: Date.now() };
const warning = generateBalanceWarning(balance);
expect(warning).toContain("⚠️");
expect(warning).toContain("low");
expect(warning).toContain("5.00");
});
it("returns critical warning for critical balance", () => {
const balance: VeniceBalance = { diem: 1, lastChecked: Date.now() };
const warning = generateBalanceWarning(balance);
expect(warning).toContain("🚨");
expect(warning).toContain("critical");
});
it("returns depleted warning for zero balance", () => {
const balance: VeniceBalance = { diem: 0, lastChecked: Date.now() };
const warning = generateBalanceWarning(balance);
expect(warning).toContain("❌");
expect(warning).toContain("depleted");
});
it("returns null when warnings disabled", () => {
const balance: VeniceBalance = { diem: 1, lastChecked: Date.now() };
const thresholds = { ...DEFAULT_VENICE_BALANCE_THRESHOLDS, enabled: false };
expect(generateBalanceWarning(balance, thresholds)).toBeNull();
});
});
describe("isVeniceBalanceError", () => {
it("detects insufficient balance error", () => {
expect(isVeniceBalanceError("insufficient balance")).toBe(true);
expect(isVeniceBalanceError("Error: insufficient_balance")).toBe(true);
});
it("detects spending cap error", () => {
expect(isVeniceBalanceError("spending_cap_exceeded")).toBe(true);
expect(isVeniceBalanceError("Spending cap reached")).toBe(true);
});
it("returns false for non-balance errors", () => {
expect(isVeniceBalanceError("rate limit exceeded")).toBe(false);
expect(isVeniceBalanceError("connection timeout")).toBe(false);
});
});
describe("formatVeniceError", () => {
it("formats insufficient balance error", () => {
const result = formatVeniceError("insufficient balance");
expect(result).toContain("Insufficient balance");
expect(result).toContain("https://venice.ai/settings/billing");
});
it("formats spending cap error", () => {
const result = formatVeniceError("spending_cap_exceeded");
expect(result).toContain("spending cap");
expect(result).toContain("API key");
});
it("returns original message for non-Venice errors", () => {
const original = "Generic error message";
expect(formatVeniceError(original)).toBe(original);
});
});
describe("isVeniceProvider", () => {
it("returns true for venice provider", () => {
expect(isVeniceProvider("venice")).toBe(true);
expect(isVeniceProvider("Venice")).toBe(true);
expect(isVeniceProvider("VENICE")).toBe(true);
});
it("returns true for venice/ prefixed provider", () => {
expect(isVeniceProvider("venice/llama-3.3-70b")).toBe(true);
});
it("returns false for other providers", () => {
expect(isVeniceProvider("openai")).toBe(false);
expect(isVeniceProvider("anthropic")).toBe(false);
});
});
describe("isVeniceApiUrl", () => {
it("returns true for Venice API URLs", () => {
expect(isVeniceApiUrl("https://api.venice.ai/api/v1/chat")).toBe(true);
expect(isVeniceApiUrl(new URL("https://api.venice.ai/v1/models"))).toBe(true);
});
it("returns false for non-Venice URLs", () => {
expect(isVeniceApiUrl("https://api.openai.com/v1/chat")).toBe(false);
});
it("handles invalid URLs gracefully", () => {
expect(isVeniceApiUrl("not-a-url")).toBe(false);
});
});
});

View File

@ -0,0 +1,356 @@
/**
* Venice balance tracking module.
*
* Extracts and tracks Venice API balance from response headers:
* - x-venice-balance-diem: DIEM token balance
* - x-venice-balance-usd: USD credit balance
* - x-venice-balance-vcu: VCU balance
*
* This module provides:
* - Balance extraction from response headers
* - Global balance state management
* - Warning/threshold evaluation
* - Fetch interceptor for automatic balance capture
*/
export interface VeniceBalance {
diem?: number;
usd?: number;
vcu?: number;
lastChecked: number;
/** Provider identifier (for multi-key scenarios) */
providerId?: string;
}
export interface VeniceBalanceThresholds {
enabled: boolean;
lowDiemThreshold: number;
criticalDiemThreshold: number;
showInStatus: boolean;
}
export const DEFAULT_VENICE_BALANCE_THRESHOLDS: VeniceBalanceThresholds = {
enabled: true,
lowDiemThreshold: 10,
criticalDiemThreshold: 2,
showInStatus: true,
};
// Singleton balance state (in-memory, cleared on restart)
let currentBalance: VeniceBalance | null = null;
let balanceUpdateCallbacks: Array<(balance: VeniceBalance) => void> = [];
/**
* Extract Venice balance from response headers.
* Returns null if no Venice balance headers are present.
*/
export function extractVeniceBalance(headers: Headers | Record<string, string>): VeniceBalance | null {
const get = (key: string): string | null => {
if (headers instanceof Headers) {
return headers.get(key);
}
// Handle plain object headers
const value = headers[key] ?? headers[key.toLowerCase()];
return typeof value === "string" ? value : null;
};
const diemRaw = get("x-venice-balance-diem");
if (!diemRaw) return null;
const diem = parseFloat(diemRaw);
if (isNaN(diem)) return null;
const usdRaw = get("x-venice-balance-usd");
const vcuRaw = get("x-venice-balance-vcu");
return {
diem,
usd: usdRaw ? parseFloat(usdRaw) : undefined,
vcu: vcuRaw ? parseFloat(vcuRaw) : undefined,
lastChecked: Date.now(),
};
}
/**
* Update the current Venice balance state.
* Called by the streaming/fetch layer when Venice headers are detected.
*/
export function updateVeniceBalance(balance: VeniceBalance): void {
currentBalance = balance;
for (const callback of balanceUpdateCallbacks) {
try {
callback(balance);
} catch {
// Ignore callback errors
}
}
}
/**
* Get the current Venice balance (if known).
*/
export function getVeniceBalance(): VeniceBalance | null {
return currentBalance;
}
/**
* Clear the current Venice balance state.
* Useful for testing or when switching accounts.
*/
export function clearVeniceBalance(): void {
currentBalance = null;
}
/**
* Subscribe to balance updates.
* Returns an unsubscribe function.
*/
export function onVeniceBalanceUpdate(
callback: (balance: VeniceBalance) => void,
): () => void {
balanceUpdateCallbacks.push(callback);
return () => {
balanceUpdateCallbacks = balanceUpdateCallbacks.filter((cb) => cb !== callback);
};
}
/**
* Evaluate balance status against thresholds.
*/
export type BalanceStatus = "ok" | "low" | "critical" | "depleted" | "unknown";
export function evaluateBalanceStatus(
balance: VeniceBalance | null,
thresholds: VeniceBalanceThresholds = DEFAULT_VENICE_BALANCE_THRESHOLDS,
): BalanceStatus {
if (!balance || balance.diem === undefined) return "unknown";
if (balance.diem <= 0) return "depleted";
if (balance.diem < thresholds.criticalDiemThreshold) return "critical";
if (balance.diem < thresholds.lowDiemThreshold) return "low";
return "ok";
}
/**
* Format balance for display in status output.
*/
export function formatVeniceBalanceStatus(
balance: VeniceBalance | null,
thresholds: VeniceBalanceThresholds = DEFAULT_VENICE_BALANCE_THRESHOLDS,
): string | null {
if (!thresholds.showInStatus) return null;
if (!balance) return null;
const status = evaluateBalanceStatus(balance, thresholds);
const diemLabel = balance.diem !== undefined ? balance.diem.toFixed(2) : "?";
const ageMs = Date.now() - balance.lastChecked;
const ageLabel = formatAge(ageMs);
const statusEmoji: Record<BalanceStatus, string> = {
ok: "✅",
low: "⚠️",
critical: "🚨",
depleted: "❌",
unknown: "❓",
};
const parts = [
`DIEM: ${diemLabel}`,
`Status: ${statusEmoji[status]} ${status.toUpperCase()}`,
`(${ageLabel})`,
];
if (balance.usd !== undefined) {
parts.splice(1, 0, `USD: $${balance.usd.toFixed(2)}`);
}
return parts.join(" · ");
}
/**
* Generate a user-facing balance warning message.
* Returns null if balance is OK or warnings are disabled.
*/
export function generateBalanceWarning(
balance: VeniceBalance | null,
thresholds: VeniceBalanceThresholds = DEFAULT_VENICE_BALANCE_THRESHOLDS,
): string | null {
if (!thresholds.enabled) return null;
if (!balance || balance.diem === undefined) return null;
const status = evaluateBalanceStatus(balance, thresholds);
const diemLabel = balance.diem.toFixed(2);
switch (status) {
case "critical":
return `🚨 Venice balance critical: ${diemLabel} DIEM remaining. Consider topping up at https://venice.ai/settings/billing`;
case "low":
return `⚠️ Venice balance low: ${diemLabel} DIEM remaining`;
case "depleted":
return `❌ Venice balance depleted (0.00 DIEM). Top up at https://venice.ai/settings/billing`;
default:
return null;
}
}
/**
* Check if an error message indicates Venice balance/billing issues.
*/
export function isVeniceBalanceError(error: string | Error): boolean {
const message = typeof error === "string" ? error : error.message;
const lower = message.toLowerCase();
return (
lower.includes("insufficient balance") ||
lower.includes("insufficient_balance") ||
lower.includes("spending_cap_exceeded") ||
lower.includes("spending cap") ||
lower.includes("credit limit") ||
lower.includes("out of credits") ||
lower.includes("balance depleted")
);
}
/**
* Format a Venice-specific error with helpful guidance.
*/
export function formatVeniceError(error: string | Error): string {
const message = typeof error === "string" ? error : error.message;
const lower = message.toLowerCase();
if (lower.includes("insufficient balance") || lower.includes("insufficient_balance")) {
const balance = getVeniceBalance();
const balanceNote = balance?.diem !== undefined ? ` (current: ${balance.diem.toFixed(2)} DIEM)` : "";
return `❌ Venice API error: Insufficient balance${balanceNote}.\nTop up at: https://venice.ai/settings/billing`;
}
if (lower.includes("spending_cap_exceeded") || lower.includes("spending cap")) {
return `❌ Venice API error: API key spending cap reached.\nIncrease cap in API key settings or use a different key.`;
}
if (lower.includes("rate_limit") || lower.includes("rate limit")) {
return `⏳ Venice API error: Rate limit exceeded.\nWait for reset or upgrade your plan.`;
}
// Return original error if not a known Venice error
return message;
}
// Helper to format age in human-readable form
function formatAge(ms: number): string {
if (ms < 0) return "unknown";
const seconds = Math.floor(ms / 1000);
if (seconds < 60) return "just now";
const minutes = Math.floor(seconds / 60);
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
/**
* Create a fetch wrapper that extracts Venice balance headers.
* Use this to wrap the global fetch when making Venice API calls.
*/
export function createVeniceFetchWrapper(
baseFetch: typeof fetch = globalThis.fetch,
): typeof fetch {
return async function veniceFetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
const response = await baseFetch(input, init);
// Try to extract Venice balance headers from response
const balance = extractVeniceBalance(response.headers);
if (balance) {
updateVeniceBalance(balance);
}
return response;
};
}
/**
* Check if a provider string represents Venice.
*/
export function isVeniceProvider(provider: string): boolean {
const normalized = provider.toLowerCase().trim();
return normalized === "venice" || normalized.startsWith("venice/");
}
/**
* Check if a URL is a Venice API endpoint.
*/
export function isVeniceApiUrl(url: string | URL | Request): boolean {
try {
const urlStr = url instanceof Request ? url.url : url.toString();
const parsed = new URL(urlStr);
return (
parsed.hostname === "api.venice.ai" ||
parsed.hostname.endsWith(".venice.ai")
);
} catch {
return false;
}
}
// Track whether fetch interceptor is installed
let fetchInterceptorInstalled = false;
let originalFetch: typeof fetch | null = null;
/**
* Install a global fetch interceptor to capture Venice balance headers.
* Safe to call multiple times - will only install once.
*
* @returns Uninstall function
*/
export function installVeniceFetchInterceptor(): () => void {
if (fetchInterceptorInstalled) {
return () => {}; // Already installed
}
originalFetch = globalThis.fetch;
fetchInterceptorInstalled = true;
globalThis.fetch = async function interceptedFetch(
input: RequestInfo | URL,
init?: RequestInit,
): Promise<Response> {
const response = await originalFetch!(input, init);
// Only process Venice API responses
if (isVeniceApiUrl(input)) {
const balance = extractVeniceBalance(response.headers);
if (balance) {
updateVeniceBalance(balance);
}
}
return response;
};
return () => {
if (originalFetch && fetchInterceptorInstalled) {
globalThis.fetch = originalFetch;
originalFetch = null;
fetchInterceptorInstalled = false;
}
};
}
/**
* Resolve Venice balance thresholds from config.
*/
export function resolveVeniceBalanceThresholds(
config?: { models?: { veniceBalanceWarning?: Partial<VeniceBalanceThresholds> } },
): VeniceBalanceThresholds {
const override = config?.models?.veniceBalanceWarning;
if (!override) return DEFAULT_VENICE_BALANCE_THRESHOLDS;
return {
enabled: override.enabled ?? DEFAULT_VENICE_BALANCE_THRESHOLDS.enabled,
lowDiemThreshold: override.lowDiemThreshold ?? DEFAULT_VENICE_BALANCE_THRESHOLDS.lowDiemThreshold,
criticalDiemThreshold: override.criticalDiemThreshold ?? DEFAULT_VENICE_BALANCE_THRESHOLDS.criticalDiemThreshold,
showInStatus: override.showInStatus ?? DEFAULT_VENICE_BALANCE_THRESHOLDS.showInStatus,
};
}