@intx/inference-discovery-google-genai 0.1.2 → 0.2.2

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.
@@ -0,0 +1,297 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolveMediaPath, } from "@intx/inference-discovery/catalog";
3
+ const TEXT_MODEL = "gemini-2.5-flash";
4
+ const IMAGE_MODEL = "gemini-2.5-flash-image";
5
+ const TEXT_MODEL_CAPABILITIES = new Set([
6
+ "plain-text",
7
+ "plain-text-streaming",
8
+ "function-calling-multi-turn",
9
+ "function-calling-multi-turn-streaming",
10
+ "function-calling-with-thinking",
11
+ "function-calling-with-thinking-streaming",
12
+ "vision-input",
13
+ "vision-input-streaming",
14
+ "audio-input",
15
+ "audio-input-streaming",
16
+ "video-input",
17
+ "video-input-streaming",
18
+ "document-input",
19
+ "document-input-streaming",
20
+ "code-execution",
21
+ "code-execution-streaming",
22
+ "grounding",
23
+ "grounding-streaming",
24
+ "files-api-reference",
25
+ "files-api-reference-streaming",
26
+ "safety-classification",
27
+ "safety-classification-streaming",
28
+ "structured-output",
29
+ "structured-output-streaming",
30
+ ]);
31
+ const IMAGE_MODEL_CAPABILITIES = new Set([
32
+ "image-output",
33
+ "image-output-streaming",
34
+ ]);
35
+ const EXTENSION_TO_MIME_TYPE = {
36
+ jpg: "image/jpeg",
37
+ jpeg: "image/jpeg",
38
+ png: "image/png",
39
+ gif: "image/gif",
40
+ webp: "image/webp",
41
+ wav: "audio/wav",
42
+ mp3: "audio/mpeg",
43
+ ogg: "audio/ogg",
44
+ flac: "audio/flac",
45
+ mp4: "video/mp4",
46
+ mov: "video/quicktime",
47
+ webm: "video/webm",
48
+ pdf: "application/pdf",
49
+ };
50
+ function modelSupportsCapability(model, capability) {
51
+ if (model === TEXT_MODEL) {
52
+ if (!TEXT_MODEL_CAPABILITIES.has(capability)) {
53
+ throw new Error(`google-genai: model ${model} does not support capability ${capability}`);
54
+ }
55
+ return;
56
+ }
57
+ if (model === IMAGE_MODEL) {
58
+ if (!IMAGE_MODEL_CAPABILITIES.has(capability)) {
59
+ throw new Error(`google-genai: model ${model} does not support capability ${capability}`);
60
+ }
61
+ return;
62
+ }
63
+ throw new Error(`google-genai: unknown model ${model}`);
64
+ }
65
+ function extensionFor(path) {
66
+ const dot = path.lastIndexOf(".");
67
+ if (dot < 0 || dot === path.length - 1) {
68
+ throw new Error(`google-genai: cannot infer media MIME type, no extension in path: ${path}`);
69
+ }
70
+ return path.slice(dot + 1).toLowerCase();
71
+ }
72
+ function mimeTypeFor(ref) {
73
+ const ext = extensionFor(ref.path);
74
+ const mime = EXTENSION_TO_MIME_TYPE[ext];
75
+ if (mime === undefined) {
76
+ throw new Error(`google-genai: no MIME type mapping for extension .${ext} (path ${ref.path})`);
77
+ }
78
+ return mime;
79
+ }
80
+ function readMediaBase64(ref) {
81
+ const absolute = resolveMediaPath(ref);
82
+ const bytes = readFileSync(absolute);
83
+ return bytes.toString("base64");
84
+ }
85
+ function expectSingleMedia(intent) {
86
+ if (intent.media === undefined || intent.media.length === 0) {
87
+ throw new Error("google-genai: media-input capability requires intent.media to be non-empty");
88
+ }
89
+ if (intent.media.length !== 1) {
90
+ throw new Error(`google-genai: media-input capability expects exactly one media reference, got ${String(intent.media.length)}`);
91
+ }
92
+ const [media] = intent.media;
93
+ if (media === undefined) {
94
+ throw new Error("google-genai: media-input capability: media[0] is unexpectedly undefined");
95
+ }
96
+ return media;
97
+ }
98
+ function expectSingleTool(intent) {
99
+ if (intent.tools === undefined || intent.tools.length === 0) {
100
+ throw new Error("google-genai: function-calling capability requires intent.tools to be non-empty");
101
+ }
102
+ if (intent.tools.length !== 1) {
103
+ throw new Error(`google-genai: function-calling capability expects exactly one tool declaration, got ${String(intent.tools.length)}`);
104
+ }
105
+ const [tool] = intent.tools;
106
+ if (tool === undefined) {
107
+ throw new Error("google-genai: function-calling capability: tools[0] is unexpectedly undefined");
108
+ }
109
+ return tool;
110
+ }
111
+ function userTextContent(prompt) {
112
+ return {
113
+ role: "user",
114
+ parts: [{ text: prompt }],
115
+ };
116
+ }
117
+ function plainTextBody(intent) {
118
+ return {
119
+ contents: [userTextContent(intent.prompt)],
120
+ };
121
+ }
122
+ function plainTextStreamingBody(intent) {
123
+ return {
124
+ contents: [userTextContent(intent.prompt)],
125
+ generationConfig: {
126
+ maxOutputTokens: 400,
127
+ thinkingConfig: {
128
+ thinkingBudget: 0,
129
+ },
130
+ },
131
+ };
132
+ }
133
+ function functionToolFromDecl(decl) {
134
+ return {
135
+ functionDeclarations: [
136
+ {
137
+ name: decl.name,
138
+ description: decl.description,
139
+ parameters: decl.parameters,
140
+ },
141
+ ],
142
+ };
143
+ }
144
+ function functionCallingBody(intent, thinking) {
145
+ const decl = expectSingleTool(intent);
146
+ const thinkingConfig = thinking.includeThoughts
147
+ ? { thinkingBudget: thinking.budget, includeThoughts: true }
148
+ : { thinkingBudget: thinking.budget };
149
+ return {
150
+ contents: [userTextContent(intent.prompt)],
151
+ tools: [functionToolFromDecl(decl)],
152
+ toolConfig: {
153
+ functionCallingConfig: {
154
+ mode: "ANY",
155
+ allowedFunctionNames: [decl.name],
156
+ },
157
+ },
158
+ generationConfig: {
159
+ thinkingConfig,
160
+ },
161
+ };
162
+ }
163
+ function inlineMediaBody(intent) {
164
+ const media = expectSingleMedia(intent);
165
+ return {
166
+ contents: [
167
+ {
168
+ role: "user",
169
+ parts: [
170
+ { text: intent.prompt },
171
+ {
172
+ inlineData: {
173
+ mimeType: mimeTypeFor(media),
174
+ data: readMediaBase64(media),
175
+ },
176
+ },
177
+ ],
178
+ },
179
+ ],
180
+ };
181
+ }
182
+ function imageOutputBody(intent) {
183
+ return {
184
+ contents: [userTextContent(intent.prompt)],
185
+ generationConfig: {
186
+ responseModalities: ["TEXT", "IMAGE"],
187
+ },
188
+ };
189
+ }
190
+ function codeExecutionBody(intent) {
191
+ return {
192
+ contents: [userTextContent(intent.prompt)],
193
+ tools: [{ codeExecution: {} }],
194
+ };
195
+ }
196
+ function groundingBody(intent) {
197
+ return {
198
+ contents: [userTextContent(intent.prompt)],
199
+ tools: [{ googleSearch: {} }],
200
+ };
201
+ }
202
+ // Shape-identical between streaming and non-streaming variants — only the
203
+ // endpoint differs (handled by buildEndpointURL). The streaming variant
204
+ // deliberately does NOT clamp `thinkingBudget: 0` the way plain-text
205
+ // streaming does: the safety classifier's engagement may depend on
206
+ // whether the model goes through a thinking phase, and the probe's job
207
+ // is to observe natural classifier behavior at default generation
208
+ // settings, not constrained ones.
209
+ function safetyClassificationBody(intent) {
210
+ return {
211
+ contents: [userTextContent(intent.prompt)],
212
+ };
213
+ }
214
+ function structuredOutputBody(intent) {
215
+ const format = intent.responseFormat;
216
+ if (format === undefined) {
217
+ throw new Error("google-genai: structured-output intent has no responseFormat");
218
+ }
219
+ const generationConfig = {};
220
+ switch (format.kind) {
221
+ case "text":
222
+ // Free-form text is Gemini's default; emit no responseMimeType.
223
+ break;
224
+ case "json":
225
+ generationConfig.responseMimeType = "application/json";
226
+ break;
227
+ case "json-schema":
228
+ generationConfig.responseMimeType = "application/json";
229
+ generationConfig.responseSchema = format.schema;
230
+ break;
231
+ }
232
+ const body = {
233
+ contents: [userTextContent(intent.prompt)],
234
+ };
235
+ if (Object.keys(generationConfig).length > 0) {
236
+ body.generationConfig = generationConfig;
237
+ }
238
+ return body;
239
+ }
240
+ export function buildRequestBody(opts) {
241
+ modelSupportsCapability(opts.model, opts.capability);
242
+ switch (opts.capability) {
243
+ case "plain-text":
244
+ return plainTextBody(opts.intent);
245
+ case "plain-text-streaming":
246
+ return plainTextStreamingBody(opts.intent);
247
+ case "function-calling-multi-turn":
248
+ case "function-calling-multi-turn-streaming":
249
+ return functionCallingBody(opts.intent, {
250
+ budget: 0,
251
+ includeThoughts: false,
252
+ });
253
+ case "function-calling-with-thinking":
254
+ case "function-calling-with-thinking-streaming":
255
+ return functionCallingBody(opts.intent, {
256
+ budget: 1024,
257
+ includeThoughts: true,
258
+ });
259
+ case "vision-input":
260
+ case "vision-input-streaming":
261
+ case "audio-input":
262
+ case "audio-input-streaming":
263
+ case "video-input":
264
+ case "video-input-streaming":
265
+ case "document-input":
266
+ case "document-input-streaming":
267
+ return inlineMediaBody(opts.intent);
268
+ case "image-output":
269
+ case "image-output-streaming":
270
+ return imageOutputBody(opts.intent);
271
+ case "code-execution":
272
+ case "code-execution-streaming":
273
+ return codeExecutionBody(opts.intent);
274
+ case "grounding":
275
+ case "grounding-streaming":
276
+ return groundingBody(opts.intent);
277
+ case "safety-classification":
278
+ case "safety-classification-streaming":
279
+ return safetyClassificationBody(opts.intent);
280
+ case "structured-output":
281
+ case "structured-output-streaming":
282
+ return structuredOutputBody(opts.intent);
283
+ case "files-api-reference":
284
+ case "files-api-reference-streaming":
285
+ throw new Error(`google-genai: capability ${opts.capability} is multi-step; use iterateCaptureSteps from the plug-in, not buildRequestBody.`);
286
+ case "function-calling":
287
+ case "reasoning-content":
288
+ case "reasoning-content-streaming":
289
+ case "redacted-thinking":
290
+ case "redacted-thinking-streaming":
291
+ throw new Error(`google-genai: capability ${opts.capability} is not supported by any google-genai model`);
292
+ default: {
293
+ const exhaustive = opts.capability;
294
+ throw new Error(`google-genai: unhandled capability ${String(exhaustive)}`);
295
+ }
296
+ }
297
+ }
package/package.json CHANGED
@@ -1,16 +1,26 @@
1
1
  {
2
2
  "name": "@intx/inference-discovery-google-genai",
3
- "version": "0.1.2",
3
+ "version": "0.2.2",
4
4
  "license": "LGPL-2.1-only",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
8
+ "intx-src": "./src/index.ts",
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
10
11
  }
