@elytracms/studio 0.0.56 → 0.0.58

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.
Files changed (84) hide show
  1. package/convex/auth.ts +7 -0
  2. package/convex/users.ts +41 -14
  3. package/dist/components/ai/ai-chat-panel-lazy.js +3 -3
  4. package/dist/components/ai/ai-chat-panel-lazy.js.map +1 -1
  5. package/dist/components/ai/ai-chat-panel.d.ts +0 -9
  6. package/dist/components/ai/ai-chat-panel.js +199 -60
  7. package/dist/components/ai/ai-chat-panel.js.map +1 -1
  8. package/dist/components/ai/chat-ui.d.ts +62 -10
  9. package/dist/components/ai/chat-ui.js +139 -23
  10. package/dist/components/ai/chat-ui.js.map +1 -1
  11. package/dist/components/ai/proposal-card.d.ts +30 -7
  12. package/dist/components/ai/proposal-card.js +81 -21
  13. package/dist/components/ai/proposal-card.js.map +1 -1
  14. package/dist/components/cms/detail-editor.js +12 -0
  15. package/dist/components/cms/detail-editor.js.map +1 -1
  16. package/dist/components/cms/document-diff-view.d.ts +12 -1
  17. package/dist/components/cms/document-diff-view.js +119 -42
  18. package/dist/components/cms/document-diff-view.js.map +1 -1
  19. package/dist/components/cms/rich-text-preview.d.ts +15 -0
  20. package/dist/components/cms/rich-text-preview.js +81 -0
  21. package/dist/components/cms/rich-text-preview.js.map +1 -0
  22. package/dist/components/cms/version-compare-text.d.ts +26 -0
  23. package/dist/components/cms/version-compare-text.js +60 -0
  24. package/dist/components/cms/version-compare-text.js.map +1 -1
  25. package/dist/lib/ai/anthropic-provider.d.ts +0 -12
  26. package/dist/lib/ai/anthropic-provider.js +3 -11
  27. package/dist/lib/ai/anthropic-provider.js.map +1 -1
  28. package/dist/lib/ai/apply-content.js +52 -10
  29. package/dist/lib/ai/apply-content.js.map +1 -1
  30. package/dist/lib/ai/chat-context.d.ts +74 -0
  31. package/dist/lib/ai/chat-context.js +100 -0
  32. package/dist/lib/ai/chat-context.js.map +1 -0
  33. package/dist/lib/ai/chat-loop.d.ts +70 -0
  34. package/dist/lib/ai/chat-loop.js +46 -0
  35. package/dist/lib/ai/chat-loop.js.map +1 -0
  36. package/dist/lib/ai/chat-state.d.ts +73 -57
  37. package/dist/lib/ai/chat-state.js +49 -90
  38. package/dist/lib/ai/chat-state.js.map +1 -1
  39. package/dist/lib/ai/fake-provider.d.ts +139 -6
  40. package/dist/lib/ai/fake-provider.js +261 -8
  41. package/dist/lib/ai/fake-provider.js.map +1 -1
  42. package/dist/lib/ai/gateway-provider.d.ts +30 -0
  43. package/dist/lib/ai/gateway-provider.js +73 -0
  44. package/dist/lib/ai/gateway-provider.js.map +1 -0
  45. package/dist/lib/ai/index.d.ts +5 -1
  46. package/dist/lib/ai/index.js +5 -1
  47. package/dist/lib/ai/index.js.map +1 -1
  48. package/dist/lib/ai/node-handler.d.ts +13 -4
  49. package/dist/lib/ai/node-handler.js +33 -3
  50. package/dist/lib/ai/node-handler.js.map +1 -1
  51. package/dist/lib/ai/rich-text-input.d.ts +27 -0
  52. package/dist/lib/ai/rich-text-input.js +25 -0
  53. package/dist/lib/ai/rich-text-input.js.map +1 -0
  54. package/dist/lib/ai/route-provider.d.ts +35 -10
  55. package/dist/lib/ai/route-provider.js +13 -6
  56. package/dist/lib/ai/route-provider.js.map +1 -1
  57. package/dist/lib/ai/route.d.ts +50 -21
  58. package/dist/lib/ai/route.js +69 -40
  59. package/dist/lib/ai/route.js.map +1 -1
  60. package/dist/lib/ai/shape.d.ts +12 -1
  61. package/dist/lib/ai/shape.js +46 -20
  62. package/dist/lib/ai/shape.js.map +1 -1
  63. package/dist/lib/ai/tools.d.ts +242 -37
  64. package/dist/lib/ai/tools.js +548 -63
  65. package/dist/lib/ai/tools.js.map +1 -1
  66. package/dist/lib/ai/types.d.ts +31 -16
  67. package/dist/lib/ai/types.js.map +1 -1
  68. package/dist/lib/ai/update-fields.d.ts +68 -0
  69. package/dist/lib/ai/update-fields.js +296 -0
  70. package/dist/lib/ai/update-fields.js.map +1 -0
  71. package/dist/lib/ai-current-document.d.ts +53 -0
  72. package/dist/lib/ai-current-document.js +54 -0
  73. package/dist/lib/ai-current-document.js.map +1 -0
  74. package/dist/lib/auth/convex-auth-adapter.d.ts +9 -1
  75. package/dist/lib/auth/convex-auth-adapter.js +28 -2
  76. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  77. package/dist/lib/cms-views/view-model.d.ts +6 -6
  78. package/dist/lib/i18n/messages.en.js +19 -1
  79. package/dist/lib/i18n/messages.en.js.map +1 -1
  80. package/dist/ui/badge.d.ts +1 -1
  81. package/package.json +4 -3
  82. package/dist/lib/ai/create-flow.d.ts +0 -132
  83. package/dist/lib/ai/create-flow.js +0 -280
  84. package/dist/lib/ai/create-flow.js.map +0 -1
