@bonginkan/maria 4.3.33 → 4.3.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6815,6 +6815,45 @@ var init_src = __esm({
6815
6815
  supportedSchemas = /* @__PURE__ */ new Set(["data:", "http:", "https:"]);
6816
6816
  }
6817
6817
  });
6818
+
6819
+ // src/services/media-orchestrator/image-post.ts
6820
+ var image_post_exports = {};
6821
+ __export(image_post_exports, {
6822
+ processImageOptional: () => processImageOptional
6823
+ });
6824
+ async function processImageOptional(bytes, format, keepExif, targetSize) {
6825
+ try {
6826
+ const sharp = (await import('sharp')).default;
6827
+ let img = sharp(bytes).toColourspace("srgb");
6828
+ if (targetSize && Number.isFinite(targetSize.width) && Number.isFinite(targetSize.height)) {
6829
+ const width = Math.max(1, Math.floor(targetSize.width));
6830
+ const height = Math.max(1, Math.floor(targetSize.height));
6831
+ img = img.resize(width, height, { fit: "cover" });
6832
+ }
6833
+ if (keepExif) img = img.withMetadata();
6834
+ const qEnv = Number(process.env.MARIA_SHARP_QUALITY || "80");
6835
+ const quality = Number.isFinite(qEnv) ? Math.max(1, Math.min(100, Math.floor(qEnv))) : 80;
6836
+ const alphaMode = String(process.env.MARIA_SHARP_ALPHA || "").toLowerCase();
6837
+ if (format === "jpg" && alphaMode !== "preserve") {
6838
+ img = img.flatten({ background: { r: 255, g: 255, b: 255 } });
6839
+ }
6840
+ switch (format) {
6841
+ case "png":
6842
+ return await img.png().toBuffer();
6843
+ case "webp":
6844
+ return await img.webp({ quality }).toBuffer();
6845
+ case "jpg":
6846
+ return await img.jpeg({ mozjpeg: true, quality }).toBuffer();
6847
+ }
6848
+ return bytes;
6849
+ } catch {
6850
+ return bytes;
6851
+ }
6852
+ }
6853
+ var init_image_post = __esm({
6854
+ "src/services/media-orchestrator/image-post.ts"() {
6855
+ }
6856
+ });
6818
6857
  var rateLimitStore = /* @__PURE__ */ new Map();
