fix: update AI model selection matrix and custom dashboard generation (#1615)

## What

Refresh the AI model selection matrix and fix a few issues in custom
dashboard generation.

### Models (`apps/backend/src/lib/ai/models.ts`)
- Replace deprecated/placeholder model IDs with current ones:
- `smart/slow` authenticated → `openai/gpt-5.5` (was
`x-ai/grok-build-0.1`)
  - `smart/fast` → `google/gemini-3.5-flash`
- `smartest` unauthenticated tiers → `z-ai/glm-5.2` /
`google/gemini-3.5-flash` (was `deepseek/deepseek-v4-flash`)
  - `dumb` unauthenticated tiers → `nvidia/nemotron-3-super-120b-a12b`

### Email template rewrite
- Forward `x-stack-*` / `x-hexclave-*` headers from the caller through
the template-source rewrite route so the inner AI call
(`/ai/query/generate`) is authenticated and resolves to the
**authenticated** model tier instead of falling back to the
unauthenticated one.
- Lower rewrite quality to `dumb` / `slow` (sufficient for this task,
cheaper/faster).

### Custom dashboard
- Speed up generation: `smart`/**fast** instead of `smart`/slow (both
`create-dashboard-preview.tsx` and `chat-adapters.ts`).
- Pin `@babel/standalone` to `7.29.7` in the sandbox host (avoid
surprise breakage from `latest`).
- Disable analytics in generated dashboards.

### Misc
- Bump MCP RPC timeout 15s → 45s (`apps/skills/src/mcp-wrapper.ts`).

## Testing
- `pnpm typecheck` 
- `pnpm lint` 

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Refreshes the model selection matrix, forwards auth headers so template
rewrites use authenticated tiers, and speeds up custom dashboard
generation with a more stable sandbox.

- **Refactors**
- Update model IDs: `openai/gpt-5.5`, `google/gemini-3.5-flash`,
`z-ai/glm-5.2`, `nvidia/nemotron-3-super-120b-a12b`.
  - Use `openai/gpt-5.5` for authenticated fast routes.
- Forward `x-stack-*` / `x-hexclave-*` headers; build via Map to avoid
prototype-pollution; inner generate call uses the authenticated tier.
  - Lower email template rewrite quality to `dumb`/`slow`.
- Switch dashboard generation to `smart`/`fast` in
`create-dashboard-preview.tsx` and `chat-adapters.ts`.
  - Disable analytics in generated dashboards.
  - Bump MCP RPC timeout from 15s to 45s.

- **Dependencies**
  - Pin `@babel/standalone` to `7.29.7` in the sandbox host.

<sup>Written for commit 94354ae0f6.
Summary will update on new commits.</sup>

<a
href="https://cubic.dev/pr/hexclave/hexclave/pull/1615?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **Performance**
* Improved AI generation speed for dashboard creation and related chat
flows by using faster AI routing.
* Increased MCP JSON-RPC request timeout to better handle long-running
operations.

* **Technical**
* Template rewriting with AI now forwards authentication-related headers
to downstream AI calls for more consistent authorized behavior.
  * Updated AI model routing/selection used by the proxy layer.

* **UI/Integration**
  * Pinned the sandbox Babel CDN script to a specific version.
  * Disabled analytics in the sandbox SDK configuration.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Mantra 2026-06-17 15:38:28 -07:00 committed by GitHub
parent aa6aae4590
commit 81068977ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 34 additions and 24 deletions

View File

@ -26,8 +26,22 @@ export const POST = createSmartRouteHandler({
tsx_source: yupString().defined(),
}).defined(),
}),
handler: async ({ body }) => {
const rewriteResult = await rewriteTemplateSourceWithAI(body.template_tsx_source);
handler: async ({ body }, fullReq) => {
// Forward the caller's Hexclave/Stack auth headers so the inner AI call
// (which is a fresh HTTP request to /ai/query/generate) is authenticated
// and resolves to the authenticated model tier rather than falling back
// to the unauthenticated one.
const authHeadersMap = new Map<string, string>();
for (const [key, value] of Object.entries(fullReq.headers)) {
if (value == null) continue;
const lower = key.toLowerCase();
if (lower.startsWith("x-stack-") || lower.startsWith("x-hexclave-")) {
authHeadersMap.set(key, value.join(","));
}
}
const authHeaders: Record<string, string> = Object.fromEntries(authHeadersMap);
const rewriteResult = await rewriteTemplateSourceWithAI(body.template_tsx_source, authHeaders);
if (rewriteResult.status === "error") {
throw new KnownErrors.TemplateSourceRewriteError(rewriteResult.error);
}

View File

@ -19,31 +19,31 @@ const MODEL_SELECTION_MATRIX: Record<
dumb: {
slow: {
authenticated: { modelId: "z-ai/glm-4.5-air:free" },
unauthenticated: { modelId: "z-ai/glm-4.5-air:free" },
unauthenticated: { modelId: "nvidia/nemotron-3-super-120b-a12b" },
},
fast: {
authenticated: { modelId: "openai/gpt-oss-120b:nitro" },
unauthenticated: { modelId: "openai/gpt-oss-120b:nitro" },
unauthenticated: { modelId: "nvidia/nemotron-3-super-120b-a12b:nitro" },
},
},
smart: {
slow: {
authenticated: { modelId: "x-ai/grok-build-0.1" },
unauthenticated: { modelId: "deepseek/deepseek-v4-flash" },
authenticated: { modelId: "openai/gpt-5.5" },
unauthenticated: { modelId: "z-ai/glm-5.2" },
},
fast: {
authenticated: { modelId: "x-ai/grok-build-0.1" },
unauthenticated: { modelId: "nvidia/nemotron-3-super-120b-a12b:nitro" },
authenticated: { modelId: "openai/gpt-5.5" },
unauthenticated: { modelId: "google/gemini-3.5-flash" },
},
},
smartest: {
slow: {
authenticated: { modelId: "openai/gpt-5.5" },
unauthenticated: { modelId: "deepseek/deepseek-v4-flash" },
unauthenticated: { modelId: "z-ai/glm-5.2" },
},
fast: {
authenticated: { modelId: "openai/gpt-5.5" },
unauthenticated: { modelId: "deepseek/deepseek-v4-flash:nitro" },
unauthenticated: { modelId: "google/gemini-3.5-flash" },
},
},
};

View File

@ -13,16 +13,16 @@ function isMockMode() {
return key === MOCK_API_KEY_SENTINEL || key === "FORWARD_TO_PRODUCTION";
}
async function rewriteTemplateSourceWithCurrentAIPlumbing(templateTsxSource: string): Promise<Result<string, string>> {
async function rewriteTemplateSourceWithCurrentAIPlumbing(templateTsxSource: string, authHeaders: Record<string, string>): Promise<Result<string, string>> {
const backendUrl = getEnvVariable("NEXT_PUBLIC_STACK_API_URL");
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), AI_REQUEST_TIMEOUT_MS);
try {
const response = await fetch(`${backendUrl}/api/latest/ai/query/generate`, {
method: "POST",
headers: { "content-type": "application/json" },
headers: { "content-type": "application/json", ...authHeaders },
body: JSON.stringify({
quality: "smart",
quality: "dumb",
speed: "slow",
tools: [],
systemPrompt: "rewrite-template-source",
@ -115,7 +115,7 @@ function stripCodeFences(text: string): string {
return output;
}
export async function rewriteTemplateSourceWithAI(templateTsxSource: string): Promise<Result<string, string>> {
export async function rewriteTemplateSourceWithAI(templateTsxSource: string, authHeaders: Record<string, string>): Promise<Result<string, string>> {
if (isMockMode()) {
const mockRewrittenSource = rewriteTemplateSourceInMockMode(templateTsxSource);
const mockRenderResult = await renderEmailWithTemplate(mockRewrittenSource, emptyEmailTheme, {
@ -130,7 +130,7 @@ export async function rewriteTemplateSourceWithAI(templateTsxSource: string): Pr
let lastError = "Unknown rewrite failure";
for (let attempt = 0; attempt < MAX_REWRITE_ATTEMPTS; attempt++) {
const rewriteResult = await rewriteTemplateSourceWithCurrentAIPlumbing(templateTsxSource);
const rewriteResult = await rewriteTemplateSourceWithCurrentAIPlumbing(templateTsxSource, authHeaders);
if (rewriteResult.status === "error") {
lastError = rewriteResult.error;
continue;

View File

@ -124,7 +124,7 @@ const CreateDashboardPreviewInner = memo(function CreateDashboardPreviewInner({
systemPrompt: "create-dashboard",
tools: ["update-dashboard"],
quality: "smart",
speed: "slow",
speed: "fast",
projectId: projectIdRef.current,
transformMessages: async (userMessages) => {
const contextMessages = await buildDashboardMessages(

View File

@ -333,12 +333,7 @@ function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string, dashbo
</head>
<body>
<div id="root"></div>
<!-- Babel (for JSX transpilation). crossorigin=anonymous is required so that
errors thrown from inside Babel (e.g. JSX SyntaxErrors from AI-generated
code) are not sanitized to "Script error." with no message unpkg sends
the matching Access-Control-Allow-Origin header. -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.7/babel.min.js" crossorigin="anonymous"></script>
<!-- Install a global error listener BEFORE any AI code runs so that Babel parse
errors, uncaught runtime throws, and async rejections all reach the parent.
@ -464,6 +459,7 @@ function getSandboxDocument(artifact: DashboardArtifact, baseUrl: string, dashbo
projectOwnerSession: async () => {
return await requestAccessToken();
},
analytics: { enabled: false },
});
// Expose under both names. AI-generated dashboards (post-PR2 prompt)

View File

@ -151,7 +151,7 @@ export function createDashboardChatAdapter(
systemPrompt: "create-dashboard",
tools,
quality: "smart",
speed: "slow",
speed: "fast",
projectId,
sanitizeContent: sanitizeAiContent,
transformMessages: async (messages) => {

View File

@ -10,7 +10,7 @@ const TOOL_ROUTE_HEADERS = {
"Access-Control-Allow-Headers": "*",
};
const MCP_RPC_TIMEOUT_MS = 15_000;
const MCP_RPC_TIMEOUT_MS = 45_000;
type JsonRecord = Record<string, unknown>;