@elytracms/studio 0.0.55 → 0.0.57

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 (55) hide show
  1. package/convex/auth.ts +152 -2
  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 +161 -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 +130 -23
  10. package/dist/components/ai/chat-ui.js.map +1 -1
  11. package/dist/lib/ai/anthropic-provider.d.ts +0 -12
  12. package/dist/lib/ai/anthropic-provider.js +3 -11
  13. package/dist/lib/ai/anthropic-provider.js.map +1 -1
  14. package/dist/lib/ai/chat-context.d.ts +58 -0
  15. package/dist/lib/ai/chat-context.js +71 -0
  16. package/dist/lib/ai/chat-context.js.map +1 -0
  17. package/dist/lib/ai/chat-loop.d.ts +70 -0
  18. package/dist/lib/ai/chat-loop.js +46 -0
  19. package/dist/lib/ai/chat-loop.js.map +1 -0
  20. package/dist/lib/ai/chat-state.d.ts +70 -57
  21. package/dist/lib/ai/chat-state.js +49 -90
  22. package/dist/lib/ai/chat-state.js.map +1 -1
  23. package/dist/lib/ai/fake-provider.d.ts +84 -6
  24. package/dist/lib/ai/fake-provider.js +193 -8
  25. package/dist/lib/ai/fake-provider.js.map +1 -1
  26. package/dist/lib/ai/gateway-provider.d.ts +30 -0
  27. package/dist/lib/ai/gateway-provider.js +73 -0
  28. package/dist/lib/ai/gateway-provider.js.map +1 -0
  29. package/dist/lib/ai/index.d.ts +3 -1
  30. package/dist/lib/ai/index.js +3 -1
  31. package/dist/lib/ai/index.js.map +1 -1
  32. package/dist/lib/ai/node-handler.d.ts +13 -4
  33. package/dist/lib/ai/node-handler.js +33 -3
  34. package/dist/lib/ai/node-handler.js.map +1 -1
  35. package/dist/lib/ai/route-provider.d.ts +35 -10
  36. package/dist/lib/ai/route-provider.js +13 -6
  37. package/dist/lib/ai/route-provider.js.map +1 -1
  38. package/dist/lib/ai/route.d.ts +50 -21
  39. package/dist/lib/ai/route.js +69 -40
  40. package/dist/lib/ai/route.js.map +1 -1
  41. package/dist/lib/ai/tools.d.ts +169 -39
  42. package/dist/lib/ai/tools.js +308 -62
  43. package/dist/lib/ai/tools.js.map +1 -1
  44. package/dist/lib/ai/types.d.ts +11 -16
  45. package/dist/lib/ai/types.js.map +1 -1
  46. package/dist/lib/auth/convex-auth-adapter.d.ts +37 -14
  47. package/dist/lib/auth/convex-auth-adapter.js +72 -16
  48. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  49. package/dist/lib/i18n/messages.en.js +9 -0
  50. package/dist/lib/i18n/messages.en.js.map +1 -1
  51. package/dist/ui/badge.d.ts +1 -1
  52. package/package.json +4 -3
  53. package/dist/lib/ai/create-flow.d.ts +0 -132
  54. package/dist/lib/ai/create-flow.js +0 -280
  55. package/dist/lib/ai/create-flow.js.map +0 -1
@@ -3,13 +3,91 @@
3
3
  * implements the same interface as the Anthropic provider, so playground / demo /
4
4
  * tests run the WHOLE loop (config gate → shape → generate → validate → repair →
5
5
  * patch) without a key. Determinism is a project principle: the same fixtures always
6
- * yield the same result. Imports no AI SDK.
6
+ * yield the same result. Imports no AI SDK at runtime — the `LanguageModelV4` spec
7
+ * below is implemented STRUCTURALLY from type-only imports (erased at compile;
8
+ * pinned in `bundle.test.ts`).
7
9
  *
8
10
  * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns
9
11
  * them in call order (the last is reused once the script is exhausted). Or pass
10
12
  * `respond` to compute the object from the request.
