Apply Velora LLM endpoints update

This commit is contained in:
Lisa Ops
2026-08-26 08:46:48 +07:00
parent 2aed637f74
commit bc86f9970f
12 changed files with 639 additions and 24 deletions

View File

@@ -42,7 +42,7 @@ Do not expose raw IDs, JSON, schemas, MCP terminology, scope strings, tokens, in
A premium real-estate discovery app. Buyers browse design-led homes, compare neighborhoods, save a private shortlist, chat with a property concierge, access permissioned documents and media, and book private viewings against authoritative availability.
### Velora
A premium AI-model booking engine based directly on the attached Luxevibe visual system. The homepage is deliberately denser than Luxevibe: smaller image-first cards show many more rated models at once, with availability, traits, pricing, quick chat, and direct session booking. Experiences, bookings, saved context, gated media, and Buckets Credits remain part of the product, while a prominent **New Chat** opens a secondary concierge drawer instead of turning the whole app into a chat interface.
A premium AI-model booking engine based directly on the attached Luxevibe product and visual system. The homepage is deliberately denser than Luxevibe: smaller image-first cards show many more rated models at once, while preserving card carousels, voice samples, availability, ratings, traits, pricing, full model profiles, gated media, reviews, direct booking, and model-specific private chat. Velora also includes scenario templates, a four-step booking engine with add-ons and credit validation, full private chat rooms with rich message interactions, a wallet/VIP flow, booking management, and a custom-model studio.
### Northline
A high-touch travel and experience concierge. Customers discover hosts and itineraries, chat across a persistent trip context, request or book experiences, access protected trip media and documents, use BC where enabled, and escalate complex requests to a human concierge.
@@ -76,7 +76,7 @@ Design principles applied across Aster House, Velora, and Northline:
- editorial, image-led discovery rather than admin-dashboard tiles;
- contextual AI assistance with a deliberate scope: persistent where useful, screen-specific where appropriate, and absent from focused conversation workspaces;
- browse → chat → activity → library → profile as the shared customer grammar;
- discovery → model/property detail → action-specific chat or booking as the customer grammar; Velora uses a dedicated full private-room chat while Aster House keeps contextual property concierge behavior;
- live availability, bookings/holds, gated media, Buckets Credits, and human support shown as customer outcomes;
- intentionally differentiated visual systems: dark-glass emerald-and-cyan real estate for Aster House, a dense dark-glass pink-and-purple AI model booking marketplace for Velora, and a premium dark-ocean editorial travel treatment for Northline;
- graceful media fallbacks so a failed external image never makes a Rokcet preview unusable.
@@ -84,4 +84,4 @@ Design principles applied across Aster House, Velora, and Northline:
Stitch project: `12830841746016621749`.
Reference screens: Aster House `f65e060bfc224460a101c24f23314363`, Velora discovery wall `70772541aa044fa98cc78a8ca3aa950b`, Velora focused rich chat `8c4df2de84ca40a69965029239e9768c`, Northline `0731a0f2da6f40379a9a8c4c899ab8f1`.
Velora uses the attached Luxevibe design system directly as its visual reference—dark frosted glass, pink/purple gradients, ratings, status pills, image-first booking cards, and a dense discovery wall—with smaller cards and many more models visible at once. Chat remains a distinct secondary concierge action rather than the product's primary structure.
Velora ports the attached Luxevibe interaction system as well as its visual reference—dark frosted glass, pink/purple gradients, card image carousels, voice samples, ratings, status pills, rich profiles, media vaults, reviews, full private chat rooms, scenarios, four-step bookings, gifts, wallet/VIP, and custom-model creation—while retaining smaller cards and many more models in the discovery wall.

View File