@@ -0,0 +1,73 @@
1
+ import { DEFAULT_ANTHROPIC_MODEL } from './anthropic-provider.js';
2
+ /** Default gateway model — the direct-Anthropic default, addressed through the gateway. */
3
+ export const DEFAULT_GATEWAY_MODEL = `anthropic/${DEFAULT_ANTHROPIC_MODEL}`;
4
+ /**
5
+ * Resolve the effective gateway `apiKey` with the apiKey-beats-OIDC foot-gun guards
6
+ * (pure core; env + warn injected for tests):
7
+ * - `undefined` passes through (OIDC / env-var auth applies downstream);
8
+ * - blank/whitespace → treated as ABSENT + loud warning (a set key, even an
9
+ * empty one, would otherwise silently beat OIDC);
10
+ * - a real key while OIDC is also available → kept, with a warning that it wins.
11
+ */
12
+ export function resolveGatewayApiKey(apiKey, env = typeof process !== 'undefined' ? process.env : {}, warn = (message) => console.warn(message)) {
13
+ if (apiKey === undefined)
14
+ return undefined;
15
+ const trimmed = apiKey.trim();
16
+ if (trimmed === '') {
17
+ warn('[elytra-ai] gateway apiKey is blank — ignoring it so AI_GATEWAY_API_KEY/OIDC can apply. ' +
18
+ 'A set apiKey always beats OIDC, even an invalid one.');
19
+ return undefined;
20
+ }
21
+ if (env.VERCEL_OIDC_TOKEN) {
22
+ warn('[elytra-ai] gateway apiKey is set while Vercel OIDC is also available — the explicit ' +
23
+ 'apiKey wins (even if invalid). Remove it to authenticate via OIDC.');
24
+ }
25
+ return trimmed;
26
+ }
27
+ /**
28
+ * Build a Vercel-AI-Gateway-backed {@link AiProvider}. Tool calling + structured
29
+ * output are supported (the gateway fronts tool-calling models), so it satisfies
30
+ * the capability gate. The AI SDK is loaded lazily on first call.
31
+ */
32
+ export function createGatewayProvider(options = {}) {
33
+ const model = options.model ?? DEFAULT_GATEWAY_MODEL;
34
+ const apiKey = resolveGatewayApiKey(options.apiKey);
35
+ // Lazily construct the SDK language model — no eager import of the AI SDK.
36
+ async function languageModel() {
37
+ const { createGateway } = await import('ai');
38
+ const gateway = createGateway({
39
+ ...(apiKey !== undefined ? { apiKey } : {}),
40
+ ...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),
41
+ });
42
+ return gateway(model);
43
+ }
44
+ return {
45
+ id: 'gateway',
46
+ model,
47
+ capabilities: () => ({ toolCalling: true, structuredOutput: true }),
48
+ languageModel,
49
+ async generateStructured(request) {
50
+ const { generateObject, jsonSchema } = await import('ai');
51
+ const languageModelInstance = await languageModel();
52
+ const result = await generateObject({
53
+ model: languageModelInstance,
54
+ schema: jsonSchema(request.schema),
55
+ ...(request.schemaName !== undefined ? { schemaName: request.schemaName } : {}),
56
+ ...(request.schemaDescription !== undefined
57
+ ? { schemaDescription: request.schemaDescription }
58
+ : {}),
59
+ ...(request.system !== undefined ? { system: request.system } : {}),
60
+ prompt: request.prompt,
61
+ });
62
+ const usage = result.usage;
63
+ return {
64
+ object: result.object,
65
+ usage: {
66
+ ...(usage?.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),
67
+ ...(usage?.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),
68
+ },
69
+ };
70
+ },
71
+ };
72
+ }
73
+ //# sourceMappingURL=gateway-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/gateway-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Vercel AI Gateway provider adapter (EC-334).\n *\n * First-class gateway support so a Vercel-hosted studio runs key-less through the\n * gateway (`AI_GATEWAY_API_KEY` env, or automatic OIDC on Vercel deployments) —\n * covering key-less OIDC, gateway-billed, and BYOK (gateway-dashboard config, no\n * code change) in one integration. Direct `ANTHROPIC_API_KEY` via\n * `createAnthropicProvider` remains the non-Vercel fallback; ratified mount pick\n * order: explicit config wins, then `ANTHROPIC_API_KEY` (direct) →\n * `AI_GATEWAY_API_KEY`/OIDC (gateway) → FakeProvider.\n *\n * Same dependency discipline as the Anthropic provider: `createGateway` is\n * re-exported by `ai` itself (backed by `@ai-sdk/gateway`, already in the\n * dependency graph — zero new install) and loads via dynamic `import()` only, so\n * importing this module (or the barrel) never eagerly loads the SDK.\n *\n * Foot-gun guarded at construction (config validation, fail-loud culture): a SET\n * `apiKey` ALWAYS wins over OIDC, even an invalid one — so a blank/whitespace key\n * (the classic `apiKey: process.env.AI_GATEWAY_API_KEY` with an empty var) is\n * treated as ABSENT with a loud warning instead of silently disabling OIDC, and a\n * real key alongside an available OIDC token warns that the key wins.\n *\n * NOTE: desk-verified only as of EC-334 slice 1 (no gateway key was available) —\n * the model-string availability and OIDC behavior await the consumer proving pass.\n */\n// Type-only (erased at compile) — the runtime SDK loads via dynamic `import()` below.\nimport type { LanguageModel } from 'ai'\nimport type { AiProvider, GenerateStructuredRequest, GenerateStructuredResult } from './types'\nimport { DEFAULT_ANTHROPIC_MODEL } from './anthropic-provider'\n\nexport interface GatewayProviderOptions {\n /**\n * Gateway API key (`AI_GATEWAY_API_KEY` / Vercel access token). Omit on Vercel\n * deployments to use automatic OIDC. A set key always beats OIDC — see the\n * module doc for the blank-key guard.\n */\n apiKey?: string\n /** Gateway model string, `<provider>/<model>` (default {@link DEFAULT_GATEWAY_MODEL}). */\n model?: string\n /** Optional base URL override (defaults to the SDK's gateway endpoint). */\n baseURL?: string\n}\n\n/** Default gateway model — the direct-Anthropic default, addressed through the gateway. */\nexport const DEFAULT_GATEWAY_MODEL = `anthropic/${DEFAULT_ANTHROPIC_MODEL}`\n\n/**\n * Resolve the effective gateway `apiKey` with the apiKey-beats-OIDC foot-gun guards\n * (pure core; env + warn injected for tests):\n * - `undefined` passes through (OIDC / env-var auth applies downstream);\n * - blank/whitespace → treated as ABSENT + loud warning (a set key, even an\n * empty one, would otherwise silently beat OIDC);\n * - a real key while OIDC is also available → kept, with a warning that it wins.\n */\nexport function resolveGatewayApiKey(\n apiKey: string | undefined,\n env: Record<string, string | undefined> = typeof process !== 'undefined' ? process.env : {},\n warn: (message: string) => void = (message) => console.warn(message),\n): string | undefined {\n if (apiKey === undefined) return undefined\n const trimmed = apiKey.trim()\n if (trimmed === '') {\n warn(\n '[elytra-ai] gateway apiKey is blank — ignoring it so AI_GATEWAY_API_KEY/OIDC can apply. ' +\n 'A set apiKey always beats OIDC, even an invalid one.',\n )\n return undefined\n }\n if (env.VERCEL_OIDC_TOKEN) {\n warn(\n '[elytra-ai] gateway apiKey is set while Vercel OIDC is also available — the explicit ' +\n 'apiKey wins (even if invalid). Remove it to authenticate via OIDC.',\n )\n }\n return trimmed\n}\n\n/** A minimal view of the AI SDK usage object (field names read best-effort). */\ninterface SdkUsage {\n inputTokens?: number\n outputTokens?: number\n}\n\n/**\n * Build a Vercel-AI-Gateway-backed {@link AiProvider}. Tool calling + structured\n * output are supported (the gateway fronts tool-calling models), so it satisfies\n * the capability gate. The AI SDK is loaded lazily on first call.\n */\nexport function createGatewayProvider(options: GatewayProviderOptions = {}): AiProvider {\n const model = options.model ?? DEFAULT_GATEWAY_MODEL\n const apiKey = resolveGatewayApiKey(options.apiKey)\n\n // Lazily construct the SDK language model — no eager import of the AI SDK.\n async function languageModel(): Promise<LanguageModel> {\n const { createGateway } = await import('ai')\n const gateway = createGateway({\n ...(apiKey !== undefined ? { apiKey } : {}),\n ...(options.baseURL !== undefined ? { baseURL: options.baseURL } : {}),\n })\n return gateway(model)\n }\n\n return {\n id: 'gateway',\n model,\n capabilities: () => ({ toolCalling: true, structuredOutput: true }),\n languageModel,\n\n async generateStructured(\n request: GenerateStructuredRequest,\n ): Promise<GenerateStructuredResult> {\n const { generateObject, jsonSchema } = await import('ai')\n const languageModelInstance = await languageModel()\n const result = await generateObject({\n model: languageModelInstance,\n schema: jsonSchema(request.schema as Parameters<typeof jsonSchema>[0]),\n ...(request.schemaName !== undefined ? { schemaName: request.schemaName } : {}),\n ...(request.schemaDescription !== undefined\n ? { schemaDescription: request.schemaDescription }\n : {}),\n ...(request.system !== undefined ? { system: request.system } : {}),\n prompt: request.prompt,\n })\n const usage = result.usage as SdkUsage | undefined\n return {\n object: result.object,\n usage: {\n ...(usage?.inputTokens !== undefined ? { inputTokens: usage.inputTokens } : {}),\n ...(usage?.outputTokens !== undefined ? { outputTokens: usage.outputTokens } : {}),\n },\n }\n },\n }\n}\n"]}
@@ -23,9 +23,12 @@ export * from './types.js';
23
23
  export * from './config.js';
