Skip to content

Phase 7g.7: WASM SDK askPrompt() / askParse() (Q9 split shape)#70

Merged
joaoh82 merged 1 commit into
mainfrom
feat/wasm-sdk-ask
May 2, 2026
Merged

Phase 7g.7: WASM SDK askPrompt() / askParse() (Q9 split shape)#70
joaoh82 merged 1 commit into
mainfrom
feat/wasm-sdk-ask

Conversation

@joaoh82
Copy link
Copy Markdown
Owner

@joaoh82 joaoh82 commented May 1, 2026

Summary

Per Q9, the WASM SDK has a different ask() shape than every other SDK. The browser tab does the schema-aware prompt construction in-page, but does NOT make the HTTP call itself. The JS caller's backend handles the LLM API call. Browser tab never sees the API key, never POSTs to a third-party LLM endpoint, never deals with CORS.

import init, { Database } from '@joaoh82/sqlrite-wasm';
await init();

const db = new Database();
db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)`);

// Step 1: build Anthropic /v1/messages request body in-browser
const payload = db.askPrompt('How many users are over 30?');

// Step 2: caller's backend forwards to Anthropic with the API key
const apiResponse = await fetch('/api/llm/complete', {
  method: 'POST', body: JSON.stringify(payload),
}).then(r => r.text());

// Step 3: WASM parses the response back
const result = db.askParse(apiResponse);
// → { sql, explanation, usage: { inputTokens, outputTokens, cacheCreationInputTokens, cacheReadInputTokens } }

The complete worked example (with a minimal Node/Express backend proxy showing where the API key lives) is in sdk/wasm/README.md per Q9's documentation requirement.

Why this design (recap from Q9)

Two reasons direct browser-to-LLM calls don't work:

  1. CORS. Browsers block direct cross-origin POSTs from a WASM module to api.anthropic.com unless the LLM provider serves CORS headers. They don't, by design — they don't want users embedding API keys in client-side JS.
  2. API key exposure. Even if CORS were OK, putting the API key into a WASM-loaded page exposes it to anyone who opens devtools.

Both problems disappear server-side. The WASM SDK split makes the trust boundary the user's own backend, where the API key already lives.

Required structural refactor (sqlrite-ask + engine)

Wiring 7g.7 forced three feature/visibility changes:

1. sqlrite-ask got an http feature flag

[features]
default = ["http"]
http = ["dep:ureq"]

[dependencies]
ureq = { version = "2", features = ["json", "tls"], optional = true }

AnthropicProvider, ask_with_schema, and the provider::anthropic module are all gated on http. The wasm-safe parts — prompt::build_system, parse_response, AskConfig, AskResponse, AskUsage, AskError, Provider trait, generic ask_with_schema_and_provider<P> — stay always-on.

2. Engine: sqlrite::ask::schema un-gated

The schema dump is pure-engine code (no sqlrite-ask dep), but it lived inside the #[cfg(feature = "ask")] module. Moved pub mod schema; to always-on; gated only the sqlrite-ask-importing parts (ConnectionAskExt + free functions) under the ask feature.

3. sqlrite_ask::parse_response made public

Was fn-private; the WASM SDK calls it on the model's text content after JS hands the API response back through askParse.

Build verification — three feature configurations clean

Build Verifies
cargo check -p sqlrite-engine --features cli,ask,file-locks REPL binary path (default-features). Existing, unchanged behavior.
cargo check -p sqlrite-engine --no-default-features Minimal library build. Existing, unchanged.
cd sdk/wasm && wasm-pack build --target web wasm32 target ✅ — generates pkg/sqlrite_wasm.d.ts with TypeScript types for askPrompt, askParse, AskPromptOptions.

What's new (WASM API surface)

JS Returns Purpose
db.askPrompt(question, options?) Object Builds Anthropic /v1/messages request body.
db.askParse(rawApiResponse) { sql, explanation, usage } Parses full Anthropic API response back into the canonical AskResponse shape.
new AskPromptOptions() AskPromptOptions Mutable model / maxTokens / cacheTtl fields. Defaults match every other SDK: claude-sonnet-4-6 / 1024 / 5m.

AskPromptOptions is the only WASM-side class because askPrompt is the only call that takes config (the LLM API call itself is the user's backend's responsibility).

Tests

This is a JS-API SDK whose Rust side delegates to existing infrastructure — no new unit tests. Verification path:

  • cargo fmt --all -- --check — clean (engine + sqlrite-ask)
  • (cd sdk/wasm && cargo fmt -- --check) — clean
  • cargo test --workspace --exclude sqlrite-desktop --exclude sqlrite-python --exclude sqlrite-nodejs — 301/301 still pass (engine + sqlrite-ask refactors moved code without changing behavior)
  • cd sdk/wasm && cargo check — native lib builds (rlib only)
  • cd sdk/wasm && wasm-pack build --target web — wasm32 target builds clean ✅
  • CI: rust matrix, python-sdk, nodejs-sdk, go-sdk, wasm-build, desktop-build all green
  • Manual smoke: cd sdk/wasm && wasm-pack build --target web && python -m http.server against the existing examples/wasm/ console; call db.askPrompt('list users') and verify the JSON shape

Why JSON-as-Anthropic-shape rather than provider-neutral

askPrompt returns Anthropic's /v1/messages body directly. The JS caller can forward it as-is to Anthropic without translation. Non-Anthropic providers (OpenAI, Ollama) translate server-side on the user's backend — the WASM SDK doesn't need to know about other providers, and the user's backend is the natural place to handle provider variety. This matches how the WASM split addresses key safety in the first place: the trust boundary is the user's backend, so logic that depends on the choice of provider lives there too.

Docs

  • sdk/wasm/README.md — "coming soon" replaced with full Q9-required worked example: two-step flow, complete Node/Express backend proxy, AskPromptOptions reference, non-Anthropic-provider note, cache-hit verification via usage.cacheReadInputTokens. About 100 LOC of new prose.
  • docs/phase-7-plan.md — 7g.7 marked ✅ with the structural-refactor list.
  • docs/roadmap.md — 7g bullet updated. 7g.8 (MCP ask tool) is the only sub-phase left in 7g.

Next up after merge

  1. Merge → dispatch release-pr.yml at v0.1.24@joaoh82/sqlrite-wasm@0.1.24 carries the new askPrompt / askParse surface.
  2. 7g.8 — MCP ask tool. Hooks into the Phase 7h MCP server framework (deferred since 7h hasn't shipped yet — pivot here might be reordering 7h before 7g.8 to avoid blocking).
  3. 7hsqlrite-mcp binary + JSON-RPC + tool framework. New product line; same shape as sqlrite-ask joined the lockstep wave in 7g.1.

🤖 Generated with Claude Code

The structurally different SDK in the wave: per Q9, the WASM module
does the schema-aware prompt construction in-page but does NOT make
the HTTP request itself. The JS caller's backend handles the LLM
API call. Browser tab never sees the API key, never POSTs to a
third-party LLM endpoint, never deals with CORS.

## Public surface — two-step JS API

```js
import init, { Database } from '@joaoh82/sqlrite-wasm';
await init();

