feat: switch free AI tier to DeepSeek with daily cost budgets

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Diego Imbert
2026-07-14 13:14:13 +02:00
parent 0258f03479
commit 4cb021c07b
8 changed files with 33 additions and 31 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT cost_nanos FROM ai_free_token_usage WHERE email = $1",
"query": "SELECT cost_nanos FROM ai_free_token_usage\n WHERE email = $1 AND day = (now() at time zone 'utc')::date",
"describe": {
"columns": [
{
@@ -18,5 +18,5 @@
false
]
},
"hash": "247486558e023ec3adf0c1e8f5664fc5e65995ce81e622c174a89befc1a527e5"
"hash": "445483e44858098da49a2e8659cf2a2e9d86ef70d0e667f0e7b291dde879608e"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO ai_free_token_usage (email, day, cost_nanos, updated_at)\n VALUES ($1, (now() at time zone 'utc')::date, $2, now())\n ON CONFLICT (email, day) DO UPDATE\n SET cost_nanos = ai_free_token_usage.cost_nanos + EXCLUDED.cost_nanos,\n updated_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "5d4138a0d49fe5f279c4d63c7ae0d90736fda75fd059254b7a105b51083a4416"
}
@@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO ai_free_token_usage (email, cost_nanos, updated_at)\n VALUES ($1, $2, now())\n ON CONFLICT (email) DO UPDATE\n SET cost_nanos = ai_free_token_usage.cost_nanos + EXCLUDED.cost_nanos,\n updated_at = now()",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar",
"Int8"
]
},
"nullable": []
},
"hash": "f8ebef344fdbf1b0707040376233d8196135130a8b0ba86bd37e3f91880ad975"
}
+1 -1
View File
@@ -1 +1 @@
a61da8ad2de819f6675314efa59371105ad974cb
583a5c32eb9e04a7bb9a2a99cf7fb6cd4fe9f7f9
@@ -1,16 +1,18 @@
-- Per-user lifetime usage of the Windmill-provided free Claude Opus tier, measured
-- as cost in nano-dollars (1e-9 USD) of Opus 4.8-equivalent spend rather than raw
-- tokens — cache reads cost 0.1x a normal input token, so a token count wildly
-- overstates the real bill. Keyed by normalized email so the allowance is shared
-- across a user's workspaces (and resistant to +tag / gmail-dot aliasing).
-- Per-user DAILY usage of the Windmill-provided free AI tier, measured as cost in
-- nano-dollars (1e-9 USD) rather than raw tokens — a prompt-cache hit costs a fraction
-- of a fresh input token, so a token count wildly overstates the real bill. Keyed by
-- normalized email so the allowance is shared across a user's workspaces (and is
-- resistant to +tag / gmail-dot aliasing).
CREATE TABLE ai_free_token_usage (
email VARCHAR(255) PRIMARY KEY,
email VARCHAR(255) NOT NULL,
day DATE NOT NULL,
cost_nanos BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (email, day)
);
-- Instance-wide daily cost ceiling (nano-dollars) for the free tier — a kill-switch
-- independent of per-user budgets. One row per UTC day.
-- independent of the per-user budget. One row per UTC day.
CREATE TABLE ai_free_token_daily_usage (
day DATE PRIMARY KEY,
cost_nanos BIGINT NOT NULL DEFAULT 0,
+1 -1
View File
@@ -38,7 +38,7 @@ account: workspace_id(char), id(int), expires_at(ts), refresh_token(char), clien
agent_token_blacklist: token(char), expires_at(ts), blacklisted_at(ts), blacklisted_by(char)
ai_agent_memory: workspace_id(char), conversation_id(uuid), step_id(char), messages(jsonb), created_at(ts), updated_at(ts)
ai_free_token_daily_usage: day(date), cost_nanos(bigint), updated_at(ts)
ai_free_token_usage: email(char), cost_nanos(bigint), updated_at(ts)
ai_free_token_usage: email(char), day(date), cost_nanos(bigint), updated_at(ts)
alerts: id(int), alert_type(char), message(text), created_at(ts), acknowledged(bool), workspace_id(text), acknowledged_workspace(bool), resource(text)
app: id(bigint), workspace_id(char), path(char), summary(char), policy(jsonb), versions(bigint[]), extra_perms(jsonb), draft_only(bool), custom_path(text), labels(text[])
FK: (workspace_id) -> workspace(id)
+2 -2
View File
@@ -962,7 +962,7 @@ async fn proxy(
if let Some(lease) = free_lease {
let body = if is_sse {
axum::body::Body::from_stream(inject_keepalives(
Box::pin(crate::ai_free_tier_oss::meter_anthropic_usage(
Box::pin(crate::ai_free_tier_oss::meter_usage(
response.bytes_stream(),
db.clone(),
lease,
@@ -971,7 +971,7 @@ async fn proxy(
))
} else {
let bytes = response.bytes().await.map_err(to_anyhow)?;
crate::ai_free_tier_oss::record_anthropic_json_usage(db.clone(), lease, &bytes);
crate::ai_free_tier_oss::record_json_usage(db.clone(), lease, &bytes);
axum::body::Body::from(bytes)
};
return Ok((status_code, headers, body));
+2 -2
View File
@@ -44,10 +44,10 @@ pub async fn free_tier_copilot_config(_db: &DB, _email: &str) -> Result<Option<A
}
#[cfg(not(feature = "private"))]
pub fn record_anthropic_json_usage(_db: DB, _lease: FreeTierLease, _bytes: &[u8]) {}
pub fn record_json_usage(_db: DB, _lease: FreeTierLease, _bytes: &[u8]) {}
#[cfg(not(feature = "private"))]
pub fn meter_anthropic_usage<S>(
pub fn meter_usage<S>(
upstream: S,
_db: DB,
_lease: FreeTierLease,