feat: tiered LLM (GLM free / Claude paid) + rate limits + quota enforcement
All checks were successful
Deploy to Production / deploy (push) Successful in 53s
All checks were successful
Deploy to Production / deploy (push) Successful in 53s
The free tier was hemorrhaging Anthropic cost with no abuse cap (no rate limit on /preview, Opus default in the build worker, 5-min cache TTL that made cache-miss the common case). This switches free users to GLM, paid users to Claude tiers, and tightens every leak found in the audit. Backend: - @bmm/llm: GLM provider via Zhipu's OpenAI-compatible endpoint, pickPreviewModel + pickBuildModel helpers, plan-aware ModelChoice - preview-cache TTL 5min -> 24h (kills the cache-miss path) - /v1/servers/preview: picks model from caller's plan, returns model name to UI - /v1/servers POST: enforces SERVER_LIMITS per plan (402), rate-limits builds - daily rate-limit on preview (5/40/150/1000) and build (3/20/100/500) - /v1/auth/me returns plan so the wizard can show the right model name - generator worker: GLM default, Anthropic Sonnet fallback if GLM errors Frontend: - Wizard fetches plan, shows "<model> is drafting the tool spec" pre-emptively, upgrade hint for hobby users, friendly errors for 402 / 429 - Pricing page: AI-model line per tier (Open-tier / Haiku / Sonnet / Opus), Team €149 -> €199, Enterprise €499 -> €999, daily-preview limit per tier - Privacy + Security: explicit subprocessor disclosure for Anthropic (US) / Zhipu (CN) and which tier uses which Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import { builds, createDb, eq, mcpServers } from '@bmm/db';
|
||||
import { GeneratorSpec } from '@bmm/types';
|
||||
import { Worker } from 'bullmq';
|
||||
import { Redis } from 'ioredis';
|
||||
import { GeneratorSpec } from '@bmm/types';
|
||||
import { builds, createDb, eq, mcpServers } from '@bmm/db';
|
||||
import { config } from './config.js';
|
||||
import { generateSpec } from './lib/claude.js';
|
||||
import { renderServerCode } from './lib/render.js';
|
||||
import { dockerBuild, prepareBuildContext, staticCheck } from './lib/build.js';
|
||||
import { generateSpec } from './lib/claude.js';
|
||||
import { allocatePort, deployContainer, dockerAvailable, stopContainer } from './lib/deploy.js';
|
||||
import { emitDone, emitError, emitLog, emitStatus } from './lib/emit.js';
|
||||
import { renderServerCode } from './lib/render.js';
|
||||
|
||||
const db = createDb();
|
||||
const connection = new Redis(config.REDIS_URL, { maxRetriesPerRequest: null });
|
||||
@@ -57,12 +57,18 @@ export const worker = new Worker<JobData>(
|
||||
const oldContainerId = priorState?.containerId ?? null;
|
||||
|
||||
try {
|
||||
await db.update(builds).set({ status: 'generating', startedAt: new Date() }).where(eq(builds.id, buildId));
|
||||
await db.update(mcpServers).set({ status: 'generating', updatedAt: new Date() }).where(eq(mcpServers.id, serverId));
|
||||
await db
|
||||
.update(builds)
|
||||
.set({ status: 'generating', startedAt: new Date() })
|
||||
.where(eq(builds.id, buildId));
|
||||
await db
|
||||
.update(mcpServers)
|
||||
.set({ status: 'generating', updatedAt: new Date() })
|
||||
.where(eq(mcpServers.id, serverId));
|
||||
await emitStatus(buildId, 'generating');
|
||||
|
||||
let spec: GeneratorSpec | null = null;
|
||||
let source: 'claude' | 'mock' | 'cached' = 'mock';
|
||||
let source: 'claude' | 'glm' | 'mock' | 'cached' = 'mock';
|
||||
|
||||
if (previewId) {
|
||||
spec = await loadCachedSpec(previewId);
|
||||
@@ -87,7 +93,10 @@ export const worker = new Worker<JobData>(
|
||||
let generatedCode: string;
|
||||
const prebuilt = previewId ? await loadPrebuiltCode(previewId) : null;
|
||||
if (prebuilt) {
|
||||
await log('info', `Using pre-rendered template code (${prebuilt.length} chars) — skipping render`);
|
||||
await log(
|
||||
'info',
|
||||
`Using pre-rendered template code (${prebuilt.length} chars) — skipping render`,
|
||||
);
|
||||
generatedCode = prebuilt;
|
||||
} else {
|
||||
generatedCode = renderServerCode(spec);
|
||||
@@ -98,11 +107,20 @@ export const worker = new Worker<JobData>(
|
||||
.where(eq(builds.id, buildId));
|
||||
|
||||
await db.update(builds).set({ status: 'building' }).where(eq(builds.id, buildId));
|
||||
await db.update(mcpServers).set({ status: 'building', toolsSchema: spec.tools, updatedAt: new Date() }).where(eq(mcpServers.id, serverId));
|
||||
await db
|
||||
.update(mcpServers)
|
||||
.set({ status: 'building', toolsSchema: spec.tools, updatedAt: new Date() })
|
||||
.where(eq(mcpServers.id, serverId));
|
||||
await emitStatus(buildId, 'building');
|
||||
await log('info', 'Preparing build context...');
|
||||
|
||||
const { contextDir, imageTag } = await prepareBuildContext(serverId, version, slug, generatedCode, spec);
|
||||
const { contextDir, imageTag } = await prepareBuildContext(
|
||||
serverId,
|
||||
version,
|
||||
slug,
|
||||
generatedCode,
|
||||
spec,
|
||||
);
|
||||
await log('info', `Build context at ${contextDir}`);
|
||||
|
||||
await log('info', 'Running static checks...');
|
||||
@@ -112,8 +130,14 @@ export const worker = new Worker<JobData>(
|
||||
const hasDocker = await dockerAvailable();
|
||||
if (!hasDocker) {
|
||||
await log('warn', 'Docker not available — skipping build/deploy. Server marked draft.');
|
||||
await db.update(builds).set({ status: 'failed', errorMessage: 'docker_unavailable', finishedAt: new Date() }).where(eq(builds.id, buildId));
|
||||
await db.update(mcpServers).set({ status: 'failed', updatedAt: new Date() }).where(eq(mcpServers.id, serverId));
|
||||
await db
|
||||
.update(builds)
|
||||
.set({ status: 'failed', errorMessage: 'docker_unavailable', finishedAt: new Date() })
|
||||
.where(eq(builds.id, buildId));
|
||||
await db
|
||||
.update(mcpServers)
|
||||
.set({ status: 'failed', updatedAt: new Date() })
|
||||
.where(eq(mcpServers.id, serverId));
|
||||
await emitDone(buildId, 'failed', serverId, null);
|
||||
return;
|
||||
}
|
||||
@@ -125,7 +149,10 @@ export const worker = new Worker<JobData>(
|
||||
await log('info', 'Image built.');
|
||||
|
||||
await db.update(builds).set({ status: 'deploying' }).where(eq(builds.id, buildId));
|
||||
await db.update(mcpServers).set({ status: 'deploying', updatedAt: new Date() }).where(eq(mcpServers.id, serverId));
|
||||
await db
|
||||
.update(mcpServers)
|
||||
.set({ status: 'deploying', updatedAt: new Date() })
|
||||
.where(eq(mcpServers.id, serverId));
|
||||
await emitStatus(buildId, 'deploying');
|
||||
|
||||
const port = await allocatePort();
|
||||
@@ -140,7 +167,10 @@ export const worker = new Worker<JobData>(
|
||||
};
|
||||
|
||||
const handle = await deployContainer({ serverId, slug, hostPort: port, imageTag, envVars });
|
||||
await log('info', `Container ${handle.containerId.slice(0, 12)} running at ${handle.publicUrl}`);
|
||||
await log(
|
||||
'info',
|
||||
`Container ${handle.containerId.slice(0, 12)} running at ${handle.publicUrl}`,
|
||||
);
|
||||
|
||||
await db
|
||||
.update(builds)
|
||||
@@ -148,7 +178,12 @@ export const worker = new Worker<JobData>(
|
||||
.where(eq(builds.id, buildId));
|
||||
await db
|
||||
.update(mcpServers)
|
||||
.set({ status: 'live', currentVersion: version, publicUrl: handle.publicUrl, updatedAt: new Date() })
|
||||
.set({
|
||||
status: 'live',
|
||||
currentVersion: version,
|
||||
publicUrl: handle.publicUrl,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(mcpServers.id, serverId));
|
||||
|
||||
// Rolling deploy: the new container is live — now retire the previous one.
|
||||
|
||||
Reference in New Issue
Block a user