export class GoogleConnection { constructor(caller, client, streaming) { Object.defineProperty(this, "caller", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "client", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "streaming", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.caller = caller; this.client = client; this.streaming = streaming ?? false; } async _request(data, options) { const url = await this.buildUrl(); const method = this.buildMethod(); const opts = { url, method, }; if (data && method === "POST") { opts.data = data; } if (this.streaming) { opts.responseType = "stream"; } else { opts.responseType = "json"; } const callResponse = await this.caller.callWithOptions({ signal: options?.signal }, async () => this.client.request(opts)); const response = callResponse; // Done for typecast safety, I guess return response; } } export class GoogleVertexAIConnection extends GoogleConnection { constructor(fields, caller, client, streaming) { super(caller, client, streaming); Object.defineProperty(this, "endpoint", { enumerable: true, configurable: true, writable: true, value: "us-central1-aiplatform.googleapis.com" }); Object.defineProperty(this, "location", { enumerable: true, configurable: true, writable: true, value: "us-central1" }); Object.defineProperty(this, "apiVersion", { enumerable: true, configurable: true, writable: true, value: "v1" }); this.caller = caller; this.endpoint = fields?.endpoint ?? this.endpoint; this.location = fields?.location ?? this.location; this.apiVersion = fields?.apiVersion ?? this.apiVersion; this.client = client; } buildMethod() { return "POST"; } } export function complexValue(value) { if (value === null || typeof value === "undefined") { // I dunno what to put here. An error, probably return undefined; } else if (typeof value === "object") { if (Array.isArray(value)) { return { list_val: value.map((avalue) => complexValue(avalue)), }; } else { const ret = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any const v = value; Object.keys(v).forEach((key) => { ret[key] = complexValue(v[key]); }); return { struct_val: ret }; } } else if (typeof value === "number") { if (Number.isInteger(value)) { return { int_val: value }; } else { return { float_val: value }; } } else { return { string_val: [value], }; } } export function simpleValue(val) { if (val && typeof val === "object" && !Array.isArray(val)) { // eslint-disable-next-line no-prototype-builtins if (val.hasOwnProperty("stringVal")) { return val.stringVal[0]; // eslint-disable-next-line no-prototype-builtins } else if (val.hasOwnProperty("boolVal")) { return val.boolVal[0]; // eslint-disable-next-line no-prototype-builtins } else if (val.hasOwnProperty("listVal")) { const { listVal } = val; return listVal.map((aval) => simpleValue(aval)); // eslint-disable-next-line no-prototype-builtins } else if (val.hasOwnProperty("structVal")) { const ret = {}; const struct = val.structVal; Object.keys(struct).forEach((key) => { ret[key] = simpleValue(struct[key]); }); return ret; } else { const ret = {}; const struct = val; Object.keys(struct).forEach((key) => { ret[key] = simpleValue(struct[key]); }); return ret; } } else if (Array.isArray(val)) { return val.map((aval) => simpleValue(aval)); } else { return val; } } export class GoogleVertexAILLMConnection extends GoogleVertexAIConnection { constructor(fields, caller, client, streaming) { super(fields, caller, client, streaming); Object.defineProperty(this, "model", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "client", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "customModelURL", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.client = client; this.model = fields?.model ?? this.model; this.customModelURL = fields?.customModelURL ?? ""; } async buildUrl() { const method = this.streaming ? "serverStreamingPredict" : "predict"; if (this.customModelURL.trim() !== "") { return `${this.customModelURL}:${method}`; } const projectId = await this.client.getProjectId(); return `https://${this.endpoint}/v1/projects/${projectId}/locations/${this.location}/publishers/google/models/${this.model}:${method}`; } formatStreamingData(inputs, parameters) { return { inputs: [inputs.map((i) => complexValue(i))], parameters: complexValue(parameters), }; } formatStandardData(instances, parameters) { return { instances, parameters, }; } formatData(instances, parameters) { return this.streaming ? this.formatStreamingData(instances, parameters) : this.formatStandardData(instances, parameters); } async request(instances, parameters, options) { const data = this.formatData(instances, parameters); const response = await this._request(data, options); return response; } } export class GoogleVertexAIStream { constructor() { Object.defineProperty(this, "_buffer", { enumerable: true, configurable: true, writable: true, value: "" }); Object.defineProperty(this, "_bufferOpen", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "_firstRun", { enumerable: true, configurable: true, writable: true, value: true }); // Set up a potential Promise that the handler can resolve. // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.defineProperty(this, "_chunkResolution", { enumerable: true, configurable: true, writable: true, value: void 0 }); // If there is no Promise (it is null), the handler must add it to the queue // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.defineProperty(this, "_chunkPending", { enumerable: true, configurable: true, writable: true, value: null }); // A queue that will collect chunks while there is no Promise // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.defineProperty(this, "_chunkQueue", { enumerable: true, configurable: true, writable: true, value: [] }); } /** * Add data to the buffer. This may cause chunks to be generated, if available. * @param data */ appendBuffer(data) { this._buffer += data; // Our first time, skip to the opening of the array if (this._firstRun) { this._skipTo("["); this._firstRun = false; } this._parseBuffer(); } /** * Indicate there is no more data that will be added to the text buffer. * This should be called when all the data has been read and added to indicate * that we should process everything remaining in the buffer. */ closeBuffer() { this._bufferOpen = false; this._parseBuffer(); } /** * Skip characters in the buffer till we get to the start of an object. * Then attempt to read a full object. * If we do read a full object, turn it into a chunk and send it to the chunk handler. * Repeat this for as much as we can. */ _parseBuffer() { let obj = null; do { this._skipTo("{"); obj = this._getFullObject(); if (obj !== null) { const chunk = this._simplifyObject(obj); this._handleChunk(chunk); } } while (obj !== null); if (!this._bufferOpen) { // No more data will be added, and we have parsed everything we could, // so everything else is garbage. this._handleChunk(null); this._buffer = ""; } } /** * If the string is present, move the start of the buffer to the first occurrence * of that string. This is useful for skipping over elements or parts that we're not * really interested in parsing. (ie - the opening characters, comma separators, etc.) * @param start The string to start the buffer with */ _skipTo(start) { const index = this._buffer.indexOf(start); if (index > 0) { this._buffer = this._buffer.slice(index); } } /** * Given what is in the buffer, parse a single object out of it. * If a complete object isn't available, return null. * Assumes that we are at the start of an object to parse. */ _getFullObject() { let ret = null; // Loop while we don't have something to return AND we have something in the buffer let index = 0; while (ret === null && this._buffer.length > index) { // Advance to the next close bracket after our current index index = this._buffer.indexOf("}", index + 1); // If we don't find one, exit with null if (index === -1) { return null; } // If we have one, try to turn it into an object to return try { const objStr = this._buffer.substring(0, index + 1); ret = JSON.parse(objStr); // We only get here if it parsed it ok // If we did turn it into an object, remove it from the buffer this._buffer = this._buffer.slice(index + 1); } catch (xx) { // It didn't parse it correctly, so we swallow the exception and continue } } return ret; } _simplifyObject(obj) { return simpleValue(obj); } /** * Register that we have another chunk available for consumption. * If we are waiting for a chunk, resolve the promise waiting for it immediately. * If not, then add it to the queue. * @param chunk */ // eslint-disable-next-line @typescript-eslint/no-explicit-any _handleChunk(chunk) { if (this._chunkPending) { this._chunkResolution(chunk); this._chunkPending = null; } else { this._chunkQueue.push(chunk); } } /** * Get the next chunk that is coming from the stream. * This chunk may be null, usually indicating the last chunk in the stream. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async nextChunk() { if (this._chunkQueue.length > 0) { // If there is data in the queue, return the next queue chunk return this._chunkQueue.shift(); } else { // Otherwise, set up a promise that handleChunk will cause to be resolved this._chunkPending = new Promise((resolve) => { this._chunkResolution = resolve; }); return this._chunkPending; } } /** * Is the stream done? * A stream is only done if all of the following are true: * - There is no more data to be added to the text buffer * - There is no more data in the text buffer * - There are no chunks that are waiting to be consumed */ get streamDone() { return (!this._bufferOpen && this._buffer.length === 0 && this._chunkQueue.length === 0 && this._chunkPending === null); } }