feat: @catalyst/cloud-ai — multi-provider AI integration with useAI hook#288
Open
mayankmahavar1mg wants to merge 17 commits into
Open
feat: @catalyst/cloud-ai — multi-provider AI integration with useAI hook#288mayankmahavar1mg wants to merge 17 commits into
mayankmahavar1mg wants to merge 17 commits into
Conversation
Consolidates AI hooks (useCloudAI, useWebAI, useNativeAI) into a single @catalyst/cloud-ai package with a multi-provider Express router supporting OpenAI and Gemini via streaming (SSE) and non-streaming (JSON) paths. - Adds @catalyst/cloud-ai with route.js (CJS, server-only), useCloudAI, useWebAI, useNativeAI, and buildAttachmentSystemPrompt - Adds @catalyst/ai, @catalyst/web-ai-local, @catalyst/native-ai-local stubs - Wires useAI.js orchestrator into catalyst-core hooks with SSR guard, webpackIgnore, and __CATALYST_PACKAGES__.cloudAI graceful degradation - Updates expressServer.js to mount AI routes via absolute path (bypasses module-alias), with graceful fallback when package is absent - Updates base.babel.js with resolve.alias and DefinePlugin for cloud-ai - Adds examples/useai test app and sync-packages.js dev helper - Updates test-all.sh and test-web.sh for AI route coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1tWZLoJqQQ7Y7sRDXUL9r
…I lint fix, KB entries - route.js: fix Gemini stateful session (Interactions API v1beta, correct event fields, conversationId from parsed.interaction.id, stateful flag from key presence in req.body), add isAIEnabled() gate on all routes, extract getAIConfig() helper, pass stateful flag to adapters - NativeBridge.kt: reflection-based AIBridge loading (Class.forName + Proxy), no static imports of io.catalyst.nativeai.* — compiles with zero classpath dep - android/ module: AIBridge.kt + AIBridgeCallbacks.kt interface in catalyst-cloud-ai, CatalystAIBridge decoupled from :app, NativeBridgeAI.kt, ServiceLoader registration - build.gradle.kts: compileOnly always, implementation gated on ai.enabled - settings.gradle.kts: conditionally include :catalyst-cloud-ai when android/ folder present - buildAndroid/index.js: syncCloudAIIfEnabled copies package before Gradle runs - useAI.js: fix lint errors — remove unused useState/useEffect imports, add __CATALYST_PACKAGES__ global comment, restructure to call all hooks unconditionally (react-compiler compliance), module-level require guarded by _pkg - mcp_v2/mcp.js: handle empty DB file, improve error messages for version mismatch - knowledge-base.json: add 5 useAI integration entries covering basic usage, genConfig, stateful sessions, attachmentComponents, and loading/progress states Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1tWZLoJqQQ7Y7sRDXUL9r
…xample config/config.json contains API keys and local IPs — excluded from tracking. config.template.json provides a blank starting point for new contributors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1tWZLoJqQQ7Y7sRDXUL9r
|
DeputyDev will no longer review pull requests automatically.To request a review, simply comment #review on your pull request—this will trigger an on-demand review whenever you need it. |
Normalize per-provider usage payloads (OpenAI chat + responses, Gemini generateContent + Interactions) into a common shape so useCloudAI can compute cost/tps/cache-savings metrics uniformly, with a token-count fallback when a provider doesn't emit a usage frame. Stub getSessionMetrics/resetSessionMetrics on useNativeAI/useWebAI so callers can invoke them unconditionally across all three AI modes. Also adds Chess and Tic-Tac-Toe demos to the useai example app and drafts RFC/discussion docs for the agentic framework work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…crash latest.release silently resolved to 0.14.0 mid-session, which throws "Can not find OpenCL library on this device" from litertlm.cc's OnError callback on first generate() call, regardless of Backend.GPU()/CPU() selection. 0.13.1 is the last confirmed-working version on-device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… useCloudAI Adds POST /ai/generate Ktor route (drains the token flow, returns one JSON response) alongside the existing SSE /ai/stream route. useNativeAI.js gains a non-streaming branch, a historyRef accumulator, and real aggregateNativeSessionMetrics()-backed getSessionMetrics/resetSessionMetrics, replacing stubs. Panel.js and TicTacToe.js updated for the native provider's distinct session-stats shape (no cost/cache fields, since native has no billing and one local model). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…useNativeAI Adds aggregateWebSessionMetrics() (device/dtype instead of cost, mirroring aggregateNativeSessionMetrics's no-billing shape), a client-side stateful mode for useWebAI (message-history replay, since Transformers.js pipelines have no server-side session), and a matching isLocal branch in Panel.js's Session Stats modal. AITest.js gains a Local Session toggle alongside the existing Cloud/Native ones. Marked experimental throughout: in-browser inference quality and backend selection (WebGPU/WASM) aren't reliable yet on larger models (e.g. Llama 3) compared to the cloud/native providers — this plumbing is ready for when that's revisited, not a claim that local inference itself is production-ready. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
# Conflicts: # packages/catalyst-core/mcp_v2/knowledge-base.json # packages/catalyst-core/src/server/expressServer.js
The ai_integration entries described stateful sessions and metrics as if cloud/native/web all worked the same way. They don't: - stateful sessions: cloud translates conversationId to previous_response_id (OpenAI) / previous_interaction_id (Gemini); native reuses a LiteRT-LM Conversation object keyed by conversationId; web (experimental) has no real session at all — it's client-side message-history replay with a locally-generated UUID. - genConfig.stream / routes: cloud and native both have real stream/generate HTTP routes (different servers — Node vs embedded Ktor); web ignores genConfig.stream entirely and always streams via postMessage. - metrics: only cloud has cost/cachedTokens/cacheSavings; native and web have their own leaner shapes and their own aggregate*SessionMetrics() functions in metrics.js. Also added the getSessionMetrics/resetSessionMetrics/isWeb fields to the Basic Integration entry's return-value list, which were missing entirely. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follows the existing per-hook doc pattern (Import/Options/Returns/Usage) used throughout this file. Documents all three providers (cloud/native/web), the per-provider genConfig routing and stateful-session mechanics, the differing metrics shapes, and attachment components — matching what's actually implemented in useCloudAI/useNativeAI/useWebAI, not a generic unified description. Notes the web/transformers provider as experimental. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… manifest regex, async cleanup) - FrameworkServerUtils.kt: wrap /ai/generate's tokenFlow.collect in a 120s withTimeout, responding 504 instead of hanging forever if the native supplier stalls. - test-catalyst-core.sh: fail fast if npm pack doesn't produce a catalyst-core tarball, instead of silently falling through to npm install pulling the published package. - buildAndroid/assets.js: scope the meta-data removal regex to a single tag ([^>]*? instead of [\s\S]*?) so it can't span tag boundaries and delete unrelated <meta-data> entries. - buildIos/assets.js: drop the unnecessary async from validateNotificationAsset (no awaits inside) so sync errors surface to the caller's try/catch instead of becoming unhandled rejections. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…alerts
CI failures (catalyst-core, create-catalyst-app jobs both failing on
`npm ci` with ETARGET):
- All four AI packages (catalyst-ai, catalyst-cloud-ai, catalyst-web-ai-local,
catalyst-native-ai-local) declared peerDependencies: { "catalyst-core":
">=0.1.0" }. catalyst-core is a prerelease (0.2.0-beta.1), and plain semver
ranges never match prereleases outside their own major.minor.patch —
confirmed via semver.satisfies('0.2.0-beta.1', '>=0.1.0') === false.
Widened to ">=0.2.0-0", which matches 0.2.0 prereleases and everything
>=0.2.0 stable.
- package-lock.json was also missing entries for all four packages (added
well after the lockfile was last regenerated) — npm ci installs strictly
from the lockfile and can't resolve what isn't pinned there, which was a
second, independent cause of the same failure. Regenerated via
`npm install --package-lock-only` and verified with the actual `npm ci` +
scripts/test-catalyst-core.sh commands CI runs.
- test-catalyst-core.sh restored as executable (mode 100644 -> 100755).
CodeQL alerts (2 high, 4 medium, all on this PR's own code):
- route.js:453,476 — provider was interpolated directly into the first arg
of console.error (format-string + log-injection risk per CodeQL's static
analysis, even though provider is validated against a fixed PROVIDERS
allowlist a few lines earlier). Switched to console.error("%s ...",
provider, ...) so it's passed as a separate argument, not part of the
format string.
- sync-packages.js:39,41 — rimraf() shelled out via execSync("rm -rf ...")
/ execSync("find ... -delete"). Replaced with fs.rmSync(dir, { recursive:
true, force: true }), which does the same thing without invoking a shell
at all, eliminating the "uncontrolled command line" class of finding.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
| try { | ||
| await adapter.stream({ apiKey: cfg.apiKey, model: resolvedModel, messages, genConfig, conversationId, stateful, res }) | ||
| } catch (err) { | ||
| console.error("[@catalyst/cloud-ai] %s stream error:", provider, err.message) |
| const result = await adapter.generate({ apiKey: cfg.apiKey, model: resolvedModel, messages, genConfig, conversationId, stateful }) | ||
| res.json({ ...result, model: resolvedModel }) | ||
| } catch (err) { | ||
| console.error("[@catalyst/cloud-ai] %s generate error:", provider, err.message) |
…-ai-local) Only @catalyst/cloud-ai is used at runtime (useAI.js requires it directly). catalyst-ai is a one-line re-export nothing imports; web-ai-local/native-ai-local are only referenced by dead DefinePlugin flags in base.babel.js that nothing reads. Deleting also removes catalyst-ai's preinstall check-peer.js, which was breaking npm ci once its lockfile entry existed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…mcp stderr) - Chess.js: reset aiThinking on generate() failure so the error-effect doesn't trigger a second fallback move for the same AI turn (critical) - expressServer.js: only skip AI router mount on genuine MODULE_NOT_FOUND; surface/log other resolve or mount errors instead of swallowing them - base.babel.js, route.js: guard AI_CONFIG JSON.parse so malformed env JSON doesn't abort webpack config eval or crash the route module - androidSetup.js: fix misplaced eslint-disable-next-line that was targeting the nosemgrep comment instead of the exec() call it guards Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…yst-core In an npm workspace install, sibling packages aren't guaranteed to be linked yet when preinstall scripts run -- npm's own lifecycle ordering varies across versions (verified locally: passes on npm 9, fails on npm 10/Node 22, which matches what CI runs). check-peer.js was hard process.exit(1)-ing on this false positive, breaking npm ci for the catalyst-core and create-catalyst-app CI jobs. peerDependencies + peerDependenciesMeta already declare the requirement, so downgrade to a warning instead of aborting the install. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
docs/agentic-framework-discussion.md, docs/github-discussion-draft.md, docs/content/useai-rfc.md, and docs/ios-native-ai-deferred.md were working/planning documents that got committed alongside the real docs update but were never meant to ship as published documentation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@catalyst/cloud-aipackage — consolidated AI package exportinguseCloudAI,useWebAI,useNativeAI, andbuildAttachmentSystemPrompt. Multi-provider server route (POST /ai/:provider/streamSSE +POST /ai/:provider/generateJSON) supporting OpenAI and Gemini. All routes gated behindAI_CONFIG.enabled.useAIhook (catalyst-core/hooks) — orchestrator that routes to the right sub-hook based onprovideroption:openai/gemini→useCloudAI,transformers→useWebAI,native→useNativeAI(falls back touseCloudAIifNativeBridge.isAIAvailable()is false). Stateful sessions supported viasessionMode='stateful'.v1beta/interactions), correct SSE event fields (event_type,parsed.interaction.id), stateful flag derived from key presence in request body.android/module ships inside@catalyst/cloud-aiwithAIBridge/AIBridgeCallbacksinterface.NativeBridge.ktuses reflection (Class.forName+Proxy.newProxyInstance) — zero static imports, compiles without:catalyst-cloud-aion classpath.build.gradle.ktsconditionally bundles LiteRT.buildAndroid/index.jssyncs the package before Gradle runs.examples/useai— full test app with Cloud/Local/Native panels, streaming toggle, provider switcher, stateful session toggle, attachment component rendering, generation settings, and system prompt editor.useAIintegration entries + mcp.js robustness fixes (empty DB, Node version mismatch errors).Test plan
npm run lint— passes cleanexamples/useaidev server, test cloud panel with OpenAI (stream + non-stream)sessionMode=stateful— verify turn 1 returnsconversationId, turn 2 maintains contexttransformersprovider — model download progress + inferenceisAIAvailable()check, fallback to cloud when bridge absentAI_CONFIG.enabled=falsereturns 403 on all/ai/*routesconfig/config.template.json→config/config.json, fill API keys, confirm no keys in git🤖 Generated with Claude Code
https://claude.ai/code/session_01A1tWZLoJqQQ7Y7sRDXUL9r
Update: OpenCL fix, native/web metrics parity, docs, CodeRabbit fixes
Root cause fix — native AI crash (
Can not find OpenCL library on this device)litertlm-androidwas pinned tolatest.release, which silently resolved to0.14.0mid-session and introduced an OpenCL sampler regression (fails at firstgenerate()call, even onBackend.CPU(), since the sampler probes OpenCL regardless of the configured backend). Confirmed via local Gradle cache timestamps (0.13.1 cached weeks earlier, 0.14.0 pulled the same day the crash appeared) and matches google-ai-edge/LiteRT#6999 and google-ai-edge/LiteRT-LM#1860. Pinned to0.13.1(last known-good) inpackages/catalyst-cloud-ai/android/build.gradle.kts.Native (
useNativeAI) — non-streaming generate + real session metricsPOST /framework-{sessionId}/ai/generateKtor route (drains the token flow, returns one JSON response) alongside the existing SSE/ai/streamroute.useNativeAI.jsgains a non-streaming branch, ahistoryRefaccumulator, and realgetSessionMetrics/resetSessionMetricsbacked by a newaggregateNativeSessionMetrics()— replacing stubs. Native's aggregate shape has no cost/cache fields (no billing, one local model), unlike cloud.ContentNegotiation(would've produced invalid JSON), andtotalTokenswas computed inconsistently between streaming (token-frame count) and non-streaming (char count) branches, which would've corrupted session aggregation across mixed-mode sessions.Web (
useWebAI) — session metrics + stateful chat (marked experimental)aggregateWebSessionMetrics()(device/dtype instead of cost, same no-billing shape reasoning as native).{role, content}history into eachgenerate()call, with a locally-generatedconversationIdfor the UI to key off.examples/useaiUIPanel.js's Session Stats modal now has a thirdisLocalbranch (was onlyisNative ? native : cloud, which would've rendered bogus$0.00000cost tiles for web).AITest.jsgains a Local Session stateful toggle alongside the existing Cloud/Native ones (previously the Local panel had nosessionModewiring at all).Docs
useAIreference section todocs/content/11-API Reference/01-Hooks.md, matching the file's existing per-hook pattern (Import/Options/Returns/Usage), with dedicated subsections for what actually differs per provider:genConfig/route behavior, stateful-session mechanics, and metrics shape.ai_integrationentries, which previously described stateful sessions and metrics as if all three providers worked identically — they don't (see above).CodeRabbit review fixes (
coderabbit review --agent --base-commit e356eb9, scoped to this branch's own commits — 8 findings total, 1 in our code, 3 more picked up and fixed since they were straightforward)FrameworkServerUtils.kt:/ai/generate'stokenFlow.collecthad no timeout — a stalled native supplier would hang the request forever. Wrapped in a 120swithTimeout, responds504on timeout.scripts/test-catalyst-core.sh: guarded against a silent fallback to the publishedcatalyst-corepackage ifnpm packdoesn't produce a tarball.buildAndroid/assets.js: scoped the notification meta-data removal regex to a single tag (was using a dot-all wildcard that could span tag boundaries and delete unrelated<meta-data>entries).buildIos/assets.js: removed an unnecessaryasyncfromvalidateNotificationAsset(noawaits inside) so sync errors surface to the caller's try/catch instead of becoming unhandled promise rejections.Security checks (semgrep, trivy vuln/secret/misconfig, npm audit) all came back clean on the files touched this session; the 7 moderate
npm auditfindings inexamples/useaiare pre-existing transitive deps (file-type,http-proxy-middleware,imagemin,sockjs,uuid,webpack-dev-server) requiring acatalyst-coremajor bump, unrelated to this PR's scope.