24
24
  export * from './provenance.js';
25
25
  export * from './knowledge.js';
26
+ export * from './rich-text-input.js';
26
27
  export * from './shape.js';
27
28
  export * from './apply-content.js';
29
+ export * from './update-fields.js';
28
30
  export * from './anthropic-provider.js';
31
+ export * from './gateway-provider.js';
29
32
  export * from './fake-provider.js';
30
33
  export * from './route.js';
31
34
  export * from './route-provider.js';
@@ -33,7 +36,8 @@ export * from './convex-session.js';
33
36
  export * from './convex-session-headers.js';
34
37
  export * from './node-handler.js';
35
38
  export * from './tools.js';
36
- export * from './create-flow.js';
39
+ export * from './chat-context.js';
40
+ export * from './chat-loop.js';
37
41
  export * from './chat-state.js';
38
42
  export * from './rich-text-translate.js';
39
43
  export * from './translate-document.js';
@@ -23,9 +23,12 @@ export * from './types.js';
23
23
  export * from './config.js';
24
24
  export * from './provenance.js';
25
25
  export * from './knowledge.js';
26
+ export * from './rich-text-input.js';
26
27
  export * from './shape.js';
27
28
  export * from './apply-content.js';
29
+ export * from './update-fields.js';
28
30
  export * from './anthropic-provider.js';
31
+ export * from './gateway-provider.js';
29
32
  export * from './fake-provider.js';
30
33
  export * from './route.js';
31
34
  export * from './route-provider.js';
@@ -33,7 +36,8 @@ export * from './convex-session.js';
33
36
  export * from './convex-session-headers.js';
34
37
  export * from './node-handler.js';
35
38
  export * from './tools.js';
36
- export * from './create-flow.js';
39
+ export * from './chat-context.js';
40
+ export * from './chat-loop.js';
37
41
  export * from './chat-state.js';
38
42
  export * from './rich-text-translate.js';
39
43
  export * from './translate-document.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './anthropic-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './create-flow'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/ai/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,0BAA0B,CAAA;AAEjD,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,SAAS,CAAA;AACvB,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,SAAS,CAAA;AACvB,cAAc,kBAAkB,CAAA;AAChC,cAAc,kBAAkB,CAAA;AAChC,cAAc,0BAA0B,CAAA;AACxC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,SAAS,CAAA;AACvB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA","sourcesContent":["/**\n * `@elytracms/studio/lib/ai` — the headless AI platform seam (EC-331, phase 1).\n *\n * Public surface: the config gate, the provider adapters (Anthropic + Fake), the\n * knowledge mechanism, the target-shape derivation, the `applyContentWithShape`\n * engine, the server-route factory, and the provenance seam. No chat UI here.\n *\n * Bundle discipline: nothing exported here statically imports `ai` / `@ai-sdk/*` —\n * only `anthropic-provider` touches them, via dynamic `import()`. So importing this\n * barrel never eagerly loads the AI SDK, and a studio without an `ai` config block\n * (which only references `StudioAiConfig` as a type) reaches none of it. The same\n * discipline keeps `jose` (the session resolver's JWT library) behind a dynamic\n * `import()` in `convex-session`.\n *\n * EC-371 adds the engine-default session gate + mounting kit: the Convex JWT\n * session resolver (server), the matching client header provider, and the Node\n * `(req, res)` bridge. The Vite DEV mount lives in its own entry —\n * `@elytracms/studio/lib/ai/vite` (`createAiDevPlugin`) — because it imports `vite`,\n * a dev-only tool that must stay out of this barrel's dependency edge.\n */\nexport const PACKAGE = '@elytracms/studio/lib/ai'\n\nexport * from './types'\nexport * from './config'\nexport * from './provenance'\nexport * from './knowledge'\nexport * from './rich-text-input'\nexport * from './shape'\nexport * from './apply-content'\nexport * from './update-fields'\nexport * from './anthropic-provider'\nexport * from './gateway-provider'\nexport * from './fake-provider'\nexport * from './route'\nexport * from './route-provider'\nexport * from './convex-session'\nexport * from './convex-session-headers'\nexport * from './node-handler'\nexport * from './tools'\nexport * from './chat-context'\nexport * from './chat-loop'\nexport * from './chat-state'\nexport * from './rich-text-translate'\nexport * from './translate-document'\n"]}
@@ -15,15 +15,24 @@
15
15
  * The DEV half (`createAiDevPlugin`, Vite middleware + `ssrLoadModule`) reuses this
16
16
  * same bridge — one implementation, both mounts. No platform dependency: the classic
17
17
  * `(IncomingMessage, ServerResponse)` signature works on every @vercel/node version
18
- * and on plain `node:http`. Note it buffers the response body (including the chat
19
- * stream) — the proven Konditorei behavior; native streaming is EC-334's scope.
18
+ * and on plain `node:http`.
19
+ *
20
+ * EC-334: the RESPONSE side STREAMS — `response.body` reader chunks are piped to
21
+ * `res.write` as they arrive, so the route's native SSE chat protocol delivers
22
+ * token-by-token through every mount (the previous `arrayBuffer()` buffering would
23
+ * have erased token streaming). The REQUEST side stays buffered (the route only
24
+ * ever reads JSON bodies). Ships to every consumer mount — streaming through
25
+ * Vercel Node functions goes on the post-release live-verify checklist.
20
26
  */
21
27
  import type { IncomingMessage, ServerResponse } from 'node:http';
22
28
  import type { AiRouteHandler } from './types.js';
23
29
  /** A Node-mountable request handler (Vercel Node function / `node:http` compatible). */
24
30
  export type NodeAiRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>;
25
31
  /**
26
- * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the body,
27
- * bridge headers both directions, copy status + payload back.
32
+ * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the request
33
+ * body, bridge headers both directions, copy the status back, and STREAM the
34
+ * response body — each chunk is written as it arrives (EC-334), so SSE token
35
+ * streaming survives the bridge. A client disconnect cancels the reader so the
36
+ * handler's stream stops being pulled.
28
37
  */
29
38
  export declare function toNodeHandler(handler: AiRouteHandler): NodeAiRouteHandler;
@@ -33,8 +33,11 @@ async function toWebRequest(req) {
33
33
  });
34
34
  }
35
35
  /**
36
- * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the body,
37
- * bridge headers both directions, copy status + payload back.
36
+ * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the request
37
+ * body, bridge headers both directions, copy the status back, and STREAM the
38
+ * response body — each chunk is written as it arrives (EC-334), so SSE token
39
+ * streaming survives the bridge. A client disconnect cancels the reader so the
40
+ * handler's stream stops being pulled.
38
41
  */