@@ -18,7 +18,7 @@ See `DEMO_APPS.md` and `ROKCET_BUCKETS_PRODUCT_MODEL.md` for the canonical gener
## Live workspace chat
Rokcet chat is strict about provider execution: it sends the current session's user/assistant history with each turn, streams provider output over `/api/chatgpt/execute/stream`, and never replaces a missing key, timeout, provider error, or unsupported model with a fabricated successful response. The model selector dispatches to the matching Google Gemini, OpenAI, Anthropic, or Groq API instead of routing every choice through Gemini.
Rokcet chat is strict about provider execution: it sends the current session's user/assistant history with each turn, streams provider output over `/api/llm/execute/stream`, and never replaces a missing key, timeout, provider error, or unsupported model with a fabricated successful response. The model selector dispatches to the matching Google Gemini, OpenAI, Anthropic, or Groq API instead of routing every choice through Gemini.
Configure provider credentials with `GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, and/or `GROQ_API_KEY`. A key entered in **Models & Keys** overrides the corresponding server environment value for that browser session/request. **Verify Providers** performs real credential checks; unconfigured or rejected providers are reported as such. OpenAI GPT-5.6 generation uses the Responses API. Provider generation timeouts are activity-based: active streams are allowed to continue. Normal GPT-5.6 chat defaults to 5 minutes for first activity and 2 minutes of inactivity; Agent Mode defaults to 10 minutes and 3 minutes. `ROKCET_PROVIDER_FIRST_ACTIVITY_TIMEOUT_MS` / `ROKCET_PROVIDER_INACTIVITY_TIMEOUT_MS` provide global overrides, while `ROKCET_AGENT_PROVIDER_FIRST_ACTIVITY_TIMEOUT_MS` / `ROKCET_AGENT_PROVIDER_INACTIVITY_TIMEOUT_MS` can tune Agent Mode independently.
@@ -32,7 +32,7 @@ Rokcet deliberately keeps the two Buckets MCP/runtime responsibilities separate:
- **Runtime:** generated application -> customer OAuth/API/conversations -> Buckets managed Agent -> authorized customer/runtime tools.
- **Integration:** normal application traffic uses Buckets APIs, OAuth, SSE, and signed webhooks. Developer MCP OAuth credentials are stored server-side per authenticated Rokcet workspace and are never shipped into generated customer applications.
The bundled showcase demonstrates three polished Buckets-powered business apps: **Aster House** (luxury real estate), **Velora** (AI models booking), and **Northline** (travel concierge). Aster House and Velora use a premium image-first discovery and booking system with a distinct contextual concierge, while Northline keeps the editorial travel workflow. The seeded experience is deterministic in preview; connected deployments use real Buckets customer OAuth, conversations, availability, bookings, media, access, Buckets Credits, and signed webhooks.
The bundled showcase demonstrates three polished Buckets-powered business apps: **Aster House** (luxury real estate), **Velora** (AI models booking), and **Northline** (travel concierge). Aster House uses a premium image-first real-estate discovery flow with a contextual property concierge; Velora ports the richer Luxevibe-style model marketplace with dense discovery, full private model chat, scenarios, gated media, credits, and multi-step booking; Northline keeps the editorial travel workflow. The seeded experience is deterministic in preview; connected deployments use real Buckets customer OAuth, conversations, availability, bookings, media, access, Buckets Credits, and signed webhooks.
### Required server-side environment

View File

@@ -4,7 +4,7 @@ import { fileURLToPath } from 'url';
import { createServer as createViteServer } from 'vite';
import healthRouter from './routes/health';
import chatgptRouter from './routes/chatgpt';
import llmRouter from './routes/llm';
import previewRouter from './routes/preview';
import artifactsRouter from './routes/artifacts';
import tenantRouter from './routes/tenant';
@@ -36,7 +36,7 @@ export function createApp() {
// Rokcet workspace capabilities are product-session protected. A Buckets central
// SSO session is exchanged for a local Rokcet session before reaching these.
app.use('/api', requireRokcetAuth, chatgptRouter);
app.use('/api', requireRokcetAuth, llmRouter);
app.use('/api', requireRokcetAuth, artifactsRouter);
app.use('/api', requireRokcetAuth, bucketsRouter);

565
server/routes/llm.ts Normal file
View File

@@ -0,0 +1,565 @@
import { randomUUID } from 'node:crypto';
import { Router } from 'express';
import {
generateMultiModelResponses,
verifyProviders,
type ConversationMessage,
type ProviderApiKeys,
} from '../services/llmProviders';
import { getBucketsMcpConnection, getLlmSettings } from '../services/rokcetDb';
import { validateDeploymentFiles } from '../services/projectAdapters';
import { validatePreviewFiles } from '../services/previewEngine';
import {
bucketsGenerationEvidence,
planBucketsCapabilities,
planBookingBuild,
projectContractWithBuckets,
provisionBucketsBuild,
type BucketsBuildEvidence,
} from '../services/agentBuckets';
import {
buildModeInstruction,
effectiveRokcetBuildMode,
normalizeRokcetBuildMode,
type RokcetBuildMode,
} from '../../shared/buildModes';
const router = Router();
function mergeProviderKeys(stored?: ProviderApiKeys, supplied?: ProviderApiKeys): ProviderApiKeys {
return {
geminiApiKey: supplied?.geminiApiKey?.trim() || stored?.geminiApiKey,
openaiApiKey: supplied?.openaiApiKey?.trim() || stored?.openaiApiKey,
anthropicApiKey: supplied?.anthropicApiKey?.trim() || stored?.anthropicApiKey,
groqApiKey: supplied?.groqApiKey?.trim() || stored?.groqApiKey,
};
}
function parseRequest(body: any): {
prompt: string;
history: ConversationMessage[];
referencedArtifacts: any[];
projectName: string;
selectedModels: string[];
effectiveKeys: ProviderApiKeys;
projectContract: unknown;
workspaceId: string;
projectId: string;
buildMode: RokcetBuildMode;
} {
const {
prompt,
history = [],
credentials,
referencedArtifacts = [],
projectName = 'Default Project',
models = [],
model,
apiKeys = {},
projectContract = undefined,
workspaceId = '',
projectId = '',
buildMode = 'working_app',
} = body || {};
if (typeof prompt !== 'string' || !prompt.trim()) throw new Error('A non-empty prompt is required.');
let selectedModels: string[] = [];
if (Array.isArray(models) && models.length > 0) selectedModels = models.filter((item) => typeof item === 'string' && item.trim());
else if (typeof model === 'string' && model.trim()) selectedModels = [model.trim()];
else if (Array.isArray(credentials?.selectedModels) && credentials.selectedModels.length > 0) selectedModels = credentials.selectedModels;
else if (credentials?.mainModel) selectedModels = [credentials.mainModel];
else selectedModels = ['gemini-3.7-flash'];
const configuredMainModel = typeof credentials?.mainModel === 'string' ? credentials.mainModel.trim() : '';
if (configuredMainModel && selectedModels.includes(configuredMainModel)) {
selectedModels = [configuredMainModel, ...selectedModels.filter((item) => item !== configuredMainModel)];
}
selectedModels = [...new Set(selectedModels)].slice(0, 4);
const cleanHistory: ConversationMessage[] = Array.isArray(history)
? history
.filter((message) => message && (message.role === 'user' || message.role === 'assistant'))
.map((message) => ({ role: message.role, content: String(message.content || '') }))
: [];
return {
prompt: prompt.trim(),
history: cleanHistory,
referencedArtifacts: Array.isArray(referencedArtifacts) ? referencedArtifacts : [],
projectName: String(projectName || 'Default Project'),
selectedModels,
projectContract,
workspaceId: String(workspaceId || '').trim(),
projectId: String(projectId || '').trim(),
buildMode: normalizeRokcetBuildMode(buildMode),
effectiveKeys: {
geminiApiKey: apiKeys.geminiApiKey || credentials?.apiKeys?.geminiApiKey,
openaiApiKey: apiKeys.openaiApiKey || credentials?.apiKeys?.openaiApiKey,
anthropicApiKey: apiKeys.anthropicApiKey || credentials?.apiKeys?.anthropicApiKey,
groqApiKey: apiKeys.groqApiKey || credentials?.apiKeys?.groqApiKey,
},
};
}
router.post('/llm/execute', async (req, res) => {
try {
const parsed = parseRequest(req.body);
const auth = (req as any).rokcetAuth;
const storedSettings = auth?.isDemo ? null : getLlmSettings(auth.user.id);
const effectiveKeys = mergeProviderKeys(storedSettings?.apiKeys, parsed.effectiveKeys);
const result = await generateMultiModelResponses(
`CHAT MODE — discussion only. Do not implement, mutate, or emit filename-addressed deliverables. You may inspect supplied project context, explain code, brainstorm, recommend changes, and show short illustrative snippets when useful. If the user asks to implement something, explain that Chat is read-only and tell them to switch to BUILD (or use Build This) to apply changes.\n\nUser message:\n${parsed.prompt}`,
parsed.projectName,
parsed.referencedArtifacts,
parsed.selectedModels,
effectiveKeys,
parsed.history,
parsed.projectContract
);
res.json({
success: true,
response: result.primaryContent,
variants: result.variants,
logs: result.primaryLogs,
deliverables: result.primaryDeliverables,
modelsUsed: result.variants.map((variant) => variant.modelId),
primaryModelId: result.primaryModelId,
modelErrors: result.modelErrors,
sessionEngineId: `engine_${randomUUID()}`,
});
} catch (error: any) {
console.error('Error in /api/llm/execute:', error?.message || error);
res.status(502).json({
success: false,
error: error?.message || 'Live provider execution failed.',
});
}
});
function candidateFiles(referencedArtifacts: any[], deliverables: any[]): Array<{ path: string; content: string }> {
const files = new Map<string, { path: string; content: string }>();
for (const artifact of referencedArtifacts || []) {
for (const file of artifact?.files || []) {
const path = String(file?.path || '').trim().replace(/^\.\//, '');
if (path) files.set(path, { path, content: String(file?.content || '') });
}
}
for (const deliverable of deliverables || []) {
for (const file of deliverable?.files || []) {
const path = String(file?.path || '').trim().replace(/^\.\//, '');
if (path) files.set(path, { path, content: String(file?.content || '') });
}
}
return [...files.values()];
}
function contractValidationErrors(contract: unknown, referencedArtifacts: any[], deliverables: any[]): string[] {
try {
validateDeploymentFiles(contract, candidateFiles(referencedArtifacts, deliverables));
return [];
} catch (error: any) {
if (Array.isArray(error?.details)) return error.details.map(String);
return [String(error?.message || 'Generated files do not satisfy the project contract.')];
}
}
function bucketsLogs(evidence: BucketsBuildEvidence): any[] {
const started = Date.now();
const logs = evidence.steps.map((step, index) => ({
id: `buckets-agent-${started}-${index}`,
timestamp: new Date().toISOString(),
step: `Buckets Developer MCP · ${step.tool}`,
status: step.status === 'completed' ? 'success' : step.status === 'error' ? 'error' : 'warning',
detail: step.detail || (step.status === 'completed' ? 'Authoritative Buckets configuration step completed.' : 'Buckets configuration step was not executed.'),
}));
if (evidence.required && evidence.steps.length === 0) {
logs.push({
id: `buckets-agent-${started}-status`,
timestamp: new Date().toISOString(),
step: 'Buckets integration',
status: evidence.status === 'configured' ? 'success' : 'warning',
detail: evidence.note || `Buckets status: ${evidence.status}`,
});
}
return logs;
}
router.post('/agent/runs', async (req, res) => {
// Agent builds can legitimately span provider generation plus many idempotent
// Buckets configuration calls. Start a chunked JSON response immediately and
// write JSON-legal whitespace while work is in progress so reverse proxies do
// not mistake an active build for a stalled origin. The browser still receives
// one ordinary JSON object and can keep using response.json().
res.status(200);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders?.();
res.socket?.setNoDelay(true);
const writeKeepalive = () => {
// 4 KiB is intentionally large enough to cross common proxy buffer
// thresholds while remaining insignificant compared with generated source.
if (!res.writableEnded && !res.destroyed) res.write(`${' '.repeat(4096)}\n`);
};
writeKeepalive();
const keepalive = setInterval(writeKeepalive, 8_000);
res.once('close', () => clearInterval(keepalive));
const finish = (payload: any) => {
clearInterval(keepalive);
if (!res.writableEnded && !res.destroyed) res.end(JSON.stringify(payload));
};
try {
const parsed = parseRequest(req.body);
const auth = (req as any).rokcetAuth;
const storedSettings = auth?.isDemo ? null : getLlmSettings(auth.user.id);
const effectiveKeys = mergeProviderKeys(storedSettings?.apiKeys, parsed.effectiveKeys);
const primaryModel = parsed.selectedModels[0] || 'gemini-3.7-flash';
const capabilityPlan = await planBucketsCapabilities({
prompt: parsed.prompt,
projectName: parsed.projectName,
modelId: primaryModel,
apiKeys: effectiveKeys,
projectContract: parsed.projectContract,
buildMode: parsed.buildMode,
});
const inferredCapabilities = capabilityPlan.capabilities;
const effectiveContract = projectContractWithBuckets(parsed.projectContract, inferredCapabilities);
if (capabilityPlan.required && !auth?.isDemo) {
if (!parsed.workspaceId || !parsed.projectId) {
throw Object.assign(new Error('Agent build requires workspaceId and projectId for Buckets provisioning.'), { code: 'PROJECT_CONTEXT_REQUIRED' });
}
if (!auth?.workspaces?.some((workspace: any) => workspace.id === parsed.workspaceId)) {
throw Object.assign(new Error('Rokcet workspace does not belong to the authenticated developer.'), { status: 403, code: 'WORKSPACE_ACCESS_DENIED' });
}
const connection = getBucketsMcpConnection(auth.user.id, parsed.workspaceId, parsed.projectId);
if (!connection) {
finish({
success: true,
code: 'BUCKETS_CONNECTION_REQUIRED',
status: 'authorization_required',
requiresAction: {
type: 'connect_buckets',
label: 'CONNECT WITH BUCKETS',
workspaceId: parsed.workspaceId,
projectId: parsed.projectId,
projectName: parsed.projectName,
capabilities: inferredCapabilities,
},
projectContract: effectiveContract,
buckets: {
required: true,
status: 'not_connected',
capabilities: inferredCapabilities,
steps: [],
note: 'Authorize this Rokcet project to its Buckets App before the Working App build continues.',
},
});
return;
}
}
let bucketsEvidence: BucketsBuildEvidence = {
required: capabilityPlan.required,
status: capabilityPlan.required ? 'planning_failed' : 'not_required',
capabilities: inferredCapabilities,
steps: [],
note: capabilityPlan.reason,
};
if (capabilityPlan.required) {
try {
const bookingSpec = inferredCapabilities.includes('bookings')
? await planBookingBuild({
prompt: parsed.prompt,
projectName: parsed.projectName,
modelId: primaryModel,
apiKeys: effectiveKeys,
projectContract: effectiveContract,
})
: undefined;
if (auth?.isDemo) {
bucketsEvidence = {
required: true,
status: 'demo_unavailable',
capabilities: inferredCapabilities,
bookingSpec,
steps: [],
note: 'Demo sessions cannot mutate a real Buckets App. Any generated demo behavior must remain clearly labelled simulation-only.',
};
} else {
if (!parsed.workspaceId || !parsed.projectId) {
throw Object.assign(new Error('Agent build requires workspaceId and projectId for Buckets provisioning.'), { code: 'PROJECT_CONTEXT_REQUIRED' });
}
bucketsEvidence = await provisionBucketsBuild({
userId: auth.user.id,
workspaceId: parsed.workspaceId,
projectId: parsed.projectId,
projectName: parsed.projectName,
capabilities: inferredCapabilities,
bookingSpec,
projectContract: effectiveContract,
});
}
} catch (error: any) {
if (error?.code === 'BUCKETS_MCP_NOT_CONNECTED' || Number(error?.status) === 409) {
bucketsEvidence = {
required: true,
status: 'not_connected',
capabilities: inferredCapabilities,
steps: [],
note: error?.message || 'Connect this Rokcet project to a Buckets App to provision operational resources.',
};
} else if (error?.code === 'WORKSPACE_ACCESS_DENIED' || error?.code === 'PROJECT_CONTEXT_REQUIRED') {
throw error;
} else {
bucketsEvidence = {
required: true,
status: 'planning_failed',
capabilities: inferredCapabilities,
steps: [],
note: `Buckets planning/configuration could not complete: ${error?.message || 'unknown error'}`,
};
}
}
}
const externalEvidence = bucketsGenerationEvidence(bucketsEvidence);
const hasExistingArtifacts = parsed.referencedArtifacts.length > 0;
const effectiveBuildMode = effectiveRokcetBuildMode(parsed.buildMode, parsed.prompt);
const selectedBuildModeInstruction = buildModeInstruction(effectiveBuildMode);
const scopeSignal = `${selectedBuildModeInstruction}
${hasExistingArtifacts
? 'This is an established project. Preserve its architecture and change only what is needed for the request; do not regenerate unrelated layers.'
: 'This is a new build. The selected Build Mode guides scope, but the user prompt remains authoritative. Produce the smallest implementation that fully satisfies both and can run in Rokcet Preview when a UI is requested.'}`;
const agentPrompt = `You are the implementation agent inside Rokcet Agent Mode.
EXECUTE the requested build. Do not answer with a tutorial, setup guide, pseudo-code, or illustrative snippets.
User request:
${parsed.prompt}
${externalEvidence}
BUILD SCOPE
Selected/effective Build Mode: ${effectiveBuildMode}
${scopeSignal}
- "Complete" means complete FOR THE USER'S REQUESTED SCOPE, not "build every possible layer".
- A frontend-only request should result in a complete, runnable frontend. Do not invent an unrelated database, auth system, webhooks, payments or persistence. Rokcet business apps still keep the Buckets.id managed-conversation/Web Chat baseline required by the active project contract.
- A full-stack/service-backed request should include the trusted server pieces actually required by those requested capabilities.
- For a new visual application, include the minimum framework files and a real renderable entry/page so Rokcet Preview can run it. Include every local import needed by that entry.
- For an existing artifact, return only files that must change; rely on the referenced project files for everything unchanged.
- Mock/local state is acceptable only when the user is asking for presentation-only/prototype behavior and no authoritative service or real persistence was requested. Never silently fake a requested real integration.
ROKCET PRODUCT FOUNDATION
For a new customer-facing Rokcet application, use one consistent FUNCTIONAL grammar inspired by conversational discovery products, without copying any third-party styling, branding or source code:
- DISCOVER: a browsable/searchable catalog of the business's services, items, content, plans or other offerings.
- CHAT: persistent customer conversation threads with New Chat, thread history and contextual “Ask AI” entry points from catalog items.
- MY ACTIVITY: the customer's enabled business state such as bookings, appointments, requests, access/entitlements or other Buckets-backed activity. Hide modules the project does not use rather than showing fake data.
- PROFILE: customer identity/profile and relevant preferences/account context.
- AI FACILITATION: the Agent should guide the customer through discovery and use Buckets-authorized tools to complete actions; it must not be a decorative chatbot bolted onto a website.
- BUCKETS.ID: every Rokcet business app is powered by Buckets.id for managed conversations/Web Chat and the configured business capabilities. Show subtle “Powered by Buckets.id” product attribution in an appropriate account/about/footer surface.
Brand, colors and visual composition remain project-specific. The functional shell is canonical; do not force identical styling. Existing projects should be evolved rather than unnecessarily regenerated into the shell.
IMPLEMENTATION RULES
1. Satisfy the user's product behavior first, within the active Rokcet project contract. The contract constrains stack/deployment/service ownership; it is not a checklist telling you to invent unused features.
2. If referenced artifact files are supplied, inspect them and preserve existing architecture, conventions, and working functionality unless the request changes them.
3. Return complete contents for every file you actually create or change. Do not emit unchanged files merely to look comprehensive.
4. Every created/changed file MUST be emitted as a fenced code block whose OPENING fence includes filename="path/to/file.ext". Example: \`\`\`tsx filename="app/page.tsx".
5. Do not put filename comments inside ordinary code fences; the filename belongs on the opening fence.
6. The resulting requested scope must be runnable on the configured stack and, when it has a UI, have a real previewable entry route/component with all local imports present.
7. For Buckets-owned capabilities, use the supplied Buckets execution evidence and runtime API contract. Never create an in-memory/local substitute for availability, bookings, credits, entitlements, managed AI, conversations or other Buckets-owned authority. Every new Rokcet business app keeps its Buckets.id Customer Identity + Agent + Web Chat baseline; only additional modules should be omitted when not required.
8. Keep secrets and privileged credentials out of browser code.
9. Do not claim an external action happened unless it is present in BUCKETS EXECUTION EVIDENCE.
10. Do not stop at instructions such as "ensure you have a project". Build the requested scope now.
OUTPUT PROTOCOL
- Start with a short user-facing build summary (roughly 2-6 bullets or a few concise sentences). No code in this summary.
- Then emit the filename-addressed code blocks required for artifact extraction. Do not narrate each block or paste duplicate code in prose.
- End with exactly one deliverable metadata marker describing the actual files built.`;
let result = await generateMultiModelResponses(
agentPrompt,
parsed.projectName,
parsed.referencedArtifacts,
[primaryModel],
effectiveKeys,
parsed.history,
effectiveContract,
undefined,
undefined,
undefined,
undefined,
'agent',
);
let validationErrors = [
...contractValidationErrors(effectiveContract, parsed.referencedArtifacts, result.primaryDeliverables),
...validatePreviewFiles(candidateFiles(parsed.referencedArtifacts, result.primaryDeliverables)),
];
if (result.primaryDeliverables.length === 0 || validationErrors.length > 0) {
const repairPrompt = `Your previous Rokcet Agent build was not importable/runnable for the requested scope. Repair the build without expanding its scope.
Original user request:
${parsed.prompt}
${externalEvidence}
BUILD SCOPE
Selected/effective Build Mode: ${effectiveBuildMode}
${scopeSignal}
"Complete" means complete for the requested scope. A frontend-only build remains frontend-only; do not add unrelated backend/database/auth/service layers merely to satisfy validation. For an existing project, change only what is necessary.
Validation failures:
${validationErrors.length ? validationErrors.map((error) => `- ${error}`).join('\n') : '- No filename-addressed files were returned.'}
Previous attempt (use only as draft material; do not repeat its tutorial prose):
${result.primaryContent.slice(0, 60_000)}
Fix the validation failures and return the smallest runnable/previewable result that satisfies the original request and active project contract. Emit a short user-facing summary first, then complete contents only for created/changed files. Every file fence MUST have filename="..." on the opening fence. Include framework/package/entry files only when they are missing from a new project or must actually change. Preserve Buckets as authority according to the execution evidence. New Rokcet business apps always keep the Buckets.id Customer Identity + Agent + Web Chat baseline; do not invent additional Buckets modules beyond the project contract/request. Never use an in-memory substitute for an authoritative requested integration. End with exactly one deliverable metadata marker.`;
const repaired = await generateMultiModelResponses(
repairPrompt,
parsed.projectName,
parsed.referencedArtifacts,
[primaryModel],
effectiveKeys,
[],
effectiveContract,
undefined,
undefined,
undefined,
undefined,
'agent',
);
const repairedErrors = [
...contractValidationErrors(effectiveContract, parsed.referencedArtifacts, repaired.primaryDeliverables),
...validatePreviewFiles(candidateFiles(parsed.referencedArtifacts, repaired.primaryDeliverables)),
];
if (repaired.primaryDeliverables.length > 0 && repairedErrors.length === 0) {
result = repaired;
validationErrors = [];
} else {
validationErrors = repairedErrors.length ? repairedErrors : ['No importable filename-addressed files were returned after the repair pass.'];
const error: any = new Error(`Agent could not produce a contract-valid artifact: ${validationErrors.join(' ')}`);
error.status = 422;
error.code = 'AGENT_ARTIFACT_INVALID';
error.details = validationErrors;
throw error;
}
}
finish({
success: true,
runId: `agent_${randomUUID()}`,
response: result.primaryContent,
variants: result.variants,
logs: [...bucketsLogs(bucketsEvidence), ...result.primaryLogs],
deliverables: result.primaryDeliverables,
modelsUsed: result.variants.map((variant) => variant.modelId),
primaryModelId: result.primaryModelId,
modelErrors: result.modelErrors,
status: 'preview_validated',
projectContract: effectiveContract,
buildMode: effectiveBuildMode,
buckets: bucketsEvidence,
});
} catch (error: any) {
console.error('Error in /api/agent/runs:', error?.message || error);
finish({
success: false,
code: error?.code || 'AGENT_RUN_FAILED',
error: error?.message || 'Agent run failed.',
details: error?.details,
httpStatus: error?.status || 502,
});
}
});
router.post('/llm/execute/stream', async (req, res) => {
let parsed: ReturnType<typeof parseRequest>;
try {
parsed = parseRequest(req.body);
} catch (error: any) {
return res.status(400).json({ success: false, error: error?.message || 'Invalid chat request.' });
}
const executionId = `engine_${randomUUID()}`;
const abortController = new AbortController();
const onClientAborted = () => abortController.abort(new Error('Client disconnected'));
req.on('aborted', onClientAborted);
res.status(200);
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache, no-transform');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
res.flushHeaders?.();
const send = (event: string, data: any) => {
if (res.writableEnded || res.destroyed) return;
res.write(`event: ${event}\n`);
res.write(`data: ${JSON.stringify(data)}\n\n`);
};
send('execution.started', { executionId, models: parsed.selectedModels, historyMessages: parsed.history.length });
const heartbeat = setInterval(() => send('execution.heartbeat', { executionId, t: Date.now() }), 15_000);
try {
const auth = (req as any).rokcetAuth;
const storedSettings = auth?.isDemo ? null : getLlmSettings(auth.user.id);
const effectiveKeys = mergeProviderKeys(storedSettings?.apiKeys, parsed.effectiveKeys);
const result = await generateMultiModelResponses(
`CHAT MODE — discussion only. Do not implement, mutate, or emit filename-addressed deliverables. You may inspect supplied project context, explain code, brainstorm, recommend changes, and show short illustrative snippets when useful. If the user asks to implement something, explain that Chat is read-only and tell them to switch to BUILD (or use Build This) to apply changes.\n\nUser message:\n${parsed.prompt}`,
parsed.projectName,
parsed.referencedArtifacts,
parsed.selectedModels,
effectiveKeys,
parsed.history,
parsed.projectContract,
abortController.signal,
(modelId, delta) => send('model.delta', { modelId, delta }),
(variant) => send('model.completed', { modelId: variant.modelId, latencyMs: variant.latencyMs }),
(modelId, error) => {
console.warn(`Streaming LLM model failed: ${modelId}: ${error.message}`);
send('model.failed', { modelId, error: error.message });
}
);
send('execution.completed', {
success: true,
executionId,
response: result.primaryContent,
variants: result.variants,
logs: result.primaryLogs,
deliverables: result.primaryDeliverables,
modelsUsed: result.variants.map((variant) => variant.modelId),
primaryModelId: result.primaryModelId,
modelErrors: result.modelErrors,
});
} catch (error: any) {
if (!abortController.signal.aborted) {
console.error('Streaming LLM execution failed:', error?.message || error);
send('execution.failed', { success: false, executionId, error: error?.message || 'Live provider execution failed.' });
}
} finally {
clearInterval(heartbeat);
req.off('aborted', onClientAborted);
if (!res.writableEnded && !res.destroyed) res.end();
}
});
router.post('/llm/providers/test', async (req, res) => {
try {
const auth = (req as any).rokcetAuth;
const storedSettings = auth?.isDemo ? null : getLlmSettings(auth.user.id);
const apiKeys = mergeProviderKeys(storedSettings?.apiKeys, req.body?.apiKeys || {});
const results = await verifyProviders(apiKeys);
res.json({ success: true, results });
} catch (error: any) {
res.status(500).json({ success: false, error: error?.message || 'Provider verification failed.' });
}
});
export default router;