6819
6858
  var RATE_LIMITS = {
6820
6859
  "/image:FREE": { windowMs: 3e3, requests: 1 },
@@ -7105,6 +7144,14 @@ var GeminiMediaProvider = class {
7105
7144
  async generateImage(req) {
7106
7145
  const modelName = this.primaryModel;
7107
7146
  const promptPreview = String(req.prompt ?? "").replace(/\s+/g, " ").slice(0, 200);
7147
+ const targetMime = (() => {
7148
+ const fmt = (req.format || "png").toLowerCase();
7149
+ if (fmt === "jpg") return "image/jpeg";
7150
+ if (fmt === "jpeg") return "image/jpeg";
7151
+ if (fmt === "png") return "image/png";
7152
+ if (fmt === "webp") return "image/webp";
7153
+ return "image/png";
7154
+ })();
7108
7155
  let resp;
7109
7156
  try {
7110
7157
  resp = await this.ai.models.generateContent({
@@ -7112,7 +7159,7 @@ var GeminiMediaProvider = class {
7112
7159
  contents: [{ role: "user", parts: [{ text: String(req.prompt) }] }],
7113
7160
  generationConfig: {
7114
7161
  responseModalities: ["IMAGE"],
7115
- responseMimeType: `image/${req.format || "png"}`
7162
+ responseMimeType: targetMime
7116
7163
  }
7117
7164
  });
7118
7165
  } catch (err) {
@@ -7134,7 +7181,7 @@ var GeminiMediaProvider = class {
7134
7181
  const mime = p?.inlineData?.mimeType || p?.inline_data?.mime_type || p?.inline_data?.mimeType || p?.inlineData?.mime_type;
7135
7182
  if (data) {
7136
7183
  const buf = Buffer.from(String(data), "base64");
7137
- if (buf.length > 0) return { bytes: buf, mime: typeof mime === "string" ? mime : `image/${req.format}` };
7184
+ if (buf.length > 0) return { bytes: buf, mime: typeof mime === "string" ? mime : targetMime };
7138
7185
  }
7139
7186
  }
7140
7187
  try {
@@ -7147,7 +7194,7 @@ var GeminiMediaProvider = class {
7147
7194
  const bytesB64 = img0?.imageBytes || img0?.bytesBase64Encoded;
7148
7195
  if (bytesB64) {
7149
7196
  const buf = Buffer.from(String(bytesB64), "base64");
7150
- if (buf.length > 0) return { bytes: buf, mime: `image/${req.format || "png"}` };
7197
+ if (buf.length > 0) return { bytes: buf, mime: targetMime };
7151
7198
  }
7152
7199
  } catch {
7153
7200
  }
@@ -7167,148 +7214,6 @@ var GeminiMediaProvider = class {
7167
7214
  }
7168
7215
  };
7169
7216
 
7170
- // src/config/plans/free-plan.json
7171
- var free_plan_default = {
7172
- id: "free",
7173
- name: "Free",
7174
- displayName: "Free Plan",
7175
- priceUsd: 0,
7176
- priceJpy: 0,
7177
- status: "active",
7178
- features: {
7179
- priority: "community",
7180
- support: "community",
7181
- accessLevel: "basic"
7182
- },
7183
- buckets: {
7184
- req: 100,
7185
- tokens: 15e4,
7186
- code: 20,
7187
- attachment: 5,
7188
- image: 25,
7189
- video: 5
7190
- },
7191
- models: [
7192
- "google:gemini-2.5-flash",
7193
- "google:gemini-2.0-flash"
7194
- ],
7195
- imageModels: [
7196
- "google:imagen-4-fast",
7197
- "google:gemini-2.5-image"
7198
- ],
7199
- videoModels: [
7200
- "google:veo-3-fast",
7201
- "google:veo-2.0-generate-001"
7202
- ],
7203
- costRules: {
7204
- longContextTokens: {
7205
- threshold: 8e3,
7206
- multiplier: 2,
7207
- description: "Long context (>8k tokens) consumes 2x"
7208
- },
7209
- imageGen: {
7210
- multiplier: 1,
7211
- description: "1 image = 1 image bucket consumption"
7212
- },
7213
- videoGen: {
7214
- multiplier: 1,
7215
- description: "1 video = 1 video bucket consumption"
7216
- }
7217
- },
7218
- limits: {
7219
- image: {
7220
- maxSize: "1024x1024",
7221
- maxWidth: 1024,
7222
- maxHeight: 1024,
7223
- maxCountPerCall: 1,
7224
- formats: ["png", "jpg", "jpeg", "webp", "svg"],
7225
- monthlyLimit: 25
7226
- },
7227
- video: {
7228
- maxDurationSec: 8,
7229
- minDurationSec: 6,
7230
- maxCountPerCall: 1,
7231
- aspectWhitelist: ["16:9", "9:16", "1:1"],
7232
- personGeneration: "DONT_ALLOW",
7233
- monthlyLimit: 5
7234
- },
7235
- code: {
7236
- maxTokensPerRequest: 8e3,
7237
- maxOutputTokens: 2048,
7238
- temperature: {
7239
- min: 0.1,
7240
- max: 0.2,
7241
- default: 0.15
7242
- }
7243
- },
7244
- rateLimit: {
7245
- requestsPerSecond: 0.33,
7246
- description: "1 request per 3 seconds"
7247
- }
7248
- },
7249
- fileSave: {
7250
- allowExtensions: [
7251
- ".ts",
7252
- ".tsx",
7253
- ".js",
7254
- ".jsx",
7255
- ".py",
7256
- ".java",
7257
- ".go",
7258
- ".rs",
7259
- ".html",
7260
- ".css",
7261
- ".scss",
7262
- ".json",
7263
- ".yml",
7264
- ".yaml",
7265
- ".sql",
7266
- ".csv",
7267
- ".md",
7268
- ".sh",
7269
- ".bash",
7270
- ".png",
7271
- ".jpg",
7272
- ".jpeg",
7273
- ".webp",
7274
- ".svg"
7275
- ],
7276
- maxFileSizeMB: 10,
7277
- naming: {
7278
- convention: "kebab-case",
7279
- pattern: "^[a-z0-9-_]+\\.[a-z]+$"
7280
- },
7281
- dirs: {
7282
- default: "outputs",
7283
- images: "outputs/images",
7284
- videos: "outputs/videos",
7285
- code: "outputs/code"
7286
- }
7287
- },
7288
- ui: {
7289
- badge: "FREE",
7290
- badgeColor: "#4CAF50",
7291
- description: "Perfect for getting started with AI development",
7292
- highlights: [
7293
- "100 requests/month",
7294
- "Gemini 2.5 Flash for code",
7295
- "25 images/month (1024px)",
7296
- "5 videos/month (8 sec)",
7297
- "Community support"
7298
- ]
7299
- },
7300
- telemetry: {
7301
- trackingEnabled: true,
7302
- anonymousId: true,
7303
- events: [
7304
- "model_usage",
7305
- "command_execution",
7306
- "error_rate",
7307
- "latency"
7308
- ]
7309
- }
7310
- };
7311
-
7312
7217
  // src/services/intelligent-model-selector/IMSFacade.ts
7313
7218
  init_SecretManagerIntegration();
7314
7219
 
@@ -9532,7 +9437,7 @@ app.get("/api/status", (req, res) => {
9532
9437
  app.get("/", (req, res) => {
9533
9438
  res.json({
9534
9439
  name: "MARIA CODE API",
9535
- version: "4.3.33",
9440
+ version: "4.3.34",
9536
9441
  status: "running",
9537
9442
  environment: process.env.NODE_ENV || "development",
9538
9443
  endpoints: {
@@ -9585,7 +9490,7 @@ function classifyMediaError(err) {
9585
9490
  return { status: 422, code: "policy_violation", message: "Request was blocked by provider policy", hint: "Modify the prompt to comply with safety policies" };
9586
9491
  }
9587
9492
  if (lower2.includes("no inline image returned") || lower2.includes("no video returned") || lower2.includes("refus")) {
9588
- return { status: 422, code: "content_refused", message: "Model refused or returned no content", hint: "Try rephrasing the prompt or a different model" };
9493
+ return { status: 422, code: "content_refused", message: "Model refused or returned no content", hint: "Try rephrasing the prompt" };
9589
9494
  }
9590
9495
  if (lower2.includes("timeout")) {
9591
9496
  return { status: 504, code: "timeout", message: "Generation timed out", hint: "Please retry later" };
@@ -9619,6 +9524,106 @@ async function decodeFirebaseToken(token) {
9619
9524
  return null;
9620
9525
  }
9621
9526
  }
9527
+ function getCurrentPeriodId() {
9528
+ const now = /* @__PURE__ */ new Date();
9529
+ return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
9530
+ }
9531
+ function nextMonthResetISO() {
9532
+ const d = /* @__PURE__ */ new Date();
9533
+ d.setUTCMonth(d.getUTCMonth() + 1, 1);
9534
+ d.setUTCHours(0, 0, 0, 0);
9535
+ return d.toISOString();
9536
+ }
9537
+ var PLAN_LIMITS = {
9538
+ free: { req: 300, tokens: 5e4, code: 40, attachment: 5 },
9539
+ starter: { req: 1400, tokens: 1e6, code: 300, attachment: 50 },
9540
+ "starter-annual": { req: 1400, tokens: 1e6, code: 300, attachment: 50 },
9541
+ pro: { req: 5e3, tokens: 35e5, code: 1200, attachment: 200 },
9542
+ "pro-annual": { req: 5e3, tokens: 35e5, code: 1200, attachment: 200 },
9543
+ ultra: { req: 1e4, tokens: 5e6, code: 5e3, attachment: 500 },
9544
+ "ultra-annual": { req: 1e4, tokens: 5e6, code: 5e3, attachment: 500 },
9545
+ enterprise: { req: -1, tokens: -1, code: -1, attachment: -1 }
9546
+ };
9547
+ function planName(planId) {
9548
+ const names = {
9549
+ free: "Free",
9550
+ starter: "Starter",
9551
+ "starter-annual": "Starter (\u5E74\u984D)",
9552
+ pro: "Pro",
9553
+ "pro-annual": "Pro (\u5E74\u984D)",
9554
+ ultra: "Ultra",
9555
+ "ultra-annual": "Ultra (\u5E74\u984D)",
9556
+ enterprise: "Enterprise"
9557
+ };
9558
+ return names[planId] || "Free";
9559
+ }
9560
+ async function getUserPlanAndLimits(uid) {
9561
+ const db = await getFirestoreSafe();
9562
+ const fallback = { planId: "free", limits: PLAN_LIMITS.free };
9563
+ if (!db) return fallback;
9564
+ try {
9565
+ const snap = await db.collection("user_subscriptions").doc(uid).get();
9566
+ const pid = snap.exists && snap.data()?.planId ? String(snap.data().planId) : "free";
9567
+ return { planId: pid, limits: PLAN_LIMITS[pid] || PLAN_LIMITS.free };
9568
+ } catch {
9569
+ return fallback;
9570
+ }
9571
+ }
9572
+ async function ensureUsageDoc(uid) {
9573
+ const db = await getFirestoreSafe();
9574
+ if (!db) return { ref: null, data: null, planId: "free", limits: PLAN_LIMITS.free };
9575
+ const periodId = getCurrentPeriodId();
9576
+ const { planId: pid, limits } = await getUserPlanAndLimits(uid);
9577
+ const ref = db.collection("users").doc(uid).collection("usage").doc(periodId);
9578
+ const snap = await ref.get();
9579
+ if (!snap.exists) {
9580
+ const nowISO = (/* @__PURE__ */ new Date()).toISOString();
9581
+ const init = {
9582
+ periodId,
9583
+ createdAt: nowISO,
9584
+ updatedAt: nowISO,
9585
+ resetAt: nextMonthResetISO(),
9586
+ limits,
9587
+ remain: limits,
9588
+ used: { req: 0, tokens: 0, code: 0, attachment: 0 }
9589
+ };
9590
+ await ref.set(init, { merge: true });
9591
+ return { ref, data: init, planId: pid, limits };
9592
+ }
9593
+ return { ref, data: snap.data(), planId: pid, limits };
9594
+ }
9595
+ async function applyConsumption(uid, consumption, idemKey) {
9596
+ const db = await getFirestoreSafe();
9597
+ if (!db) return null;
9598
+ const { ref, data, planId: pid, limits } = await ensureUsageDoc(uid);
9599
+ const nowISO = (/* @__PURE__ */ new Date()).toISOString();
9600
+ if (idemKey) {
9601
+ const idemRef = ref.collection("consumptions").doc(idemKey);
9602
+ const idemSnap = await idemRef.get();
9603
+ if (!idemSnap.exists) await idemRef.set({ createdAt: nowISO, consumption });
9604
+ else return data;
9605
+ }
9606
+ const toNum = (v) => typeof v === "number" && isFinite(v) ? v : 0;
9607
+ const incReq = toNum(consumption.requests) + toNum(consumption.req);
9608
+ const incTokens = toNum(consumption.tokens);
9609
+ const incCode = toNum(consumption.code) + toNum(consumption.image) + toNum(consumption.video);
9610
+ const incAttachment = toNum(consumption.attachment);
9611
+ const used = data?.used || { req: 0, tokens: 0, code: 0, attachment: 0 };
9612
+ const newUsed = {
9613
+ req: Math.max(0, used.req + incReq),
9614
+ tokens: Math.max(0, used.tokens + incTokens),
9615
+ code: Math.max(0, used.code + incCode),
9616
+ attachment: Math.max(0, used.attachment + incAttachment)
9617
+ };
9618
+ const remain = {
9619
+ req: limits.req < 0 ? -1 : Math.max(0, limits.req - newUsed.req),
9620
+ tokens: limits.tokens < 0 ? -1 : Math.max(0, limits.tokens - newUsed.tokens),
9621
+ code: limits.code < 0 ? -1 : Math.max(0, limits.code - newUsed.code),
9622
+ attachment: limits.attachment < 0 ? -1 : Math.max(0, limits.attachment - newUsed.attachment)
9623
+ };
9624
+ await ref.set({ used: newUsed, remain, updatedAt: nowISO }, { merge: true });
9625
+ return { ...data || {}, used: newUsed, remain, limits, periodId: getCurrentPeriodId(), updatedAt: nowISO };
9626
+ }
9622
9627
  app.get("/api/user/profile", async (req, res) => {
9623
9628
  try {
9624
9629
  const authHeader = req.headers.authorization || "";
@@ -9639,19 +9644,21 @@ app.get("/api/user/profile", async (req, res) => {
9639
9644
  if (Array.isArray(ids["github.com"]) && ids["github.com"].length > 0) provider = "github";
9640
9645
  else if (Array.isArray(ids["google.com"]) && ids["google.com"].length > 0) provider = "google";
9641
9646
  }
9642
- const plan = "FREE";
9647
+ const { planId: pid, limits } = await getUserPlanAndLimits(uid);
9648
+ const periodRef = await ensureUsageDoc(uid);
9649
+ const currentUsedReq = Number(periodRef?.data?.used?.req || 0);
9643
9650
  const response2 = {
9644
9651
  id: uid,
9645
9652
  email,
9646
9653
  name: displayName,
9647
9654
  provider: provider || "unknown",
9648
- plan,
9655
+ plan: String(pid).toUpperCase(),
9649
9656
  usage: {
9650
- requests: 0,
9651
- requestLimit: 1e3,
9652
- tokens: 0,
9653
- tokenLimit: 5e5,
9654
- resetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3).toISOString()
9657
+ requests: currentUsedReq,
9658
+ requestLimit: limits.req < 0 ? Number.MAX_SAFE_INTEGER : limits.req,
9659
+ tokens: Number(periodRef?.data?.used?.tokens || 0),
9660
+ tokenLimit: limits.tokens < 0 ? Number.MAX_SAFE_INTEGER : limits.tokens,
9661
+ resetDate: periodRef?.data?.resetAt || nextMonthResetISO()
9655
9662
  },
9656
9663
  models: ["gpt-5", "gemini-2.5-pro"]
9657
9664
  };
@@ -9762,7 +9769,9 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
9762
9769
  const decoded = await decodeFirebaseToken(idToken).catch(() => null);
9763
9770
  if (!decoded) return res.status(401).json({ error: "unauthorized", message: "Invalid login session", hint: "Re-login to continue" });
9764
9771
  const uid = decoded?.uid || decoded?.sub || "current";
9765
- const { prompt, model, size = "1024x1024", format = "png", count = 1, seed } = req.body || {};
9772
+ const { prompt, model, size = "1024x1024", format: formatRaw = "png", count = 1, seed } = req.body || {};
9773
+ const fmt0 = String(formatRaw || "png").toLowerCase();
9774
+ const format = fmt0 === "jpeg" ? "jpg" : ["png", "jpg", "webp"].includes(fmt0) ? fmt0 : "png";
9766
9775
  if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
9767
9776
  const m2 = /^(\d{2,4})x(\d{2,4})$/.exec(String(size));
9768
9777
  if (!m2) return res.status(400).json({ error: "bad_request", message: "size must be WxH" });
@@ -9772,7 +9781,13 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
9772
9781
  const buffers = [];
9773
9782
  for (let i2 = 0; i2 < Math.max(1, Math.min(8, Number(count || 1))); i2++) {
9774
9783
  const r2 = await provider.generateImage({ prompt, width: w, height: h2, format, seed: (seed ?? 0) + i2 });
9775
- buffers.push(r2.bytes);
9784
+ const processed = await (await Promise.resolve().then(() => (init_image_post(), image_post_exports))).processImageOptional(
9785
+ r2.bytes,
9786
+ String(format),
9787
+ false,
9788
+ { width: w, height: h2 }
9789
+ );
9790
+ buffers.push(processed);
9776
9791
  }
9777
9792
  const promptHash = hashPrompt(prompt);
9778
9793
  const manifest = {
@@ -9793,10 +9808,10 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
9793
9808
  uid
9794
9809
  });
9795
9810
  const idemKey = req.headers["idempotency-key"] || void 0;
9796
- await recordConsumption(uid, { requests: 1, image: Math.max(1, buffers.length) }, idemKey);
9811
+ await applyConsumption(uid, { requests: 1, image: Math.max(1, buffers.length) }, idemKey);
9797
9812
  const filesInline = buffers.map((b, idx) => ({
9798
9813
  file: saved.files[idx] || "",
9799
- mime: `image/${format}`,
9814
+ mime: format === "jpg" ? "image/jpeg" : `image/${format}`,
9800
9815
  bytesBase64: b.toString("base64")
9801
9816
  }));
9802
9817
  return res.json({ success: true, data: { url: saved.manifestPath, files: saved.files, filesInline, jobId: manifest.trace } });
@@ -10038,7 +10053,7 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
10038
10053
  const idToken = auth.substring("Bearer ".length).trim();
10039
10054
  const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10040
10055
  const uid = decoded?.uid || decoded?.sub || "current";
10041
- await recordConsumption(uid, { requests: 1, video: 1 }, idemKey);
10056
+ await applyConsumption(uid, { requests: 1, video: 1 }, idemKey);
10042
10057
  jobIndex.set(String(manifest.trace), {
10043
10058
  id: String(manifest.trace),
10044
10059
  status: "completed",
@@ -10091,6 +10106,12 @@ app.get("/api/v1/jobs/:id", async (req, res) => {
10091
10106
  });
10092
10107
  app.post("/api/v1/code", rateLimitMiddleware, async (req, res) => {
10093
10108
  try {
10109
+ const auth = req.headers.authorization;
10110
+ if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10111
+ const idToken = auth.substring("Bearer ".length).trim();
10112
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10113
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10114
+ const uid = decoded?.uid || decoded?.sub;
10094
10115
  const { prompt, language = "typescript", model = "gpt-4" } = req.body;
10095
10116
  if (!prompt) {
10096
10117
  return res.status(400).json({
@@ -10098,6 +10119,8 @@ app.post("/api/v1/code", rateLimitMiddleware, async (req, res) => {
10098
10119
  message: "Prompt is required"
10099
10120
  });
10100
10121
  }
10122
+ const idemKey = req.headers["idempotency-key"] || void 0;
10123
+ await applyConsumption(uid, { requests: 1, code: 1 }, idemKey);
10101
10124
  return res.json({
10102
10125
  success: true,
10103
10126
  data: {
@@ -10121,6 +10144,12 @@ function example() {
10121
10144
  });
10122
10145
  app.post("/api/v1/chat", rateLimitMiddleware, async (req, res) => {
10123
10146
  try {
10147
+ const auth = req.headers.authorization;
10148
+ if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10149
+ const idToken = auth.substring("Bearer ".length).trim();
10150
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10151
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10152
+ const uid = decoded?.uid || decoded?.sub;
10124
10153
  const { message, model = "gpt-4" } = req.body;
10125
10154
  if (!message) {
10126
10155
  return res.status(400).json({
@@ -10128,6 +10157,8 @@ app.post("/api/v1/chat", rateLimitMiddleware, async (req, res) => {
10128
10157
  message: "Message is required"
10129
10158
  });
10130
10159
  }
10160
+ const idemKey = req.headers["idempotency-key"] || void 0;
10161
+ await applyConsumption(uid, { requests: 1 }, idemKey);
10131
10162
  return res.json({
10132
10163
  success: true,
10133
10164
  data: {
@@ -10150,6 +10181,11 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10150
10181
  if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10151
10182
  const { prompt, taskType } = req.body || {};
10152
10183
  if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
10184
+ const idToken = auth.substring("Bearer ".length).trim();
10185
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10186
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10187
+ const uid = decoded?.uid || decoded?.sub;
10188
+ const idemKey = req.headers["idempotency-key"] || void 0;
10153
10189
  if (process.env.MARIA_TELEMETRY === "1") {
10154
10190
  try {
10155
10191
  console.log(JSON.stringify({ ev: "ai_proxy_request", taskType: taskType || "unknown", promptLen: String(prompt).length }));
@@ -10196,6 +10232,12 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10196
10232
  } catch {
10197
10233
  }
10198
10234
  }
10235
+ const consumption2 = { requests: 1 };
10236
+ if (taskType === "code" || taskType === "evaluation") consumption2.code = 1;
10237
+ try {
10238
+ await applyConsumption(uid, consumption2, idemKey);
10239
+ } catch {
10240
+ }
10199
10241
  return res.json({ data: { content: content2, routedModel: { vendor: "google", family: "gemini", name: modelName, reason: taskType || "code" } } });
10200
10242
  } catch (e2) {
10201
10243
  console.warn("[AI Proxy] Gemini path failed, falling back to OpenAI:", e2?.message || e2);
@@ -10221,6 +10263,7 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10221
10263
  const client = new OpenAI2({ apiKey: openaiKey });
10222
10264
  let model = process.env.MARIA_CODE_MODEL || "gpt-5-mini";
10223
10265
  let content = "";
10266
+ let totalTokens = 0;
10224
10267
  try {
10225
10268
  const r2 = await client.responses.create({
10226
10269
  model,
@@ -10230,6 +10273,12 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10230
10273
  ]
10231
10274
  });
10232
10275
  content = r2?.output_text || r2?.content?.[0]?.text || "";
10276
+ const u = r2?.usage;
10277
+ if (u) {
10278
+ const inTok = Number(u.input_tokens || u.inputTokens || 0);
10279
+ const outTok = Number(u.output_tokens || u.outputTokens || 0);
10280
+ totalTokens = Number(u.total_tokens || u.totalTokens || inTok + outTok || 0);
10281
+ }
10233
10282
  } catch (_e) {
10234
10283
  model = "gpt-4o-mini";
10235
10284
  const r2 = await client.chat.completions.create({
@@ -10240,6 +10289,8 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10240
10289
  ]
10241
10290
  });
10242
10291
  content = r2.choices?.[0]?.message?.content || "";
10292
+ const u2 = r2?.usage;
10293
+ if (u2) totalTokens = Number(u2.total_tokens || 0);
10243
10294
  }
10244
10295
  if (process.env.MARIA_TELEMETRY === "1") {
10245
10296
  try {
@@ -10247,6 +10298,13 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10247
10298
  } catch {
10248
10299
  }
10249
10300
  }
10301
+ const consumption = { requests: 1 };
10302
+ if (totalTokens > 0) consumption.tokens = totalTokens;
10303
+ if (taskType === "code" || taskType === "evaluation") consumption.code = 1;
10304
+ try {
10305
+ await applyConsumption(uid, consumption, idemKey);
10306
+ } catch {
10307
+ }
10250
10308
  return res.json({ data: { content, routedModel: { vendor: "openai", family: "gpt", name: model, reason: taskType || "code" } } });
10251
10309
  } catch (error) {
10252
10310
  console.error("[AI Proxy] Error:", error);
@@ -10274,89 +10332,35 @@ async function getFirestoreSafe() {
10274
10332
  return null;
10275
10333
  }
10276
10334
  }
10277
- function calcNextReset() {
10278
- const d = /* @__PURE__ */ new Date();
10279
- d.setUTCMonth(d.getUTCMonth() + 1, 1);
10280
- d.setUTCHours(0, 0, 0, 0);
10281
- return d.toISOString();
10282
- }
10283
- async function recordConsumption(uid, consumption, idempotencyKey) {
10284
- const db = await getFirestoreSafe();
10285
- if (!db) return;
10286
- const docPath = `projects/default/usage/${uid}`;
10287
- const usageRef = db.doc(docPath);
10288
- const nowISO = (/* @__PURE__ */ new Date()).toISOString();
10289
- if (idempotencyKey) {
10290
- const idemRef = db.doc(`projects/default/usage/${uid}/consumptions/${idempotencyKey}`);
10291
- const idemSnap = await idemRef.get();
10292
- if (idemSnap.exists) return;
10293
- await idemRef.set({ createdAt: nowISO, consumption });
10294
- }
10295
- const snap = await usageRef.get();
10296
- let data = snap.exists ? snap.data() : null;
10297
- if (!data) {
10298
- data = {
10299
- plan: { name: "FREE", limits: { requests: free_plan_default.buckets.req } },
10300
- monthly: {
10301
- requests: { used: 0, limit: free_plan_default.buckets.req },
10302
- image: { used: 0, limit: free_plan_default.buckets.image },
10303
- video: { used: 0, limit: free_plan_default.buckets.video },
10304
- code: { used: 0, limit: free_plan_default.buckets.code },
10305
- resetAt: calcNextReset()
10306
- },
10307
- updatedAt: nowISO
10308
- };
10309
- }
10310
- const m2 = data.monthly || {};
10311
- if (consumption?.requests) m2.requests.used = Math.max(0, (m2.requests.used || 0) + Number(consumption.requests));
10312
- if (consumption?.image) m2.image.used = Math.max(0, (m2.image.used || 0) + Number(consumption.image));
10313
- if (consumption?.video) m2.video.used = Math.max(0, (m2.video.used || 0) + Number(consumption.video));
10314
- if (consumption?.code) m2.code.used = Math.max(0, (m2.code.used || 0) + Number(consumption.code));
10315
- data.monthly = m2;
10316
- data.updatedAt = nowISO;
10317
- await usageRef.set(data, { merge: true });
10318
- }
10319
10335
  app.get("/api/v1/usage", rateLimitMiddleware, async (req, res) => {
10320
10336
  try {
10321
10337
  const auth = req.headers.authorization;
10322
10338
  if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10323
- const db = await getFirestoreSafe();
10324
- const uid = "current";
10325
- const docPath = `projects/default/usage/${uid}`;
10326
- let data = null;
10327
- if (db) {
10328
- const snap = await db.doc(docPath).get();
10329
- data = snap.exists ? snap.data() : null;
10330
- if (!data) {
10331
- data = {
10332
- plan: { name: "FREE", limits: { requests: free_plan_default.buckets.req } },
10333
- monthly: {
10334
- requests: { used: 0, limit: free_plan_default.buckets.req },
10335
- image: { used: 0, limit: free_plan_default.buckets.image },
10336
- video: { used: 0, limit: free_plan_default.buckets.video },
10337
- code: { used: 0, limit: free_plan_default.buckets.code },
10338
- resetAt: calcNextReset()
10339
- },
10340
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
10341
- };
10342
- await db.doc(docPath).set(data);
10343
- }
10344
- const historySnap = await db.collection(`projects/default/usage/${uid}/consumptions`).orderBy("createdAt", "desc").limit(20).get();
10345
- const history = historySnap.docs.map((d) => ({ id: d.id, ...d.data() }));
10346
- data.history = history;
10347
- } else {
10348
- data = {
10349
- plan: { name: "FREE", limits: { requests: free_plan_default.buckets.req } },
10350
- monthly: {
10351
- requests: { used: 0, limit: free_plan_default.buckets.req },
10352
- image: { used: 0, limit: free_plan_default.buckets.image },
10353
- video: { used: 0, limit: free_plan_default.buckets.video },
10354
- code: { used: 0, limit: free_plan_default.buckets.code },
10355
- resetAt: calcNextReset()
10356
- }
10357
- };
10358
- }
10359
- return res.json(data);
10339
+ const idToken = auth.substring("Bearer ".length).trim();
10340
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10341
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10342
+ const uid = decoded?.uid || decoded?.sub;
10343
+ const { planId: pid, limits } = await getUserPlanAndLimits(uid);
10344
+ const { data } = await ensureUsageDoc(uid);
10345
+ const periodId = getCurrentPeriodId();
10346
+ const used = data?.used || { req: 0, tokens: 0, code: 0, attachment: 0 };
10347
+ const remain = data?.remain || limits;
10348
+ const percentage = {
10349
+ req: limits.req > 0 ? Math.round(used.req / limits.req * 100) : 0,
10350
+ tokens: limits.tokens > 0 ? Math.round(used.tokens / limits.tokens * 100) : 0,
10351
+ code: limits.code > 0 ? Math.round(used.code / limits.code * 100) : 0,
10352
+ attachment: limits.attachment > 0 ? Math.round(used.attachment / limits.attachment * 100) : 0
10353
+ };
10354
+ return res.json({
10355
+ periodId,
10356
+ planCode: pid,
10357
+ planName: planName(pid),
10358
+ used,
10359
+ remain,
10360
+ limits,
10361
+ percentage,
10362
+ resetAt: data?.resetAt || nextMonthResetISO()
10363
+ });
10360
10364
  } catch (e2) {
10361
10365
  console.error("[Usage] GET error", e2);
10362
10366
  return res.status(500).json({ error: "internal_error" });
@@ -10368,18 +10372,23 @@ app.post("/api/v1/usage", rateLimitMiddleware, async (req, res) => {
10368
10372
  if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10369
10373
  const { consumption } = req.body || {};
10370
10374
  const idemKey = req.headers["idempotency-key"] || void 0;
10371
- const db = await getFirestoreSafe();
10372
- const uid = "current";
10373
- const docPath = `projects/default/usage/${uid}`;
10374
- let data = null;
10375
- if (db) {
10376
- await recordConsumption(uid, consumption || {}, idemKey);
10377
- const snap = await db.doc(docPath).get();
10378
- data = snap.exists ? snap.data() : { ok: true };
10379
- } else {
10380
- data = { ok: true };
10381
- }
10382
- return res.json(data);
10375
+ const idToken = auth.substring("Bearer ".length).trim();
10376
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10377
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10378
+ const uid = decoded?.uid || decoded?.sub;
10379
+ await applyConsumption(uid, consumption || {}, idemKey);
10380
+ const { data } = await ensureUsageDoc(uid);
10381
+ const { planId: pid, limits } = await getUserPlanAndLimits(uid);
10382
+ const periodId = getCurrentPeriodId();
10383
+ const used = data?.used || { req: 0, tokens: 0, code: 0, attachment: 0 };
10384
+ const remain = data?.remain || limits;
10385
+ const percentage = {
10386
+ req: limits.req > 0 ? Math.round(used.req / limits.req * 100) : 0,
10387
+ tokens: limits.tokens > 0 ? Math.round(used.tokens / limits.tokens * 100) : 0,
10388
+ code: limits.code > 0 ? Math.round(used.code / limits.code * 100) : 0,
10389
+ attachment: limits.attachment > 0 ? Math.round(used.attachment / limits.attachment * 100) : 0
10390
+ };
10391
+ return res.json({ periodId, planCode: pid, planName: planName(pid), used, remain, limits, percentage, resetAt: data?.resetAt || nextMonthResetISO() });
10383
10392
  } catch (e2) {
10384
10393
  console.error("[Usage] POST error", e2);
10385
10394
  return res.status(500).json({ error: "internal_error" });