39
42
  export function toNodeHandler(handler) {
40
43
  return async (req, res) => {
@@ -42,7 +45,34 @@ export function toNodeHandler(handler) {
42
45
  const response = await handler(request);
43
46
  res.statusCode = response.status;
44
47
  response.headers.forEach((value, key) => res.setHeader(key, value));
45
- res.end(new Uint8Array(await response.arrayBuffer()));
48
+ const body = response.body;
49
+ if (!body) {
50
+ res.end();
51
+ return;
52
+ }
53
+ const reader = body.getReader();
54
+ let finished = false;
55
+ res.on('close', () => {
56
+ // Client gone before the stream ended — stop pulling from the handler.
57
+ if (!finished)
58
+ void reader.cancel().catch(() => undefined);
59
+ });
60
+ try {
61
+ for (;;) {
62
+ const { done, value } = await reader.read();
63
+ if (done)
64
+ break;
65
+ if (value !== undefined)
66
+ res.write(value);
67
+ }
68
+ }
69
+ catch {
70
+ // Stream error mid-flight (or cancelled by disconnect): terminate below.
71
+ }
72
+ finally {
73
+ finished = true;
74
+ res.end();
75
+ }
46
76
  };
47
77
  }
48
78
  //# sourceMappingURL=node-handler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"node-handler.js","sourceRoot":"","sources":["../../../src/lib/ai/node-handler.ts"],"names":[],"mappings":"AA0BA,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAA;AAEjC,SAAS,WAAW,CAAC,MAAoB;IACvC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,KAAK,IAAI,MAAM;QAAE,KAAK,IAAI,KAAK,CAAC,UAAU,CAAA;IACrD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,CAAA;IACjC,IAAI,MAAM,GAAG,CAAC,CAAA;IACd,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACtB,MAAM,IAAI,KAAK,CAAC,UAAU,CAAA;IAC5B,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,wFAAwF;AACxF,KAAK,UAAU,YAAY,CAAC,GAAoB;IAC9C,MAAM,MAAM,GAAiB,EAAE,CAAA;IAC/B,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QAC9B,MAAM,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAE,KAAoB,CAAC,CAAA;IACxF,CAAC;IACD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QACvD,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;aACjD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,cAAc,GAAG,GAAG,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAA;IACvD,MAAM,KAAK,GACT,CAAC,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,IAAI,MAAM,CAAA;IACxF,OAAO,IAAI,OAAO,CAAC,GAAG,KAAK,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,WAAW,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,EAAE,EAAE;QACnF,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,OAAO;QACP,IAAI,EAAE,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC;KACtF,CAAC,CAAA;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,OAAuB;IACnD,OAAO,KAAK,EAAE,GAAoB,EAAE,GAAmB,EAAiB,EAAE;QACxE,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAA;QACvC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;QACvC,GAAG,CAAC,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAA;QAChC,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;QACnE,GAAG,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAA;IACvD,CAAC,CAAA;AACH,CAAC","sourcesContent":["/**\n * `toNodeHandler` — bridge the framework-agnostic Web `Request → Response`\n * {@link AiRouteHandler} to Node's `(req, res)` signature (EC-371; closes EC-369\n * finding #2, where the identical ~25-line bridge was hand-copied four times across\n * consumers). This is the PROD half of the mounting kit: a Vercel Node function (or\n * any Node server) mounts the route as\n *\n * ```ts\n * // api/ai.ts (Vercel filesystem routing beats the SPA catch-all rewrite)\n * import { toNodeHandler } from '@elytracms/studio/lib/ai'\n * import { aiRouteHandler } from '../src/server/ai-handler.js'\n * export default toNodeHandler(aiRouteHandler)\n * ```\n *\n * The DEV half (`createAiDevPlugin`, Vite middleware + `ssrLoadModule`) reuses this\n * same bridge — one implementation, both mounts. No platform dependency: the classic\n * `(IncomingMessage, ServerResponse)` signature works on every @vercel/node version\n * and on plain `node:http`. Note it buffers the response body (including the chat\n * stream) — the proven Konditorei behavior; native streaming is EC-334's scope.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { AiRouteHandler } from './types'\n\n/** A Node-mountable request handler (Vercel Node function / `node:http` compatible). */\nexport type NodeAiRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>\n\nconst encoder = new TextEncoder()\n\nfunction concatBytes(chunks: Uint8Array[]): Uint8Array<ArrayBuffer> {\n let total = 0\n for (const chunk of chunks) total += chunk.byteLength\n const out = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n out.set(chunk, offset)\n offset += chunk.byteLength\n }\n return out\n}\n\n/** Reconstruct a Web `Request` from a Node request (headers, method, buffered body). */\nasync function toWebRequest(req: IncomingMessage): Promise<Request> {\n const chunks: Uint8Array[] = []\n for await (const chunk of req) {\n chunks.push(typeof chunk === 'string' ? encoder.encode(chunk) : (chunk as Uint8Array))\n }\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(key, value)\n else if (Array.isArray(value)) headers.set(key, value.join(', '))\n }\n const forwardedProto = req.headers['x-forwarded-proto']\n const proto =\n (typeof forwardedProto === 'string' && forwardedProto.split(',')[0]?.trim()) || 'http'\n return new Request(`${proto}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {\n method: req.method,\n headers,\n body: req.method === 'GET' || req.method === 'HEAD' ? undefined : concatBytes(chunks),\n })\n}\n\n/**\n * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the body,\n * bridge headers both directions, copy status + payload back.\n */\nexport function toNodeHandler(handler: AiRouteHandler): NodeAiRouteHandler {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const request = await toWebRequest(req)\n const response = await handler(request)\n res.statusCode = response.status\n response.headers.forEach((value, key) => res.setHeader(key, value))\n res.end(new Uint8Array(await response.arrayBuffer()))\n }\n}\n"]}
1
+ {"version":3,"file":"node-handler.js","sourceRoot":"","sources":["../../../src/lib/ai/node-handler.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `toNodeHandler` — bridge the framework-agnostic Web `Request → Response`\n * {@link AiRouteHandler} to Node's `(req, res)` signature (EC-371; closes EC-369\n * finding #2, where the identical ~25-line bridge was hand-copied four times across\n * consumers). This is the PROD half of the mounting kit: a Vercel Node function (or\n * any Node server) mounts the route as\n *\n * ```ts\n * // api/ai.ts (Vercel filesystem routing beats the SPA catch-all rewrite)\n * import { toNodeHandler } from '@elytracms/studio/lib/ai'\n * import { aiRouteHandler } from '../src/server/ai-handler.js'\n * export default toNodeHandler(aiRouteHandler)\n * ```\n *\n * The DEV half (`createAiDevPlugin`, Vite middleware + `ssrLoadModule`) reuses this\n * same bridge — one implementation, both mounts. No platform dependency: the classic\n * `(IncomingMessage, ServerResponse)` signature works on every @vercel/node version\n * and on plain `node:http`.\n *\n * EC-334: the RESPONSE side STREAMS — `response.body` reader chunks are piped to\n * `res.write` as they arrive, so the route's native SSE chat protocol delivers\n * token-by-token through every mount (the previous `arrayBuffer()` buffering would\n * have erased token streaming). The REQUEST side stays buffered (the route only\n * ever reads JSON bodies). Ships to every consumer mount — streaming through\n * Vercel Node functions goes on the post-release live-verify checklist.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http'\nimport type { AiRouteHandler } from './types'\n\n/** A Node-mountable request handler (Vercel Node function / `node:http` compatible). */\nexport type NodeAiRouteHandler = (req: IncomingMessage, res: ServerResponse) => Promise<void>\n\nconst encoder = new TextEncoder()\n\nfunction concatBytes(chunks: Uint8Array[]): Uint8Array<ArrayBuffer> {\n let total = 0\n for (const chunk of chunks) total += chunk.byteLength\n const out = new Uint8Array(total)\n let offset = 0\n for (const chunk of chunks) {\n out.set(chunk, offset)\n offset += chunk.byteLength\n }\n return out\n}\n\n/** Reconstruct a Web `Request` from a Node request (headers, method, buffered body). */\nasync function toWebRequest(req: IncomingMessage): Promise<Request> {\n const chunks: Uint8Array[] = []\n for await (const chunk of req) {\n chunks.push(typeof chunk === 'string' ? encoder.encode(chunk) : (chunk as Uint8Array))\n }\n const headers = new Headers()\n for (const [key, value] of Object.entries(req.headers)) {\n if (typeof value === 'string') headers.set(key, value)\n else if (Array.isArray(value)) headers.set(key, value.join(', '))\n }\n const forwardedProto = req.headers['x-forwarded-proto']\n const proto =\n (typeof forwardedProto === 'string' && forwardedProto.split(',')[0]?.trim()) || 'http'\n return new Request(`${proto}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, {\n method: req.method,\n headers,\n body: req.method === 'GET' || req.method === 'HEAD' ? undefined : concatBytes(chunks),\n })\n}\n\n/**\n * Wrap an {@link AiRouteHandler} as a Node `(req, res)` handler: buffer the request\n * body, bridge headers both directions, copy the status back, and STREAM the\n * response body — each chunk is written as it arrives (EC-334), so SSE token\n * streaming survives the bridge. A client disconnect cancels the reader so the\n * handler's stream stops being pulled.\n */\nexport function toNodeHandler(handler: AiRouteHandler): NodeAiRouteHandler {\n return async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\n const request = await toWebRequest(req)\n const response = await handler(request)\n res.statusCode = response.status\n response.headers.forEach((value, key) => res.setHeader(key, value))\n\n const body = response.body\n if (!body) {\n res.end()\n return\n }\n\n const reader = body.getReader()\n let finished = false\n res.on('close', () => {\n // Client gone before the stream ended — stop pulling from the handler.\n if (!finished) void reader.cancel().catch(() => undefined)\n })\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (value !== undefined) res.write(value)\n }\n } catch {\n // Stream error mid-flight (or cancelled by disconnect): terminate below.\n } finally {\n finished = true\n res.end()\n }\n }\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import type { RichTextValue } from '@elytracms/core/rich-text';
2
+ /**
3
+ * The model-input policy for rich-text field values (EC-379), shared by the
4
+ * create engine (`applyContentWithShape`) and the edit flow
5
+ * (`buildUpdateProposal`):
6
+ *
7
+ * - a **string is ALWAYS markdown** (never JSON-sniffed — deterministic): it is
8
+ * converted through the engine's strict subset converter, and out-of-subset
9
+ * constructs come back as explicit problems the caller feeds to the model for
10
+ * repair;
11
+ * - a `{ version, doc }` **envelope object is still accepted** for robustness
12
+ * (the model sees envelopes in `getDocument` output whenever a document is
13
+ * not losslessly expressible as markdown, and may echo them back) — it is
14
+ * schema-validated here and leash-validated downstream by `validateDocument`
15
+ * + the injected `validateRichTextEmbeds`;
16
+ * - anything else is a problem. The target SHAPE only ever advertises the
17
+ * markdown string form (`shape.ts`), so markdown is what the model is
18
+ * steered to author.
19
+ */
20
+ export type RichTextInputResult = {
21
+ ok: true;
22
+ value: RichTextValue;
23
+ } | {
24
+ ok: false;
25
+ problems: string[];
26
+ };
27
+ export declare function richTextValueFromModelInput(raw: unknown): RichTextInputResult;
@@ -0,0 +1,25 @@
1
+ import { MARKDOWN_PROSE_DESCRIPTION, markdownProseToRichTextValue, parseRichTextValue, } from '@elytracms/core/rich-text';
2
+ export function richTextValueFromModelInput(raw) {
3
+ if (typeof raw === 'string') {
4
+ const converted = markdownProseToRichTextValue(raw);
5
+ if (converted.ok)
6
+ return { ok: true, value: converted.value };
7
+ return { ok: false, problems: converted.issues.map((issue) => issue.message) };
8
+ }
9
+ if (typeof raw === 'object' && raw !== null) {
10
+ const parsed = parseRichTextValue(raw);
11
+ if (parsed.ok)
12
+ return { ok: true, value: parsed.value };
13
+ return {
14
+ ok: false,
15
+ problems: [
16
+ `Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}); the provided object is not a valid { version, doc } storage envelope either.`,
17
+ ],
18
+ };
19
+ }
20
+ return {
21
+ ok: false,
22
+ problems: [`Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}).`],
23
+ };
24
+ }
25
+ //# sourceMappingURL=rich-text-input.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text-input.js","sourceRoot":"","sources":["../../../src/lib/ai/rich-text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,0BAA0B,EAC1B,4BAA4B,EAC5B,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAyBlC,MAAM,UAAU,2BAA2B,CAAC,GAAY;IACtD,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC5B,MAAM,SAAS,GAAG,4BAA4B,CAAC,GAAG,CAAC,CAAA;QACnD,IAAI,SAAS,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAA;QAC7D,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAA;IAChF,CAAC;IACD,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,MAAM,CAAC,EAAE;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAA;QACvD,OAAO;YACL,EAAE,EAAE,KAAK;YACT,QAAQ,EAAE;gBACR,oDAAoD,0BAA0B,iFAAiF;aAChK;SACF,CAAA;IACH,CAAC;IACD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,QAAQ,EAAE,CAAC,oDAAoD,0BAA0B,IAAI,CAAC;KAC/F,CAAA;AACH,CAAC","sourcesContent":["import {\n MARKDOWN_PROSE_DESCRIPTION,\n markdownProseToRichTextValue,\n parseRichTextValue,\n} from '@elytracms/core/rich-text'\nimport type { RichTextValue } from '@elytracms/core/rich-text'\n\n/**\n * The model-input policy for rich-text field values (EC-379), shared by the\n * create engine (`applyContentWithShape`) and the edit flow\n * (`buildUpdateProposal`):\n *\n * - a **string is ALWAYS markdown** (never JSON-sniffed — deterministic): it is\n * converted through the engine's strict subset converter, and out-of-subset\n * constructs come back as explicit problems the caller feeds to the model for\n * repair;\n * - a `{ version, doc }` **envelope object is still accepted** for robustness\n * (the model sees envelopes in `getDocument` output whenever a document is\n * not losslessly expressible as markdown, and may echo them back) — it is\n * schema-validated here and leash-validated downstream by `validateDocument`\n * + the injected `validateRichTextEmbeds`;\n * - anything else is a problem. The target SHAPE only ever advertises the\n * markdown string form (`shape.ts`), so markdown is what the model is\n * steered to author.\n */\nexport type RichTextInputResult =\n | { ok: true; value: RichTextValue }\n | { ok: false; problems: string[] }\n\nexport function richTextValueFromModelInput(raw: unknown): RichTextInputResult {\n if (typeof raw === 'string') {\n const converted = markdownProseToRichTextValue(raw)\n if (converted.ok) return { ok: true, value: converted.value }\n return { ok: false, problems: converted.issues.map((issue) => issue.message) }\n }\n if (typeof raw === 'object' && raw !== null) {\n const parsed = parseRichTextValue(raw)\n if (parsed.ok) return { ok: true, value: parsed.value }\n return {\n ok: false,\n problems: [\n `Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}); the provided object is not a valid { version, doc } storage envelope either.`,\n ],\n }\n }\n return {\n ok: false,\n problems: [`Rich text must be authored as a markdown string (${MARKDOWN_PROSE_DESCRIPTION}).`],\n }\n}\n"]}
@@ -1,14 +1,17 @@
1
1
  /**
2
2
  * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the
3
- * browser (EC-199). It is a thin proxy that forwards `generateStructured` /
4
- * `streamChat` to the host's mounted {@link createAiRouteHandler} endpoint over
5
- * `fetch`. The provider KEY never touches the browser: the key-holding
6
- * {@link createAnthropicProvider} lives only inside the server route; the studio's
7
- * `defineStudio({ ai: { provider } })` gets this route provider (or the FakeProvider
8
- * for a keyless playground), so the whole authoring loop —
9
- * `applyContentWithShape` included — proxies its model calls through the session-gated
10
- * route while the tools (search / list / apply) execute client-side with the user's
11
- * own session.
3
+ * browser (EC-199). It is a thin proxy that forwards `generateStructured` to the
4
+ * host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider
5
+ * KEY never touches the browser: the key-holding {@link createAnthropicProvider}
6
+ * lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`
7
+ * gets this route provider (or the FakeProvider for a keyless playground), so the
8
+ * whole authoring loop — `applyContentWithShape` included — proxies its model calls
9
+ * through the session-gated route while the tools (search / list / apply) execute
10
+ * client-side with the user's own session. It exposes NO `languageModel()` — the
11
+ * conversational loop talks to the route's native chat branch directly (EC-334,
12
+ * `useChat` + `DefaultChatTransport`), not through this provider; the provider
13
+ * only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are
14
+ * configured once.
12
15
  *
13
16
  * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`
14
17
  * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test
@@ -36,6 +39,28 @@ export interface RouteProviderOptions {
36
39
  }
37
40
  /** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */
38
41
  export declare const DEFAULT_AI_ENDPOINT = "/api/ai";
42
+ /**
43
+ * The chat transport target (EC-334): where the panel's `useChat` posts its
44
+ * native `{ messages, context }` requests, and which extra headers ride along
45
+ * (the session token thunk). `createRouteProvider` exposes its own endpoint +
46
+ * headers here, so the ONE configured provider is the single source of truth
47
+ * for both the structured proxy and the chat transport.
48
+ */
49
+ export interface AiChatTarget {
50
+ endpoint: string;
51
+ headers?: RouteHeaders;
52
+ }
53
+ /** An {@link AiProvider} that also names the chat transport target it fronts. */
54
+ export interface RouteAiProvider extends AiProvider {
55
+ readonly chatTarget: AiChatTarget;
56
+ }
57
+ /**
58
+ * The chat target behind a configured provider: the route provider's own
59
+ * endpoint + headers, or the default mount path for any other provider (the
60
+ * chat loop always talks to the mounted route — a missing mount fails loud as
61
+ * a visible route error in the panel, never silently).
62
+ */
63
+ export declare function resolveChatTarget(provider: AiProvider | null): AiChatTarget;
39
64
  /** A structured error thrown when the route rejects (surfaced by the chat UI). */
40
65
  export declare class AiRouteError extends Error {
41
66
  readonly status: number;
@@ -47,4 +72,4 @@ export declare class AiRouteError extends Error {
47
72
  * (so the FakeProvider mounted behind the route drives the whole client loop in the
48
73
  * playground). Never holds a key; never imports the AI SDK.
49
74
  */
50
- export declare function createRouteProvider(options?: RouteProviderOptions): AiProvider;
75
+ export declare function createRouteProvider(options?: RouteProviderOptions): RouteAiProvider;
@@ -1,5 +1,17 @@
1
1
  /** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */
2
2
  export const DEFAULT_AI_ENDPOINT = '/api/ai';
3
+ /**
4
+ * The chat target behind a configured provider: the route provider's own
5
+ * endpoint + headers, or the default mount path for any other provider (the
6
+ * chat loop always talks to the mounted route — a missing mount fails loud as
7
+ * a visible route error in the panel, never silently).
8
+ */
9
+ export function resolveChatTarget(provider) {
10
+ const candidate = provider?.chatTarget;
11
+ if (candidate && typeof candidate.endpoint === 'string')
12
+ return candidate;
13
+ return { endpoint: DEFAULT_AI_ENDPOINT };
14
+ }
3
15
  async function resolveHeaders(headers) {
4
16
  if (typeof headers === 'function')
5
17
  return headers();
@@ -42,6 +54,7 @@ export function createRouteProvider(options = {}) {
42
54
  return {
43
55
  id: options.id ?? 'route',
44
56
  ...(options.model !== undefined ? { model: options.model } : {}),
57
+ chatTarget: { endpoint, ...(options.headers !== undefined ? { headers: options.headers } : {}) },
45
58
  capabilities: () => ({ toolCalling: true, structuredOutput: true }),
46
59
  async generateStructured(request) {
47
60
  const response = await post({ kind: 'generate', request });
@@ -49,12 +62,6 @@ export function createRouteProvider(options = {}) {
49
62
  throw new AiRouteError(response.status, await errorCode(response));
50
63
  return (await response.json());
51
64
  },
52
- async streamChat(request) {
53
- const response = await post({ kind: 'chat', ...request });
54
- if (!response.ok)
55
- throw new AiRouteError(response.status, await errorCode(response));
56
- return response;
57
- },
58
65
  };
59
66
  }
60
67
  //# sourceMappingURL=route-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/route-provider.ts"],"names":[],"mappings":"AA+CA,0FAA0F;AAC1F,MAAM,CAAC,MAAM,mBAAmB,GAAG,SAAS,CAAA;AAE5C,KAAK,UAAU,cAAc,CAAC,OAAiC;IAC7D,IAAI,OAAO,OAAO,KAAK,UAAU;QAAE,OAAO,OAAO,EAAE,CAAA;IACnD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,kFAAkF;AAClF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAE1B;IACA;IAFX,YACW,MAAc,EACd,IAAY;QAErB,KAAK,CAAC,4BAA4B,MAAM,KAAK,IAAI,GAAG,CAAC,CAAA;QAH5C,WAAM,GAAN,MAAM,CAAQ;QACd,SAAI,GAAJ,IAAI,CAAQ;QAGrB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAA;IAC5B,CAAC;CACF;AAED,KAAK,UAAU,SAAS,CAAC,QAAkB;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAwB,CAAA;QAC3D,OAAO,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB,CAAA;IACvE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,gBAAgB,CAAA;IACzB,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAgC,EAAE;IACpE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,mBAAmB,CAAA;IACxD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,KAAK,CAAA;IAE5C,KAAK,UAAU,IAAI,CAAC,IAAa;QAC/B,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QACnD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,KAAK,CAAC,CAAA;QAClC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAA;QAC/C,OAAO,SAAS,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACrF,CAAC;IAED,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,OAAO;QACzB,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,YAAY,EAAE,GAA2B,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC;QAE3F,KAAK,CAAC,kBAAkB,CACtB,OAAkC;YAElC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAA;YAC1D,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpF,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAA6B,CAAA;QAC5D,CAAC;QAED,KAAK,CAAC,UAAU,CAAC,OAA0B;YACzC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;YACzD,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;YACpF,OAAO,QAAQ,CAAA;QACjB,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the\n * browser (EC-199). It is a thin proxy that forwards `generateStructured` /\n * `streamChat` to the host's mounted {@link createAiRouteHandler} endpoint over\n * `fetch`. The provider KEY never touches the browser: the key-holding\n * {@link createAnthropicProvider} lives only inside the server route; the studio's\n * `defineStudio({ ai: { provider } })` gets this route provider (or the FakeProvider\n * for a keyless playground), so the whole authoring loop —\n * `applyContentWithShape` included — proxies its model calls through the session-gated\n * route while the tools (search / list / apply) execute client-side with the user's\n * own session.\n *\n * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`\n * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test\n * pins — the route provider can sit on the studio critical path without pulling the\n * SDK. The capability gate passes: the route only ever fronts a gate-checked provider\n * (validated at the server mount), so the proxy declares both required capabilities.\n */\nimport type {\n AiProvider,\n AiProviderCapabilities,\n GenerateStructuredRequest,\n GenerateStructuredResult,\n StreamChatRequest,\n} from './types'\n\n/** How extra request headers are supplied — a static set or a (possibly async) thunk. */\nexport type RouteHeaders =\n | HeadersInit\n | (() => HeadersInit | undefined | Promise<HeadersInit | undefined>)\n\nexport interface RouteProviderOptions {\n /** The mounted route path/URL (default `/api/ai`). */\n endpoint?: string\n /** `fetch` implementation (default the global). Injected in tests. */\n fetchImpl?: typeof fetch\n /**\n * Extra headers per request — e.g. the studio session token the route's `getSession`\n * checks. A thunk is re-evaluated on every call so a refreshed token is picked up.\n */\n headers?: RouteHeaders\n /** Provider id recorded in provenance (default `route`). */\n id?: string\n /** Model id recorded in provenance (the downstream model, when the host fixes one). */\n model?: string\n}\n\n/** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */\nexport const DEFAULT_AI_ENDPOINT = '/api/ai'\n\nasync function resolveHeaders(headers: RouteHeaders | undefined): Promise<HeadersInit | undefined> {\n if (typeof headers === 'function') return headers()\n return headers\n}\n\n/** A structured error thrown when the route rejects (surfaced by the chat UI). */\nexport class AiRouteError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n ) {\n super(`AI route request failed (${status}: ${code})`)\n this.name = 'AiRouteError'\n }\n}\n\nasync function errorCode(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown }\n return typeof body.error === 'string' ? body.error : 'request-failed'\n } catch {\n return 'request-failed'\n }\n}\n\n/**\n * Build a route-backed {@link AiProvider}. Deterministic given a deterministic route\n * (so the FakeProvider mounted behind the route drives the whole client loop in the\n * playground). Never holds a key; never imports the AI SDK.\n */\nexport function createRouteProvider(options: RouteProviderOptions = {}): AiProvider {\n const endpoint = options.endpoint ?? DEFAULT_AI_ENDPOINT\n const fetchImpl = options.fetchImpl ?? fetch\n\n async function post(body: unknown): Promise<Response> {\n const extra = await resolveHeaders(options.headers)\n const headers = new Headers(extra)\n headers.set('content-type', 'application/json')\n return fetchImpl(endpoint, { method: 'POST', headers, body: JSON.stringify(body) })\n }\n\n return {\n id: options.id ?? 'route',\n ...(options.model !== undefined ? { model: options.model } : {}),\n capabilities: (): AiProviderCapabilities => ({ toolCalling: true, structuredOutput: true }),\n\n async generateStructured(\n request: GenerateStructuredRequest,\n ): Promise<GenerateStructuredResult> {\n const response = await post({ kind: 'generate', request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return (await response.json()) as GenerateStructuredResult\n },\n\n async streamChat(request: StreamChatRequest): Promise<Response> {\n const response = await post({ kind: 'chat', ...request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return response\n },\n }\n}\n"]}
1
+ {"version":3,"file":"route-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/route-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `createRouteProvider` — the CLIENT-side {@link AiProvider} the studio runs in the\n * browser (EC-199). It is a thin proxy that forwards `generateStructured` to the\n * host's mounted {@link createAiRouteHandler} endpoint over `fetch`. The provider\n * KEY never touches the browser: the key-holding {@link createAnthropicProvider}\n * lives only inside the server route; the studio's `defineStudio({ ai: { provider } })`\n * gets this route provider (or the FakeProvider for a keyless playground), so the\n * whole authoring loop — `applyContentWithShape` included — proxies its model calls\n * through the session-gated route while the tools (search / list / apply) execute\n * client-side with the user's own session. It exposes NO `languageModel()` — the\n * conversational loop talks to the route's native chat branch directly (EC-334,\n * `useChat` + `DefaultChatTransport`), not through this provider; the provider\n * only NAMES that target ({@link AiChatTarget}) so endpoint + session headers are\n * configured once.\n *\n * Bundle-safe by construction: this module imports nothing from `ai` / `@ai-sdk/*`\n * (it only uses `fetch`), so it stays off the AI-SDK dependency edge the bundle test\n * pins — the route provider can sit on the studio critical path without pulling the\n * SDK. The capability gate passes: the route only ever fronts a gate-checked provider\n * (validated at the server mount), so the proxy declares both required capabilities.\n */\nimport type {\n AiProvider,\n AiProviderCapabilities,\n GenerateStructuredRequest,\n GenerateStructuredResult,\n} from './types'\n\n/** How extra request headers are supplied — a static set or a (possibly async) thunk. */\nexport type RouteHeaders =\n | HeadersInit\n | (() => HeadersInit | undefined | Promise<HeadersInit | undefined>)\n\nexport interface RouteProviderOptions {\n /** The mounted route path/URL (default `/api/ai`). */\n endpoint?: string\n /** `fetch` implementation (default the global). Injected in tests. */\n fetchImpl?: typeof fetch\n /**\n * Extra headers per request — e.g. the studio session token the route's `getSession`\n * checks. A thunk is re-evaluated on every call so a refreshed token is picked up.\n */\n headers?: RouteHeaders\n /** Provider id recorded in provenance (default `route`). */\n id?: string\n /** Model id recorded in provenance (the downstream model, when the host fixes one). */\n model?: string\n}\n\n/** Default mount path a consuming studio app exposes for {@link createAiRouteHandler}. */\nexport const DEFAULT_AI_ENDPOINT = '/api/ai'\n\n/**\n * The chat transport target (EC-334): where the panel's `useChat` posts its\n * native `{ messages, context }` requests, and which extra headers ride along\n * (the session token thunk). `createRouteProvider` exposes its own endpoint +\n * headers here, so the ONE configured provider is the single source of truth\n * for both the structured proxy and the chat transport.\n */\nexport interface AiChatTarget {\n endpoint: string\n headers?: RouteHeaders\n}\n\n/** An {@link AiProvider} that also names the chat transport target it fronts. */\nexport interface RouteAiProvider extends AiProvider {\n readonly chatTarget: AiChatTarget\n}\n\n/**\n * The chat target behind a configured provider: the route provider's own\n * endpoint + headers, or the default mount path for any other provider (the\n * chat loop always talks to the mounted route — a missing mount fails loud as\n * a visible route error in the panel, never silently).\n */\nexport function resolveChatTarget(provider: AiProvider | null): AiChatTarget {\n const candidate = (provider as Partial<RouteAiProvider> | null)?.chatTarget\n if (candidate && typeof candidate.endpoint === 'string') return candidate\n return { endpoint: DEFAULT_AI_ENDPOINT }\n}\n\nasync function resolveHeaders(headers: RouteHeaders | undefined): Promise<HeadersInit | undefined> {\n if (typeof headers === 'function') return headers()\n return headers\n}\n\n/** A structured error thrown when the route rejects (surfaced by the chat UI). */\nexport class AiRouteError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n ) {\n super(`AI route request failed (${status}: ${code})`)\n this.name = 'AiRouteError'\n }\n}\n\nasync function errorCode(response: Response): Promise<string> {\n try {\n const body = (await response.json()) as { error?: unknown }\n return typeof body.error === 'string' ? body.error : 'request-failed'\n } catch {\n return 'request-failed'\n }\n}\n\n/**\n * Build a route-backed {@link AiProvider}. Deterministic given a deterministic route\n * (so the FakeProvider mounted behind the route drives the whole client loop in the\n * playground). Never holds a key; never imports the AI SDK.\n */\nexport function createRouteProvider(options: RouteProviderOptions = {}): RouteAiProvider {\n const endpoint = options.endpoint ?? DEFAULT_AI_ENDPOINT\n const fetchImpl = options.fetchImpl ?? fetch\n\n async function post(body: unknown): Promise<Response> {\n const extra = await resolveHeaders(options.headers)\n const headers = new Headers(extra)\n headers.set('content-type', 'application/json')\n return fetchImpl(endpoint, { method: 'POST', headers, body: JSON.stringify(body) })\n }\n\n return {\n id: options.id ?? 'route',\n ...(options.model !== undefined ? { model: options.model } : {}),\n chatTarget: { endpoint, ...(options.headers !== undefined ? { headers: options.headers } : {}) },\n capabilities: (): AiProviderCapabilities => ({ toolCalling: true, structuredOutput: true }),\n\n async generateStructured(\n request: GenerateStructuredRequest,\n ): Promise<GenerateStructuredResult> {\n const response = await post({ kind: 'generate', request })\n if (!response.ok) throw new AiRouteError(response.status, await errorCode(response))\n return (await response.json()) as GenerateStructuredResult\n },\n }\n}\n"]}
@@ -1,36 +1,65 @@
1
1
  /**
2
- * Server-route factory (EC-331): a streaming chat/completion endpoint the consuming
3
- * studio app mounts. Key custody = host route (never the browser, never Convex): the
2
+ * Server-route factory (EC-331): the session-gated endpoint the consuming studio
3
+ * app mounts. Key custody = host route (never the browser, never Convex): the
4
4
  * `provider` holds the host-env key and this handler is a THIN model proxy — NO data
5
- * access, NO service credentials.
5
+ * access, NO service credentials, NO server-side tool execution.
6
6
  *
7
7
  * Framework-agnostic on purpose: it returns a Web `Request → Response` handler the
8
- * host wires into whatever server entry it has (a TanStack Start server route, a
9
- * Convex `httpAction`, a plain fetch handler). The current demo studio builds as a
10
- * pure client SPA with server functions disabled (EC-234), so actually mounting this
11
- * is an EC-199 integration step; phase 1 ships the handler + its contract.
8
+ * host wires into whatever server entry it has (`createAiDevPlugin` in dev,
9
+ * `toNodeHandler` on a Vercel Node function, any fetch handler).
12
10
  *
13
11
  * Session-gated: `getSession` (host-wired, reading its own auth — this module never
14
12
  * touches `lib/auth`) must return a session for authenticated callers; anonymous
15
- * callers are rejected. Plus a soft, in-memory per-project rate counter (spend guard).
13
+ * callers are rejected with 401 BEFORE any stream byte. Plus a soft, in-memory
14
+ * per-project rate counter (spend guard) — also the backstop for the SDK's
15
+ * client-driven tool-call continuations, each of which is a fresh request here.
16
16
  *
17
- * EC-199: the route serves TWO model operations — both thin proxies, neither with any
18
- * data access — discriminated by the request body's `kind`:
19
- * - `{ kind: 'chat', messages, system? }` → `provider.streamChat` (text stream);
20
- * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON).
21
- * The structured mode is what lets `applyContentWithShape` run CLIENT-side against a
22
- * {@link createRouteProvider} whose key never touches the browser — the creation flow's
23
- * whole authoring loop proxies its model calls here while the tools (search/list/apply)
24
- * execute in the client with the user's own session. A body with `messages` and no
25
- * `kind` is still treated as a chat request (back-compat with the phase-1 contract).
17
+ * The route serves TWO model operations — both thin proxies, neither with any data
18
+ * access:
19
+ * - `{ kind: 'generate', request }` → `provider.generateStructured` (JSON) — the
20
+ * proxy `applyContentWithShape` / `translateDocument` run CLIENT-side against,
21
+ * so the key never touches the browser;
22
+ * - `{ messages: UIMessage[], context? }` (EC-334, kind-less — the shape the AI
23
+ * SDK's `DefaultChatTransport` posts) → the SDK's native streaming chat
24
+ * protocol: `validateUIMessages` → `streamText` over
25
+ * `provider.languageModel()` with {@link chatToolDefinitions} declared WITHOUT
26
+ * `execute` (every tool call forwards to the client, which executes it against
27
+ * the operations layer with the user's own session) →
28
+ * `createUIMessageStreamResponse` + `toUIMessageStream` (the non-deprecated
29
+ * pair — `result.toUIMessageStreamResponse()` dies in SDK v8). The `system`
30
+ * prompt is assembled from the request's `context` sections; conversation
31
+ * history rides natively in `messages`.
32
+ *
33
+ * BREAKING (EC-334, ratified): the legacy `{ kind: 'chat', messages:
34
+ * AiChatMessage[] }` wire is gone — route factory and client ship lockstep in
35
+ * `@elytracms/studio`, so consumers re-mount the same factory on upgrade.
26
36
  */
27
37
  import type { AiRouteHandler, AiRouteHandlerOptions } from './types.js';
28
- /** The wire discriminator selecting which model operation the route proxies. */
29
- export type AiRouteKind = 'chat' | 'generate';
38
+ /**
39
+ * The wire discriminator for the structured-generation branch. The native chat
40
+ * branch (EC-334) is deliberately kind-less: the SDK's `DefaultChatTransport`
41
+ * posts `{ messages, … }` with no `kind`.
42
+ */
43
+ export type AiRouteKind = 'generate';
44
+ /** The outcome of folding the request `context` into a system prompt. */
45
+ export type AssembledSystem = {
46
+ ok: true;
47
+ system?: string;
48
+ } | {
49
+ ok: false;
50
+ };
51
+ /**
52
+ * Fold the request-body `context` into the system prompt (EC-334). The client sends
53
+ * `context` as one string or ordered string sections (collections-catalog summary,
54
+ * client-resolved knowledge, EC-378's current-document block) — the route only
55
+ * ASSEMBLES them; it resolves no data itself (zero-data-access proxy). Malformed
56
+ * context is a structural 400, never silently dropped (validation-first).
57
+ */
58
+ export declare function assembleSystemPrompt(context: unknown): AssembledSystem;
30
59
  /**
31
60
  * Build the mounted route handler. The returned function is a plain
32
61
  * `(Request) => Promise<Response>` — session-gated, soft-rate-limited, and a thin
33
- * proxy to `provider.streamChat`. Never throws to the caller: every failure is a
34
- * structured JSON response with an appropriate status.
62
+ * proxy to the provider. Never throws to the caller: every failure is a structured
63
+ * JSON response with an appropriate status.
35
64
  */
36
65
  export declare function createAiRouteHandler(options: AiRouteHandlerOptions): AiRouteHandler;