View File

@@ -958,7 +958,7 @@ export default function App() {
apiKeys: credentials.apiKeys,
};
const res = await rokcetFetch('/api/chatgpt/execute/stream', {
const res = await rokcetFetch('/api/llm/execute/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
credentials: 'include',
@@ -1014,7 +1014,7 @@ export default function App() {
// Some proxies/browsers can close an SSE stream before the final event when
// providers are slow to emit tokens. Retry once through the non-streaming
// endpoint before surfacing a hard failure to the user.
const fallbackRes = await rokcetFetch('/api/chatgpt/execute', {
const fallbackRes = await rokcetFetch('/api/llm/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@@ -1193,7 +1193,7 @@ export default function App() {
activeVariantIndex: 0,
selectedModelId: data.primaryModelId,
snapshotId: generatedSnapshotId,
chatgptChatId: data.executionId,
executionId: data.executionId,
};
const finalMessages = [...updatedUserMessages, assistantMsg];
@@ -1520,7 +1520,7 @@ export default function App() {
activeVariantIndex: 0,
selectedModelId: data.primaryModelId,
snapshotId: generatedSnapshotId,
chatgptChatId: data.runId,
executionId: data.runId,
};
const finalMessages = [...messagesBeforeWorking, assistantMsg];
const finalSess = { ...currentSess, updatedAt: new Date().toISOString(), messages: finalMessages };

View File

@@ -277,7 +277,7 @@ export function LiveSandboxPreview({
NO_ARTIFACT_SELECTED
</h3>
<p className="text-xs text-zinc-600 font-bold leading-relaxed">
Select an artifact deliverable version from the Vault or ChatGPT session to launch real local URL execution.
Select an artifact deliverable version from the Vault or generation session to launch real local URL execution.
</p>
</div>
</div>

View File

@@ -91,7 +91,7 @@ export function ModelsConfigDrawer({
setProviderTestError('');
setProviderTestResults(null);
try {
const response = await rokcetFetch('/api/chatgpt/providers/test', {
const response = await rokcetFetch('/api/llm/providers/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ apiKeys }),

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { FocusBackdrop } from './FocusBackdrop';
import { RenameThreadModal } from './RenameThreadModal';
import { Workspace, Project, Session, ChatGPTCredentials, ActiveTabType } from '../types';
import { Workspace, Project, Session, LlmEngineConfig, ActiveTabType } from '../types';
import {
Layers,
BriefcaseBusiness,
@@ -37,7 +37,7 @@ interface SidebarProps {
onRenameSession: (sessionId: string, title: string) => void;
onDeleteSession: (sessionId: string) => void;
credentials: ChatGPTCredentials;
credentials: LlmEngineConfig;
onOpenSettingsModal: () => void;
activeTab: ActiveTabType;
setActiveTab: (tab: ActiveTabType) => void;

View File

@@ -1,5 +1,6 @@
import type { Artifact } from '../types';
import { getRokcetStackConfig } from '../../shared/stackConfigs';
import { VELORA_TEMPLATE_FILES } from './veloraTemplateSources';
export type BucketsTemplateId = 'business_app' | 'booking_concierge' | 'gated_knowledge' | 'consultant_portal';
@@ -583,7 +584,38 @@ function readme(config: BucketsTemplateConfig) {
return `# ${config.brandTitle} — ${composition}\n\nGenerated by Rokcet as a Buckets.id reference application.\n\n## Project contract\n\n- Application profile: **fullstack_web**\n- Stack adapter: **nextjs_typescript**\n- Deployment adapter: **vercel** (replaceable through the Rokcet project contract)\n- Buckets: service adapter for customer identity, managed AI, media, gated content/entitlements, Buckets Credits, bookings/knowledge where applicable, and signed webhooks.\n\nThe application itself is provider-neutral: it uses normal Next.js Route Handlers and environment variables. Vercel-specific deployment behavior stays in Rokcet's deployment adapter rather than in the application source.\n\n## Demo preview vs live runtime\n\n- Rokcet preview renders a deterministic simulation so the shared demo account can exercise the product without owning a real Buckets App or creating fabricated runtime traces.\n- The simulation is visibly labelled **DEMO SIMULATION** and does not write Buckets observability data.\n- A normal deployment uses the generated Next.js Route Handlers and real Buckets customer OAuth, global Buckets Credits, Access Offers/entitlements, conversations, bookings and signed webhooks.\n\n## Architecture\n\n- **Build time:** Rokcet coding agent → project contract → stack/deployment/service adapters → Buckets Developer MCP when configuration is required.\n- **Runtime:** browser → Next.js Route Handlers → Buckets customer APIs/conversations → managed Agent → authorized customer/runtime tools.\n- Developer MCP is never used by customer runtime traffic.\n\n## Security\n\nOAuth client secrets and webhook signing secrets stay in server-side environment variables. The frontend receives no Buckets management credentials.\n`;
}
function nextVeloraScaffoldFiles(config: BucketsTemplateConfig) {
const stack = getRokcetStackConfig('nextjs_typescript');
const controlled = stack.controlledPackages;
const title = config.appName || config.brandTitle;
const sourceCss = VELORA_TEMPLATE_FILES.find((file) => file.path === 'src/index.css')?.content || '';
const sourceFiles = VELORA_TEMPLATE_FILES.filter((file) => file.path !== 'src/index.css' && file.path !== 'src/main.tsx');
return [
{ path: 'app/page.tsx', language: 'typescript', content: `import DemoApp from '../components/DemoApp';\n\nexport default function Page() { return <DemoApp />; }\n` },
{ path: 'app/layout.tsx', language: 'typescript', content: `import type { Metadata } from 'next';\nimport './globals.css';\n\nexport const metadata: Metadata = { title: ${JSON.stringify(title)}, description: ${JSON.stringify(config.serviceName)} };\n\nexport default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {\n return <html lang="en"><body>{children}</body></html>;\n}\n` },
{ path: 'app/globals.css', language: 'css', content: `@import "tailwindcss";\n${sourceCss}` },
{ path: 'components/DemoApp.tsx', language: 'typescript', content: `'use client';\nimport VeloraApp from '../src/App';\nexport default function DemoApp(){ return <VeloraApp />; }\n` },
...sourceFiles,
{ path: 'app/api/[[...path]]/route.ts', language: 'typescript', content: nextRouteHandler(config) },
{ path: 'package.json', language: 'json', content: JSON.stringify({
name: 'velora-ai-models-booking-engine',
private: true,
version: '2.0.0',
scripts: { dev: 'next dev', build: 'next build', start: 'next start', typecheck: 'tsc --noEmit' },
dependencies: { next: controlled.next.version, react: controlled.react.version, 'react-dom': controlled['react-dom'].version, 'lucide-react': '^0.468.0' },
devDependencies: { '@tailwindcss/postcss': '^4.1.0', '@types/node': '^24.0.0', '@types/react': '^19.0.0', '@types/react-dom': '^19.0.0', postcss: '^8.5.0', tailwindcss: '^4.1.0', typescript: controlled.typescript.version },
engines: { node: stack.runtime.node },
}, null, 2) },
{ path: 'tsconfig.json', language: 'json', content: JSON.stringify({ compilerOptions: { target: 'ES2022', lib: ['DOM', 'DOM.Iterable', 'ES2022'], allowJs: false, skipLibCheck: true, strict: true, noEmit: true, esModuleInterop: true, module: 'ESNext', moduleResolution: 'Bundler', resolveJsonModule: true, isolatedModules: true, jsx: 'preserve', incremental: true, plugins: [{ name: 'next' }] }, include: ['next-env.d.ts', 'app/**/*.ts', 'app/**/*.tsx', 'components/**/*.ts', 'components/**/*.tsx', 'src/**/*.ts', 'src/**/*.tsx', '.next/types/**/*.ts'], exclude: ['node_modules'] }, null, 2) },
{ path: 'next-env.d.ts', language: 'typescript', content: `/// <reference types="next" />\n/// <reference types="next/image-types/global" />\n\n// This file is managed by Next.js.\n` },
{ path: 'postcss.config.mjs', language: 'javascript', content: `export default { plugins: { '@tailwindcss/postcss': {} } };\n` },
{ path: '.env.example', language: 'dotenv', content: `NODE_ENV=development\nBUCKETS_API_URL=${config.targetDomain}\nBUCKETS_IDENTITY_URL=${config.targetDomain}\nBUCKETS_APP_ID=${config.bucketsAppId || ''}\nBUCKETS_BUCKET_ID=${config.bucketId || ''}\nBUCKETS_CHANNEL_CONNECTION_ID=\nBUCKETS_OAUTH_CLIENT_ID=${config.oauthClientId || ''}\nBUCKETS_OAUTH_CLIENT_SECRET=\nBUCKETS_OAUTH_REDIRECT_URI=http://localhost:3000/api/auth/callback\nBUCKETS_WEBHOOK_SECRET=\n` },
{ path: 'README.md', language: 'markdown', content: readme(config) + `\n## Velora functionality\n\nThis demo preserves the Luxevibe reference interaction model: dense model discovery, rich model profiles, voice samples, gated media, reviews, full private chat rooms, suggested reply templates, gifts, affection/mood state, session timers, scenarios, four-step booking, booking add-ons, wallet/VIP, and the custom-model studio. Rokcet preview uses deterministic local simulation; deployed runtime routes managed conversations and bookings through Buckets.\n\n## Run locally\n\nCopy \`.env.example\` to your local environment, fill the Buckets values, then run \`npm install && npm run dev\`.\n` },
];
}
function nextScaffoldFiles(appSource: string, config: BucketsTemplateConfig) {
if (config.projectId === 'proj-framelab') return nextVeloraScaffoldFiles(config);
const stack = getRokcetStackConfig('nextjs_typescript');
const controlled = stack.controlledPackages;
const title = config.appName || config.brandTitle;

File diff suppressed because one or more lines are too long

View File

@@ -1,16 +1,16 @@
import { rokcetFetch } from './rokcetFetch';
import { Artifact, ChatGPTCredentials } from '../types';
import { Artifact, LlmEngineConfig } from '../types';
export async function executeChatGptAutomation(params: {
export async function executeLlmAutomation(params: {
prompt: string;
credentials: ChatGPTCredentials;
credentials: LlmEngineConfig;
referencedArtifacts: Artifact[];
projectName: string;
sessionId: string;
driverOptions: any;
history?: { role: 'user' | 'assistant'; content: string }[];
}) {
const response = await rokcetFetch('/api/chatgpt/execute', {
const response = await rokcetFetch('/api/llm/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params),

View File

@@ -65,7 +65,7 @@ export interface Session {
updatedAt: string;
referencedArtifacts: { artifactId: string; versionNumber: number }[];
messages: ChatMessage[];
chatgptChatId?: string;
executionId?: string;
}
export type LlmProvider = 'google' | 'openai' | 'anthropic' | 'groq';
@@ -96,9 +96,6 @@ export interface LlmEngineConfig {
apiKeyConfigured?: Record<keyof ProviderApiKeys, boolean>;
}
// Backward compatibility alias
export type ChatGPTCredentials = LlmEngineConfig;
export interface AutomationLog {
id: string;
timestamp: string;
@@ -147,7 +144,7 @@ export interface ChatMessage {
logs?: AutomationLog[];
deliverablesExtracted?: DeliverableExtracted[];
isExecuting?: boolean;
chatgptChatId?: string;
executionId?: string;
// Multi-model execution:
variants?: ModelResponseVariant[];
activeVariantIndex?: number;