chore: Move Azure models to azure package (#1888)

This commit is contained in:
Marcus Schiesser
2025-05-16 15:50:12 +07:00
committed by GitHub
parent c719b968f3
commit 3e66ddc10d
23 changed files with 185 additions and 226 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@llamaindex/doc": patch
---
Fixed broken links in docs and validate-links script
+6
View File
@@ -0,0 +1,6 @@
---
"@llamaindex/azure": patch
"@llamaindex/openai": minor
---
Move Azure models to azure package
+7 -10
View File
@@ -8,8 +8,9 @@ import {
AzureCosmosDBNoSqlVectorStore,
AzureCosmosNoSqlDocumentStore,
AzureCosmosNoSqlIndexStore,
AzureOpenAI,
AzureOpenAIEmbedding,
} from "@llamaindex/azure";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import {
Document,
Settings,
@@ -38,17 +39,13 @@ import {
"https://cognitiveservices.azure.com/.default",
);
const azure = {
Settings.llm = new AzureOpenAI({
azureADTokenProvider,
deployment: process.env.AZURE_DEPLOYMENT_NAME,
};
Settings.llm = new OpenAI({ azure });
Settings.embedModel = new OpenAIEmbedding({
model: process.env.EMBEDDING_MODEL,
azure: {
...azure,
deployment: process.env.EMBEDDING_MODEL,
},
});
Settings.embedModel = new AzureOpenAIEmbedding({
azureADTokenProvider,
deployment: process.env.EMBEDDING_MODEL,
});
const docStore = AzureCosmosNoSqlDocumentStore.fromAadToken();
console.log({ docStore });
+6 -9
View File
@@ -2,7 +2,7 @@ import {
DefaultAzureCredential,
getBearerTokenProvider,
} from "@azure/identity";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import { AzureOpenAI, AzureOpenAIEmbedding } from "@llamaindex/azure";
import "dotenv/config";
const AZURE_COGNITIVE_SERVICES_SCOPE =
@@ -15,11 +15,10 @@ const AZURE_COGNITIVE_SERVICES_SCOPE =
AZURE_COGNITIVE_SERVICES_SCOPE,
);
const azure = {
const llm = new AzureOpenAI({
azureADTokenProvider,
deployment: process.env.AZURE_DEPLOYMENT_NAME ?? "gpt-35-turbo",
};
const llm = new OpenAI({ azure });
});
// complete api
const response1 = await llm.complete({ prompt: "How are you?" });
console.log(response1.text);
@@ -31,11 +30,9 @@ const AZURE_COGNITIVE_SERVICES_SCOPE =
console.log(response2.message.content);
// embeddings
const embedModel = new OpenAIEmbedding({
azure: {
...azure,
deployment: process.env.EMBEDDING_MODEL,
},
const embedModel = new AzureOpenAIEmbedding({
azureADTokenProvider,
deployment: process.env.EMBEDDING_MODEL,
});
const texts = ["hello", "world"];
+7 -10
View File
@@ -9,11 +9,12 @@ import {
} from "@azure/search-documents";
import {
AzureAISearchVectorStore,
AzureOpenAI,
AzureOpenAIEmbedding,
type FilterableMetadataFieldKeysType,
IndexManagement,
MetadataIndexFieldType,
} from "@llamaindex/azure";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import { SimpleDirectoryReader } from "@llamaindex/readers/directory";
import dotenv from "dotenv";
import {
@@ -67,17 +68,13 @@ function processResults(response: NodeWithScore[], mode: VectorStoreQueryMode) {
"https://cognitiveservices.azure.com/.default",
);
// You need to deploy your own embedding model as well as your own chat completion model
const azure = {
Settings.llm = new AzureOpenAI({
azureADTokenProvider,
deployment: process.env.AZURE_DEPLOYMENT_NAME,
};
Settings.llm = new OpenAI({ azure });
Settings.embedModel = new OpenAIEmbedding({
model: process.env.EMBEDDING_MODEL,
azure: {
...azure,
deployment: process.env.EMBEDDING_MODEL,
},
});
Settings.embedModel = new AzureOpenAIEmbedding({
azureADTokenProvider,
deployment: process.env.EMBEDDING_MODEL,
});
// ---------------------------------------------------------
@@ -4,8 +4,7 @@ import {
DefaultAzureCredential,
getBearerTokenProvider,
} from "@azure/identity";
import { AzureDynamicSessionTool } from "@llamaindex/azure";
import { OpenAI } from "@llamaindex/openai";
import { AzureDynamicSessionTool, AzureOpenAI } from "@llamaindex/azure";
import { ReActAgent } from "llamaindex";
async function main() {
@@ -15,14 +14,10 @@ async function main() {
"https://cognitiveservices.azure.com/.default",
);
const azure = {
// configure LLM model
const llm = new AzureOpenAI({
azureADTokenProvider,
deployment: process.env.AZURE_OPENAI_DEPLOYMENT ?? "gpt-35-turbo",
};
// configure LLM model
const llm = new OpenAI({
azure,
});
const azureDynamicSession = new AzureDynamicSessionTool();
@@ -2,10 +2,11 @@ import { CosmosClient } from "@azure/cosmos";
import { DefaultAzureCredential } from "@azure/identity";
import {
AzureCosmosDBNoSQLConfig,
AzureOpenAI,
AzureOpenAIEmbedding,
SimpleCosmosDBReader,
SimpleCosmosDBReaderLoaderConfig,
} from "@llamaindex/azure";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import * as dotenv from "dotenv";
import {
Settings,
@@ -31,23 +32,19 @@ const vectorCollectionName =
// This example uses Azure OpenAI llm and embedding models
const llmInit = {
azure: {
apiVersion: process.env.AZURE_OPENAI_LLM_API_VERSION,
endpoint: process.env.AZURE_OPENAI_LLM_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_LLM_API_KEY,
},
apiVersion: process.env.AZURE_OPENAI_LLM_API_VERSION,
endpoint: process.env.AZURE_OPENAI_LLM_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_LLM_API_KEY,
};
const embedModelInit = {
azure: {
apiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION,
endpoint: process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_EMBEDDING_API_KEY,
},
apiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION,
endpoint: process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_EMBEDDING_API_KEY,
};
Settings.llm = new OpenAI(llmInit);
Settings.embedModel = new OpenAIEmbedding(embedModelInit);
Settings.llm = new AzureOpenAI(llmInit);
Settings.embedModel = new AzureOpenAIEmbedding(embedModelInit);
// Initialize the CosmosDB client
async function initializeCosmosClient() {
@@ -1,7 +1,10 @@
import { CosmosClient } from "@azure/cosmos";
import { DefaultAzureCredential } from "@azure/identity";
import { AzureCosmosDBNoSQLConfig } from "@llamaindex/azure";
import { OpenAI, OpenAIEmbedding } from "@llamaindex/openai";
import {
AzureCosmosDBNoSQLConfig,
AzureOpenAI,
AzureOpenAIEmbedding,
} from "@llamaindex/azure";
import * as dotenv from "dotenv";
import {
Settings,
@@ -25,23 +28,19 @@ const containerName =
process.env.AZURE_COSMOSDB_VECTOR_CONTAINER_NAME || "vectorContainer";
const llmInit = {
azure: {
apiVersion: process.env.AZURE_OPENAI_LLM_API_VERSION,
endpoint: process.env.AZURE_OPENAI_LLM_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_LLM_API_KEY,
},
apiVersion: process.env.AZURE_OPENAI_LLM_API_VERSION,
endpoint: process.env.AZURE_OPENAI_LLM_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_LLM_API_KEY,
};
const embedModelInit = {
azure: {
apiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION,
endpoint: process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_EMBEDDING_API_KEY,
},
apiVersion: process.env.AZURE_OPENAI_EMBEDDING_API_VERSION,
endpoint: process.env.AZURE_OPENAI_EMBEDDING_ENDPOINT,
apiKey: process.env.AZURE_OPENAI_EMBEDDING_API_KEY,
};
Settings.llm = new OpenAI(llmInit);
Settings.embedModel = new OpenAIEmbedding(embedModelInit);
Settings.llm = new AzureOpenAI(llmInit);
Settings.embedModel = new AzureOpenAIEmbedding(embedModelInit);
async function initializeStores() {
// Create a configuration object for the Azure CosmosDB NoSQL Vector Store
+14 -58
View File
@@ -2,17 +2,9 @@ import { BaseEmbedding } from "@llamaindex/core/embeddings";
import { getEnv } from "@llamaindex/env";
import { Tokenizers } from "@llamaindex/env/tokenizers";
import type {
AzureClientOptions,
AzureOpenAI as AzureOpenAILLM,
ClientOptions as OpenAIClientOptions,
OpenAI as OpenAILLM,
} from "openai";
import {
AzureOpenAIWithUserAgent,
getAzureConfigFromEnv,
getAzureModel,
shouldUseAzure,
} from "./azure.js";
export const ALL_OPENAI_EMBEDDING_MODELS = {
"text-embedding-ada-002": {
@@ -36,10 +28,7 @@ export const ALL_OPENAI_EMBEDDING_MODELS = {
type ModelKeys = keyof typeof ALL_OPENAI_EMBEDDING_MODELS;
type LLMInstance = Pick<
AzureOpenAILLM | OpenAILLM,
"embeddings" | "apiKey" | "baseURL"
>;
type LLMInstance = Pick<OpenAILLM, "embeddings" | "apiKey" | "baseURL">;
export class OpenAIEmbedding extends BaseEmbedding {
/** embeddding model. defaults to "text-embedding-ada-002" */
@@ -77,9 +66,8 @@ export class OpenAIEmbedding extends BaseEmbedding {
* @param init - initial parameters
*/
constructor(
init?: Omit<Partial<OpenAIEmbedding>, "lazySession"> & {
init?: Omit<Partial<OpenAIEmbedding>, "session"> & {
session?: LLMInstance | undefined;
azure?: AzureClientOptions;
},
) {
super();
@@ -100,52 +88,20 @@ export class OpenAIEmbedding extends BaseEmbedding {
if (key) {
this.embedInfo = ALL_OPENAI_EMBEDDING_MODELS[key];
}
if (init?.azure || shouldUseAzure()) {
const azureConfig = {
...getAzureConfigFromEnv({
model: getAzureModel(this.model),
}),
...init?.azure,
};
this.apiKey =
init?.session?.apiKey ?? azureConfig.apiKey ?? getEnv("OPENAI_API_KEY");
this.baseURL =
init?.session?.baseURL ??
azureConfig.baseURL ??
getEnv("OPENAI_BASE_URL");
this.lazySession = async () =>
import("openai").then(async ({ AzureOpenAI }) => {
AzureOpenAI = AzureOpenAIWithUserAgent(AzureOpenAI);
return (
init?.session ??
new AzureOpenAI({
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
...azureConfig,
})
);
this.apiKey = init?.session?.apiKey ?? init?.apiKey; // Don't fallback to env here, handled in lazySession
this.baseURL = init?.session?.baseURL ?? init?.baseURL; // Don't fallback to env here, handled in lazySession
this.lazySession = async () =>
init?.session ??
import("openai").then(({ OpenAI }) => {
return new OpenAI({
apiKey: this.apiKey ?? getEnv("OPENAI_API_KEY"), // Fallback here
baseURL: this.baseURL ?? getEnv("OPENAI_BASE_URL"), // Fallback here
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
});
} else {
this.apiKey =
init?.session?.apiKey ?? init?.apiKey ?? getEnv("OPENAI_API_KEY");
this.baseURL =
init?.session?.baseURL ?? init?.baseURL ?? getEnv("OPENAI_BASE_URL");
this.lazySession = async () =>
import("openai").then(({ OpenAI }) => {
return (
init?.session ??
new OpenAI({
apiKey: this.apiKey,
baseURL: this.baseURL,
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
})
);
});
}
});
}
/**
+1
View File
@@ -2,3 +2,4 @@ export * from "./agent";
export * from "./embedding";
export * from "./llm";
export * from "./responses";
export { type LLMInstance } from "./utils";
+12 -44
View File
@@ -16,7 +16,6 @@ import { extractText } from "@llamaindex/core/utils";
import { getEnv, uint8ArrayToBase64 } from "@llamaindex/env";
import { Tokenizers } from "@llamaindex/env/tokenizers";
import type {
AzureClientOptions,
ClientOptions as OpenAIClientOptions,
OpenAI as OpenAILLM,
} from "openai";
@@ -37,12 +36,6 @@ import type {
ResponseFormatJSONObject,
ResponseFormatJSONSchema,
} from "openai/resources/index.js";
import {
AzureOpenAIWithUserAgent,
getAzureConfigFromEnv,
getAzureModel,
shouldUseAzure,
} from "./azure.js";
import {
ALL_AVAILABLE_OPENAI_MODELS,
isFunctionCallingModel,
@@ -86,7 +79,6 @@ export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
constructor(
init?: Omit<Partial<OpenAI>, "session"> & {
session?: LLMInstance | undefined;
azure?: AzureClientOptions;
},
) {
super();
@@ -103,44 +95,20 @@ export class OpenAI extends ToolCallLLM<OpenAIAdditionalChatOptions> {
this.timeout = init?.timeout ?? 60 * 1000; // Default is 60 seconds
this.additionalChatOptions = init?.additionalChatOptions;
this.additionalSessionOptions = init?.additionalSessionOptions;
this.apiKey =
init?.session?.apiKey ?? init?.apiKey ?? getEnv("OPENAI_API_KEY");
this.baseURL =
init?.session?.baseURL ?? init?.baseURL ?? getEnv("OPENAI_BASE_URL");
this.apiKey = init?.session?.apiKey ?? init?.apiKey;
this.baseURL = init?.session?.baseURL ?? init?.baseURL;
if (init?.azure || shouldUseAzure()) {
const azureConfig = {
...getAzureConfigFromEnv({
model: getAzureModel(this.model),
}),
...init?.azure,
};
this.lazySession = async () =>
init?.session ??
import("openai").then(({ AzureOpenAI }) => {
AzureOpenAI = AzureOpenAIWithUserAgent(AzureOpenAI);
return new AzureOpenAI({
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
...azureConfig,
});
this.lazySession = async () =>
init?.session ??
import("openai").then(({ OpenAI }) => {
return new OpenAI({
apiKey: this.apiKey ?? getEnv("OPENAI_API_KEY"),
baseURL: this.baseURL ?? getEnv("OPENAI_BASE_URL"),
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
});
} else {
this.lazySession = async () =>
init?.session ??
import("openai").then(({ OpenAI }) => {
return new OpenAI({
apiKey: this.apiKey,
baseURL: this.baseURL,
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
});
});
}
});
}
get supportToolCall() {
+15 -44
View File
@@ -17,20 +17,9 @@ import {
import type { StoredValue } from "@llamaindex/core/schema";
import { extractText } from "@llamaindex/core/utils";
import { getEnv, uint8ArrayToBase64 } from "@llamaindex/env";
import {
OpenAI as OpenAILLM,
type AzureClientOptions,
type ClientOptions as OpenAIClientOptions,
} from "openai";
import { wrapEventCaller } from "@llamaindex/core/decorator";
import { Tokenizers } from "@llamaindex/env/tokenizers";
import {
AzureOpenAIWithUserAgent,
getAzureConfigFromEnv,
getAzureModel,
shouldUseAzure,
} from "./azure";
import {
ALL_AVAILABLE_OPENAI_MODELS,
isFunctionCallingModel,
@@ -45,6 +34,11 @@ import {
type StreamState,
} from "./utils";
import {
OpenAI as OpenAILLM,
type ClientOptions as OpenAIClientOptions,
} from "openai";
export class OpenAIResponses extends ToolCallLLM<OpenAIResponsesChatOptions> {
model: string;
temperature: number;
@@ -76,7 +70,6 @@ export class OpenAIResponses extends ToolCallLLM<OpenAIResponsesChatOptions> {
constructor(
init?: Omit<Partial<OpenAIResponses>, "session"> & {
session?: LLMInstance | undefined;
azure?: AzureClientOptions;
},
) {
super();
@@ -112,39 +105,17 @@ export class OpenAIResponses extends ToolCallLLM<OpenAIResponsesChatOptions> {
this.previousResponseId = init?.previousResponseId ?? null;
this.truncation = init?.truncation ?? null;
if (init?.azure || shouldUseAzure()) {
const azureConfig = {
...getAzureConfigFromEnv({
model: getAzureModel(this.model),
}),
...init?.azure,
};
this.lazySession = async () =>
init?.session ??
import("openai").then(({ AzureOpenAI }) => {
AzureOpenAI = AzureOpenAIWithUserAgent(AzureOpenAI);
return new AzureOpenAI({
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
...azureConfig,
});
this.lazySession = async () =>
init?.session ??
import("openai").then(({ OpenAI }) => {
return new OpenAI({
apiKey: this.apiKey ?? getEnv("OPENAI_API_KEY"),
baseURL: this.baseURL ?? getEnv("OPENAI_BASE_URL"),
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
});
} else {
this.lazySession = async () =>
init?.session ??
import("openai").then(({ OpenAI }) => {
return new OpenAI({
apiKey: this.apiKey,
baseURL: this.baseURL,
maxRetries: this.maxRetries,
timeout: this.timeout!,
...this.additionalSessionOptions,
});
});
}
});
}
get session() {
+3 -3
View File
@@ -1,5 +1,5 @@
import type { LLM, PartialToolCall } from "@llamaindex/core/llms";
import { AzureOpenAI as AzureOpenAILLM, OpenAI as OpenAILLM } from "openai";
import { OpenAI as OpenAILLM } from "openai";
import type { ChatModel } from "openai/resources.mjs";
import { OpenAI } from "./llm";
@@ -173,8 +173,8 @@ export type OpenAIAdditionalChatOptions = Omit<
>;
export type LLMInstance = Pick<
AzureOpenAILLM | OpenAILLM,
"chat" | "apiKey" | "baseURL" | "responses"
OpenAILLM,
"chat" | "apiKey" | "baseURL" | "responses" | "embeddings"
>;
export type OpenAIResponsesChatOptions = Omit<
+1 -1
View File
@@ -7,7 +7,7 @@
"outDir": "./lib",
"tsBuildInfoFile": "./lib/.tsbuildinfo"
},
"include": ["./src", "package.json"],
"include": ["./src"],
"references": [
{
"path": "../../core/tsconfig.json"
@@ -59,7 +59,6 @@
"devDependencies": {
"dotenv": "^16.4.7",
"vitest": "^2.1.5",
"@llamaindex/openai": "workspace:*",
"@llamaindex/core": "workspace:*",
"@llamaindex/env": "workspace:*",
"@types/node": "^22.9.0"
@@ -72,6 +71,8 @@
"@azure/cosmos": "^4.1.1",
"@azure/identity": "^4.4.1",
"@azure/search-documents": "^12.1.0",
"@llamaindex/openai": "workspace:*",
"openai": "^4.90.0",
"mongodb": "^6.7.0"
}
}
@@ -9,3 +9,5 @@ export * from "./vectorStore/AzureQueryResultSearch.js";
export * from "./tools/AzureDynamicSessionTool.node.js";
export * from "./readers/index.js";
export * from "./model/index.js";
@@ -1,7 +1,8 @@
import { getEnv, process } from "@llamaindex/env";
import type { LLMInstance } from "@llamaindex/openai";
import type FinalRequestOptions from "openai";
import { type AzureClientOptions } from "openai";
import pkg from "../package.json";
import { AzureOpenAI, type AzureClientOptions } from "openai";
import pkg from "../../package.json";
// NOTE we're not supporting the legacy models as they're not available for new deployments
// https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/legacy-models
@@ -154,3 +155,25 @@ export function AzureOpenAIWithUserAgent<K extends Constructor>(Base: K) {
}
};
}
export type AzureInitSession = AzureClientOptions & {
session?: AzureOpenAI;
};
export const lazySession = (init?: AzureInitSession) => {
return async () => {
if (init?.session) {
return init?.session as unknown as LLMInstance;
}
const AzureOpenAILib = AzureOpenAIWithUserAgent(AzureOpenAI);
return new AzureOpenAILib({
// Use base class properties for retries, timeout, etc.
maxRetries: init?.maxRetries ?? 10,
timeout: init?.timeout ?? 60 * 1000,
// Apply Azure specific config
...getAzureConfigFromEnv(),
...init,
}) as unknown as LLMInstance;
};
};
@@ -0,0 +1,18 @@
import { OpenAIEmbedding } from "@llamaindex/openai";
import { lazySession, type AzureInitSession } from "./azure";
export class AzureOpenAIEmbedding extends OpenAIEmbedding {
/**
* Azure OpenAI Embedding
* @param init - initial parameters
*/
constructor(
init?: Omit<Partial<OpenAIEmbedding>, "session"> & AzureInitSession,
) {
super({
...init,
session: undefined,
});
this.lazySession = lazySession(init);
}
}
@@ -0,0 +1,3 @@
export * from "./embedding";
export * from "./llm";
export * from "./responses";
@@ -0,0 +1,12 @@
import { OpenAI } from "@llamaindex/openai";
import { lazySession, type AzureInitSession } from "./azure";
export class AzureOpenAI extends OpenAI {
constructor(init?: Omit<Partial<OpenAI>, "session"> & AzureInitSession) {
super({
...init,
session: undefined,
});
this.lazySession = lazySession(init);
}
}
@@ -0,0 +1,18 @@
import { OpenAIResponses } from "@llamaindex/openai";
import { lazySession, type AzureInitSession } from "./azure.js";
export class AzureOpenAIResponses extends OpenAIResponses {
/**
* Azure OpenAI Responses
* @param init - initial parameters
*/
constructor(
init?: Omit<Partial<OpenAIResponses>, "session"> & AzureInitSession,
) {
super({
...init,
session: undefined,
});
this.lazySession = lazySession(init);
}
}
@@ -8,7 +8,7 @@
"tsBuildInfoFile": "./lib/.tsbuildinfo",
"types": ["node"]
},
"include": ["./src"],
"include": ["./src", "package.json"],
"references": [
{
"path": "../../../core/tsconfig.json"
+6 -3
View File
@@ -1410,9 +1410,15 @@ importers:
'@azure/search-documents':
specifier: ^12.1.0
version: 12.1.0
'@llamaindex/openai':
specifier: workspace:*
version: link:../../openai
mongodb:
specifier: ^6.7.0
version: 6.7.0(@aws-sdk/credential-providers@3.810.0)(socks@2.8.4)
openai:
specifier: ^4.90.0
version: 4.94.0(ws@8.18.1(bufferutil@4.0.9))(zod@3.24.4)
devDependencies:
'@llamaindex/core':
specifier: workspace:*
@@ -1420,9 +1426,6 @@ importers:
'@llamaindex/env':
specifier: workspace:*
version: link:../../../env
'@llamaindex/openai':
specifier: workspace:*
version: link:../../openai
'@types/node':
specifier: ^22.9.0
version: 22.9.0