13
+ *
14
+ * EC-334 adds {@link createFakeChatModel}: a scripted fake `LanguageModel` served by
15
+ * the ROUTE's native chat branch, so the keyless playground / CI drive the FULL
16
+ * `streamText` → SSE wire → client-tool loop. It is a STATELESS prompt-inspecting
17
+ * responder (state read from the prompt, the FakeProvider trick): a prompt without a
18
+ * tool result emits a tool call; a prompt carrying one streams a text answer.
19
+ */
20
+ import type { LanguageModel } from 'ai';
21
+ import type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest } from './types.js';
22
+ /** The SDK's v4 language-model spec, extracted structurally from the type-only union. */
23
+ export type FakeLanguageModelV4 = Extract<LanguageModel, {
24
+ specificationVersion: 'v4';
25
+ }>;
26
+ /** The call options a fake chat model receives (`LanguageModelV4CallOptions`). */
27
+ export type FakeChatModelCallOptions = Parameters<FakeLanguageModelV4['doGenerate']>[0];
28
+ /** One provider-spec stream part the fake emits (`LanguageModelV4StreamPart`). */
29
+ export type FakeChatModelStreamPart = Awaited<ReturnType<FakeLanguageModelV4['doStream']>>['stream'] extends ReadableStream<infer PART> ? PART : never;
30
+ /** Default preamble streamed before the scripted tool call. */
31
+ export declare const FAKE_CHAT_PREAMBLE = "Let me look that up. ";
32
+ /** Default final answer streamed once the prompt carries a tool result. */
33
+ export declare const FAKE_CHAT_ANSWER = "Found it \u2014 the tool result is in, and this is the answer.";
34
+ export interface FakeChatModelOptions {
35
+ /** Model id reported to the SDK (default `fake-chat-model`). */
36
+ modelId?: string;
37
+ /** Text streamed before the tool call on the first (tool-result-free) turn. */
38
+ preambleText?: string;
39
+ /** The tool the first turn requests (default `searchDocuments`). */
40
+ toolName?: string;
41
+ /** The tool-call input (default `{ collection: 'post' }`). */
42
+ toolInput?: Record<string, unknown>;
43
+ /** The answer streamed word-by-word once the prompt carries a tool result. */
44
+ answerText?: string;
45
+ /**
46
+ * Full override: script the exact provider-spec stream parts for a call
47
+ * (zero-based call index). The default prompt-inspecting behavior is skipped.
48
+ */
49
+ script?: (options: FakeChatModelCallOptions, callIndex: number) => FakeChatModelStreamPart[];
50
+ }
51
+ /** A fake chat model that also records the calls it received, for assertions. */
52
+ export type FakeChatModel = FakeLanguageModelV4 & {
53
+ /** Every `doStream` / `doGenerate` call options received, in order. */
54
+ readonly calls: FakeChatModelCallOptions[];
55
+ };
56
+ /**
57
+ * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,
58
+ * implemented structurally — zero SDK runtime import). Default behavior:
59
+ * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for
60
+ * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);
61
+ * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).
62
+ * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks
63
+ * are `tool-input-start/delta/end` + `tool-call`.
64
+ */
65
+ export declare function createFakeChatModel(options?: FakeChatModelOptions): FakeChatModel;
66
+ export interface FakeCreateFlowChatModelOptions {
67
+ /** Model id reported to the SDK (default `fake-create-flow-model`). */
68
+ modelId?: string;
69
+ /** The collection the first stage searches (a relation target; default `author`). */
70
+ searchCollection?: string;
71
+ /** The collection the terminal stage creates in (default `post`). */
72
+ createCollection?: string;
73
+ /** The final answer streamed word-by-word after the proposal lands. */
74
+ answerText?: string;
75
+ }
76
+ /** Default final answer of the scripted create flow. */
77
+ export declare const FAKE_CREATE_FLOW_ANSWER = "Done \u2014 review the proposal below and apply it as a draft.";
78
+ /**
79
+ * A scripted, STATELESS fake chat model that drives the FULL create flow over the
80
+ * native loop (EC-334): keyless playground / CI exercise user message → search
81
+ * tool → `createDocument` terminal tool (the client runs `applyContentWithShape`)
82
+ * → streamed final answer. Stage is read from the CURRENT user turn's tool
83
+ * results (prompt suffix after the last user message), so every follow-up turn
84
+ * starts a fresh flow — multi-turn works without model state:
85
+ *
86
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
87
+ * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`
88
+ * 3. create result present → the answer text, word by word.
11
89
  */
12
- import type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest, StreamChatRequest } from './types.js';
90
+ export declare function createFakeCreateFlowChatModel(options?: FakeCreateFlowChatModelOptions): FakeChatModel;
13
91
  export interface FakeProviderOptions {
14
92
  /** Provider id (default `fake`). */
15
93
  id?: string;
@@ -21,14 +99,14 @@ export interface FakeProviderOptions {
21
99
  objects?: unknown[];
22
100
  /** Compute the object from the request + zero-based call index (overrides `objects`). */
23
101
  respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown;
24
- /** Fixed text `streamChat` streams back (default: echoes the last user message). */
25
- streamText?: string;
102
+ /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */
103
+ chatModel?: FakeChatModel;
26
104
  }
27
105
  /** A FakeProvider that also records the requests it received, for assertions. */
28
106
  export interface FakeProvider extends AiProvider {
29
107
  /** Every `generateStructured` request received, in order. */
30
108
  readonly calls: GenerateStructuredRequest[];
31
- /** Every `streamChat` request received, in order. */
32
- readonly streamCalls: StreamChatRequest[];
109
+ /** The scripted fake chat model behind `languageModel()` (records its calls). */
110
+ readonly chatModel: FakeChatModel;
33
111
  }
34
112
  export declare function createFakeProvider(options?: FakeProviderOptions): FakeProvider;
@@ -1,14 +1,205 @@
1
+ /** Zero-usage report (the fake spends nothing). Shape = `LanguageModelV4Usage`. */
2
+ const FAKE_USAGE = {
3
+ inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
4
+ outputTokens: { total: 0, text: 0, reasoning: 0 },
5
+ };
6
+ /** Default preamble streamed before the scripted tool call. */
7
+ export const FAKE_CHAT_PREAMBLE = 'Let me look that up. ';
8
+ /** Default final answer streamed once the prompt carries a tool result. */
9
+ export const FAKE_CHAT_ANSWER = 'Found it — the tool result is in, and this is the answer.';
10
+ /** Does any prompt message carry a tool RESULT part? (The fake's only "state".) */
11
+ function promptHasToolResult(prompt) {
12
+ return prompt.some((message) => Array.isArray(message.content) &&
13
+ message.content.some((part) => part.type === 'tool-result'));
14
+ }
15
+ /** Index of the LAST user message in the prompt (`-1` when none). */
16
+ function lastUserIndex(prompt) {
17
+ for (let i = prompt.length - 1; i >= 0; i -= 1) {
18
+ if (prompt[i]?.role === 'user')
19
+ return i;
20
+ }
21
+ return -1;
22
+ }
23
+ /** The last user message's text (the fake's "instruction" source). */
24
+ function lastUserText(prompt) {
25
+ const index = lastUserIndex(prompt);
26
+ const message = prompt[index];
27
+ if (!message || !Array.isArray(message.content))
28
+ return '';
29
+ return message.content
30
+ .filter((part) => part.type === 'text')
31
+ .map((part) => part.text)
32
+ .join(' ')
33
+ .trim();
34
+ }
35
+ /**
36
+ * Tool names with a RESULT in the CURRENT user turn (prompt suffix after the
37
+ * last user message) — the stateless stage detector for scripted flows.
38
+ */
39
+ function toolResultNamesInCurrentTurn(prompt) {
40
+ const names = new Set();
41
+ for (const message of prompt.slice(lastUserIndex(prompt) + 1)) {
42
+ if (!Array.isArray(message.content))
43
+ continue;
44
+ for (const part of message.content) {
45
+ const candidate = part;
46
+ if (candidate.type === 'tool-result' && typeof candidate.toolName === 'string') {
47
+ names.add(candidate.toolName);
48
+ }
49
+ }
50
+ }
51
+ return names;
52
+ }
53
+ /** Provider-spec parts for one text-preamble + tool-call turn (finish `tool-calls`). */
54
+ function toolTurnParts(preamble, toolName, input, callId) {
55
+ const encoded = JSON.stringify(input);
56
+ return [
57
+ { type: 'stream-start', warnings: [] },
58
+ { type: 'text-start', id: 'text-1' },
59
+ { type: 'text-delta', id: 'text-1', delta: preamble },
60
+ { type: 'text-end', id: 'text-1' },
61
+ { type: 'tool-input-start', id: callId, toolName },
62
+ { type: 'tool-input-delta', id: callId, delta: encoded },
63
+ { type: 'tool-input-end', id: callId },
64
+ { type: 'tool-call', toolCallId: callId, toolName, input: encoded },
65
+ {
66
+ type: 'finish',
67
+ usage: FAKE_USAGE,
68
+ finishReason: { unified: 'tool-calls', raw: 'fake-tool-calls' },
69
+ },
70
+ ];
71
+ }
72
+ /** Provider-spec parts for a word-by-word streamed text answer (finish `stop`). */
73
+ function answerTurnParts(answer) {
74
+ return [
75
+ { type: 'stream-start', warnings: [] },
76
+ { type: 'text-start', id: 'text-1' },
77
+ ...wordDeltas(answer).map((delta) => ({ type: 'text-delta', id: 'text-1', delta })),
78
+ { type: 'text-end', id: 'text-1' },
79
+ { type: 'finish', usage: FAKE_USAGE, finishReason: { unified: 'stop', raw: 'fake-stop' } },
80
+ ];
81
+ }
82
+ /** Split text into word deltas (trailing spaces preserved) for visible streaming. */
83
+ function wordDeltas(text) {
84
+ const words = text.split(' ');
85
+ return words.map((word, index) => (index < words.length - 1 ? `${word} ` : word));
86
+ }
87
+ /**
88
+ * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,
89
+ * implemented structurally — zero SDK runtime import). Default behavior:
90
+ * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for
91
+ * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);
92
+ * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).
93
+ * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks
94
+ * are `tool-input-start/delta/end` + `tool-call`.
95
+ */
96
+ export function createFakeChatModel(options = {}) {
97
+ const preamble = options.preambleText ?? FAKE_CHAT_PREAMBLE;
98
+ const toolName = options.toolName ?? 'searchDocuments';
99
+ const toolInput = options.toolInput ?? { collection: 'post' };
100
+ const answer = options.answerText ?? FAKE_CHAT_ANSWER;
101
+ const calls = [];
102
+ let callIndex = 0;
103
+ function partsFor(callOptions) {
104
+ if (options.script)
105
+ return options.script(callOptions, callIndex);
106
+ if (!promptHasToolResult(callOptions.prompt)) {
107
+ return toolTurnParts(preamble, toolName, toolInput, 'call-1');
108
+ }
109
+ return answerTurnParts(answer);
110
+ }
111
+ /** Fold the scripted stream parts into a non-streaming `doGenerate` result. */
112
+ function materialize(parts) {
113
+ const content = [];
114
+ const textById = new Map();
115
+ let finishReason = { unified: 'stop', raw: 'fake-stop' };
116
+ for (const part of parts) {
117
+ if (part.type === 'text-delta') {
118
+ textById.set(part.id, (textById.get(part.id) ?? '') + part.delta);
119
+ }
120
+ else if (part.type === 'tool-call') {
121
+ content.push(part);
122
+ }
123
+ else if (part.type === 'finish') {
124
+ finishReason = part.finishReason;
125
+ }
126
+ }
127
+ for (const text of textById.values())
128
+ content.unshift({ type: 'text', text });
129
+ return { content, finishReason, usage: FAKE_USAGE, warnings: [] };
130
+ }
131
+ return {
132
+ specificationVersion: 'v4',
133
+ provider: 'fake',
134
+ modelId: options.modelId ?? 'fake-chat-model',
135
+ supportedUrls: {},
136
+ calls,
137
+ doGenerate(callOptions) {
138
+ calls.push(callOptions);
139
+ const parts = partsFor(callOptions);
140
+ callIndex += 1;
141
+ return Promise.resolve(materialize(parts));
142
+ },
143
+ doStream(callOptions) {
144
+ calls.push(callOptions);
145
+ const parts = partsFor(callOptions);
146
+ callIndex += 1;
147
+ return Promise.resolve({
148
+ stream: new ReadableStream({
149
+ start(controller) {
150
+ for (const part of parts)
151
+ controller.enqueue(part);
152
+ controller.close();
153
+ },
154
+ }),
155
+ });
156
+ },
157
+ };
158
+ }
159
+ /** Default final answer of the scripted create flow. */
160
+ export const FAKE_CREATE_FLOW_ANSWER = 'Done — review the proposal below and apply it as a draft.';
161
+ /**
162
+ * A scripted, STATELESS fake chat model that drives the FULL create flow over the
163
+ * native loop (EC-334): keyless playground / CI exercise user message → search
164
+ * tool → `createDocument` terminal tool (the client runs `applyContentWithShape`)
165
+ * → streamed final answer. Stage is read from the CURRENT user turn's tool
166
+ * results (prompt suffix after the last user message), so every follow-up turn
167
+ * starts a fresh flow — multi-turn works without model state:
168
+ *
169
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
170
+ * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`
171
+ * 3. create result present → the answer text, word by word.
172
+ */
173
+ export function createFakeCreateFlowChatModel(options = {}) {
174
+ const searchCollection = options.searchCollection ?? 'author';
175
+ const createCollection = options.createCollection ?? 'post';
176
+ const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER;
177
+ return createFakeChatModel({
178
+ modelId: options.modelId ?? 'fake-create-flow-model',
179
+ script: (callOptions) => {
180
+ const done = toolResultNamesInCurrentTurn(callOptions.prompt);
181
+ if (done.has('createDocument'))
182
+ return answerTurnParts(answer);
183
+ if (done.has('searchDocuments')) {
184
+ const instruction = lastUserText(callOptions.prompt) || 'Write a short document.';
185
+ return toolTurnParts('Drafting a proposal. ', 'createDocument', { collection: createCollection, instruction }, 'call-create-1');
186
+ }
187
+ return toolTurnParts(FAKE_CHAT_PREAMBLE, 'searchDocuments', { collection: searchCollection }, 'call-search-1');
188
+ },
189
+ });
190
+ }
1
191
  export function createFakeProvider(options = {}) {
2
192
  const calls = [];
3
- const streamCalls = [];
4
193
  const objects = options.objects ?? [];
194
+ const chatModel = options.chatModel ?? createFakeChatModel();
5
195
  let index = 0;
6
196
  return {
7
197
  id: options.id ?? 'fake',
8
198
  model: options.model ?? 'fake-model',
9
199
  calls,
10
- streamCalls,
200
+ chatModel,
11
201
  capabilities: () => options.capabilities ?? { toolCalling: true, structuredOutput: true },
202
+ languageModel: () => Promise.resolve(chatModel),
12
203
  generateStructured(request) {
13
204
  calls.push(request);
14
205
  const callIndex = index;
@@ -20,12 +211,6 @@ export function createFakeProvider(options = {}) {
20
211
  : {};
21
212
  return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } });
22
213
  },
23
- streamChat(request) {
24
- streamCalls.push(request);
25
- const last = request.messages.filter((m) => m.role === 'user').at(-1);
26
- const text = options.streamText ?? last?.content ?? '';
27
- return Promise.resolve(new Response(text, { headers: { 'content-type': 'text/plain; charset=utf-8' } }));
28
- },
29
214
  };