11
12
  },
12
13
  "dependencies": {
13
- "@intx/inference-discovery": "0.0.0",
14
+ "@intx/inference-discovery": "0.2.2",
14
15
  "arktype": "^2.1.29"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "sideEffects": false,
23
+ "publishConfig": {
24
+ "access": "public"
15
25
  }
16
26
  }
package/src/auth.ts DELETED
@@ -1,8 +0,0 @@
1
- export const AUTH_HEADER = "x-goog-api-key";
2
-
3
- export function buildAuthHeaders(apiKey: string): Record<string, string> {
4
- if (apiKey.length === 0) {
5
- throw new Error("google-genai: apiKey must be a non-empty string");
6
- }
7
- return { [AUTH_HEADER]: apiKey };
8
- }
package/src/endpoint.ts DELETED
@@ -1,20 +0,0 @@
1
- import type { Capability } from "@intx/inference-discovery/catalog";
2
-
3
- export const GEMINI_BASE = "https://generativelanguage.googleapis.com/v1beta";
4
-
5
- export function isStreamingCapability(capability: Capability): boolean {
6
- return capability.endsWith("-streaming");
7
- }
8
-
9
- export function buildEndpointURL(opts: {
10
- model: string;
11
- capability: Capability;
12
- }): string {
13
- if (opts.model.length === 0) {
14
- throw new Error("google-genai: model must be a non-empty string");
15
- }
16
- if (isStreamingCapability(opts.capability)) {
17
- return `${GEMINI_BASE}/models/${opts.model}:streamGenerateContent?alt=sse`;
18
- }
19
- return `${GEMINI_BASE}/models/${opts.model}:generateContent`;
20
- }
package/src/index.ts DELETED
@@ -1,327 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import {
3
- resolveMediaPath,
4
- type Capability,
5
- type CapabilityIntent,
6
- type MediaRef,
7
- } from "@intx/inference-discovery/catalog";
8
- import type {
9
- CaptureStep,
10
- CapturedResponse,
11
- IterateCaptureStepsOpts,
12
- ProviderPlugin,
13
- } from "@intx/inference-discovery";
14
- import { buildAuthHeaders } from "./auth";
15
- import { buildEndpointURL } from "./endpoint";
16
- import { buildRequestBody } from "./request-body";
17
-
18
- const PROVIDER_NAME = "google-genai";
19
- const MODELS = ["gemini-2.5-flash", "gemini-2.5-flash-image"] as const;
20
- const REDACT_REQUEST_HEADERS = ["x-goog-api-key"] as const;
21
- const REDACT_RESPONSE_HEADERS: readonly string[] = [];
22
-
23
- const FILES_API_UPLOAD_URL =
24
- "https://generativelanguage.googleapis.com/upload/v1beta/files";
25
-
26
- const MULTI_TURN_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
27
- "function-calling-multi-turn",
28
- "function-calling-multi-turn-streaming",
29
- "function-calling-with-thinking",
30
- "function-calling-with-thinking-streaming",
31
- ]);
32
-
33
- const FILES_API_CAPABILITIES: ReadonlySet<Capability> = new Set<Capability>([
34
- "files-api-reference",
35
- "files-api-reference-streaming",
36
- ]);
37
-
38
- export interface GoogleGenaiPluginOptions {
39
- apiKey: string;
40
- }
41
-
42
- function isRecord(value: unknown): value is Record<string, unknown> {
43
- return typeof value === "object" && value !== null && !Array.isArray(value);
44
- }
45
-
46
- interface UploadStepDescriptor {
47
- url: string;
48
- mimeType: string;
49
- displayName: string;
50
- bytes: Uint8Array;
51
- }
52
-
53
- function buildUploadDescriptor(intent: CapabilityIntent): UploadStepDescriptor {
54
- const media = intent.media?.[0];
55
- if (media === undefined) {
56
- throw new Error(
57
- "google-genai files-API: intent.media[0] is required; the catalog's " +
58
- "files-api-reference intent must declare the document to upload.",
59
- );
60
- }
61
- const bytes = readFileSync(resolveMediaPath(media));
62
- return {
63
- url: FILES_API_UPLOAD_URL,
64
- mimeType: mimeTypeForMedia(media),
65
- displayName: basename(media.path),
66
- bytes: new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength),
67
- };
68
- }
69
-
70
- function basename(path: string): string {
71
- const slash = path.lastIndexOf("/");
72
- return slash < 0 ? path : path.slice(slash + 1);
73
- }
74
-
75
- function mimeTypeForMedia(ref: MediaRef): string {
76
- if (ref.kind === "document") return "application/pdf";
77
- if (ref.kind === "image") return "image/jpeg";
78
- if (ref.kind === "audio") return "audio/wav";
79
- if (ref.kind === "video") return "video/mp4";
80
- throw new Error(`google-genai: unsupported media kind ${String(ref.kind)}`);
81
- }
82
-
83
- function extractFileUri(parsed: unknown): string {
84
- if (!isRecord(parsed)) {
85
- throw new Error(
86
- "google-genai files-API: upload response is not a JSON object",
87
- );
88
- }
89
- const file = parsed.file;
90
- if (!isRecord(file)) {
91
- throw new Error(
92
- "google-genai files-API: upload response missing 'file' object",
93
- );
94
- }
95
- const uri = file.uri;
96
- if (typeof uri !== "string" || uri.length === 0) {
97
- throw new Error(
98
- "google-genai files-API: upload response has no string 'file.uri'",
99
- );
100
- }
101
- return uri;
102
- }
103
-
104
- function extractMimeTypeFromUpload(parsed: unknown): string {
105
- if (!isRecord(parsed)) {
106
- throw new Error(
107
- "google-genai files-API: upload response is not a JSON object",
108
- );
109
- }
110
- const file = parsed.file;
111
- if (!isRecord(file)) {
112
- throw new Error(
113
- "google-genai files-API: upload response missing 'file' object",
114
- );
115
- }
116
- const mime = file.mimeType;
117
- if (typeof mime !== "string" || mime.length === 0) {
118
- throw new Error(
119
- "google-genai files-API: upload response has no string 'file.mimeType'",
120
- );
121
- }
122
- return mime;
123
- }
124
-
125
- function buildFilesApiGenerateBody(opts: {
126
- intent: CapabilityIntent;
127
- fileUri: string;
128
- mimeType: string;
129
- }): unknown {
130
- return {
131
- contents: [
132
- {
133
- role: "user",
134
- parts: [
135
- { text: opts.intent.prompt },
136
- {
137
- fileData: {
138
- mimeType: opts.mimeType,
139
- fileUri: opts.fileUri,
140
- },
141
- },
142
- ],
143
- },
144
- ],
145
- };
146
- }
147
-
148
- function extractAssistantContent(parsed: unknown): unknown {
149
- if (!isRecord(parsed)) {
150
- throw new Error(
151
- "google-genai multi-turn: turn-1 response is not a JSON object",
152
- );
153
- }
154
- const candidates = parsed.candidates;
155
- if (!Array.isArray(candidates) || candidates.length === 0) {
156
- throw new Error(
157
- "google-genai multi-turn: turn-1 response has no candidates array",
158
- );
159
- }
160
- const first = candidates[0];
161
- if (!isRecord(first)) {
162
- throw new Error(
163
- "google-genai multi-turn: turn-1 response candidates[0] is not an object",
164
- );
165
- }
166
- const content = first.content;
167
- if (!isRecord(content)) {
168
- throw new Error(
169
- "google-genai multi-turn: turn-1 response candidates[0].content is not an object",
170
- );
171
- }
172
- return content;
173
- }
174
-
175
- function deriveToolFollowUp(intent: CapabilityIntent): {
176
- toolName: string;
177
- content: string;
178
- } {
179
- const followUp = intent.followUp;
180
- if (followUp !== undefined) {
181
- for (const step of followUp) {
182
- if (step.role === "tool") {
183
- return { toolName: step.toolName, content: step.content };
184
- }
185
- }
186
- }
187
- const tools = intent.tools;
188
- if (tools === undefined || tools.length === 0) {
189
- throw new Error(
190
- "google-genai multi-turn: intent has neither followUp nor tools",
191
- );
192
- }
193
- const [tool] = tools;
194
- if (tool === undefined) {
195
- throw new Error("google-genai multi-turn: intent.tools[0] is undefined");
196
- }
197
- return { toolName: tool.name, content: "{}" };
198
- }
199
-
200
- function buildMultiTurnTurn2Body(opts: {
201
- capability: Capability;
202
- intent: CapabilityIntent;
203
- turn1Body: unknown;
204
- turn1Response: unknown;
205
- }): unknown {
206
- if (!isRecord(opts.turn1Body)) {
207
- throw new Error("google-genai multi-turn: turn-1 body is not an object");
208
- }
209
- const turn1Contents = opts.turn1Body.contents;
210
- if (!Array.isArray(turn1Contents)) {
211
- throw new Error(
212
- "google-genai multi-turn: turn-1 body.contents is not an array",
213
- );
214
- }
215
- const assistantContent = extractAssistantContent(opts.turn1Response);
216
- const tool = deriveToolFollowUp(opts.intent);
217
- const toolResponseObject: unknown = JSON.parse(tool.content);
218
-
219
- const turn1Tools = opts.turn1Body.tools;
220
- const turn1GenerationConfig = opts.turn1Body.generationConfig;
221
-
222
- const body: Record<string, unknown> = {
223
- contents: [
224
- ...turn1Contents,
225
- assistantContent,
226
- {
227
- role: "user",
228
- parts: [
229
- {
230
- functionResponse: {
231
- name: tool.toolName,
232
- response: toolResponseObject,
233
- },
234
- },
235
- ],
236
- },
237
- ],
238
- };
239
- if (turn1Tools !== undefined) {
240
- body.tools = turn1Tools;
241
- }
242
- if (turn1GenerationConfig !== undefined) {
243
- body.generationConfig = turn1GenerationConfig;
244
- }
245
- return body;
246
- }
247
-
248
- export function* iterateCaptureSteps(
249
- opts: IterateCaptureStepsOpts,
250
- ): Generator<CaptureStep, void, CapturedResponse> {
251
- const { model, capability, intent } = opts;
252
-
253
- if (FILES_API_CAPABILITIES.has(capability)) {
254
- const upload = buildUploadDescriptor(intent);
255
- const uploadResponse = yield {
256
- kind: "raw",
257
- subdir: "upload",
258
- url: upload.url,
259
- method: "POST",
260
- contentType: upload.mimeType,
261
- headers: {
262
- "X-Goog-Upload-Protocol": "raw",
263
- "X-Goog-Upload-File-Name": upload.displayName,
264
- },
265
- body: upload.bytes,
266
- };
267
- const fileUri = extractFileUri(uploadResponse.parsed);
268
- const mimeType = extractMimeTypeFromUpload(uploadResponse.parsed);
269
- const generateBody = buildFilesApiGenerateBody({
270
- intent,
271
- fileUri,
272
- mimeType,
273
- });
274
- yield {
275
- kind: "json",
276
- subdir: "generate",
277
- url: buildEndpointURL({ model, capability }),
278
- body: generateBody,
279
- };
280
- return;
281
- }
282
-
283
- if (MULTI_TURN_CAPABILITIES.has(capability)) {
284
- const turn1Body = buildRequestBody({ model, capability, intent });
285
- const url = buildEndpointURL({ model, capability });
286
- const turn1Response = yield {
287
- kind: "json",
288
- subdir: "turn-1",
289
- url,
290
- body: turn1Body,
291
- };
292
- const turn2Body = buildMultiTurnTurn2Body({
293
- capability,
294
- intent,
295
- turn1Body,
296
- turn1Response: turn1Response.parsed,
297
- });
298
- yield {
299
- kind: "json",
300
- subdir: "turn-2",
301
- url,
302
- body: turn2Body,
303
- };
304
- return;
305
- }
306
-
307
- yield {
308
- kind: "json",
309
- subdir: null,
310
- url: buildEndpointURL({ model, capability }),
311
- body: buildRequestBody({ model, capability, intent }),
312
- };
313
- }
314
-
315
- export function createGoogleGenaiPlugin(
316
- opts: GoogleGenaiPluginOptions,
317
- ): ProviderPlugin {
318
- const apiKey = opts.apiKey;
319
- return {
320
- name: PROVIDER_NAME,
321
- models: MODELS,
322
- redactRequestHeaders: REDACT_REQUEST_HEADERS,
323
- redactResponseHeaders: REDACT_RESPONSE_HEADERS,
324
- buildAuthHeaders: () => buildAuthHeaders(apiKey),
325
- iterateCaptureSteps,
326
- };
327
- }