From 4cb021c07b4e5d4849c2bcb5cef60f6f91f320e7 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Tue, 14 Jul 2026 13:14:13 +0200 Subject: [PATCH] feat: switch free AI tier to DeepSeek with daily cost budgets Co-Authored-By: Claude Opus 4.8 (1M context) --- ...f2a2e9d86ef70d0e667f0e7b291dde879608e.json} | 4 ++-- ...e0d90736fda75fd059254b7a105b51083a4416.json | 15 +++++++++++++++ ...33d8196135130a8b0ba86bd37e3f91880ad975.json | 15 --------------- backend/ee-repo-ref.txt | 2 +- .../20260622072905_ai_free_token_usage.up.sql | 18 ++++++++++-------- backend/summarized_schema.txt | 2 +- backend/windmill-api/src/ai.rs | 4 ++-- backend/windmill-api/src/ai_free_tier_oss.rs | 4 ++-- 8 files changed, 33 insertions(+), 31 deletions(-) rename backend/.sqlx/{query-247486558e023ec3adf0c1e8f5664fc5e65995ce81e622c174a89befc1a527e5.json => query-445483e44858098da49a2e8659cf2a2e9d86ef70d0e667f0e7b291dde879608e.json} (57%) create mode 100644 backend/.sqlx/query-5d4138a0d49fe5f279c4d63c7ae0d90736fda75fd059254b7a105b51083a4416.json delete mode 100644 backend/.sqlx/query-f8ebef344fdbf1b0707040376233d8196135130a8b0ba86bd37e3f91880ad975.json diff --git a/backend/.sqlx/query-247486558e023ec3adf0c1e8f5664fc5e65995ce81e622c174a89befc1a527e5.json b/backend/.sqlx/query-445483e44858098da49a2e8659cf2a2e9d86ef70d0e667f0e7b291dde879608e.json similarity index 57% rename from backend/.sqlx/query-247486558e023ec3adf0c1e8f5664fc5e65995ce81e622c174a89befc1a527e5.json rename to backend/.sqlx/query-445483e44858098da49a2e8659cf2a2e9d86ef70d0e667f0e7b291dde879608e.json index 4ae74ff53c..3c34a5e59c 100644 --- a/backend/.sqlx/query-247486558e023ec3adf0c1e8f5664fc5e65995ce81e622c174a89befc1a527e5.json +++ b/backend/.sqlx/query-445483e44858098da49a2e8659cf2a2e9d86ef70d0e667f0e7b291dde879608e.json @@ -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" } diff --git a/backend/.sqlx/query-5d4138a0d49fe5f279c4d63c7ae0d90736fda75fd059254b7a105b51083a4416.json b/backend/.sqlx/query-5d4138a0d49fe5f279c4d63c7ae0d90736fda75fd059254b7a105b51083a4416.json new file mode 100644 index 0000000000..184dc03f16 --- /dev/null +++ b/backend/.sqlx/query-5d4138a0d49fe5f279c4d63c7ae0d90736fda75fd059254b7a105b51083a4416.json @@ -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" +} diff --git a/backend/.sqlx/query-f8ebef344fdbf1b0707040376233d8196135130a8b0ba86bd37e3f91880ad975.json b/backend/.sqlx/query-f8ebef344fdbf1b0707040376233d8196135130a8b0ba86bd37e3f91880ad975.json deleted file mode 100644 index 033aa7116d..0000000000 --- a/backend/.sqlx/query-f8ebef344fdbf1b0707040376233d8196135130a8b0ba86bd37e3f91880ad975.json +++ /dev/null @@ -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" -} diff --git a/backend/ee-repo-ref.txt b/backend/ee-repo-ref.txt index 3975aa3922..2fbdacb17d 100644 --- a/backend/ee-repo-ref.txt +++ b/backend/ee-repo-ref.txt @@ -1 +1 @@ -a61da8ad2de819f6675314efa59371105ad974cb \ No newline at end of file +583a5c32eb9e04a7bb9a2a99cf7fb6cd4fe9f7f9 \ No newline at end of file diff --git a/backend/migrations/20260622072905_ai_free_token_usage.up.sql b/backend/migrations/20260622072905_ai_free_token_usage.up.sql index 169c1f2408..c782ee9565 100644 --- a/backend/migrations/20260622072905_ai_free_token_usage.up.sql +++ b/backend/migrations/20260622072905_ai_free_token_usage.up.sql @@ -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, diff --git a/backend/summarized_schema.txt b/backend/summarized_schema.txt index abd39814b6..c12cd0765e 100644 --- a/backend/summarized_schema.txt +++ b/backend/summarized_schema.txt @@ -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) diff --git a/backend/windmill-api/src/ai.rs b/backend/windmill-api/src/ai.rs index 0276efcd1a..ff4d20f061 100644 --- a/backend/windmill-api/src/ai.rs +++ b/backend/windmill-api/src/ai.rs @@ -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)); diff --git a/backend/windmill-api/src/ai_free_tier_oss.rs b/backend/windmill-api/src/ai_free_tier_oss.rs index d4116a9eb4..b4aea53f5e 100644 --- a/backend/windmill-api/src/ai_free_tier_oss.rs +++ b/backend/windmill-api/src/ai_free_tier_oss.rs @@ -44,10 +44,10 @@ pub async fn free_tier_copilot_config(_db: &DB, _email: &str) -> Result( +pub fn meter_usage( upstream: S, _db: DB, _lease: FreeTierLease,