feat(ai): enable all image models with server-side tier enforcement
This commit is contained in:
14
convex/ai.ts
14
convex/ai.ts
@@ -416,6 +416,12 @@ export const generateImage = action({
|
|||||||
throw new Error(`Unknown model: ${modelId}`);
|
throw new Error(`Unknown model: ${modelId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const subscription = await ctx.runQuery(api.credits.getSubscription, {});
|
||||||
|
const userTier = normalizePublicTier(subscription?.tier);
|
||||||
|
if (!isImageModelAllowedForTier(modelConfig.minTier, userTier)) {
|
||||||
|
throw new Error(`Model ${modelId} requires ${modelConfig.minTier} tier`);
|
||||||
|
}
|
||||||
|
|
||||||
await ctx.runMutation(internal.credits.checkAbuseLimits, {});
|
await ctx.runMutation(internal.credits.checkAbuseLimits, {});
|
||||||
|
|
||||||
let usageIncremented = false;
|
let usageIncremented = false;
|
||||||
@@ -506,6 +512,14 @@ function isVideoModelAllowedForTier(modelTier: "free" | "starter" | "pro", userT
|
|||||||
return tierOrder[userTier] >= tierOrder[modelTier];
|
return tierOrder[userTier] >= tierOrder[modelTier];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isImageModelAllowedForTier(
|
||||||
|
minTier: "free" | "starter" | "pro" | "max",
|
||||||
|
userTier: "free" | "starter" | "pro" | "max"
|
||||||
|
) {
|
||||||
|
const tierOrder = { free: 0, starter: 1, pro: 2, max: 3 } as const;
|
||||||
|
return tierOrder[userTier] >= tierOrder[minTier];
|
||||||
|
}
|
||||||
|
|
||||||
export const setVideoTaskInfo = internalMutation({
|
export const setVideoTaskInfo = internalMutation({
|
||||||
args: {
|
args: {
|
||||||
nodeId: v.id("nodes"),
|
nodeId: v.id("nodes"),
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ export interface OpenRouterModel {
|
|||||||
estimatedCostPerImage: number; // in Euro-Cent (for credit reservation)
|
estimatedCostPerImage: number; // in Euro-Cent (for credit reservation)
|
||||||
/** Gleiche Einheit wie UI „Cr“ / lib/ai-models creditCost */
|
/** Gleiche Einheit wie UI „Cr“ / lib/ai-models creditCost */
|
||||||
creditCost: number;
|
creditCost: number;
|
||||||
|
minTier: "free" | "starter" | "pro" | "max";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: Gemini 2.5 Flash Image only.
|
|
||||||
// Add more models here in Phase 2 when the model selector UI is built.
|
|
||||||
export const IMAGE_MODELS: Record<string, OpenRouterModel> = {
|
export const IMAGE_MODELS: Record<string, OpenRouterModel> = {
|
||||||
"google/gemini-2.5-flash-image": {
|
"google/gemini-2.5-flash-image": {
|
||||||
id: "google/gemini-2.5-flash-image",
|
id: "google/gemini-2.5-flash-image",
|
||||||
@@ -20,6 +19,71 @@ export const IMAGE_MODELS: Record<string, OpenRouterModel> = {
|
|||||||
tier: "standard",
|
tier: "standard",
|
||||||
estimatedCostPerImage: 4, // ~€0.04 in Euro-Cent
|
estimatedCostPerImage: 4, // ~€0.04 in Euro-Cent
|
||||||
creditCost: 4,
|
creditCost: 4,
|
||||||
|
minTier: "free",
|
||||||
|
},
|
||||||
|
"black-forest-labs/flux.2-klein-4b": {
|
||||||
|
id: "black-forest-labs/flux.2-klein-4b",
|
||||||
|
name: "FLUX.2 Klein 4B",
|
||||||
|
tier: "budget",
|
||||||
|
estimatedCostPerImage: 2,
|
||||||
|
creditCost: 2,
|
||||||
|
minTier: "free",
|
||||||
|
},
|
||||||
|
"bytedance-seed/seedream-4.5": {
|
||||||
|
id: "bytedance-seed/seedream-4.5",
|
||||||
|
name: "Seedream 4.5",
|
||||||
|
tier: "standard",
|
||||||
|
estimatedCostPerImage: 5,
|
||||||
|
creditCost: 5,
|
||||||
|
minTier: "free",
|
||||||
|
},
|
||||||
|
"google/gemini-3.1-flash-image-preview": {
|
||||||
|
id: "google/gemini-3.1-flash-image-preview",
|
||||||
|
name: "Gemini 3.1 Flash Image",
|
||||||
|
tier: "standard",
|
||||||
|
estimatedCostPerImage: 6,
|
||||||
|
creditCost: 6,
|
||||||
|
minTier: "free",
|
||||||
|
},
|
||||||
|
"openai/gpt-5-image-mini": {
|
||||||
|
id: "openai/gpt-5-image-mini",
|
||||||
|
name: "GPT-5 Image Mini",
|
||||||
|
tier: "premium",
|
||||||
|
estimatedCostPerImage: 8,
|
||||||
|
creditCost: 8,
|
||||||
|
minTier: "starter",
|
||||||
|
},
|
||||||
|
"sourceful/riverflow-v2-fast": {
|
||||||
|
id: "sourceful/riverflow-v2-fast",
|
||||||
|
name: "Riverflow V2 Fast",
|
||||||
|
tier: "premium",
|
||||||
|
estimatedCostPerImage: 9,
|
||||||
|
creditCost: 9,
|
||||||
|
minTier: "starter",
|
||||||
|
},
|
||||||
|
"sourceful/riverflow-v2-pro": {
|
||||||
|
id: "sourceful/riverflow-v2-pro",
|
||||||
|
name: "Riverflow V2 Pro",
|
||||||
|
tier: "premium",
|
||||||
|
estimatedCostPerImage: 12,
|
||||||
|
creditCost: 12,
|
||||||
|
minTier: "starter",
|
||||||
|
},
|
||||||
|
"google/gemini-3-pro-image-preview": {
|
||||||
|
id: "google/gemini-3-pro-image-preview",
|
||||||
|
name: "Gemini 3 Pro Image",
|
||||||
|
tier: "premium",
|
||||||
|
estimatedCostPerImage: 13,
|
||||||
|
creditCost: 13,
|
||||||
|
minTier: "starter",
|
||||||
|
},
|
||||||
|
"openai/gpt-5-image": {
|
||||||
|
id: "openai/gpt-5-image",
|
||||||
|
name: "GPT-5 Image",
|
||||||
|
tier: "premium",
|
||||||
|
estimatedCostPerImage: 15,
|
||||||
|
creditCost: 15,
|
||||||
|
minTier: "starter",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getAvailableImageModels,
|
getAvailableImageModels,
|
||||||
getModel,
|
getModel,
|
||||||
} from "@/lib/ai-models";
|
} from "@/lib/ai-models";
|
||||||
|
import { IMAGE_MODELS as OPENROUTER_MODELS } from "@/convex/openrouter";
|
||||||
|
|
||||||
describe("ai image models registry", () => {
|
describe("ai image models registry", () => {
|
||||||
it("contains all 9 PRD models in stable order", () => {
|
it("contains all 9 PRD models in stable order", () => {
|
||||||
@@ -32,4 +33,12 @@ describe("ai image models registry", () => {
|
|||||||
it("resolves model lookup", () => {
|
it("resolves model lookup", () => {
|
||||||
expect(getModel(DEFAULT_MODEL_ID)?.creditCost).toBeGreaterThan(0);
|
expect(getModel(DEFAULT_MODEL_ID)?.creditCost).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps frontend and backend image model ids and credit costs in sync", () => {
|
||||||
|
expect(Object.keys(OPENROUTER_MODELS)).toEqual(IMAGE_MODELS.map((model) => model.id));
|
||||||
|
|
||||||
|
for (const model of IMAGE_MODELS) {
|
||||||
|
expect(OPENROUTER_MODELS[model.id]?.creditCost).toBe(model.creditCost);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user