const db = new Database();
db.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)`);

// Step 1: build the request body in-browser
const payload = db.askPrompt('How many users are over 30?');
// → Anthropic /v1/messages body: { model, max_tokens, system, messages }

// Step 2: caller's backend forwards to Anthropic with the API key
const apiResponse = await fetch('/api/llm/complete', {
  method: 'POST',
  body: JSON.stringify(payload),
}).then(r => r.text());

// Step 3: WASM parses the response back
const result = db.askParse(apiResponse);
// → { sql, explanation, usage: {...} }
```

Backend proxy is ~10 lines of Express / Cloudflare Worker / Vercel
Edge — README has the worked example (Q9 doc requirement).

## What's new

  * `Database.askPrompt(question, options?)` — builds Anthropic's
    /v1/messages request body. The cache_control marker on the
    schema block is preserved so prompt caching works through the
    proxy.
  * `Database.askParse(rawApiResponse)` — parses the full Anthropic
    API response (text content + usage) back into
    `{ sql, explanation, usage: { inputTokens, outputTokens,
       cacheCreationInputTokens, cacheReadInputTokens } }`.
    Tolerant of fenced JSON / leading prose in the model's text
    (same parser the other SDKs use).
  * `AskPromptOptions` class with `model`, `maxTokens`, `cacheTtl`
    fields. Defaults match every other SDK: claude-sonnet-4-6 /
    1024 / 5m.

