@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.
- package/README.md +9 -3
- package/dist/READY.manifest.json +1 -1
- package/dist/bin/maria.cjs +1723 -1011
- package/dist/bin/maria.cjs.map +1 -1
- package/dist/cli.cjs +1723 -1011
- package/dist/cli.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/server/express-server.cjs +434 -74
- package/dist/server/express-server.js +434 -74
- package/dist/server-express.cjs +434 -74
- package/dist/server-express.cjs.map +1 -1
- package/package.json +3 -3
- package/src/slash-commands/READY.manifest.json +1 -1
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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() +
|
|
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,
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
10387
|
-
|
|
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 =
|
|
10392
|
-
const
|
|
10393
|
-
let
|
|
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
|
-
|
|
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
|
|
10411
|
-
|
|
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: !
|
|
10599
|
+
console.log(JSON.stringify({ ev: "ai_proxy_route", vendor: "google", model: modelName, empty: !content }));
|
|
10416
10600
|
} catch {
|
|
10417
10601
|
}
|
|
10418
10602
|
}
|
|
10419
|
-
const
|
|
10420
|
-
if (taskType === "code" || taskType === "evaluation")
|
|
10603
|
+
const consumption = { requests: 1 };
|
|
10604
|
+
if (taskType === "code" || taskType === "evaluation") consumption.code = 1;
|
|
10421
10605
|
try {
|
|
10422
|
-
await applyConsumption(uid,
|
|
10606
|
+
await applyConsumption(uid, consumption, idemKey);
|
|
10423
10607
|
} catch {
|
|
10424
10608
|
}
|
|
10425
|
-
return res.json({ data: { content
|
|
10609
|
+
return res.json({ data: { content, routedModel: { vendor: "google", family: "gemini", name: modelName, reason: taskType || "code" } } });
|
|
10426
10610
|
} catch (e2) {
|
|
10427
|
-
|
|
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
|
-
|
|
10437
|
-
|
|
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: "
|
|
10709
|
+
console.log(JSON.stringify({ ev: "ai_proxy_route", vendor: "openai", model, empty: !content }));
|
|
10441
10710
|
} catch {
|
|
10442
10711
|
}
|
|
10443
10712
|
}
|
|
10444
|
-
|
|
10445
|
-
|
|
10446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|