@ohos-ports/genkit 1.42.0-beta.0

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 (202) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +434 -0
  3. package/lib/beta.d.mts +18 -0
  4. package/lib/beta.d.ts +18 -0
  5. package/lib/beta.js +88 -0
  6. package/lib/beta.js.map +1 -0
  7. package/lib/beta.mjs +64 -0
  8. package/lib/beta.mjs.map +1 -0
  9. package/lib/client/agent.d.mts +52 -0
  10. package/lib/client/agent.d.ts +52 -0
  11. package/lib/client/agent.js +90 -0
  12. package/lib/client/agent.js.map +1 -0
  13. package/lib/client/agent.mjs +68 -0
  14. package/lib/client/agent.mjs.map +1 -0
  15. package/lib/client/client.d.mts +80 -0
  16. package/lib/client/client.d.ts +80 -0
  17. package/lib/client/client.js +164 -0
  18. package/lib/client/client.js.map +1 -0
  19. package/lib/client/client.mjs +139 -0
  20. package/lib/client/client.mjs.map +1 -0
  21. package/lib/client/index.d.mts +6 -0
  22. package/lib/client/index.d.ts +6 -0
  23. package/lib/client/index.js +55 -0
  24. package/lib/client/index.js.map +1 -0
  25. package/lib/client/index.mjs +31 -0
  26. package/lib/client/index.mjs.map +1 -0
  27. package/lib/client/types.d.mts +17 -0
  28. package/lib/client/types.d.ts +17 -0
  29. package/lib/client/types.js +17 -0
  30. package/lib/client/types.js.map +1 -0
  31. package/lib/client/types.mjs +1 -0
  32. package/lib/client/types.mjs.map +1 -0
  33. package/lib/common.d.mts +3 -0
  34. package/lib/common.d.ts +3 -0
  35. package/lib/common.js +151 -0
  36. package/lib/common.js.map +1 -0
  37. package/lib/common.mjs +128 -0
  38. package/lib/common.mjs.map +1 -0
  39. package/lib/context.d.mts +1 -0
  40. package/lib/context.d.ts +1 -0
  41. package/lib/context.js +50 -0
  42. package/lib/context.js.map +1 -0
  43. package/lib/context.mjs +28 -0
  44. package/lib/context.mjs.map +1 -0
  45. package/lib/embedder.d.mts +1 -0
  46. package/lib/embedder.d.ts +1 -0
  47. package/lib/embedder.js +48 -0
  48. package/lib/embedder.js.map +1 -0
  49. package/lib/embedder.mjs +26 -0
  50. package/lib/embedder.mjs.map +1 -0
  51. package/lib/evaluator.d.mts +1 -0
  52. package/lib/evaluator.d.ts +1 -0
  53. package/lib/evaluator.js +60 -0
  54. package/lib/evaluator.js.map +1 -0
  55. package/lib/evaluator.mjs +38 -0
  56. package/lib/evaluator.mjs.map +1 -0
  57. package/lib/extract.d.mts +1 -0
  58. package/lib/extract.d.ts +1 -0
  59. package/lib/extract.js +48 -0
  60. package/lib/extract.js.map +1 -0
  61. package/lib/extract.mjs +23 -0
  62. package/lib/extract.mjs.map +1 -0
  63. package/lib/formats.d.mts +1 -0
  64. package/lib/formats.d.ts +1 -0
  65. package/lib/formats.js +34 -0
  66. package/lib/formats.js.map +1 -0
  67. package/lib/formats.mjs +18 -0
  68. package/lib/formats.mjs.map +1 -0
  69. package/lib/genkit-beta.d.mts +183 -0
  70. package/lib/genkit-beta.d.ts +183 -0
  71. package/lib/genkit-beta.js +178 -0
  72. package/lib/genkit-beta.js.map +1 -0
  73. package/lib/genkit-beta.mjs +159 -0
  74. package/lib/genkit-beta.mjs.map +1 -0
  75. package/lib/genkit.d.mts +10 -0
  76. package/lib/genkit.d.ts +10 -0
  77. package/lib/genkit.js +471 -0
  78. package/lib/genkit.js.map +1 -0
  79. package/lib/genkit.mjs +489 -0
  80. package/lib/genkit.mjs.map +1 -0
  81. package/lib/index-BSkuxdwl.d.mts +311 -0
  82. package/lib/index-BSkuxdwl.d.ts +311 -0
  83. package/lib/index.d.mts +10 -0
  84. package/lib/index.d.ts +10 -0
  85. package/lib/index.js +51 -0
  86. package/lib/index.js.map +1 -0
  87. package/lib/index.mjs +24 -0
  88. package/lib/index.mjs.map +1 -0
  89. package/lib/logging.d.mts +1 -0
  90. package/lib/logging.d.ts +1 -0
  91. package/lib/logging.js +46 -0
  92. package/lib/logging.js.map +1 -0
  93. package/lib/logging.mjs +22 -0
  94. package/lib/logging.mjs.map +1 -0
  95. package/lib/middleware.d.mts +1 -0
  96. package/lib/middleware.d.ts +1 -0
  97. package/lib/middleware.js +56 -0
  98. package/lib/middleware.js.map +1 -0
  99. package/lib/middleware.mjs +34 -0
  100. package/lib/middleware.mjs.map +1 -0
  101. package/lib/model.d.mts +1 -0
  102. package/lib/model.d.ts +1 -0
  103. package/lib/model.js +92 -0
  104. package/lib/model.js.map +1 -0
  105. package/lib/model.mjs +70 -0
  106. package/lib/model.mjs.map +1 -0
  107. package/lib/plugin.d.mts +10 -0
  108. package/lib/plugin.d.ts +10 -0
  109. package/lib/plugin.js +143 -0
  110. package/lib/plugin.js.map +1 -0
  111. package/lib/plugin.mjs +113 -0
  112. package/lib/plugin.mjs.map +1 -0
  113. package/lib/registry.d.mts +1 -0
  114. package/lib/registry.d.ts +1 -0
  115. package/lib/registry.js +46 -0
  116. package/lib/registry.js.map +1 -0
  117. package/lib/registry.mjs +24 -0
  118. package/lib/registry.mjs.map +1 -0
  119. package/lib/reranker.d.mts +1 -0
  120. package/lib/reranker.d.ts +1 -0
  121. package/lib/reranker.js +54 -0
  122. package/lib/reranker.js.map +1 -0
  123. package/lib/reranker.mjs +32 -0
  124. package/lib/reranker.mjs.map +1 -0
  125. package/lib/retriever.d.mts +1 -0
  126. package/lib/retriever.d.ts +1 -0
  127. package/lib/retriever.js +58 -0
  128. package/lib/retriever.js.map +1 -0
  129. package/lib/retriever.mjs +36 -0
  130. package/lib/retriever.mjs.map +1 -0
  131. package/lib/schema.d.mts +1 -0
  132. package/lib/schema.d.ts +1 -0
  133. package/lib/schema.js +54 -0
  134. package/lib/schema.js.map +1 -0
  135. package/lib/schema.mjs +32 -0
  136. package/lib/schema.mjs.map +1 -0
  137. package/lib/testing.d.mts +1 -0
  138. package/lib/testing.d.ts +1 -0
  139. package/lib/testing.js +50 -0
  140. package/lib/testing.js.map +1 -0
  141. package/lib/testing.mjs +28 -0
  142. package/lib/testing.mjs.map +1 -0
  143. package/lib/tool.d.mts +1 -0
  144. package/lib/tool.d.ts +1 -0
  145. package/lib/tool.js +54 -0
  146. package/lib/tool.js.map +1 -0
  147. package/lib/tool.mjs +32 -0
  148. package/lib/tool.mjs.map +1 -0
  149. package/lib/tracing.d.mts +1 -0
  150. package/lib/tracing.d.ts +1 -0
  151. package/lib/tracing.js +82 -0
  152. package/lib/tracing.js.map +1 -0
  153. package/lib/tracing.mjs +60 -0
  154. package/lib/tracing.mjs.map +1 -0
  155. package/package.json +233 -0
  156. package/src/beta.ts +85 -0
  157. package/src/client/agent.ts +160 -0
  158. package/src/client/client.ts +237 -0
  159. package/src/client/index.ts +72 -0
  160. package/src/client/types.ts +41 -0
  161. package/src/common.ts +175 -0
  162. package/src/context.ts +37 -0
  163. package/src/embedder.ts +41 -0
  164. package/src/evaluator.ts +50 -0
  165. package/src/extract.ts +30 -0
  166. package/src/formats.ts +30 -0
  167. package/src/genkit-beta.ts +275 -0
  168. package/src/genkit.ts +818 -0
  169. package/src/index.ts +29 -0
  170. package/src/logging.ts +30 -0
  171. package/src/middleware.ts +38 -0
  172. package/src/model.ts +87 -0
  173. package/src/plugin.ts +172 -0
  174. package/src/registry.ts +36 -0
  175. package/src/reranker.ts +42 -0
  176. package/src/retriever.ts +55 -0
  177. package/src/schema.ts +40 -0
  178. package/src/testing.ts +47 -0
  179. package/src/tool.ts +40 -0
  180. package/src/tracing.ts +55 -0
  181. package/tests/agent_client_test.ts +497 -0
  182. package/tests/embed_test.ts +142 -0
  183. package/tests/evaluate_test.ts +52 -0
  184. package/tests/flow_test.ts +94 -0
  185. package/tests/formats_test.ts +168 -0
  186. package/tests/generate_test.ts +1631 -0
  187. package/tests/genkit_test.ts +33 -0
  188. package/tests/helpers.ts +176 -0
  189. package/tests/mock-model_test.ts +490 -0
  190. package/tests/plugins_test.ts +259 -0
  191. package/tests/prompts/badSchemaRef.prompt +9 -0
  192. package/tests/prompts/chat_preamble.prompt +5 -0
  193. package/tests/prompts/kitchensink.prompt +25 -0
  194. package/tests/prompts/output.prompt +11 -0
  195. package/tests/prompts/schemaRef.prompt +9 -0
  196. package/tests/prompts/sub/test.prompt +5 -0
  197. package/tests/prompts/test.prompt +5 -0
  198. package/tests/prompts/test.variant.prompt +8 -0
  199. package/tests/prompts/toolPrompt.prompt +6 -0
  200. package/tests/prompts_test.ts +1442 -0
  201. package/tests/session_test.ts +17 -0
  202. package/typedoc.json +22 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import * as assert from 'assert';