## Required structural refactor

Wiring the WASM SDK forced three feature/visibility changes that
weren't strictly necessary before:

1. **`sqlrite-ask` got an `http` feature flag.** `ureq` is now
   optional behind `[features] default = ["http"], http =
   ["dep:ureq"]`. The `AnthropicProvider`, `ask_with_schema`, and
   `provider::anthropic` module are all gated on `http`. The
   wasm-safe parts — `prompt::build_system`, `parse_response`,
   `AskConfig`, `AskResponse`, `AskUsage`, `AskError`, `Provider`
   trait, generic `ask_with_schema_and_provider<P>` — stay
   always-on. The WASM SDK depends on sqlrite-ask with
   `default-features = false`.

2. **Engine: `sqlrite::ask::schema` un-gated.** The schema dump is
   pure-engine code (no sqlrite-ask dep), but it lived inside the
   `#[cfg(feature = "ask")]` module so wasm-safe consumers couldn't
   reach it. Moved the `schema` submodule to always-on; gated only
   the sqlrite-ask-importing parts (ConnectionAskExt + free
   functions) under the `ask` feature.

3. **`sqlrite_ask::parse_response` made public.** Was `fn`-private
   before; the WASM SDK calls it on the model's text content after
   JS hands the API response back through `askParse`.

Verified all three configurations build clean:
  * `cargo check -p sqlrite-engine --features cli,ask,file-locks`
    — REPL binary build (existing, unchanged).
  * `cargo check -p sqlrite-engine --no-default-features` — minimal
    library build (existing, unchanged).
  * `cd sdk/wasm && wasm-pack build --target web` — wasm32 target
    builds clean (~30s release build), generates `pkg/sqlrite_wasm.d.ts`
    with proper TypeScript types for `askPrompt` / `askParse` /
    `AskPromptOptions`.

## Why JSON-as-Anthropic-shape rather than a provider-neutral form

`askPrompt` returns Anthropic's `/v1/messages` body shape directly.
The JS caller can forward it as-is to Anthropic without translation.
Non-Anthropic providers (OpenAI, Ollama) translate server-side on
the user's backend — the WASM SDK doesn't need to know about other
providers, and the user's backend is the natural place to handle
provider variety anyway. This matches how the WASM split addresses
key safety in the first place: the trust boundary is the user's
backend, so logic that depends on the choice of provider lives
there too.

## Tests

This is a JS-API SDK whose Rust side has no testable behavior
beyond "delegates to existing infrastructure" — no new unit tests
on the WASM side. Verification path:

  * `cargo test --workspace --exclude sqlrite-desktop --exclude
    sqlrite-python --exclude sqlrite-nodejs` — 301/301 still pass
    (the engine + sqlrite-ask refactors moved code without changing
    behavior).
  * `cd sdk/wasm && cargo check && wasm-pack build --target web` —
    both clean.
  * Browser-side smoke: load the build into the existing
    `examples/wasm/` console, run a CREATE TABLE + askPrompt() +
    look at the JSON.

## Docs

  * `sdk/wasm/README.md` — "coming soon" section replaced with the
    full Q9-required worked example: two-step flow, complete
    Node/Express backend proxy showing where the API key lives,
    `AskPromptOptions` reference, non-Anthropic-provider note,
    cache-hit verification via `usage.cacheReadInputTokens`. About
    100 LOC of new prose.
  * `docs/phase-7-plan.md` — 7g.7 marked ✅ with the structural-
    refactor list and the `wasm-pack build` verification note.
  * `docs/roadmap.md` — 7g bullet updated; 7g.8 (MCP `ask` tool) is
    the only sub-phase left.

cargo fmt clean across all touched crates. Pre-existing 2 warnings
on `sdk/wasm/src/lib.rs` are unchanged (cosmetic `let mut` on lines
that don't need it; in functions I didn't touch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joaoh82 joaoh82 merged commit 39f60b0 into main May 2, 2026
15 of 16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant