@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
@@ -1 +1 @@
1
- {"version":3,"file":"chat-state.js","sourceRoot":"","sources":["../../../src/lib/ai/chat-state.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The chat panel's pure state core (EC-199).\n *\n * House testing pattern (AGENTS.md): no jsdom / renderHook — the panel's behaviour\n * lives in this deterministic reducer (submit → busy + activity, tool steps append,\n * a result becomes a message or a reviewable proposal, apply/reject transition the\n * proposal), tested directly; the React component is a thin shell over it. Ids are a\n * monotonic counter in state, so a scripted action sequence yields identical output\n * every run (determinism principle).\n */\nimport type { FieldValues } from '@elytracms/core/cms-core'\nimport type { AiProvenance, ContentPatch } from './types'\nimport type { CreateFlowResult, CreateFlowStep, ProposalOrigin } from './create-flow'\n\n/** A proposal the editor reviews as a diff and then applies (as a draft) or rejects. */\nexport interface ProposalReview {\n id: string\n /** Fresh authoring ('create') vs a fill-empty translation ('translate') of an existing doc. */\n origin: ProposalOrigin\n collection: string\n documentId: string\n patch: ContentPatch\n provenance: AiProvenance\n /** For a translation: the existing values the diff renders \"before\". */\n before?: { values?: FieldValues; localized?: Record<string, FieldValues> }\n status: 'pending' | 'applied' | 'rejected'\n}\n\n/** One rendered turn in the conversation. */\nexport type ChatMessage =\n | { id: string; role: 'user'; text: string }\n | { id: string; role: 'assistant'; kind: 'text'; text: string }\n | { id: string; role: 'assistant'; kind: 'activity'; steps: CreateFlowStep[] }\n | { id: string; role: 'assistant'; kind: 'proposal'; proposal: ProposalReview }\n\nexport interface ChatState {\n status: 'idle' | 'busy'\n messages: ChatMessage[]\n /** Id of the live activity message the current turn appends tool steps to. */\n activityId: string | null\n /** Last blocking error (route/provider failure), surfaced then cleared on next submit. */\n error: string | null\n /** Monotonic id source — keeps the reducer pure + deterministic. */\n seq: number\n}\n\nexport const initialChatState: ChatState = {\n status: 'idle',\n messages: [],\n activityId: null,\n error: null,\n seq: 0,\n}\n\nexport type ChatAction =\n | { type: 'submit'; text: string }\n | { type: 'step'; step: CreateFlowStep }\n | { type: 'result'; result: CreateFlowResult }\n | { type: 'fail'; message: string }\n | { type: 'apply'; proposalId: string }\n | { type: 'reject'; proposalId: string }\n\nfunction id(state: ChatState): [string, number] {\n return [`m${state.seq}`, state.seq + 1]\n}\n\n/** Drop a still-empty activity message (a turn that produced no tool steps). */\nfunction pruneEmptyActivity(messages: ChatMessage[], activityId: string | null): ChatMessage[] {\n if (!activityId) return messages\n return messages.filter(\n (m) =>\n !(\n m.id === activityId &&\n m.role === 'assistant' &&\n m.kind === 'activity' &&\n m.steps.length === 0\n ),\n )\n}\n\nexport function chatReducer(state: ChatState, action: ChatAction): ChatState {\n switch (action.type) {\n case 'submit': {\n const text = action.text.trim()\n if (text.length === 0 || state.status === 'busy') return state\n const [userId, s1] = id(state)\n const activityId = `m${s1}`\n const messages: ChatMessage[] = [\n ...state.messages,\n { id: userId, role: 'user', text },\n { id: activityId, role: 'assistant', kind: 'activity', steps: [] },\n ]\n return { ...state, status: 'busy', error: null, messages, activityId, seq: s1 + 1 }\n }\n case 'step': {\n if (!state.activityId) return state\n return {\n ...state,\n messages: state.messages.map((m) =>\n m.id === state.activityId && m.role === 'assistant' && m.kind === 'activity'\n ? { ...m, steps: [...m.steps, action.step] }\n : m,\n ),\n }\n }\n case 'result': {\n const messages = pruneEmptyActivity(state.messages, state.activityId)\n const [msgId, nextSeq] = id({ ...state, seq: state.seq })\n if (action.result.kind === 'proposal') {\n const proposal: ProposalReview = {\n id: msgId,\n origin: action.result.origin,\n collection: action.result.collection,\n documentId: action.result.documentId,\n patch: action.result.patch,\n provenance: action.result.provenance,\n ...(action.result.before ? { before: action.result.before } : {}),\n status: 'pending',\n }\n return {\n ...state,\n status: 'idle',\n activityId: null,\n seq: nextSeq,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'proposal', proposal }],\n }\n }\n const text =\n action.result.kind === 'message' ? action.result.text : action.result.message\n return {\n ...state,\n status: 'idle',\n activityId: null,\n seq: nextSeq,\n error: action.result.kind === 'error' ? text : null,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'text', text }],\n }\n }\n case 'fail': {\n const messages = pruneEmptyActivity(state.messages, state.activityId)\n const [msgId, nextSeq] = id(state)\n return {\n ...state,\n status: 'idle',\n activityId: null,\n error: action.message,\n seq: nextSeq,\n messages: [...messages, { id: msgId, role: 'assistant', kind: 'text', text: action.message }],\n }\n }\n case 'apply':\n case 'reject': {\n const next = action.type === 'apply' ? 'applied' : 'rejected'\n return {\n ...state,\n messages: state.messages.map((m) =>\n m.role === 'assistant' && m.kind === 'proposal' && m.proposal.id === action.proposalId\n ? { ...m, proposal: { ...m.proposal, status: next } }\n : m,\n ),\n }\n }\n default:\n return state\n }\n}\n"]}
1
+ {"version":3,"file":"chat-state.js","sourceRoot":"","sources":["../../../src/lib/ai/chat-state.ts"],"names":[],"mappings":"AA+DA,MAAM,CAAC,MAAM,sBAAsB,GAAoB;IACrD,SAAS,EAAE,EAAE;IACb,KAAK,EAAE,IAAI;CACZ,CAAA;AAWD,SAAS,UAAU,CACjB,KAAsB,EACtB,UAAkB,EAClB,MAA8B;IAE9B,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;IAC5C,mEAAmE;IACnE,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IAC5D,OAAO;QACL,GAAG,KAAK;QACR,SAAS,EAAE,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,QAAQ,EAAE,MAAM,EAAE,EAAE;KACzE,CAAA;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAsB,EACtB,MAAwB;IAExB,QAAQ,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,KAAK,SAAS;YACZ,OAAO;gBACL,GAAG,KAAK;gBACR,SAAS,EAAE;oBACT,GAAG,KAAK,CAAC,SAAS;oBAClB,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE;iBACtF;aACF,CAAA;QACH,KAAK,OAAO;YACV,OAAO,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;QACxD,KAAK,QAAQ;YACX,OAAO,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,EAAE,UAAU,CAAC,CAAA;QACzD,KAAK,MAAM;YACT,OAAO,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,CAAA;QACrF,KAAK,aAAa;YAChB,OAAO,KAAK,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;QACjE,KAAK,OAAO;YACV,OAAO,sBAAsB,CAAA;QAC/B;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAmB,EACnB,MAA0E;IAE1E,OAAO;QACL,MAAM,EAAE;YACN,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,MAAM,EAAE,MAAM,EAAE,MAAM,IAAI,EAAE;YAC5B,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9D;QACD,KAAK,EAAE;YACL,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,MAAM,EAAE,KAAK,CAAC,MAAM;YACpB,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D;KACF,CAAA;AACH,CAAC","sourcesContent":["/**\n * The chat panel's proposal-review state (EC-199, reshaped by EC-334).\n *\n * `useChat` (the AI SDK) owns the conversation: messages, streaming text, tool\n * parts, busy-status. This reducer keeps ONLY what the SDK does not own:\n *\n * - the proposal review map — the FULL proposals the terminal tools\n * (`createDocument` / `translateDocument`) produce, keyed by `toolCallId`\n * (the model receives only a compact summary as the tool output; the editor\n * reviews the real patch here), each `pending` → `applied` | `rejected`;\n * - the last blocking error (route/provider/step-cap failure), surfaced as a\n * visible note and cleared on the next submit.\n *\n * EC-380 (`clear`): a new conversation resets this state entirely — pending\n * proposals are discarded with REJECT semantics (they were never applied and\n * nothing half-applied survives; an applied draft already lives in the CMS and\n * is untouched). The panel pairs the dispatch with a fresh `useChat` id.\n *\n * House testing pattern (AGENTS.md): the panel's behaviour lives in this\n * deterministic reducer, tested directly; the React component is a thin shell.\n */\nimport type { CmsDocument, CmsValidationIssue, FieldValues } from '@elytracms/core/cms-core'\nimport type { AiProvenance, ContentPatch } from './types'\n\n/**\n * A proposal's kind: fresh authoring, a fill-empty translation of an existing\n * doc, or an EC-378 field update of an existing doc (old → new diff).\n */\nexport type ProposalOrigin = 'create' | 'translate' | 'update'\n\n/**\n * The full proposal payload a terminal tool hands to the review UI (via\n * `onProposal` on the executor — see `tools.ts`). The model never sees this;\n * it gets a compact summary as the tool output.\n */\nexport interface ChatProposal {\n origin: ProposalOrigin\n collection: string\n documentId: string\n patch: ContentPatch\n provenance: AiProvenance\n /** Generation attempts made (1 = happy path, >1 = repaired). */\n attempts: number\n /** Non-error issues on the accepted content (warnings). */\n issues: CmsValidationIssue[]\n /** For a translation/update: the existing document values the diff renders \"before\". */\n before?: { values?: FieldValues; localized?: Record<string, FieldValues> }\n}\n\n/** A proposal under review: {@link ChatProposal} + its review status. */\nexport interface ProposalReview extends ChatProposal {\n /** The tool call that produced it (`toolCallId`) — the review key. */\n id: string\n status: 'pending' | 'applied' | 'rejected'\n}\n\nexport interface ChatReviewState {\n /** Proposals keyed by `toolCallId`. */\n proposals: Record<string, ProposalReview>\n /** Last blocking error (route/provider/step-cap), surfaced then cleared on submit. */\n error: string | null\n}\n\nexport const initialChatReviewState: ChatReviewState = {\n proposals: {},\n error: null,\n}\n\nexport type ChatReviewAction =\n | { type: 'propose'; toolCallId: string; proposal: ChatProposal }\n | { type: 'apply'; toolCallId: string }\n | { type: 'reject'; toolCallId: string }\n | { type: 'fail'; message: string }\n | { type: 'clear-error' }\n /** EC-380: new conversation — full reset; pending proposals discarded (reject semantics). */\n | { type: 'clear' }\n\nfunction transition(\n state: ChatReviewState,\n toolCallId: string,\n status: 'applied' | 'rejected',\n): ChatReviewState {\n const existing = state.proposals[toolCallId]\n // Only a PENDING proposal transitions — apply/reject are one-shot.\n if (!existing || existing.status !== 'pending') return state\n return {\n ...state,\n proposals: { ...state.proposals, [toolCallId]: { ...existing, status } },\n }\n}\n\nexport function chatReviewReducer(\n state: ChatReviewState,\n action: ChatReviewAction,\n): ChatReviewState {\n switch (action.type) {\n case 'propose':\n return {\n ...state,\n proposals: {\n ...state.proposals,\n [action.toolCallId]: { ...action.proposal, id: action.toolCallId, status: 'pending' },\n },\n }\n case 'apply':\n return transition(state, action.toolCallId, 'applied')\n case 'reject':\n return transition(state, action.toolCallId, 'rejected')\n case 'fail':\n return state.error === action.message ? state : { ...state, error: action.message }\n case 'clear-error':\n return state.error === null ? state : { ...state, error: null }\n case 'clear':\n return initialChatReviewState\n default:\n return state\n }\n}\n\n/**\n * Turn a proposal patch into the before/after document pair the EC-317 diff\n * engine (`diffDocuments`) compares. For a fresh document `before` is empty\n * (the whole proposal reads as \"added\"); for a translation pass the existing\n * document's values. Pure — the reviewable diff is a straight\n * `diffDocuments(collection, before, after)`.\n */\nexport function proposalDocuments(\n patch: ContentPatch,\n before?: { values?: FieldValues; localized?: Record<string, FieldValues> },\n): { before: CmsDocument; after: CmsDocument } {\n return {\n before: {\n collection: patch.collection,\n id: patch.id,\n values: before?.values ?? {},\n ...(before?.localized ? { localized: before.localized } : {}),\n },\n after: {\n collection: patch.collection,\n id: patch.id,\n values: patch.values,\n ...(patch.localized ? { localized: patch.localized } : {}),\n },\n }\n}\n"]}
@@ -3,13 +3,146 @@
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
+ /**
72
+ * EC-381: when set, the create flow inserts a `listAssets` stage after the
73
+ * relation search and before `createDocument`, so a collection with an image
74
+ * field gets it filled from a REAL library asset (the enum leash). Optional
75
+ * search text passed to `listAssets` (omit to list all).
76
+ */
77
+ searchAssets?: boolean;
78
+ /** Optional query passed to the scripted `listAssets` stage (EC-381). */
79
+ assetQuery?: string;
80
+ /** The collection the terminal stage creates in (default `post`). */
81
+ createCollection?: string;
82
+ /** The final answer streamed word-by-word after the proposal lands. */
83
+ answerText?: string;
84
+ /**
85
+ * EC-378: the field the scripted UPDATE flow changes (default `title`). A user
86
+ * turn matching {@link FakeCreateFlowChatModelOptions.updatePattern} routes to
87
+ * the update flow instead of the create flow.
88
+ */
89
+ updateField?: string;
90
+ /** Routes a user turn to the update flow (default {@link FAKE_UPDATE_PATTERN}). */
91
+ updatePattern?: RegExp;
92
+ /** The final answer of the scripted update flow. */
93
+ updateAnswerText?: string;
94
+ }
95
+ /** Default final answer of the scripted create flow. */
96
+ export declare const FAKE_CREATE_FLOW_ANSWER = "Done \u2014 review the proposal below and apply it as a draft.";
97
+ /** Default final answer of the scripted update flow (EC-378). */
98
+ export declare const FAKE_UPDATE_FLOW_ANSWER = "Done \u2014 review the proposed change below and apply it as a draft.";
99
+ /** Default edit-request detector of the scripted assistant (EC-378). */
100
+ export declare const FAKE_UPDATE_PATTERN: RegExp;
101
+ /**
102
+ * The scripted update flow's new value for the target field: the text after the
103
+ * FIRST colon when present (`Set the title to: New Title` → `New Title`), else
104
+ * the whole user text — deterministic either way. First (not last) colon so a
105
+ * markdown body value may itself contain colons (EC-379).
106
+ */
107
+ export declare function fakeUpdateValueFrom(userText: string): string;
108
+ /**
109
+ * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the
110
+ * <field> to: …` names it (so the playground can drive a markdown-authored
111
+ * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything
112
+ * else falls back to the configured default field.
113
+ */
114
+ export declare function fakeUpdateFieldFrom(userText: string, fallback: string): string;
115
+ /**
116
+ * A scripted, STATELESS fake chat model that drives the FULL create flow — and,
117
+ * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the
118
+ * native loop (EC-334): keyless playground / CI exercise the whole client-tool
119
+ * round-trip deterministically. Stage is read from the CURRENT user turn's tool
120
+ * results (prompt suffix after the last user message), so every follow-up turn
121
+ * starts a fresh flow — multi-turn works without model state.
122
+ *
123
+ * Create flow (default):
124
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
125
+ * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`
126
+ * 3. create result present → the answer text, word by word.
127
+ *
128
+ * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:
129
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
130
+ * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`
131
+ * 3. listAssets done, no create → `createDocument({ collection, instruction })`
132
+ * 4. create result present → the answer text, word by word.
133
+ *
134
+ * Update flow (user text matches `updatePattern`, e.g. "Change the title to: X"):
135
+ * 1. no tool result yet → `getDocument({})` — target omitted on purpose:
136
+ * the executor resolves the document OPEN in the
137
+ * editor (the EC-378 current-document seam)
138
+ * 2. document read, no update → `updateFields({ changes: [{ field:
139
+ * fakeUpdateFieldFrom(<text>, updateField),
140
+ * value: fakeUpdateValueFrom(<last user text>) }] })`
141
+ * — "Set the body to: ## Neu" drives a markdown
142
+ * rich-text update deterministically (EC-379)
143
+ * 3. update result present → the update answer, word by word.
11
144
  */