18
+ import { describe, it } from 'node:test';
19
+ import { genkit, getClientHeader } from '../src/index.js';
20
+
21
+ describe('genkit', () => {
22
+ it('sets the client headers', async () => {
23
+ genkit({});
24
+
25
+ assert.ok(getClientHeader().includes('genkit-node/'));
26
+ assert.ok(!getClientHeader().includes('foo'));
27
+
28
+ genkit({ clientHeader: 'foo' });
29
+
30
+ assert.ok(getClientHeader().includes('genkit-node/'));
31
+ assert.ok(getClientHeader().includes('foo'));
32
+ });
33
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import type { MessageData } from '@genkit-ai/ai';
18
+ import type { BaseEvalDataPoint } from '@genkit-ai/ai/evaluator';
19
+ import type { ModelAction, ModelInfo } from '@genkit-ai/ai/model';
20
+ import type { SessionData, SessionStore } from '@genkit-ai/ai/session';
21
+ import type { StreamingCallback } from '@genkit-ai/core';
22
+ import type { Genkit } from '../src/genkit';
23
+ import type {
24
+ GenerateRequest,
25
+ GenerateResponseChunkData,
26
+ GenerateResponseData,
27
+ } from '../src/model';
28
+
29
+ export function defineEchoModel(
30
+ ai: Genkit,
31
+ modelInfo?: ModelInfo
32
+ ): ModelAction {
33
+ const model = ai.defineModel(
34
+ {
35
+ name: 'echoModel',
36
+ ...modelInfo,
37
+ },
38
+ async (request, sendChunk) => {
39
+ (model as any).__test__lastRequest = request;
40
+ (model as any).__test__lastStreamingCallback = sendChunk;
41
+ if (sendChunk) {
42
+ await runAsync(() => {
43
+ sendChunk({
44
+ content: [
45
+ {
46
+ text: '3',
47
+ },
48
+ ],
49
+ });
50
+ });
51
+ await runAsync(() => {
52
+ sendChunk({
53
+ content: [
54
+ {
55
+ text: '2',
56
+ },
57
+ ],
58
+ });
59
+ });
60
+ await runAsync(() => {
61
+ sendChunk({
62
+ content: [
63
+ {
64
+ text: '1',
65
+ },
66
+ ],
67
+ });
68
+ });
69
+ }
70
+ return await runAsync(() => ({
71
+ message: {
72
+ role: 'model',
73
+ content: [
74
+ {
75
+ text:
76
+ 'Echo: ' +
77
+ request.messages
78
+ .map(
79
+ (m) =>
80
+ (m.role === 'user' || m.role === 'model'
81
+ ? ''
82
+ : `${m.role}: `) + m.content.map((c) => c.text).join()
83
+ )
84
+ .join(),
85
+ },
86
+ {
87
+ text: '; config: ' + JSON.stringify(request.config),
88
+ },
89
+ ],
90
+ },
91
+ finishReason: 'stop',
92
+ }));
93
+ }
94
+ );
95
+ return model;
96
+ }
97
+
98
+ export function bonknessEvaluator(ai: Genkit) {
99
+ return ai.defineEvaluator(
100
+ {
101
+ name: 'bonkness',
102
+ displayName: 'Bonkness',
103
+ definition: 'Judges whether a statement is bonk',
104
+ },
105
+ async (datapoint: BaseEvalDataPoint) => {
106
+ return {
107
+ testCaseId: datapoint.testCaseId,
108
+ evaluation: {
109
+ score: 'Much bonk',
110
+ details: {
111
+ reasoning: 'Because I said so!',
112
+ },
113
+ },
114
+ };
115
+ }
116
+ );
117
+ }
118
+
119
+ export async function runAsync<O>(fn: () => O): Promise<O> {
120
+ return new Promise((resolve) => {
121
+ setTimeout(() => resolve(fn()), 0);
122
+ });
123
+ }
124
+
125
+ export class TestMemorySessionStore<S> implements SessionStore<S> {
126
+ private data: Record<string, SessionData<S>> = {};
127
+
128
+ async get(sessionId: string): Promise<SessionData<S> | undefined> {
129
+ return this.data[sessionId];
130
+ }
131
+
132
+ async save(sessionId: string, sessionData: SessionData<S>): Promise<void> {
133
+ this.data[sessionId] = sessionData;
134
+ }
135
+ }
136
+ export function defineStaticResponseModel(
137
+ ai: Genkit,
138
+ message: MessageData
139
+ ): ModelAction {
140
+ return ai.defineModel(
141
+ {
142
+ name: 'staticResponseModel',
143
+ },
144
+ async () => {
145
+ return await runAsync(() => ({
146
+ message,
147
+ }));
148
+ }
149
+ );
150
+ }
151
+
152
+ export type ProgrammableModel = ModelAction & {
153
+ handleResponse: (
154
+ req: GenerateRequest,
155
+ sendChunk?: StreamingCallback<GenerateResponseChunkData>
156
+ ) => Promise<GenerateResponseData>;
157
+
158
+ lastRequest?: GenerateRequest;
159
+ };
160
+
161
+ export function defineProgrammableModel(ai: Genkit): ProgrammableModel {
162
+ const pm = ai.defineModel(
163
+ {
164
+ name: 'programmableModel',
165
+ supports: {
166
+ tools: true,
167
+ },
168
+ },
169
+ async (request, sendChunk) => {
170
+ pm.lastRequest = JSON.parse(JSON.stringify(request));
171
+ return pm.handleResponse(request, sendChunk);
172
+ }
173
+ ) as ProgrammableModel;
174
+
175
+ return pm;
176
+ }
@@ -0,0 +1,490 @@
1
+ /**
2
+ * Copyright 2026 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { echoModel, mockModel, type MockResponse } from '@genkit-ai/ai/testing';
18
+ import { z } from '@genkit-ai/core';
19
+ import { initNodeFeatures } from '@genkit-ai/core/node';
20
+ import * as assert from 'assert';
21
+ import { beforeEach, describe, it } from 'node:test';
22
+ import { genkit as genkitBeta, type GenkitBeta } from '../src/beta';
23
+ import { genkit, type Genkit } from '../src/genkit';
24
+
25
+ initNodeFeatures();
26
+
27
+ describe('mockModel', () => {
28
+ let ai: Genkit;
29
+
30
+ beforeEach(() => {
31
+ ai = genkit({});
32
+ });
33
+
34
+ it('returns the scripted text and records the request', async () => {
35
+ const model = mockModel(ai, { respond: () => ({ text: 'a summary' }) });
36
+
37
+ const summarize = ai.defineFlow(
38
+ 'summarize',
39
+ async (doc: string) =>
40
+ (await ai.generate({ model, prompt: `Summarize: ${doc}` })).text
41
+ );
42
+
43
+ assert.strictEqual(await summarize('long text'), 'a summary');
44
+ assert.strictEqual(model.requestCount, 1);
45
+ assert.match(model.lastRequestMessage!.text, /Summarize: long text/);
46
+ });
47
+
48
+ it('accepts a bare string as shorthand for text', async () => {
49
+ const model = mockModel(ai, { respond: () => 'hi there' });
50
+ const res = await ai.generate({ model, prompt: 'x' });
51
+ assert.strictEqual(res.text, 'hi there');
52
+ });
53
+
54
+ it('streams chunks via sendChunk', async () => {
55
+ const model = mockModel(ai, {
56
+ respond: (_req, { sendChunk }) => {
57
+ sendChunk('hel');
58
+ sendChunk('lo');
59
+ return { text: 'hello' };
60
+ },
61
+ });
62
+
63
+ const { response, stream } = ai.generateStream({ model, prompt: 'hi' });
64
+ const chunks: string[] = [];
65
+ for await (const chunk of stream) {
66
+ chunks.push(chunk.text);
67
+ }
68
+
69
+ assert.deepStrictEqual(chunks, ['hel', 'lo']);
70
+ assert.strictEqual((await response).text, 'hello');
71
+ });
72
+
73
+ it('emits tool requests that the framework dispatches', async () => {
74
+ const lookup = ai.defineTool(
75
+ {
76
+ name: 'lookup',
77
+ description: 'look something up',
78
+ inputSchema: z.object({ id: z.number() }),
79
+ outputSchema: z.string(),
80
+ },
81
+ async ({ id }) => `item-${id}`
82
+ );
83
+
84
+ const model = mockModel(ai, {
85
+ info: { supports: { tools: true } },
86
+ respond: (req) => {
87
+ const toolResponded = req.messages.some((m) =>
88
+ m.content.some((c) => c.toolResponse)
89
+ );
90
+ return toolResponded
91
+ ? { text: 'done' }
92
+ : { toolRequests: [{ name: 'lookup', input: { id: 1 } }] };
93
+ },
94
+ });
95
+
96
+ const res = await ai.generate({ model, prompt: 'go', tools: [lookup] });
97
+
98
+ assert.strictEqual(res.text, 'done');
99
+ assert.strictEqual(model.requestCount, 2);
100
+ });
101
+
102
+ it('records every request, oldest first', async () => {
103
+ const model = mockModel(ai, { respond: () => ({ text: 'ok' }) });
104
+ await ai.generate({ model, prompt: 'first' });
105
+ await ai.generate({ model, prompt: 'second' });
106
+
107
+ assert.strictEqual(model.requests.length, 2);
108
+ assert.match(model.requests[0].messages.at(-1)!.content[0].text!, /first/);
109
+ assert.match(model.requests[1].messages.at(-1)!.content[0].text!, /second/);
110
+ });
111
+
112
+ it('snapshots the request so later runs do not mutate history', async () => {
113
+ const model = mockModel(ai, { respond: () => ({ text: 'ok' }) });
114
+ await ai.generate({ model, prompt: 'first' });
115
+ const captured = model.lastRequest;
116
+ await ai.generate({ model, prompt: 'second' });
117
+
118
+ assert.match(captured!.messages.at(-1)!.content[0].text!, /first/);
119
+ });
120
+
121
+ it('separates adjacent messages so their text does not fuse', async () => {
122
+ const model = mockModel(ai, { respond: () => ({ text: 'ok' }) });
123
+ await ai.generate({ model, system: 'alpha', prompt: 'beta' });
124
+
125
+ // The last word of one message must not run into the first of the next,
126
+ // or boundary-spanning assertions silently break.
127
+ assert.doesNotMatch(model.lastRequestText!, /alphabeta/);
128
+ assert.match(model.lastRequestText!, /alpha\nbeta/);
129
+ });
130
+
131
+ it('returns a defensive copy from lastRequest and requests', async () => {
132
+ const model = mockModel(ai, { respond: () => ({ text: 'ok' }) });
133
+ await ai.generate({ model, prompt: 'hello' });
134
+
135
+ // Mutating the view must not corrupt recorded history.
136
+ model.lastRequest!.messages.length = 0;
137
+ model.requests[0].messages.length = 0;
138
+
139
+ assert.ok(model.lastRequest!.messages.length > 0);
140
+ assert.ok(model.requests[0].messages.length > 0);
141
+ });
142
+
143
+ it('defaults finishReason to stop when a full response leaves it undefined', async () => {
144
+ const model = mockModel(ai, {
145
+ respond: () => ({
146
+ message: { role: 'model', content: [{ text: 'x' }] },
147
+ finishReason: undefined,
148
+ }),
149
+ });
150
+
151
+ const res = await ai.generate({ model, prompt: 'hi' });
152
+ assert.strictEqual(res.finishReason, 'stop');
153
+ });
154
+
155
+ it('consumes an array of responses one per call, repeating the last', async () => {
156
+ const model = mockModel(ai, { respond: ['first', 'second'] });
157
+
158
+ assert.strictEqual(
159
+ (await ai.generate({ model, prompt: 'a' })).text,
160
+ 'first'
161
+ );
162
+ assert.strictEqual(
163
+ (await ai.generate({ model, prompt: 'b' })).text,
164
+ 'second'
165
+ );
166
+ // Past the end, the last response repeats rather than throwing.
167
+ assert.strictEqual(
168
+ (await ai.generate({ model, prompt: 'c' })).text,
169
+ 'second'
170
+ );
171
+ assert.strictEqual(model.requestCount, 3);
172
+ });
173
+
174
+ it('throws a queued Error to inject a failure on a given turn', async () => {
175
+ const model = mockModel(ai, {
176
+ respond: ['ok', new Error('rate limited')],
177
+ });
178
+
179
+ assert.strictEqual((await ai.generate({ model, prompt: 'a' })).text, 'ok');
180
+ await assert.rejects(ai.generate({ model, prompt: 'b' }), /rate limited/);
181
+ });
182
+
183
+ it('treats an empty-string response as empty text, not a missing message', async () => {
184
+ const model = mockModel(ai, { respond: '' });
185
+ const res = await ai.generate({ model, prompt: 'x' });
186
+ assert.strictEqual(res.text, '');
187
+ assert.deepStrictEqual(res.message?.content, [{ text: '' }]);
188
+ });
189
+
190
+ it('accepts a single static response, returned on every call', async () => {
191
+ const model = mockModel(ai, { respond: 'always this' });
192
+
193
+ assert.strictEqual(
194
+ (await ai.generate({ model, prompt: 'a' })).text,
195
+ 'always this'
196
+ );
197
+ assert.strictEqual(
198
+ (await ai.generate({ model, prompt: 'b' })).text,
199
+ 'always this'
200
+ );
201
+ });
202
+
203
+ it('swaps behavior for subsequent calls via respondWith', async () => {
204
+ const model = mockModel(ai, { respond: 'initial' });
205
+
206
+ assert.strictEqual(
207
+ (await ai.generate({ model, prompt: 'a' })).text,
208
+ 'initial'
209
+ );
210
+
211
+ model.respondWith(['first', 'second']);
212
+ assert.strictEqual(
213
+ (await ai.generate({ model, prompt: 'b' })).text,
214
+ 'first'
215
+ );
216
+ assert.strictEqual(
217
+ (await ai.generate({ model, prompt: 'c' })).text,
218
+ 'second'
219
+ );
220
+ // respondWith leaves recorded history untouched.
221
+ assert.strictEqual(model.requestCount, 3);
222
+ });
223
+
224
+ it('clears history and re-arms the construction respond via reset', async () => {
225
+ const model = mockModel(ai, { respond: ['first', 'second'] });
226
+
227
+ await ai.generate({ model, prompt: 'a' });
228
+ await ai.generate({ model, prompt: 'b' });
229
+ model.respondWith('overridden');
230
+ assert.strictEqual(
231
+ (await ai.generate({ model, prompt: 'c' })).text,
232
+ 'overridden'
233
+ );
234
+
235
+ model.reset();
236
+
237
+ assert.strictEqual(model.requestCount, 0);
238
+ assert.deepStrictEqual(model.requests, []);
239
+ assert.strictEqual(model.lastRequest, undefined);
240
+ // Back to the construction-time queue, re-armed from its first item.
241
+ assert.strictEqual(
242
+ (await ai.generate({ model, prompt: 'd' })).text,
243
+ 'first'
244
+ );
245
+ });
246
+
247
+ it('exposes tool results fed back to the model via toolResponses', async () => {
248
+ const lookup = ai.defineTool(
249
+ {
250
+ name: 'lookup',
251
+ description: 'look something up',
252
+ inputSchema: z.object({ id: z.number() }),
253
+ outputSchema: z.string(),
254
+ },
255
+ async ({ id }) => `item-${id}`
256
+ );
257
+
258
+ const model = mockModel(ai, {
259
+ info: { supports: { tools: true } },
260
+ // A queued tool loop: request the tool, then answer.
261
+ respond: [
262
+ { toolRequests: [{ name: 'lookup', input: { id: 1 } }] },
263
+ 'done',
264
+ ],
265
+ });
266
+
267
+ await ai.generate({ model, prompt: 'go', tools: [lookup] });
268
+
269
+ assert.deepStrictEqual(
270
+ model.toolResponses.map((t) => t.name),
271
+ ['lookup']
272
+ );
273
+ assert.strictEqual(model.toolResponses[0].output, 'item-1');
274
+ });
275
+
276
+ it('treats a falsy respond result as an empty response', async () => {
277
+ // A callback that streams but forgets to return (void) must not crash.
278
+ const model = mockModel(ai, {
279
+ respond: () => undefined as unknown as MockResponse,
280
+ });
281
+ const res = await ai.generate({ model, prompt: 'x' });
282
+ assert.strictEqual(res.text, '');
283
+ });
284
+
285
+ it('records a request even when it carries a non-serializable value', async () => {
286
+ const model = mockModel(ai, { respond: () => 'ok' });
287
+ // A function in config can't be structuredClone'd; recording must not throw.
288
+ await ai.generate({
289
+ model,
290
+ prompt: 'hi',
291
+ config: { onEvent: () => 1 } as unknown as Record<string, unknown>,
292
+ });
293
+ assert.strictEqual(model.requestCount, 1);
294
+ assert.match(model.lastRequestText!, /hi/);
295
+ });
296
+
297
+ it('flattens the whole assembled request via lastRequestText', async () => {
298
+ // Works even with an output schema, where echoModel can't be used: the mock
299
+ // returns conforming JSON, and assembly is asserted by inspection.
300
+ const model = mockModel(ai, {
301
+ respond: () => ({ text: JSON.stringify({ x: 'ok' }) }),
302
+ });
303
+
304
+ await ai.generate({
305
+ model,
306
+ system: 'Be terse',
307
+ prompt: 'hello',
308
+ output: { schema: z.object({ x: z.string() }) },
309
+ });
310
+
311
+ assert.match(model.lastRequestText!, /system: Be terse/);
312
+ assert.match(model.lastRequestText!, /hello/);
313
+ });
314
+
315
+ it('exposes the output schema to respond by default (native constrained)', async () => {
316
+ let seenSchema: unknown;
317
+ const model = mockModel(ai, {
318
+ respond: (req) => {
319
+ seenSchema = req.output?.schema;
320
+ return { text: JSON.stringify({ x: 'ok' }) };
321
+ },
322
+ });
323
+
324
+ await ai.generate({
325
+ model,
326
+ prompt: 'hello',
327
+ output: { schema: z.object({ x: z.string() }) },
328
+ });
329
+
330
+ // Native constrained: the schema is handed to the model on the request...
331
+ assert.ok(seenSchema, 'respond should see request.output.schema');
332
+ // ...not injected into the prompt.
333
+ assert.doesNotMatch(
334
+ model.lastRequestText!,
335
+ /conform to the following schema/i
336
+ );
337
+ });
338
+
339
+ it('simulates constrained output when supports.constrained is none', async () => {
340
+ let seenSchema: unknown = 'unset';
341
+ const model = mockModel(ai, {
342
+ info: { supports: { constrained: 'none' } },
343
+ respond: (req) => {
344
+ seenSchema = req.output?.schema;
345
+ return { text: JSON.stringify({ x: 'ok' }) };
346
+ },
347
+ });
348
+
349
+ await ai.generate({
350
+ model,
351
+ prompt: 'hello',
352
+ output: { schema: z.object({ x: z.string() }) },
353
+ });
354
+
355
+ // Simulated path strips the schema from what the model sees...
356
+ assert.strictEqual(seenSchema, undefined);
357
+ // ...and injects it into the prompt instead.
358
+ assert.match(model.lastRequestText!, /conform to the following schema/i);
359
+ });
360
+ });
361
+
362
+ describe('echoModel', () => {
363
+ let ai: Genkit;
364
+
365
+ beforeEach(() => {
366
+ ai = genkit({});
367
+ });
368
+
369
+ it('echoes the rendered request, for prompt-assembly assertions', async () => {
370
+ const model = echoModel(ai);
371
+ const res = await ai.generate({
372
+ model,
373
+ system: 'Be terse',
374
+ prompt: 'hello',
375
+ });
376
+
377
+ assert.match(res.text, /system: Be terse/);
378
+ assert.match(res.text, /hello/);
379
+ });
380
+
381
+ it('throws a clear error when the request carries an output schema', async () => {
382
+ const model = echoModel(ai);
383
+
384
+ await assert.rejects(
385
+ ai.generate({
386
+ model,
387
+ prompt: 'hi',
388
+ output: { schema: z.object({ x: z.string() }) },
389
+ }),
390
+ /can't satisfy an output schema/
391
+ );
392
+ });
393
+ });
394
+
395
+ // These cover the two features most likely to need dedicated helpers —
396
+ // interrupts (human-in-the-loop) and agent handoff — and show that `mockModel`
397
+ // already tests them with no extra machinery: an interrupt is just a tool
398
+ // request the framework turns into a pause, and a handoff is just a
399
+ // prompt-as-tool request. Both live on the beta surface (`chat`, resume).
400
+ describe('mockModel — interrupts and agent handoff', () => {
401
+ let ai: GenkitBeta;
402
+
403
+ beforeEach(() => {
404
+ ai = genkitBeta({});
405
+ });
406
+
407
+ it('drives an interrupt round-trip, then resumes to completion', async () => {
408
+ // A human-in-the-loop tool: it interrupts on first run, and completes once
409
+ // the generation is resumed with an answer.
410
+ const confirmAction = ai.defineTool(
411
+ { name: 'confirmAction', description: 'needs human confirmation' },
412
+ async (_input, { interrupt, resumed }) => {
413
+ if (resumed) return 'approved';
414
+ return interrupt({ ask: 'Proceed?' });
415
+ }
416
+ );
417
+
418
+ let turn = 0;
419
+ const model = mockModel(ai, {
420
+ info: { supports: { tools: true } },
421
+ respond: () =>
422
+ turn++ === 0
423
+ ? { toolRequests: [{ name: 'confirmAction', input: {} }] }
424
+ : { text: 'all done' },
425
+ });
426
+
427
+ // Turn 1: the model calls the tool, the tool interrupts, generation pauses.
428
+ const paused = await ai.generate({
429
+ model,
430
+ prompt: 'do the thing',
431
+ tools: [confirmAction],
432
+ });
433
+ assert.strictEqual(paused.interrupts.length, 1);
434
+ assert.strictEqual(paused.interrupts[0].toolRequest.name, 'confirmAction');
435
+
436
+ // Turn 2: a human approves; resuming re-runs the tool and the model finishes.
437
+ const finished = await ai.generate({
438
+ model,
439
+ messages: paused.messages,
440
+ tools: [confirmAction],
441
+ resume: { respond: confirmAction.respond(paused.interrupts[0], 'yes') },
442
+ });
443
+
444
+ assert.strictEqual(finished.text, 'all done');
445
+ assert.strictEqual(model.requestCount, 2);
446
+ });
447
+
448
+ it('drives a defineAgent tool loop with mockModel, exposing the preamble', async () => {
449
+ // Migrated from an `app.chat(promptAgent)` handoff test: the beta Chat API
450
+ // (and its cross-agent preamble swap) was removed in #5248. This keeps the
451
+ // same mockModel surface under test — a multi-turn tool loop, with the
452
+ // agent's rendered system preamble asserted via `lastRequestText` — using
453
+ // the current `defineAgent(...).chat().send(...)` API.
454
+ const app = genkitBeta({ model: 'agentModel' });
455
+
456
+ app.defineTool(
457
+ { name: 'lookup', description: "Look up today's special." },
458
+ async () => 'Mushroom risotto'
459
+ );
460
+
461
+ const model = mockModel(app, {
462
+ name: 'agentModel',
463
+ info: { supports: { tools: true } },
464
+ respond: (req) => {
465
+ // The model's two-turn view of one `send`: call the tool, then once its
466
+ // result is in the conversation, finish.
467
+ const sawToolResult = req.messages.some((m) =>
468
+ m.content.some((c) => c.toolResponse?.output === 'Mushroom risotto')
469
+ );
470
+ return sawToolResult
471
+ ? { text: 'Booked: Mushroom risotto' }
472
+ : { toolRequests: [{ name: 'lookup', input: {} }] };
473
+ },
474
+ });
475
+
476
+ const agent = app.defineAgent({
477
+ name: 'concierge',
478
+ model: 'agentModel',
479
+ system: 'You are the booking concierge.',
480
+ tools: ['lookup'],
481
+ });
482
+
483
+ const { text } = await agent.chat().send('book me the special');
484
+
485
+ assert.strictEqual(text, 'Booked: Mushroom risotto');
486
+ assert.strictEqual(model.requestCount, 2);
487
+ // The agent's system preamble is what the model saw.
488
+ assert.match(model.lastRequestText!, /You are the booking concierge/);
489
+ });
490
+ });