@bonginkan/maria 4.3.46 → 4.4.1

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.
@@ -6854,6 +6854,65 @@ var init_image_post = __esm({
6854
6854
  "src/services/media-orchestrator/image-post.ts"() {
6855
6855
  }
6856
6856
  });
6857
+
6858
+ // src/services/routing/model-routing.ts
6859
+ var model_routing_exports = {};
6860
+ __export(model_routing_exports, {
6861
+ determineRouting: () => determineRouting,
6862
+ providerFromModel: () => providerFromModel
6863
+ });
6864
+ function providerFromModel(model) {
6865
+ const m2 = (model || "").toLowerCase();
6866
+ if (!m2) return void 0;
6867
+ if (m2.startsWith("gemini") || m2.startsWith("veo")) return "google";
6868
+ if (m2.startsWith("gpt") || m2.startsWith("sora")) return "openai";
6869
+ return void 0;
6870
+ }
6871
+ function determineRouting(input) {
6872
+ const requestedProvider = normalizeProvider(input.requestedProvider);
6873
+ const requestedModel = input.requestedModel?.trim();
6874
+ const isPro = !!input.isProOrAbove;
6875
+ if (requestedModel && requestedModel.length > 0) {
6876
+ const p = providerFromModel(requestedModel);
6877
+ if (!p) return { error: "unknown_model", message: `Unknown model family for '${requestedModel}'` };
6878
+ if (p === "google" && !input.hasGeminiKey) return { error: "missing_key", message: `Requested model '${requestedModel}' requires Google API key` };
6879
+ if (p === "openai" && !input.hasOpenAIKey) return { error: "missing_key", message: `Requested model '${requestedModel}' requires OpenAI API key` };
6880
+ return { provider: p, model: requestedModel };
6881
+ }
6882
+ if (requestedProvider) {
6883
+ if (requestedProvider === "google") {
6884
+ if (!input.hasGeminiKey) return { error: "missing_key", message: "Google API key is not configured" };
6885
+ return { provider: "google", model: "gemini-2.5-flash" };
6886
+ }
6887
+ if (requestedProvider === "openai") {
6888
+ if (!input.hasOpenAIKey) return { error: "missing_key", message: "OpenAI API key is not configured" };
6889
+ return { provider: "openai", model: isPro ? "gpt-5" : "gpt-5-mini" };
6890
+ }
6891
+ }
6892
+ if ((input.taskType || "").toLowerCase() === "research") {
6893
+ if (!input.hasGeminiKey) return { error: "missing_key", message: "Google API key is required for research task" };
6894
+ return { provider: "google", model: "gemini-2.5-flash" };
6895
+ }
6896
+ if (isPro && input.hasOpenAIKey) {
6897
+ return { provider: "openai", model: "gpt-5" };
6898
+ }
6899
+ if (input.hasGeminiKey) {
6900
+ return { provider: "google", model: "gemini-2.5-flash" };
6901
+ }
6902
+ if (input.hasOpenAIKey) {
6903
+ return { provider: "openai", model: "gpt-5-mini" };
6904
+ }
6905
+ return { error: "no_provider_available", message: "No valid provider API key configured" };
6906
+ }
6907
+ function normalizeProvider(p) {
6908
+ const s2 = (p || "").toLowerCase().trim();
6909
+ if (s2 === "google" || s2 === "openai") return s2;
6910
+ return void 0;
6911
+ }
6912
+ var init_model_routing = __esm({
6913
+ "src/services/routing/model-routing.ts"() {
6914
+ }
6915
+ });
6857
6916
  var rateLimitStore = /* @__PURE__ */ new Map();
6858
6917
  var RATE_LIMITS = {
6859
6918
  "/image:FREE": { windowMs: 3e3, requests: 1 },
@@ -7219,7 +7278,7 @@ var GeminiMediaProvider = class {
7219
7278
  const fps = typeof req?.fps === "number" ? req.fps : "n/a";
7220
7279
  const duration = typeof req?.duration === "number" ? req.duration : "n/a";
7221
7280
  throw new Error(
7222
- `GeminiMediaProvider.generateVideoFrames is not supported locally. Use server /api/v1/video (veo-3.0-generate-001). requested_fps=${fps}; requested_duration=${duration}`
7281
+ `GeminiMediaProvider.generateVideoFrames is not supported locally. Use server /api/v1/video (veo-3.1-generate-preview). requested_fps=${fps}; requested_duration=${duration}`
7223
7282
  );
7224
7283
  }
7225
7284
  };
@@ -9447,7 +9506,7 @@ app.get("/api/status", (req, res) => {
9447
9506
  app.get("/", (req, res) => {
9448
9507
  res.json({
9449
9508
  name: "MARIA CODE API",
9450
- version: "4.3.46",
9509
+ version: "4.4.1",
9451
9510
  status: "running",
9452
9511
  environment: process.env.NODE_ENV || "development",
9453
9512
  endpoints: {
@@ -9931,7 +9990,8 @@ app.post("/api/v1/image", rateLimitMiddleware, async (req, res) => {
9931
9990
  mime: format === "jpg" ? "image/jpeg" : `image/${format}`,
9932
9991
  bytesBase64: b.toString("base64")
9933
9992
  }));
9934
- return res.json({ success: true, data: { url: saved.manifestPath, files: saved.files, filesInline, jobId: manifest.trace } });
9993
+ const publicUrl = saved.manifestPath.startsWith("/") ? saved.manifestPath : `/${saved.manifestPath}`;
9994
+ return res.json({ success: true, data: { url: publicUrl, files: saved.files, filesInline, jobId: manifest.trace } });
9935
9995
  } catch (error) {
9936
9996
  console.error("[Image API] Error:", error);
9937
9997
  const mapped = classifyMediaError(error);
@@ -9943,7 +10003,7 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
9943
10003
  await loadProviderKeys();
9944
10004
  const auth = req.headers.authorization;
9945
10005
  if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized", message: "Login required", hint: "Sign in and retry" });
9946
- const { prompt, duration = 8, fps = 24, res: resStr = "1280x720", aspect: aspectStr, format = "mp4", model, seed } = req.body || {};
10006
+ const { prompt, duration = 8, fps = 24, res: resStr = "1280x720", aspect: aspectStr, format = "mp4", model, seed, provider: reqProvider } = req.body || {};
9947
10007
  if (!prompt) return res.status(400).json({ error: "bad_request", message: "prompt required" });
9948
10008
  let w, h2;
9949
10009
  const aspect = typeof aspectStr === "string" && (aspectStr === "16:9" || aspectStr === "9:16") ? aspectStr : void 0;
@@ -9971,15 +10031,108 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
9971
10031
  } else {
9972
10032
  return res.status(400).json({ error: "bad_request", message: "res must be WxH or 720|1080" });
9973
10033
  }
10034
+ const vidToken = auth.substring("Bearer ".length).trim();
10035
+ const vidDecoded = await decodeFirebaseToken(vidToken).catch(() => null);
10036
+ const vidUid = vidDecoded?.uid || vidDecoded?.sub || "current";
10037
+ let isProOrAbove = false;
10038
+ try {
10039
+ const { planId: _pid } = await getUserPlanAndLimits(vidUid);
10040
+ const p = String(_pid || "free").toLowerCase();
10041
+ isProOrAbove = ["pro", "pro-annual", "ultra", "ultra-annual", "enterprise"].includes(p);
10042
+ } catch {
10043
+ }
10044
+ const requestedProvider = typeof reqProvider === "string" ? reqProvider.toLowerCase() : void 0;
10045
+ const requestedModel = typeof model === "string" ? String(model).trim().toLowerCase() : void 0;
10046
+ const providerFromModel2 = (() => {
10047
+ if (!requestedModel) return void 0;
10048
+ if (requestedModel.startsWith("sora")) return "openai";
10049
+ if (requestedModel.startsWith("veo") || requestedModel.startsWith("gemini")) return "google";
10050
+ return void 0;
10051
+ })();
10052
+ const openaiKey = process.env.OPENAI_API_KEY;
10053
+ const goOpenAI = providerFromModel2 === "openai" || requestedProvider === "openai" || !providerFromModel2 && !requestedProvider && (isProOrAbove && !!openaiKey);
10054
+ if (goOpenAI && openaiKey) {
10055
+ const OpenAI2 = (await import('openai')).default;
10056
+ const client = new OpenAI2({ apiKey: openaiKey });
10057
+ const soraModel = requestedModel && requestedModel.startsWith("sora") ? requestedModel : "sora-2";
10058
+ const secondsStr = (() => {
10059
+ const d = Number(duration) || 8;
10060
+ if (d <= 4) return "4";
10061
+ if (d <= 8) return "8";
10062
+ return "12";
10063
+ })();
10064
+ const size = `${w}x${h2}`;
10065
+ const startedMs2 = Date.now();
10066
+ let job;
10067
+ try {
10068
+ job = await client.videos.create({ model: soraModel, prompt: String(prompt), seconds: secondsStr, size });
10069
+ } catch (e2) {
10070
+ try {
10071
+ console.warn("[Video API][Sora] videos.create failed:", e2?.message || String(e2));
10072
+ } catch {
10073
+ }
10074
+ throw e2;
10075
+ }
10076
+ let safetyDeadline = Date.now() + 72e4;
10077
+ while (job && (job.status === "queued" || job.status === "in_progress")) {
10078
+ if (Date.now() > safetyDeadline) return res.status(504).json({ error: "timeout", message: "video generation timed out" });
10079
+ await new Promise((r2) => setTimeout(r2, 8e3));
10080
+ job = await client.videos.retrieve(job.id);
10081
+ }
10082
+ if (!job || job.status !== "completed") {
10083
+ return res.status(500).json({ error: "internal_error", message: `video generation failed: ${job?.status || "unknown"}` });
10084
+ }
10085
+ const downloadWithBackoff = async () => {
10086
+ let delay = 2e3;
10087
+ for (let i2 = 0; i2 < 6; i2++) {
10088
+ try {
10089
+ const content = await client.videos.downloadContent(job.id);
10090
+ const ab = await content.arrayBuffer();
10091
+ return Buffer.from(ab);
10092
+ } catch (e2) {
10093
+ if (i2 === 5) throw e2;
10094
+ await new Promise((r2) => setTimeout(r2, delay));
10095
+ delay = Math.min(15e3, Math.floor(delay * 1.8));
10096
+ }
10097
+ }
10098
+ throw new Error("download failed after retries");
10099
+ };
10100
+ const videoBytes2 = await downloadWithBackoff();
10101
+ const outExt2 = ".mp4";
10102
+ const traceId2 = Math.random().toString(36).slice(2, 8).toUpperCase();
10103
+ const promptHash2 = hashPrompt(prompt);
10104
+ const manifest2 = {
10105
+ kind: "video",
10106
+ request: { promptHash: promptHash2, seed, params: { size: [w, h2], fps: Number(fps) || 24, duration: Number(secondsStr), format: "mp4" }, model: soraModel, provider: "openai" },
10107
+ artifacts: [],
10108
+ metrics: { durationMs: Date.now() - startedMs2, retries: 0, fallbacks: 0 },
10109
+ trace: traceId2
10110
+ };
10111
+ const baseDir2 = path__namespace.default.join("artifacts", "media", "videos", traceId2);
10112
+ const saved2 = await saveArtifacts({ root: process.cwd(), kind: "video", baseDir: baseDir2, flat: true, trace: traceId2 }, [{ bytes: videoBytes2, ext: outExt2, logicalName: "video" }], manifest2);
10113
+ const idemKey2 = req.headers["idempotency-key"] || void 0;
10114
+ await applyConsumption(vidUid, { requests: 1, video: 1 }, idemKey2);
10115
+ jobIndex.set(String(manifest2.trace), { id: String(manifest2.trace), status: "completed", kind: "video", manifestPath: saved2.manifestPath, createdAt: (/* @__PURE__ */ new Date()).toISOString(), updatedAt: (/* @__PURE__ */ new Date()).toISOString(), uid: vidUid });
10116
+ const publicUrl = saved2.manifestPath.startsWith("/") ? saved2.manifestPath : `/${saved2.manifestPath}`;
10117
+ return res.json({ success: true, data: { url: publicUrl || (saved2.files[0] ? `/${saved2.files[0]}` : void 0), files: saved2.files, jobId: manifest2.trace, applied: { durationSeconds: Number(secondsStr), frameRate: Number(fps) || 24 } } });
10118
+ }
9974
10119
  const { GoogleGenAI } = __require("@google/genai");
9975
10120
  const apiKey = process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
9976
10121
  if (!apiKey) return res.status(503).json({ error: "provider_unavailable", message: "Provider API key is not configured", hint: "Set GOOGLE_API_KEY or GEMINI_API_KEY on the server" });
9977
10122
  const ai = new GoogleGenAI({ apiKey });
9978
- const veoModel = model && String(model).trim() || process.env.MARIA_VIDEO_MODEL || "veo-3.0-generate-001";
9979
- const aspectRatio = "16:9";
10123
+ const veoModel = model && String(model).trim() || process.env.MARIA_VIDEO_MODEL || "veo-3.1-generate-preview";
10124
+ const aspectRatio = w >= h2 ? "16:9" : "9:16";
9980
10125
  const startedMs = Date.now();
9981
10126
  const requestedDuration = Number(duration);
9982
- const effectiveDuration = Number.isFinite(requestedDuration) && requestedDuration === 8 ? 8 : 8;
10127
+ const maxEdge = Math.max(w, h2);
10128
+ const is1080 = maxEdge >= 1920;
10129
+ const effectiveDuration = (() => {
10130
+ if (is1080) return 8;
10131
+ const d = Number.isFinite(requestedDuration) ? Math.floor(requestedDuration) : 8;
10132
+ if (d <= 4) return 4;
10133
+ if (d <= 6) return 6;
10134
+ return 8;
10135
+ })();
9983
10136
  const requestedFps = Number(fps);
9984
10137
  const effectiveFps = Number.isFinite(requestedFps) ? Math.min(60, Math.max(1, Math.floor(requestedFps))) : 24;
9985
10138
  const resolution = Math.max(w, h2) >= 1920 ? "1080p" : "720p";
@@ -9994,12 +10147,12 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
9994
10147
  frameRate: effectiveFps
9995
10148
  }
9996
10149
  });
9997
- const deadline = Date.now() + 72e4;
10150
+ const deadline = Date.now() + 9e5;
9998
10151
  while (!operation?.done) {
9999
10152
  if (Date.now() > deadline) {
10000
10153
  return res.status(504).json({ error: "timeout", message: "video generation timed out" });
10001
10154
  }
10002
- await new Promise((r2) => setTimeout(r2, 1e4));
10155
+ await new Promise((r2) => setTimeout(r2, 12e3));
10003
10156
  operation = await ai.operations.getVideosOperation({ operation });
10004
10157
  }
10005
10158
  await new Promise((r2) => setTimeout(r2, 2500));
@@ -10145,7 +10298,7 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
10145
10298
  return null;
10146
10299
  };
10147
10300
  let videoBytes = null;
10148
- const maxAttempts = 10;
10301
+ const maxAttempts = 15;
10149
10302
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
10150
10303
  let bytes = await trySdkDownloadOnce();
10151
10304
  if (!bytes) bytes = await tryManualDownload();
@@ -10156,7 +10309,7 @@ app.post("/api/v1/video", rateLimitMiddleware, async (req, res) => {
10156
10309
  break;
10157
10310
  }
10158
10311
  }
10159
- await new Promise((r2) => setTimeout(r2, Math.min(8e3, 1500 * attempt)));
10312
+ await new Promise((r2) => setTimeout(r2, Math.min(12e3, 1800 * attempt)));
10160
10313
  }