12
- import type { AiProvider, AiProviderCapabilities, GenerateStructuredRequest, StreamChatRequest } from './types.js';
145
+ export declare function createFakeCreateFlowChatModel(options?: FakeCreateFlowChatModelOptions): FakeChatModel;
13
146
  export interface FakeProviderOptions {
14
147
  /** Provider id (default `fake`). */
15
148
  id?: string;
@@ -21,14 +154,14 @@ export interface FakeProviderOptions {
21
154
  objects?: unknown[];
22
155
  /** Compute the object from the request + zero-based call index (overrides `objects`). */
23
156
  respond?: (request: GenerateStructuredRequest, callIndex: number) => unknown;
24
- /** Fixed text `streamChat` streams back (default: echoes the last user message). */
25
- streamText?: string;
157
+ /** The scripted chat model `languageModel()` serves (default {@link createFakeChatModel}). */
158
+ chatModel?: FakeChatModel;
26
159
  }
27
160
  /** A FakeProvider that also records the requests it received, for assertions. */
28
161
  export interface FakeProvider extends AiProvider {
29
162
  /** Every `generateStructured` request received, in order. */
30
163
  readonly calls: GenerateStructuredRequest[];
31
- /** Every `streamChat` request received, in order. */
32
- readonly streamCalls: StreamChatRequest[];
164
+ /** The scripted fake chat model behind `languageModel()` (records its calls). */
165
+ readonly chatModel: FakeChatModel;
33
166
  }
34
167
  export declare function createFakeProvider(options?: FakeProviderOptions): FakeProvider;
@@ -1,14 +1,273 @@
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
+ /** Default final answer of the scripted update flow (EC-378). */
162
+ export const FAKE_UPDATE_FLOW_ANSWER = 'Done — review the proposed change below and apply it as a draft.';
163
+ /** Default edit-request detector of the scripted assistant (EC-378). */
164
+ export const FAKE_UPDATE_PATTERN = /\b(change|update|edit|remove|replace|rewrite|fix|set)\b/i;
165
+ /**
166
+ * The scripted update flow's new value for the target field: the text after the
167
+ * FIRST colon when present (`Set the title to: New Title` → `New Title`), else
168
+ * the whole user text — deterministic either way. First (not last) colon so a
169
+ * markdown body value may itself contain colons (EC-379).
170
+ */
171
+ export function fakeUpdateValueFrom(userText) {
172
+ const colon = userText.indexOf(':');
173
+ const value = colon >= 0 ? userText.slice(colon + 1) : userText;
174
+ return value.trim() || 'Updated by the assistant';
175
+ }
176
+ /**
177
+ * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the
178
+ * <field> to: …` names it (so the playground can drive a markdown-authored
179
+ * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything
180
+ * else falls back to the configured default field.
181
+ */
182
+ export function fakeUpdateFieldFrom(userText, fallback) {
183
+ const match = /\b(?:set|change|update)\s+the\s+([A-Za-z]\w*)\s+to\s*:/i.exec(userText);
184
+ return match?.[1] ?? fallback;
185
+ }
186
+ /**
187
+ * A scripted, STATELESS fake chat model that drives the FULL create flow — and,
188
+ * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the
189
+ * native loop (EC-334): keyless playground / CI exercise the whole client-tool
190
+ * round-trip deterministically. Stage is read from the CURRENT user turn's tool
191
+ * results (prompt suffix after the last user message), so every follow-up turn
192
+ * starts a fresh flow — multi-turn works without model state.
193
+ *
194
+ * Create flow (default):
195
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
196
+ * 2. search result, no create → `createDocument({ collection, instruction: <last user text> })`
197
+ * 3. create result present → the answer text, word by word.
198
+ *
199
+ * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:
200
+ * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`
201
+ * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`
202
+ * 3. listAssets done, no create → `createDocument({ collection, instruction })`
203
+ * 4. create result present → the answer text, word by word.
204
+ *
205
+ * Update flow (user text matches `updatePattern`, e.g. "Change the title to: X"):
206
+ * 1. no tool result yet → `getDocument({})` — target omitted on purpose:
207
+ * the executor resolves the document OPEN in the
208
+ * editor (the EC-378 current-document seam)
209
+ * 2. document read, no update → `updateFields({ changes: [{ field:
210
+ * fakeUpdateFieldFrom(<text>, updateField),
211
+ * value: fakeUpdateValueFrom(<last user text>) }] })`
212
+ * — "Set the body to: ## Neu" drives a markdown
213
+ * rich-text update deterministically (EC-379)
214
+ * 3. update result present → the update answer, word by word.
215
+ */
216
+ export function createFakeCreateFlowChatModel(options = {}) {
217
+ const searchCollection = options.searchCollection ?? 'author';
218
+ const createCollection = options.createCollection ?? 'post';
219
+ const searchAssets = options.searchAssets ?? false;
220
+ const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER;
221
+ const updateField = options.updateField ?? 'title';
222
+ const updatePattern = options.updatePattern ?? FAKE_UPDATE_PATTERN;
223
+ const updateAnswer = options.updateAnswerText ?? FAKE_UPDATE_FLOW_ANSWER;
224
+ return createFakeChatModel({
225
+ modelId: options.modelId ?? 'fake-create-flow-model',
226
+ script: (callOptions) => {
227
+ const done = toolResultNamesInCurrentTurn(callOptions.prompt);
228
+ const userText = lastUserText(callOptions.prompt);
229
+ if (updatePattern.test(userText)) {
230
+ if (done.has('updateFields'))
231
+ return answerTurnParts(updateAnswer);
232
+ if (done.has('getDocument')) {
233
+ return toolTurnParts('Proposing the change. ', 'updateFields', {
234
+ changes: [
235
+ {
236
+ field: fakeUpdateFieldFrom(userText, updateField),
237
+ value: fakeUpdateValueFrom(userText),
238
+ },
239
+ ],
240
+ }, 'call-update-1');
241
+ }
242
+ return toolTurnParts('Reading the document. ', 'getDocument', {}, 'call-get-1');
243
+ }
244
+ if (done.has('createDocument'))
245
+ return answerTurnParts(answer);
246
+ if (done.has('searchDocuments')) {
247
+ // EC-381 — search the media library before creating (a topically fitting,
248
+ // REAL asset fills the image field via the enum leash).
249
+ if (searchAssets && !done.has('listAssets')) {
250
+ return toolTurnParts('Looking for a fitting image. ', 'listAssets', { ...(options.assetQuery ? { query: options.assetQuery } : {}) }, 'call-assets-1');
251
+ }
252
+ const instruction = userText || 'Write a short document.';
253
+ return toolTurnParts('Drafting a proposal. ', 'createDocument', { collection: createCollection, instruction }, 'call-create-1');
254
+ }
255
+ return toolTurnParts(FAKE_CHAT_PREAMBLE, 'searchDocuments', { collection: searchCollection }, 'call-search-1');
256
+ },
257
+ });
258
+ }
1
259
  export function createFakeProvider(options = {}) {
2
260
  const calls = [];
3
- const streamCalls = [];
4
261
  const objects = options.objects ?? [];
262
+ const chatModel = options.chatModel ?? createFakeChatModel();
5
263
  let index = 0;
6
264
  return {
7
265
  id: options.id ?? 'fake',
8
266
  model: options.model ?? 'fake-model',
9
267
  calls,
10
- streamCalls,
268
+ chatModel,
11
269
  capabilities: () => options.capabilities ?? { toolCalling: true, structuredOutput: true },
270
+ languageModel: () => Promise.resolve(chatModel),
12
271
  generateStructured(request) {
13
272
  calls.push(request);
14
273
  const callIndex = index;
@@ -20,12 +279,6 @@ export function createFakeProvider(options = {}) {
20
279
  : {};
21
280
  return Promise.resolve({ object, usage: { inputTokens: 0, outputTokens: 0 } });
22
281
  },
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
282
  };
30
283
  }
31
284
  //# 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":"AAwCA,mFAAmF;AACnF,MAAM,UAAU,GAA4B;IAC1C,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAClE,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;CAClD,CAAA;AAED,+DAA+D;AAC/D,MAAM,CAAC,MAAM,kBAAkB,GAAG,uBAAuB,CAAA;AACzD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,2DAA2D,CAAA;AA0B3F,mFAAmF;AACnF,SAAS,mBAAmB,CAAC,MAA0C;IACrE,OAAO,MAAM,CAAC,IAAI,CAChB,CAAC,OAAO,EAAE,EAAE,CACV,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;QAC9B,OAAO,CAAC,OAAO,CAAC,IAAI,CAClB,CAAC,IAAI,EAAE,EAAE,CAAE,IAA2B,CAAC,IAAI,KAAK,aAAa,CAC9D,CACJ,CAAA;AACH,CAAC;AAED,qEAAqE;AACrE,SAAS,aAAa,CAAC,MAA0C;IAC/D,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/C,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,MAAM;YAAE,OAAO,CAAC,CAAA;IAC1C,CAAC;IACD,OAAO,CAAC,CAAC,CAAA;AACX,CAAC;AAED,sEAAsE;AACtE,SAAS,YAAY,CAAC,MAA0C;IAC9D,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,CAAA;IACnC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAC7B,IAAI,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,CAAA;IAC1D,OAAO,OAAO,CAAC,OAAO;SACnB,MAAM,CAAC,CAAC,IAAI,EAA0C,EAAE,CAAE,IAA2B,CAAC,IAAI,KAAK,MAAM,CAAC;SACtG,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC;SACxB,IAAI,CAAC,GAAG,CAAC;SACT,IAAI,EAAE,CAAA;AACX,CAAC;AAED;;;GAGG;AACH,SAAS,4BAA4B,CAAC,MAA0C;IAC9E,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;IAC/B,KAAK,MAAM,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QAC9D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC;YAAE,SAAQ;QAC7C,KAAK,MAAM,IAAI,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;YACnC,MAAM,SAAS,GAAG,IAA8C,CAAA;YAChE,IAAI,SAAS,CAAC,IAAI,KAAK,aAAa,IAAI,OAAO,SAAS,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBAC/E,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,wFAAwF;AACxF,SAAS,aAAa,CACpB,QAAgB,EAChB,QAAgB,EAChB,KAA8B,EAC9B,MAAc;IAEd,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IACrC,OAAO;QACL,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,EAAE;QACtC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE;QACpC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE;QACrD,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE;QAClC,EAAE,IAAI,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE;QAClD,EAAE,IAAI,EAAE,kBAAkB,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE;QACxD,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAAE,EAAE,MAAM,EAAE;QACtC,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE;QACnE;YACE,IAAI,EAAE,QAAQ;YACd,KAAK,EAAE,UAAU;YACjB,YAAY,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,iBAAiB,EAAE;SAChE;KACF,CAAA;AACH,CAAC;AAED,mFAAmF;AACnF,SAAS,eAAe,CAAC,MAAc;IACrC,OAAO;QACL,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,EAAE,EAAE;QACtC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE;QACpC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,CACvB,CAAC,KAAK,EAA2B,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAClF;QACD,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE;QAClC,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,EAAE;KAC3F,CAAA;AACH,CAAC;AAED,qFAAqF;AACrF,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC7B,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;AACnF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,UAAgC,EAAE;IACpE,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,IAAI,kBAAkB,CAAA;IAC3D,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,iBAAiB,CAAA;IACtD,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,gBAAgB,CAAA;IACrD,MAAM,KAAK,GAA+B,EAAE,CAAA;IAC5C,IAAI,SAAS,GAAG,CAAC,CAAA;IAEjB,SAAS,QAAQ,CAAC,WAAqC;QACrD,IAAI,OAAO,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,SAAS,CAAC,CAAA;QACjE,IAAI,CAAC,mBAAmB,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7C,OAAO,aAAa,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAA;QAC/D,CAAC;QACD,OAAO,eAAe,CAAC,MAAM,CAAC,CAAA;IAChC,CAAC;IAED,+EAA+E;IAC/E,SAAS,WAAW,CAAC,KAAgC;QACnD,MAAM,OAAO,GAAkC,EAAE,CAAA;QACjD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;QAC1C,IAAI,YAAY,GAAmC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,CAAA;QACxF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBAC/B,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAA;YACnE,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;gBACrC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACpB,CAAC;iBAAM,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAClC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAA;YAClC,CAAC;QACH,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,MAAM,EAAE;YAAE,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7E,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAA;IACnE,CAAC;IAED,OAAO;QACL,oBAAoB,EAAE,IAAI;QAC1B,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,iBAAiB;QAC7C,aAAa,EAAE,EAAE;QACjB,KAAK;QACL,UAAU,CAAC,WAAW;YACpB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;YACnC,SAAS,IAAI,CAAC,CAAA;YACd,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAA;QAC5C,CAAC;QACD,QAAQ,CAAC,WAAW;YAClB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACvB,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAA;YACnC,SAAS,IAAI,CAAC,CAAA;YACd,OAAO,OAAO,CAAC,OAAO,CAAC;gBACrB,MAAM,EAAE,IAAI,cAAc,CAA0B;oBAClD,KAAK,CAAC,UAAU;wBACd,KAAK,MAAM,IAAI,IAAI,KAAK;4BAAE,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;wBAClD,UAAU,CAAC,KAAK,EAAE,CAAA;oBACpB,CAAC;iBACF,CAAC;aACH,CAAC,CAAA;QACJ,CAAC;KACF,CAAA;AACH,CAAC;AAkCD,wDAAwD;AACxD,MAAM,CAAC,MAAM,uBAAuB,GAClC,2DAA2D,CAAA;AAE7D,iEAAiE;AACjE,MAAM,CAAC,MAAM,uBAAuB,GAClC,kEAAkE,CAAA;AAEpE,wEAAwE;AACxE,MAAM,CAAC,MAAM,mBAAmB,GAAG,0DAA0D,CAAA;AAE7F;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAgB;IAClD,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACnC,MAAM,KAAK,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAC/D,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,0BAA0B,CAAA;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,QAAgB,EAAE,QAAgB;IACpE,MAAM,KAAK,GAAG,yDAAyD,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACtF,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAA;AAC/B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,6BAA6B,CAC3C,UAA0C,EAAE;IAE5C,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,QAAQ,CAAA;IAC7D,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,MAAM,CAAA;IAC3D,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,KAAK,CAAA;IAClD,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,IAAI,uBAAuB,CAAA;IAC5D,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,OAAO,CAAA;IAClD,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,mBAAmB,CAAA;IAClE,MAAM,YAAY,GAAG,OAAO,CAAC,gBAAgB,IAAI,uBAAuB,CAAA;IACxE,OAAO,mBAAmB,CAAC;QACzB,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,wBAAwB;QACpD,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE;YACtB,MAAM,IAAI,GAAG,4BAA4B,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YAC7D,MAAM,QAAQ,GAAG,YAAY,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YACjD,IAAI,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,IAAI,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC;oBAAE,OAAO,eAAe,CAAC,YAAY,CAAC,CAAA;gBAClE,IAAI,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;oBAC5B,OAAO,aAAa,CAClB,wBAAwB,EACxB,cAAc,EACd;wBACE,OAAO,EAAE;4BACP;gCACE,KAAK,EAAE,mBAAmB,CAAC,QAAQ,EAAE,WAAW,CAAC;gCACjD,KAAK,EAAE,mBAAmB,CAAC,QAAQ,CAAC;6BACrC;yBACF;qBACF,EACD,eAAe,CAChB,CAAA;gBACH,CAAC;gBACD,OAAO,aAAa,CAAC,wBAAwB,EAAE,aAAa,EAAE,EAAE,EAAE,YAAY,CAAC,CAAA;YACjF,CAAC;YACD,IAAI,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC;gBAAE,OAAO,eAAe,CAAC,MAAM,CAAC,CAAA;YAC9D,IAAI,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;gBAChC,0EAA0E;gBAC1E,wDAAwD;gBACxD,IAAI,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;oBAC5C,OAAO,aAAa,CAClB,+BAA+B,EAC/B,YAAY,EACZ,EAAE,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAChE,eAAe,CAChB,CAAA;gBACH,CAAC;gBACD,MAAM,WAAW,GAAG,QAAQ,IAAI,yBAAyB,CAAA;gBACzD,OAAO,aAAa,CAClB,uBAAuB,EACvB,gBAAgB,EAChB,EAAE,UAAU,EAAE,gBAAgB,EAAE,WAAW,EAAE,EAC7C,eAAe,CAChB,CAAA;YACH,CAAC;YACD,OAAO,aAAa,CAClB,kBAAkB,EAClB,iBAAiB,EACjB,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAChC,eAAe,CAChB,CAAA;QACH,CAAC;KACF,CAAC,CAAA;AACJ,CAAC;AA2BD,MAAM,UAAU,kBAAkB,CAAC,UAA+B,EAAE;IAClE,MAAM,KAAK,GAAgC,EAAE,CAAA;IAC7C,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,EAAE,CAAA;IACrC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,mBAAmB,EAAE,CAAA;IAC5D,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,SAAS;QACT,YAAY,EAAE,GAA2B,EAAE,CACzC,OAAO,CAAC,YAAY,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE;QAEvE,aAAa,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC;QAE/C,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;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 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 /**\n * EC-381: when set, the create flow inserts a `listAssets` stage after the\n * relation search and before `createDocument`, so a collection with an image\n * field gets it filled from a REAL library asset (the enum leash). Optional\n * search text passed to `listAssets` (omit to list all).\n */\n searchAssets?: boolean\n /** Optional query passed to the scripted `listAssets` stage (EC-381). */\n assetQuery?: 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 * EC-378: the field the scripted UPDATE flow changes (default `title`). A user\n * turn matching {@link FakeCreateFlowChatModelOptions.updatePattern} routes to\n * the update flow instead of the create flow.\n */\n updateField?: string\n /** Routes a user turn to the update flow (default {@link FAKE_UPDATE_PATTERN}). */\n updatePattern?: RegExp\n /** The final answer of the scripted update flow. */\n updateAnswerText?: 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/** Default final answer of the scripted update flow (EC-378). */\nexport const FAKE_UPDATE_FLOW_ANSWER =\n 'Done — review the proposed change below and apply it as a draft.'\n\n/** Default edit-request detector of the scripted assistant (EC-378). */\nexport const FAKE_UPDATE_PATTERN = /\\b(change|update|edit|remove|replace|rewrite|fix|set)\\b/i\n\n/**\n * The scripted update flow's new value for the target field: the text after the\n * FIRST colon when present (`Set the title to: New Title` → `New Title`), else\n * the whole user text — deterministic either way. First (not last) colon so a\n * markdown body value may itself contain colons (EC-379).\n */\nexport function fakeUpdateValueFrom(userText: string): string {\n const colon = userText.indexOf(':')\n const value = colon >= 0 ? userText.slice(colon + 1) : userText\n return value.trim() || 'Updated by the assistant'\n}\n\n/**\n * The scripted update flow's TARGET FIELD (EC-379): `set/change/update the\n * <field> to: …` names it (so the playground can drive a markdown-authored\n * rich-text update deterministically, e.g. `Set the body to: ## Neu`); anything\n * else falls back to the configured default field.\n */\nexport function fakeUpdateFieldFrom(userText: string, fallback: string): string {\n const match = /\\b(?:set|change|update)\\s+the\\s+([A-Za-z]\\w*)\\s+to\\s*:/i.exec(userText)\n return match?.[1] ?? fallback\n}\n\n/**\n * A scripted, STATELESS fake chat model that drives the FULL create flow — and,\n * for a user turn matching the update pattern, the EC-378 UPDATE flow — over the\n * native loop (EC-334): keyless playground / CI exercise the whole client-tool\n * round-trip deterministically. 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 * Create flow (default):\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 *\n * With `searchAssets` (EC-381), a `listAssets` stage is inserted between 1 and 2:\n * 1. no tool result yet → `searchDocuments({ collection: searchCollection })`\n * 2. search done, no listAssets → `listAssets({ query?: assetQuery })`\n * 3. listAssets done, no create → `createDocument({ collection, instruction })`\n * 4. create result present → the answer text, word by word.\n *\n * Update flow (user text matches `updatePattern`, e.g. \"Change the title to: X\"):\n * 1. no tool result yet → `getDocument({})` — target omitted on purpose:\n * the executor resolves the document OPEN in the\n * editor (the EC-378 current-document seam)\n * 2. document read, no update → `updateFields({ changes: [{ field:\n * fakeUpdateFieldFrom(<text>, updateField),\n * value: fakeUpdateValueFrom(<last user text>) }] })`\n * — \"Set the body to: ## Neu\" drives a markdown\n * rich-text update deterministically (EC-379)\n * 3. update result present → the update answer, 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 searchAssets = options.searchAssets ?? false\n const answer = options.answerText ?? FAKE_CREATE_FLOW_ANSWER\n const updateField = options.updateField ?? 'title'\n const updatePattern = options.updatePattern ?? FAKE_UPDATE_PATTERN\n const updateAnswer = options.updateAnswerText ?? FAKE_UPDATE_FLOW_ANSWER\n return createFakeChatModel({\n modelId: options.modelId ?? 'fake-create-flow-model',\n script: (callOptions) => {\n const done = toolResultNamesInCurrentTurn(callOptions.prompt)\n const userText = lastUserText(callOptions.prompt)\n if (updatePattern.test(userText)) {\n if (done.has('updateFields')) return answerTurnParts(updateAnswer)\n if (done.has('getDocument')) {\n return toolTurnParts(\n 'Proposing the change. ',\n 'updateFields',\n {\n changes: [\n {\n field: fakeUpdateFieldFrom(userText, updateField),\n value: fakeUpdateValueFrom(userText),\n },\n ],\n },\n 'call-update-1',\n )\n }\n return toolTurnParts('Reading the document. ', 'getDocument', {}, 'call-get-1')\n }\n if (done.has('createDocument')) return answerTurnParts(answer)\n if (done.has('searchDocuments')) {\n // EC-381 — search the media library before creating (a topically fitting,\n // REAL asset fills the image field via the enum leash).\n if (searchAssets && !done.has('listAssets')) {\n return toolTurnParts(\n 'Looking for a fitting image. ',\n 'listAssets',\n { ...(options.assetQuery ? { query: options.assetQuery } : {}) },\n 'call-assets-1',\n )\n }\n const instruction = userText || '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;