30
215
  }
31
216
  //# sourceMappingURL=fake-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fake-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/fake-provider.ts"],"names":[],"mappings":"AAyCA,MAAM,UAAU,kBAAkB,CAAC,UAA+B,EAAE;IAClE,MAAM,KAAK,GAAgC,EAAE,CAAA;IAC7C,MAAM,WAAW,GAAwB,EAAE,CAAA;IAC3C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAA;IACrC,IAAI,KAAK,GAAG,CAAC,CAAA;IAEb,OAAO;QACL,EAAE,EAAE,OAAO,CAAC,EAAE,IAAI,MAAM;QACxB,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,YAAY;QACpC,KAAK;QACL,WAAW;QACX,YAAY,EAAE,GAA2B,EAAE,CACzC,OAAO,CAAC,YAAY,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE;QAEvE,kBAAkB,CAAC,OAAO;YACxB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACnB,MAAM,SAAS,GAAG,KAAK,CAAA;YACvB,KAAK,IAAI,CAAC,CAAA;YACV,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO;gBAC5B,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC;gBACrC,CAAC,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC;oBAClB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;oBAClD,CAAC,CAAC,EAAE,CAAA;YACR,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;QAChF,CAAC;QAED,UAAU,CAAC,OAAO;YAChB,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YACzB,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;YACrE,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,IAAI,IAAI,EAAE,OAAO,IAAI,EAAE,CAAA;YACtD,OAAO,OAAO,CAAC,OAAO,CACpB,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,2BAA2B,EAAE,EAAE,CAAC,CACjF,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * FakeProvider (EC-331): a deterministic, fixture-driven {@link AiProvider} that\n * implements the same interface as the Anthropic provider, so playground / demo /\n * tests run the WHOLE loop (config gate → shape → generate → validate → repair →\n * patch) without a key. Determinism is a project principle: the same fixtures always\n * yield the same result. Imports no AI SDK.\n *\n * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns\n * them in call order (the last is reused once the script is exhausted). Or pass\n * `respond` to compute the object from the request.\n */\nimport type {\n AiProvider,\n AiProviderCapabilities,\n GenerateStructuredRequest,\n StreamChatRequest,\n} from './types'\n\nexport interface FakeProviderOptions {\n /** Provider id (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Reported capabilities (default: both true). Set to test the capability gate. */\n capabilities?: AiProviderCapabilities\n /** Scripted objects returned in call order; the last entry is reused when exhausted. */\n objects?: unknown[]\n /** Compute the object from the request + zero-based call index (overrides `objects`). */\n respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown\n /** Fixed text `streamChat` streams back (default: echoes the last user message). */\n streamText?: string\n}\n\n/** A FakeProvider that also records the requests it received, for assertions. */\nexport interface FakeProvider extends AiProvider {\n /** Every `generateStructured` request received, in order. */\n readonly calls: GenerateStructuredRequest[]\n /** Every `streamChat` request received, in order. */\n readonly streamCalls: StreamChatRequest[]\n}\n\nexport function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {\n const calls: GenerateStructuredRequest[] = []\n const streamCalls: StreamChatRequest[] = []\n const objects = options.objects ?? []\n let index = 0\n\n return {\n id: options.id ?? 'fake',\n model: options.model ?? 'fake-model',\n calls,\n streamCalls,\n capabilities: (): AiProviderCapabilities =>\n options.capabilities ?? { toolCalling: true, structuredOutput: true },\n\n generateStructured(request) {\n calls.push(request)\n const callIndex = index\n index += 1\n const object = options.respond\n ? options.respond(request, callIndex)\n : objects.length > 0\n ? objects[Math.min(callIndex, objects.length - 1)]\n : {}\n return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } })\n },\n\n streamChat(request) {\n streamCalls.push(request)\n const last = request.messages.filter((m) => m.role === 'user').at(-1)\n const text = options.streamText ?? last?.content ?? ''\n return Promise.resolve(\n new Response(text, { headers: { 'content-type': 'text/plain; charset=utf-8' } }),\n )\n },\n }\n}\n"]}
1
+ {"version":3,"file":"fake-provider.js","sourceRoot":"","sources":["../../../src/lib/ai/fake-provider.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * FakeProvider (EC-331): a deterministic, fixture-driven {@link AiProvider} that\n * implements the same interface as the Anthropic provider, so playground / demo /\n * tests run the WHOLE loop (config gate → shape → generate → validate → repair →\n * patch) without a key. Determinism is a project principle: the same fixtures always\n * yield the same result. Imports no AI SDK at runtime — the `LanguageModelV4` spec\n * below is implemented STRUCTURALLY from type-only imports (erased at compile;\n * pinned in `bundle.test.ts`).\n *\n * For the repair loop, pass `objects: [invalidDoc, validDoc]` — the provider returns\n * them in call order (the last is reused once the script is exhausted). Or pass\n * `respond` to compute the object from the request.\n *\n * EC-334 adds {@link createFakeChatModel}: a scripted fake `LanguageModel` served by\n * the ROUTE's native chat branch, so the keyless playground / CI drive the FULL\n * `streamText` → SSE wire → client-tool loop. It is a STATELESS prompt-inspecting\n * responder (state read from the prompt, the FakeProvider trick): a prompt without a\n * tool result emits a tool call; a prompt carrying one streams a text answer.\n */\n// Type-only (erased): the fake implements the SDK's v4 model spec structurally.\nimport type { LanguageModel } from 'ai'\nimport type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest } from './types'\n\n// --- Fake chat model (EC-334) -------------------------------------------------------\n\n/** The SDK's v4 language-model spec, extracted structurally from the type-only union. */\nexport type FakeLanguageModelV4 = Extract<LanguageModel, { specificationVersion: 'v4' }>\n\n/** The call options a fake chat model receives (`LanguageModelV4CallOptions`). */\nexport type FakeChatModelCallOptions = Parameters<FakeLanguageModelV4['doGenerate']>[0]\n\n/** One provider-spec stream part the fake emits (`LanguageModelV4StreamPart`). */\nexport type FakeChatModelStreamPart =\n Awaited<ReturnType<FakeLanguageModelV4['doStream']>>['stream'] extends ReadableStream<infer PART>\n ? PART\n : never\n\ntype FakeFinishPart = Extract<FakeChatModelStreamPart, { type: 'finish' }>\ntype FakeGenerateResult = Awaited<ReturnType<FakeLanguageModelV4['doGenerate']>>\n\n/** Zero-usage report (the fake spends nothing). Shape = `LanguageModelV4Usage`. */\nconst FAKE_USAGE: FakeFinishPart['usage'] = {\n inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },\n outputTokens: { total: 0, text: 0, reasoning: 0 },\n}\n\n/** Default preamble streamed before the scripted tool call. */\nexport const FAKE_CHAT_PREAMBLE = 'Let me look that up. '\n/** Default final answer streamed once the prompt carries a tool result. */\nexport const FAKE_CHAT_ANSWER = 'Found it — the tool result is in, and this is the answer.'\n\nexport interface FakeChatModelOptions {\n /** Model id reported to the SDK (default `fake-chat-model`). */\n modelId?: string\n /** Text streamed before the tool call on the first (tool-result-free) turn. */\n preambleText?: string\n /** The tool the first turn requests (default `searchDocuments`). */\n toolName?: string\n /** The tool-call input (default `{ collection: 'post' }`). */\n toolInput?: Record<string, unknown>\n /** The answer streamed word-by-word once the prompt carries a tool result. */\n answerText?: string\n /**\n * Full override: script the exact provider-spec stream parts for a call\n * (zero-based call index). The default prompt-inspecting behavior is skipped.\n */\n script?: (options: FakeChatModelCallOptions, callIndex: number) => FakeChatModelStreamPart[]\n}\n\n/** A fake chat model that also records the calls it received, for assertions. */\nexport type FakeChatModel = FakeLanguageModelV4 & {\n /** Every `doStream` / `doGenerate` call options received, in order. */\n readonly calls: FakeChatModelCallOptions[]\n}\n\n/** Does any prompt message carry a tool RESULT part? (The fake's only \"state\".) */\nfunction promptHasToolResult(prompt: FakeChatModelCallOptions['prompt']): boolean {\n return prompt.some(\n (message) =>\n Array.isArray(message.content) &&\n message.content.some(\n (part) => (part as { type?: unknown }).type === 'tool-result',\n ),\n )\n}\n\n/** Index of the LAST user message in the prompt (`-1` when none). */\nfunction lastUserIndex(prompt: FakeChatModelCallOptions['prompt']): number {\n for (let i = prompt.length - 1; i >= 0; i -= 1) {\n if (prompt[i]?.role === 'user') return i\n }\n return -1\n}\n\n/** The last user message's text (the fake's \"instruction\" source). */\nfunction lastUserText(prompt: FakeChatModelCallOptions['prompt']): string {\n const index = lastUserIndex(prompt)\n const message = prompt[index]\n if (!message || !Array.isArray(message.content)) return ''\n return message.content\n .filter((part): part is { type: 'text'; text: string } => (part as { type?: unknown }).type === 'text')\n .map((part) => part.text)\n .join(' ')\n .trim()\n}\n\n/**\n * Tool names with a RESULT in the CURRENT user turn (prompt suffix after the\n * last user message) — the stateless stage detector for scripted flows.\n */\nfunction toolResultNamesInCurrentTurn(prompt: FakeChatModelCallOptions['prompt']): Set<string> {\n const names = new Set<string>()\n for (const message of prompt.slice(lastUserIndex(prompt) + 1)) {\n if (!Array.isArray(message.content)) continue\n for (const part of message.content) {\n const candidate = part as { type?: unknown; toolName?: unknown }\n if (candidate.type === 'tool-result' && typeof candidate.toolName === 'string') {\n names.add(candidate.toolName)\n }\n }\n }\n return names\n}\n\n/** Provider-spec parts for one text-preamble + tool-call turn (finish `tool-calls`). */\nfunction toolTurnParts(\n preamble: string,\n toolName: string,\n input: Record<string, unknown>,\n callId: string,\n): FakeChatModelStreamPart[] {\n const encoded = JSON.stringify(input)\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n { type: 'text-delta', id: 'text-1', delta: preamble },\n { type: 'text-end', id: 'text-1' },\n { type: 'tool-input-start', id: callId, toolName },\n { type: 'tool-input-delta', id: callId, delta: encoded },\n { type: 'tool-input-end', id: callId },\n { type: 'tool-call', toolCallId: callId, toolName, input: encoded },\n {\n type: 'finish',\n usage: FAKE_USAGE,\n finishReason: { unified: 'tool-calls', raw: 'fake-tool-calls' },\n },\n ]\n}\n\n/** Provider-spec parts for a word-by-word streamed text answer (finish `stop`). */\nfunction answerTurnParts(answer: string): FakeChatModelStreamPart[] {\n return [\n { type: 'stream-start', warnings: [] },\n { type: 'text-start', id: 'text-1' },\n ...wordDeltas(answer).map(\n (delta): FakeChatModelStreamPart => ({ type: 'text-delta', id: 'text-1', delta }),\n ),\n { type: 'text-end', id: 'text-1' },\n { type: 'finish', usage: FAKE_USAGE, finishReason: { unified: 'stop', raw: 'fake-stop' } },\n ]\n}\n\n/** Split text into word deltas (trailing spaces preserved) for visible streaming. */\nfunction wordDeltas(text: string): string[] {\n const words = text.split(' ')\n return words.map((word, index) => (index < words.length - 1 ? `${word} ` : word))\n}\n\n/**\n * Build a scripted, stateless fake `LanguageModel` (the `LanguageModelV4` spec,\n * implemented structurally — zero SDK runtime import). Default behavior:\n * - prompt WITHOUT a tool result → preamble text, then a `tool-call` for\n * {@link FakeChatModelOptions.toolName} (finish reason `tool-calls`);\n * - prompt WITH a tool result → the answer text streamed word-by-word (`stop`).\n * Chunk shapes follow the v4 spec: `finishReason` is `{ unified, raw }`, tool chunks\n * are `tool-input-start/delta/end` + `tool-call`.\n */\nexport function createFakeChatModel(options: FakeChatModelOptions = {}): FakeChatModel {\n const preamble = options.preambleText ?? FAKE_CHAT_PREAMBLE\n const toolName = options.toolName ?? 'searchDocuments'\n const toolInput = options.toolInput ?? { collection: 'post' }\n const answer = options.answerText ?? FAKE_CHAT_ANSWER\n const calls: FakeChatModelCallOptions[] = []\n let callIndex = 0\n\n function partsFor(callOptions: FakeChatModelCallOptions): FakeChatModelStreamPart[] {\n if (options.script) return options.script(callOptions, callIndex)\n if (!promptHasToolResult(callOptions.prompt)) {\n return toolTurnParts(preamble, toolName, toolInput, 'call-1')\n }\n return answerTurnParts(answer)\n }\n\n /** Fold the scripted stream parts into a non-streaming `doGenerate` result. */\n function materialize(parts: FakeChatModelStreamPart[]): FakeGenerateResult {\n const content: FakeGenerateResult['content'] = []\n const textById = new Map<string, string>()\n let finishReason: FakeFinishPart['finishReason'] = { unified: 'stop', raw: 'fake-stop' }\n for (const part of parts) {\n if (part.type === 'text-delta') {\n textById.set(part.id, (textById.get(part.id) ?? '') + part.delta)\n } else if (part.type === 'tool-call') {\n content.push(part)\n } else if (part.type === 'finish') {\n finishReason = part.finishReason\n }\n }\n for (const text of textById.values()) content.unshift({ type: 'text', text })\n return { content, finishReason, usage: FAKE_USAGE, warnings: [] }\n }\n\n return {\n specificationVersion: 'v4',\n provider: 'fake',\n modelId: options.modelId ?? 'fake-chat-model',\n supportedUrls: {},\n calls,\n doGenerate(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve(materialize(parts))\n },\n doStream(callOptions) {\n calls.push(callOptions)\n const parts = partsFor(callOptions)\n callIndex += 1\n return Promise.resolve({\n stream: new ReadableStream<FakeChatModelStreamPart>({\n start(controller) {\n for (const part of parts) controller.enqueue(part)\n controller.close()\n },\n }),\n })\n },\n }\n}\n\n// --- Fake create-flow chat model (EC-334 slice 2) -------------------------------------\n\nexport interface FakeCreateFlowChatModelOptions {\n /** Model id reported to the SDK (default `fake-create-flow-model`). */\n modelId?: string\n /** The collection the first stage searches (a relation target; default `author`). */\n searchCollection?: string\n /** The collection the terminal stage creates in (default `post`). */\n createCollection?: string\n /** The final answer streamed word-by-word after the proposal lands. */\n answerText?: string\n}\n\n/** Default final answer of the scripted create flow. */\nexport const FAKE_CREATE_FLOW_ANSWER =\n 'Done — review the proposal below and apply it as a draft.'\n\n/**\n * A scripted, STATELESS fake chat model that drives the FULL create flow over the\n * native loop (EC-334): keyless playground / CI exercise user message → search\n * tool → `createDocument` terminal tool (the client runs `applyContentWithShape`)\n * → streamed final answer. Stage is read from the CURRENT user turn's tool\n * results (prompt suffix after the last user message), so every follow-up turn\n * starts a fresh flow — multi-turn works without model state:\n *\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`\n * 3. create result present → the answer text, word by word.\n */\nexport function createFakeCreateFlowChatModel(\n options: FakeCreateFlowChatModelOptions = {},\n): FakeChatModel {\n const searchCollection = options.searchCollection ?? 'author'\n const createCollection = options.createCollection ?? 'post'\n const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER\n return createFakeChatModel({\n modelId: options.modelId ?? 'fake-create-flow-model',\n script: (callOptions) => {\n const done = toolResultNamesInCurrentTurn(callOptions.prompt)\n if (done.has('createDocument')) return answerTurnParts(answer)\n if (done.has('searchDocuments')) {\n const instruction = lastUserText(callOptions.prompt) || 'Write a short document.'\n return toolTurnParts(\n 'Drafting a proposal. ',\n 'createDocument',\n { collection: createCollection, instruction },\n 'call-create-1',\n )\n }\n return toolTurnParts(\n FAKE_CHAT_PREAMBLE,\n 'searchDocuments',\n { collection: searchCollection },\n 'call-search-1',\n )\n },\n })\n}\n\n// --- Fake provider ------------------------------------------------------------------\n\nexport interface FakeProviderOptions {\n /** Provider id (default `fake`). */\n id?: string\n /** Model id recorded in provenance (default `fake-model`). */\n model?: string\n /** Reported capabilities (default: both true). Set to test the capability gate. */\n capabilities?: AiProviderCapabilities\n /** Scripted objects returned in call order; the last entry is reused when exhausted. */\n objects?: unknown[]\n /** Compute the object from the request + zero-based call index (overrides `objects`). */\n respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown\n /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */\n chatModel?: FakeChatModel\n}\n\n/** A FakeProvider that also records the requests it received, for assertions. */\nexport interface FakeProvider extends AiProvider {\n /** Every `generateStructured` request received, in order. */\n readonly calls: GenerateStructuredRequest[]\n /** The scripted fake chat model behind `languageModel()` (records its calls). */\n readonly chatModel: FakeChatModel\n}\n\nexport function createFakeProvider(options: FakeProviderOptions = {}): FakeProvider {\n const calls: GenerateStructuredRequest[] = []\n const objects = options.objects ?? []\n const chatModel = options.chatModel ?? createFakeChatModel()\n let index = 0\n\n return {\n id: options.id ?? 'fake',\n model: options.model ?? 'fake-model',\n calls,\n chatModel,\n capabilities: (): AiProviderCapabilities =>\n options.capabilities ?? { toolCalling: true, structuredOutput: true },\n\n languageModel: () => Promise.resolve(chatModel),\n\n generateStructured(request) {\n calls.push(request)\n const callIndex = index\n index += 1\n const object = options.respond\n ? options.respond(request, callIndex)\n : objects.length > 0\n ? objects[Math.min(callIndex, objects.length - 1)]\n : {}\n return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } })\n },\n }\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import type { AiProvider } from './types.js';
2
+ export interface GatewayProviderOptions {
3
+ /**
4
+ * Gateway API key (`AI_GATEWAY_API_KEY` / Vercel access token). Omit on Vercel
5
+ * deployments to use automatic OIDC. A set key always beats OIDC — see the
6
+ * module doc for the blank-key guard.
7
+ */
8
+ apiKey?: string;
9
+ /** Gateway model string, `<provider>/<model>` (default {@link DEFAULT_GATEWAY_MODEL}). */
10
+ model?: string;
11
+ /** Optional base URL override (defaults to the SDK's gateway endpoint). */
12
+ baseURL?: string;
13
+ }
14
+ /** Default gateway model — the direct-Anthropic default, addressed through the gateway. */
15
+ export declare const DEFAULT_GATEWAY_MODEL = "anthropic/claude-opus-4-8";
16
+ /**
17
+ * Resolve the effective gateway `apiKey` with the apiKey-beats-OIDC foot-gun guards
18
+ * (pure core; env + warn injected for tests):
19
+ * - `undefined` passes through (OIDC / env-var auth applies downstream);
20
+ * - blank/whitespace → treated as ABSENT + loud warning (a set key, even an
21
+ * empty one, would otherwise silently beat OIDC);
22
+ * - a real key while OIDC is also available → kept, with a warning that it wins.
23
+ */
24
+ export declare function resolveGatewayApiKey(apiKey: string | undefined, env?: Record<string, string | undefined>, warn?: (message: string) => void): string | undefined;
25
+ /**
26
+ * Build a Vercel-AI-Gateway-backed {@link AiProvider}. Tool calling + structured
27
+ * output are supported (the gateway fronts tool-calling models), so it satisfies
28
+ * the capability gate. The AI SDK is loaded lazily on first call.
29
+ */
30
+ export declare function createGatewayProvider(options?: GatewayProviderOptions): AiProvider;
@@ -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"]}
@@ -26,6 +26,7 @@ export * from './knowledge.js';
26
26
  export * from './shape.js';
27
27
  export * from './apply-content.js';
28
28
  export * from './anthropic-provider.js';
29
+ export * from './gateway-provider.js';
29
30
  export * from './fake-provider.js';
30
31
  export * from './route.js';
31
32
  export * from './route-provider.js';
@@ -33,7 +34,8 @@ export * from './convex-session.js';
33
34
  export * from './convex-session-headers.js';
34
35
  export * from './node-handler.js';
35
36
  export * from './tools.js';
36
- export * from './create-flow.js';
37
+ export * from './chat-context.js';
38
+ export * from './chat-loop.js';
37
39
  export * from './chat-state.js';
38
40
  export * from './rich-text-translate.js';
39
41
  export * from './translate-document.js';
@@ -26,6 +26,7 @@ export * from './knowledge.js';
26
26
  export * from './shape.js';
27
27
  export * from './apply-content.js';
28
28
  export * from './anthropic-provider.js';
29
+ export * from './gateway-provider.js';
29
30
  export * from './fake-provider.js';
30
31
  export * from './route.js';
31
32
  export * from './route-provider.js';
@@ -33,7 +34,8 @@ export * from './convex-session.js';
33
34
  export * from './convex-session-headers.js';
34
35
  export * from './node-handler.js';
35
36
  export * from './tools.js';
36
- export * from './create-flow.js';
37
+ export * from './chat-context.js';
38
+ export * from './chat-loop.js';
37
39
  export * from './chat-state.js';
38
40
  export * from './rich-text-translate.js';
39
41
  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,SAAS,CAAA;AACvB,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 './shape'\nexport * from './apply-content'\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;