10161
10314
  if (!videoBytes) throw new Error("failed to obtain fully materialized video");
10162
10315
  const header = videoBytes.subarray(0, 256);
@@ -10258,6 +10411,13 @@ app.post("/api/v1/code", rateLimitMiddleware, async (req, res) => {
10258
10411
  const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10259
10412
  if (!decoded) return res.status(401).json({ error: "unauthorized" });
10260
10413
  const uid = decoded?.uid || decoded?.sub;
10414
+ let isProOrAbove = false;
10415
+ try {
10416
+ const { planId: _pid } = await getUserPlanAndLimits(uid);
10417
+ const p = String(_pid || "free").toLowerCase();
10418
+ isProOrAbove = ["pro", "pro-annual", "ultra", "ultra-annual", "enterprise"].includes(p);
10419
+ } catch {
10420
+ }
10261
10421
  const { prompt, language = "typescript", model = "gpt-4" } = req.body;
10262
10422
  if (!prompt) {
10263
10423
  return res.status(400).json({
@@ -10351,7 +10511,8 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10351
10511
  const idemKey = req.headers["idempotency-key"] || void 0;
10352
10512
  if (process.env.MARIA_TELEMETRY === "1") {
10353
10513
  try {
10354
- console.log(JSON.stringify({ ev: "ai_proxy_request", taskType: taskType || "unknown", promptLen: String(effectivePrompt || research?.query || "").length }));
10514
+ const attCount = Array.isArray((req.body?.metadata || {}).attachments) ? (req.body.metadata.attachments || []).filter(Boolean).length : 0;
10515
+ console.log(JSON.stringify({ ev: "ai_proxy_request", taskType: taskType || "unknown", promptLen: String(effectivePrompt || research?.query || "").length, requestedProvider: reqProvider || null, requestedModel: reqModel || null, attachments: attCount }));
10355
10516
  } catch {
10356
10517
  }
10357
10518
  }
@@ -10383,18 +10544,40 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10383
10544
  const gemKey = sanitizeKey2(keys?.googleApiKey || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY);
10384
10545
  const requestedProvider = typeof reqProvider === "string" ? reqProvider.toLowerCase() : void 0;
10385
10546
  const requestedModel = typeof reqModel === "string" ? String(reqModel).trim() : void 0;
10386
- const wantsGoogle = requestedProvider === "google" || !!requestedModel && requestedModel.toLowerCase().startsWith("gemini") || !requestedProvider && !requestedModel;
10387
- if (gemKey && wantsGoogle) {
10547
+ let isProOrAbove = false;
10548
+ try {
10549
+ const { planId: _pid } = await getUserPlanAndLimits(uid);
10550
+ const p = String(_pid || "free").toLowerCase();
10551
+ isProOrAbove = ["pro", "pro-annual", "ultra", "ultra-annual", "enterprise"].includes(p);
10552
+ } catch {
10553
+ }
10554
+ const { determineRouting: determineRouting2 } = await Promise.resolve().then(() => (init_model_routing(), model_routing_exports));
10555
+ const decision = determineRouting2({
10556
+ requestedModel,
10557
+ requestedProvider,
10558
+ taskType,
10559
+ isProOrAbove,
10560
+ hasGeminiKey: !!gemKey,
10561
+ hasOpenAIKey: !!(keys?.openaiApiKey || process.env.OPENAI_API_KEY)
10562
+ });
10563
+ if (decision.error) {
10564
+ const map = { missing_key: 503, unknown_model: 400, no_provider_available: 503 };
10565
+ const status = map[decision.error] || 400;
10566
+ return res.status(status).json({ error: decision.error, message: decision.message });
10567
+ }
10568
+ const effectiveProvider = decision.provider;
10569
+ const effectiveModel = decision.model;
10570
+ if (effectiveProvider === "google") {
10388
10571
  try {
10389
10572
  const { GoogleGenerativeAI: GoogleGenerativeAI2 } = await import('@google/generative-ai');
10390
10573
  const ai = new GoogleGenerativeAI2(gemKey);
10391
- const modelName = requestedModel && requestedModel.length > 0 ? requestedModel : process.env.MARIA_CODE_MODEL || "gemini-2.5-flash";
10392
- const model2 = ai.getGenerativeModel({ model: modelName });
10393
- let content2 = "";
10574
+ const modelName = effectiveModel || "gemini-2.5-flash";
10575
+ const model = ai.getGenerativeModel({ model: modelName });
10576
+ let content = "";
10394
10577
  if (taskType === "research" && research?.query) {
10395
10578
  const tool = { googleSearch: {} };
10396
10579
  const r2 = await ai.models.generateContent({ model: modelName, contents: String(research.query), config: { tools: [tool] } });
10397
- content2 = String(r2?.text?.() || r2?.text || r2?.response?.candidates?.[0]?.content?.parts?.[0]?.text || "");
10580
+ content = String(r2?.text?.() || r2?.text || r2?.response?.candidates?.[0]?.content?.parts?.[0]?.text || "");
10398
10581
  } else {
10399
10582
  const attachments = Array.isArray((req.body?.metadata || {}).attachments) ? (req.body.metadata.attachments || []).filter(Boolean) : [];
10400
10583
  const parts = [{ text: String(effectivePrompt || "") }];
@@ -10403,93 +10586,140 @@ app.post("/v1/ai-proxy", rateLimitMiddleware, async (req, res) => {
10403
10586
  const b64 = String(a.data_base64 || "");
10404
10587
  const mime = String(a.mime || "application/octet-stream");
10405
10588
  if (!b64) continue;
10589
+ if (Buffer.byteLength(b64, "base64") > 10 * 1024 * 1024) continue;
10406
10590
  parts.push({ inlineData: { data: b64, mimeType: mime } });
10407
10591
  } catch {
10408
10592
  }
10409
10593
  }
10410
- const resp = await model2.generateContent({ contents: [{ role: "user", parts }] });
10411
- content2 = resp?.response?.text?.() || resp?.response?.candidates?.[0]?.content?.parts?.[0]?.text || "";
10594
+ const resp = await model.generateContent({ contents: [{ role: "user", parts }] });
10595
+ content = resp?.response?.text?.() || resp?.response?.candidates?.[0]?.content?.parts?.[0]?.text || "";
10412
10596
  }
10413
10597
  if (process.env.MARIA_TELEMETRY === "1") {
10414
10598
  try {
10415
- console.log(JSON.stringify({ ev: "ai_proxy_route", vendor: "google", model: modelName, empty: !content2 }));
10599
+ console.log(JSON.stringify({ ev: "ai_proxy_route", vendor: "google", model: modelName, empty: !content }));
10416
10600
  } catch {
10417
10601
  }
10418
10602
  }
10419
- const consumption2 = { requests: 1 };
10420
- if (taskType === "code" || taskType === "evaluation") consumption2.code = 1;
10603
+ const consumption = { requests: 1 };
10604
+ if (taskType === "code" || taskType === "evaluation") consumption.code = 1;
10421
10605
  try {
10422
- await applyConsumption(uid, consumption2, idemKey);
10606
+ await applyConsumption(uid, consumption, idemKey);
10423
10607
  } catch {
10424
10608
  }
10425
- return res.json({ data: { content: content2, routedModel: { vendor: "google", family: "gemini", name: modelName, reason: taskType || "code" } } });
10609
+ return res.json({ data: { content, routedModel: { vendor: "google", family: "gemini", name: modelName, reason: taskType || "code" } } });
10426
10610
  } catch (e2) {
10427
- console.warn("[AI Proxy] Gemini path failed, falling back to OpenAI:", e2?.message || e2);
10611
+ const msg = e2?.message || "Google provider request failed";
10428
10612
  if (process.env.MARIA_TELEMETRY === "1") {
10429
10613
  try {
10430
10614
  console.log(JSON.stringify({ ev: "ai_proxy_google_error", message: e2?.message || String(e2) }));
10431
10615
  } catch {
10432
10616
  }
10433
10617
  }
10618
+ return res.status(502).json({ error: "provider_error", message: String(msg) });
10434
10619
  }
10435
10620
  }
10436
- const openaiKey = sanitizeKey2(keys?.openaiApiKey || process.env.OPENAI_API_KEY);
10437
- if (!openaiKey) {
10621
+ if (effectiveProvider === "openai") {
10622
+ const openaiKey = sanitizeKey2(keys?.openaiApiKey || process.env.OPENAI_API_KEY);
10623
+ if (!openaiKey) {
10624
+ if (process.env.MARIA_TELEMETRY === "1") {
10625
+ try {
10626
+ console.log(JSON.stringify({ ev: "ai_proxy_no_keys" }));
10627
+ } catch {
10628
+ }
10629
+ }
10630
+ return res.status(503).json({ error: "provider_unavailable", message: "No valid OpenAI API key" });
10631
+ }
10632
+ const OpenAI2 = (await import('openai')).default;
10633
+ const client = new OpenAI2({ apiKey: openaiKey });
10634
+ let model = effectiveModel || (isProOrAbove ? "gpt-5" : "gpt-5-mini");
10635
+ let content = "";
10636
+ let totalTokens = 0;
10637
+ const attachments = Array.isArray((req.body?.metadata || {}).attachments) ? (req.body.metadata.attachments || []).filter(Boolean) : [];
10638
+ try {
10639
+ if (attachments.length > 0) {
10640
+ const vs = await client.vectorStores.create({ name: `ai-proxy-${uid}-${Date.now()}` });
10641
+ for (const a of attachments) {
10642
+ try {
10643
+ const b64 = String(a.data_base64 || "");
10644
+ if (!b64) continue;
10645
+ const buffer = Buffer.from(b64, "base64");
10646
+ const filename = a.name || "attachment.txt";
10647
+ const toFile = OpenAI2.toFile;
10648
+ const fileInput = toFile ? await toFile(buffer, filename) : buffer;
10649
+ const file = await client.files.create({ file: fileInput, purpose: "assistants" });
10650
+ await client.vectorStores.files.create(vs.id, { file_id: file.id });
10651
+ } catch {
10652
+ }
10653
+ }
10654
+ const r2 = await client.responses.create({
10655
+ model,
10656
+ input: String(effectivePrompt || ""),
10657
+ tools: [{ type: "file_search", vector_store_ids: [vs.id] }]
10658
+ });
10659
+ content = r2?.output_text || r2?.content?.[0]?.text || "";
10660
+ const u = r2?.usage;
10661
+ if (u) {
10662
+ const inTok = Number(u.input_tokens || u.inputTokens || 0);
10663
+ const outTok = Number(u.output_tokens || u.outputTokens || 0);
10664
+ totalTokens = Number(u.total_tokens || u.totalTokens || inTok + outTok || 0);
10665
+ }
10666
+ } else {
10667
+ const r2 = await client.responses.create({
10668
+ model,
10669
+ input: [
10670
+ { role: "system", content: "You output only code blocks when asked for code." },
10671
+ { role: "user", content: effectivePrompt || "" }
10672
+ ]
10673
+ });
10674
+ content = r2?.output_text || r2?.content?.[0]?.text || "";
10675
+ const u = r2?.usage;
10676
+ if (u) {
10677
+ const inTok = Number(u.input_tokens || u.inputTokens || 0);
10678
+ const outTok = Number(u.output_tokens || u.outputTokens || 0);
10679
+ totalTokens = Number(u.total_tokens || u.totalTokens || inTok + outTok || 0);
10680
+ }
10681
+ }
10682
+ } catch (_e) {
10683
+ try {
10684
+ console.warn("[AI Proxy][OpenAI] responses.create failed:", _e?.message || String(_e));
10685
+ } catch {
10686
+ }
10687
+ model = "gpt-5-mini";
10688
+ try {
10689
+ const r2 = await client.chat.completions.create({
10690
+ model,
10691
+ messages: [
10692
+ { role: "system", content: "You output only code blocks when asked for code." },
10693
+ { role: "user", content: effectivePrompt || "" }
10694
+ ]
10695
+ });
10696
+ content = r2.choices?.[0]?.message?.content || "";
10697
+ const u2 = r2?.usage;
10698
+ if (u2) totalTokens = Number(u2.total_tokens || 0);
10699
+ } catch (e2) {
10700
+ try {
10701
+ console.error("[AI Proxy][OpenAI] chat.completions fallback failed:", e2?.message || String(e2));
10702
+ } catch {
10703
+ }
10704
+ throw e2;
10705
+ }
10706
+ }
10438
10707
  if (process.env.MARIA_TELEMETRY === "1") {
10439
10708
  try {
10440
- console.log(JSON.stringify({ ev: "ai_proxy_no_keys" }));
10709
+ console.log(JSON.stringify({ ev: "ai_proxy_route", vendor: "openai", model, empty: !content }));
10441
10710
  } catch {
10442
10711
  }
10443
10712
  }
10444
- return res.status(503).json({ error: "provider_unavailable", message: "No valid provider key (set GEMINI_API_KEY/GOOGLE_API_KEY or OPENAI_API_KEY)" });
10445
- }
10446
- const OpenAI2 = (await import('openai')).default;
10447
- const client = new OpenAI2({ apiKey: openaiKey });
10448
- let model = requestedProvider === "openai" && requestedModel ? requestedModel : process.env.MARIA_CODE_MODEL || "gpt-5-mini";
10449
- let content = "";
10450
- let totalTokens = 0;
10451
- try {
10452
- const r2 = await client.responses.create({
10453
- model,
10454
- input: [
10455
- { role: "system", content: "You output only code blocks when asked for code." },
10456
- { role: "user", content: effectivePrompt || "" }
10457
- ]
10458
- });
10459
- content = r2?.output_text || r2?.content?.[0]?.text || "";
10460
- const u = r2?.usage;
10461
- if (u) {
10462
- const inTok = Number(u.input_tokens || u.inputTokens || 0);
10463
- const outTok = Number(u.output_tokens || u.outputTokens || 0);
10464
- totalTokens = Number(u.total_tokens || u.totalTokens || inTok + outTok || 0);
10465
- }
10466
- } catch (_e) {
10467
- model = "gpt-4o-mini";
10468
- const r2 = await client.chat.completions.create({
10469
- model,
10470
- messages: [
10471
- { role: "system", content: "You output only code blocks when asked for code." },
10472
- { role: "user", content: effectivePrompt || "" }
10473
- ]
10474
- });
10475
- content = r2.choices?.[0]?.message?.content || "";
10476
- const u2 = r2?.usage;
10477
- if (u2) totalTokens = Number(u2.total_tokens || 0);
10478
- }
10479
- if (process.env.MARIA_TELEMETRY === "1") {
10713
+ const consumption = { requests: 1 };
10714
+ if (totalTokens > 0) consumption.tokens = totalTokens;
10715
+ if (taskType === "code" || taskType === "evaluation") consumption.code = 1;
10480
10716
  try {
10481
- console.log(JSON.stringify({ ev: "ai_proxy_route", vendor: "openai", model, empty: !content }));
10717
+ await applyConsumption(uid, consumption, idemKey);
10482
10718
  } catch {
10483
10719
  }
10720
+ return res.json({ data: { content, routedModel: { vendor: "openai", family: "gpt", name: model, reason: taskType || "code" } } });
10484
10721
  }
10485
- const consumption = { requests: 1 };
10486
- if (totalTokens > 0) consumption.tokens = totalTokens;
10487
- if (taskType === "code" || taskType === "evaluation") consumption.code = 1;
10488
- try {
10489
- await applyConsumption(uid, consumption, idemKey);
10490
- } catch {
10491
- }
10492
- return res.json({ data: { content, routedModel: { vendor: "openai", family: "gpt", name: model, reason: taskType || "code" } } });
10722
+ return res.status(500).json({ error: "routing_error", message: "No provider selected for AI proxy" });
10493
10723
  } catch (error) {
10494
10724
  console.error("[AI Proxy] Error:", error);
10495
10725
  if (process.env.MARIA_TELEMETRY === "1") {
@@ -10516,6 +10746,136 @@ async function getFirestoreSafe() {
10516
10746
  return null;
10517
10747
  }
10518
10748
  }
10749
+ app.post("/api/v1/snapshots", rateLimitMiddleware, async (req, res) => {
10750
+ try {
10751
+ const auth = req.headers.authorization;
10752
+ if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10753
+ const idToken = auth.substring("Bearer ".length).trim();
10754
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10755
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10756
+ const uid = decoded?.uid || decoded?.sub;
10757
+ const { projectId, taskId, summary, decisions, artifacts, refs, resumePrompt } = req.body || {};
10758
+ const pid = typeof projectId === "string" && projectId.trim() ? String(projectId).trim() : "default";
10759
+ const tid = typeof taskId === "string" && taskId.trim() ? String(taskId).trim() : "";
10760
+ const sum = typeof summary === "string" && summary.trim() ? String(summary).trim() : "";
10761
+ try {
10762
+ console.log("[Snapshots][POST] incoming", { uid: String(uid).slice(0, 8) + "\u2026", projectId: pid, taskId: tid, hasSummary: !!sum });
10763
+ } catch {
10764
+ }
10765
+ if (!tid || !sum) return res.status(400).json({ error: "bad_request", message: "taskId and summary are required" });
10766
+ const db = await getFirestoreSafe();
10767
+ if (!db) return res.status(503).json({ error: "unavailable", message: "database is not configured" });
10768
+ const tsId = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/g, "").split(".")[0] + "Z";
10769
+ const userRef = db.collection("users").doc(uid);
10770
+ const projRef = userRef.collection("projects").doc(pid);
10771
+ const taskRef = projRef.collection("tasks").doc(tid);
10772
+ const ref = taskRef.collection("snapshots").doc(tsId);
10773
+ const nowISO = (/* @__PURE__ */ new Date()).toISOString();
10774
+ try {
10775
+ await Promise.all([
10776
+ userRef.set({ updatedAt: nowISO }, { merge: true }),
10777
+ projRef.set({ projectId: pid, updatedAt: nowISO }, { merge: true }),
10778
+ taskRef.set({ taskId: tid, updatedAt: nowISO }, { merge: true })
10779
+ ]);
10780
+ } catch (e2) {
10781
+ try {
10782
+ console.warn("[Snapshots][POST] parent set warn:", e2?.message || String(e2));
10783
+ } catch {
10784
+ }
10785
+ }
10786
+ await ref.set({
10787
+ snapshotVersion: 1,
10788
+ uid,
10789
+ projectId: pid,
10790
+ taskId: tid,
10791
+ timestamp: nowISO,
10792
+ summary: sum,
10793
+ decisions: Array.isArray(decisions) ? decisions.slice(0, 50).map(String) : [],
10794
+ artifacts: Array.isArray(artifacts) ? artifacts.slice(0, 200).map(String) : [],
10795
+ links: Array.isArray(refs) ? refs.slice(0, 200).map((r2) => ({ type: "fs", ref: String(r2) })) : [],
10796
+ resumePrompt: typeof resumePrompt === "string" ? String(resumePrompt) : ""
10797
+ });
10798
+ try {
10799
+ console.log("[Snapshots][POST] saved", { path: `users/${uid}/projects/${pid}/tasks/${tid}/snapshots/${tsId}` });
10800
+ } catch {
10801
+ }
10802
+ return res.json({ success: true });
10803
+ } catch (e2) {
10804
+ try {
10805
+ console.error("[Snapshots][POST] error:", e2?.message || String(e2));
10806
+ } catch {
10807
+ }
10808
+ return res.status(500).json({ error: "internal_error" });
10809
+ }
10810
+ });
10811
+ app.get("/api/v1/get-snapshots", rateLimitMiddleware, async (req, res) => {
10812
+ try {
10813
+ const auth = req.headers.authorization;
10814
+ if (!auth || !auth.startsWith("Bearer ")) return res.status(401).json({ error: "unauthorized" });
10815
+ const idToken = auth.substring("Bearer ".length).trim();
10816
+ const decoded = await decodeFirebaseToken(idToken).catch(() => null);
10817
+ if (!decoded) return res.status(401).json({ error: "unauthorized" });
10818
+ const uid = decoded?.uid || decoded?.sub;
10819
+ const projectId = typeof req.query.projectId === "string" && req.query.projectId.trim() ? String(req.query.projectId).trim() : "default";
10820
+ const taskId = typeof req.query.taskId === "string" && req.query.taskId.trim() ? String(req.query.taskId).trim() : void 0;
10821
+ const dateISO = typeof req.query.date === "string" && req.query.date.trim() ? String(req.query.date).trim() : void 0;
10822
+ const limit = Number.isFinite(Number(req.query.limit)) ? Math.max(1, Math.min(50, Number(req.query.limit))) : 5;
10823
+ const db = await getFirestoreSafe();
10824
+ if (!db) return res.status(503).json({ error: "unavailable", message: "database is not configured" });
10825
+ let docs = [];
10826
+ if (taskId) {
10827
+ const q = db.collection("users").doc(uid).collection("projects").doc(projectId).collection("tasks").doc(taskId).collection("snapshots").orderBy("timestamp", "desc").limit(limit);
10828
+ const ss = await q.get();
10829
+ docs = ss.docs.map((d) => d.data());
10830
+ } else if (dateISO) {
10831
+ const start = new Date(dateISO).toISOString();
10832
+ const end = new Date(new Date(dateISO).getTime() + 24 * 60 * 60 * 1e3).toISOString();
10833
+ try {
10834
+ const ss = await db.collectionGroup("snapshots").where("uid", "==", uid).where("projectId", "==", projectId).where("timestamp", ">=", start).where("timestamp", "<", end).orderBy("timestamp", "desc").limit(limit).get();
10835
+ docs = ss.docs.map((d) => d.data());
10836
+ } catch (e2) {
10837
+ const projRef = db.collection("users").doc(uid).collection("projects").doc(projectId);
10838
+ const tasksSnap = await projRef.collection("tasks").get();
10839
+ const collected = [];
10840
+ for (const t2 of tasksSnap.docs) {
10841
+ try {
10842
+ const snaps = await t2.ref.collection("snapshots").orderBy("timestamp", "desc").limit(50).get();
10843
+ for (const s2 of snaps.docs) {
10844
+ const data = s2.data();
10845
+ const ts = String(data?.timestamp || "");
10846
+ if (ts >= start && ts < end) collected.push(data);
10847
+ }
10848
+ } catch {
10849
+ }
10850
+ }
10851
+ collected.sort((a, b) => String(b?.timestamp || "").localeCompare(String(a?.timestamp || "")));
10852
+ docs = collected.slice(0, limit);
10853
+ }
10854
+ } else {
10855
+ try {
10856
+ const ss = await db.collectionGroup("snapshots").where("uid", "==", uid).where("projectId", "==", projectId).orderBy("timestamp", "desc").limit(limit).get();
10857
+ docs = ss.docs.map((d) => d.data());
10858
+ } catch (e2) {
10859
+ const projRef = db.collection("users").doc(uid).collection("projects").doc(projectId);
10860
+ const tasksSnap = await projRef.collection("tasks").get();
10861
+ const perTaskTop = [];
10862
+ for (const t2 of tasksSnap.docs) {
10863
+ try {
10864
+ const snaps = await t2.ref.collection("snapshots").orderBy("timestamp", "desc").limit(Math.max(1, Math.ceil(limit / Math.max(1, tasksSnap.size)))).get();
10865
+ for (const s2 of snaps.docs) perTaskTop.push(s2.data());
10866
+ } catch {
10867
+ }
10868
+ }
10869
+ perTaskTop.sort((a, b) => String(b?.timestamp || "").localeCompare(String(a?.timestamp || "")));
10870
+ docs = perTaskTop.slice(0, limit);
10871
+ }
10872
+ }
10873
+ return res.json({ success: true, data: { snapshots: docs } });
10874
+ } catch (e2) {
10875
+ console.error("[Snapshots] GET error", e2);
10876
+ return res.status(500).json({ error: "internal_error" });
10877
+ }
10878
+ });
10519
10879
  app.get("/api/v1/usage", rateLimitMiddleware, async (req, res) => {
10520
10880
  try {
10521
10881
  const auth = req.headers.authorization;