Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ const mock = {

const k8s = require('@kubernetes/client-node');
const {
restartAgentRuntime, provisionAgentRuntime, buildPvcVirtualKeyReadScript, issueLiteLLMVirtualKey,
restartAgentRuntime, provisionAgentRuntime, buildPvcVirtualKeyReadScript, issueLiteLLMVirtualKey, issueLiteLLMOpenRouterKey,
readOpenClawIdentityFile, getAgentRuntimeLogs,
} = require('../../../services/agentProvisionerServiceK8s');
const { CYCLES_DIRECTIVE_MARKER } = require('../../../routes/registry/presets');
Expand Down Expand Up @@ -560,6 +560,31 @@ describe('issueLiteLLMVirtualKey never sends the master key to LiteLLM', () => {
expect(axios.get).toHaveBeenCalledWith(expect.stringContaining('/key/info?key=sk-virtual-theo'), expect.anything());
expect(key).toBe('sk-virtual-theo');
});

// D111: these keys now route to a paid model, and the $60 group budget is on
// the deepseek-v4-flash model name, which alias callers never touch.
it('mints a fresh key with a monthly spend budget', async () => {
k8s.__mock.execStdout = '';

await issueLiteLLMVirtualKey('theo');

const [url, body] = axios.post.mock.calls.find(([u]) => u.endsWith('/key/generate'));
expect(url).toBe('http://litellm:4000/key/generate');
expect(body.max_budget).toBe(30);
expect(body.budget_duration).toBe('30d');
});

it('mints a fresh community key with the same monthly spend budget', async () => {
k8s.__mock.execStdout = '';

await issueLiteLLMOpenRouterKey('community-agent');

const [url, body] = axios.post.mock.calls.find(([u]) => u.endsWith('/key/generate'));
expect(url).toBe('http://litellm:4000/key/generate');
expect(body.user_id).toBe('community-agent');
expect(body.max_budget).toBe(30);
expect(body.budget_duration).toBe('30d');
});
});


Expand Down
15 changes: 14 additions & 1 deletion backend/services/agentProvisionerServiceK8s.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ const AGENT_TOLERATIONS = (() => {
// misbehaving agent can't monopolize the single-replica LiteLLM proxy.
const LITELLM_KEY_MAX_PARALLEL_REQUESTS = 4;
const LITELLM_KEY_RPM_LIMIT = 120;
// Monthly spend cap per issued key (D111, 2026-10-27). Unlike the breakers above
// this IS a usage quota: since the GPT aliases were repointed at DeepSeek, and dead
// routes fall back to it, calls through these keys are paid, while the
// deepseek-v4-flash group budget sits on that model name, which alias callers never
// touch. The whole proxy runs ~$15/mo, so $30 per agent only trips on a runaway.
// A reused key keeps whatever budget it already carries; this stamps new ones.
const LITELLM_KEY_MAX_BUDGET_USD = 30;
const LITELLM_KEY_BUDGET_DURATION = '30d';

/**
* Resolve OpenClaw account ID from agent name and instance ID
Expand Down Expand Up @@ -1650,6 +1658,8 @@ const issueLiteLLMVirtualKey = async (agentId: any) => {
// trips only on runaway flooding (see constant comment).
max_parallel_requests: LITELLM_KEY_MAX_PARALLEL_REQUESTS,
rpm_limit: LITELLM_KEY_RPM_LIMIT,
max_budget: LITELLM_KEY_MAX_BUDGET_USD,
budget_duration: LITELLM_KEY_BUDGET_DURATION,
metadata: { agent_id: agentId, provisioned_at: new Date().toISOString() },
},
{ headers },
Expand Down Expand Up @@ -1729,6 +1739,8 @@ const issueLiteLLMOpenRouterKey = async (agentId: any) => {
// trips only on runaway flooding (see constant comment).
max_parallel_requests: LITELLM_KEY_MAX_PARALLEL_REQUESTS,
rpm_limit: LITELLM_KEY_RPM_LIMIT,
max_budget: LITELLM_KEY_MAX_BUDGET_USD,
budget_duration: LITELLM_KEY_BUDGET_DURATION,
metadata: { agent_id: agentId, key_type: 'openrouter', provisioned_at: new Date().toISOString() },
},
{ headers },
Expand Down Expand Up @@ -3535,8 +3547,9 @@ const refreshCodexOAuthTokenIfNeeded = async ({ thresholdDays = 3 } = {}) => {

module.exports = {
buildPvcVirtualKeyReadScript,
// Test seam: the dev issuer, so a test can assert what it sends to LiteLLM.
// Test seams: the dev and community issuers, so a test can assert what each sends to LiteLLM.
issueLiteLLMVirtualKey,
issueLiteLLMOpenRouterKey,
getAgentSessionSizes,
provisionAgentRuntime,
startAgentRuntime,
Expand Down
Loading