/** * @license * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { expect, use } from "chai"; import { match, restore, stub } from "sinon"; import * as sinonChai from "sinon-chai"; import * as chaiAsPromised from "chai-as-promised"; import { DEFAULT_API_VERSION, DEFAULT_BASE_URL, RequestUrl, Task, constructModelRequest, makeModelRequest, } from "./request"; import { GoogleGenerativeAIFetchError, GoogleGenerativeAIRequestInputError, } from "../errors"; use(sinonChai); use(chaiAsPromised); describe("request methods", () => { afterEach(() => { restore(); }); describe("RequestUrl", () => { it("stream", async () => { const url = new RequestUrl( "models/model-name", Task.GENERATE_CONTENT, "key", true, {}, ); expect(url.toString()).to.include("models/model-name:generateContent"); expect(url.toString()).to.not.include("key"); expect(url.toString()).to.include("alt=sse"); }); it("non-stream", async () => { const url = new RequestUrl( "models/model-name", Task.GENERATE_CONTENT, "key", false, {}, ); expect(url.toString()).to.include("models/model-name:generateContent"); expect(url.toString()).to.not.include("key"); expect(url.toString()).to.not.include("alt=sse"); }); it("default apiVersion and baseUrl", async () => { const url = new RequestUrl( "models/model-name", Task.GENERATE_CONTENT, "key", false, {}, ); expect(url.toString()).to.include(DEFAULT_API_VERSION); expect(url.toString()).to.include(DEFAULT_BASE_URL); }); it("custom apiVersion", async () => { const url = new RequestUrl( "models/model-name", Task.GENERATE_CONTENT, "key", false, { apiVersion: "v2beta" }, ); expect(url.toString()).to.include("/v2beta/models/model-name"); }); it("custom baseUrl", async () => { const url = new RequestUrl( "models/model-name", Task.GENERATE_CONTENT, "key", false, { baseUrl: "http://my.staging.website" }, ); expect(url.toString()).to.include("http://my.staging.website"); }); it("non-stream - tunedModels/", async () => { const url = new RequestUrl( "tunedModels/model-name", Task.GENERATE_CONTENT, "key", false, {}, ); expect(url.toString()).to.include( "tunedModels/model-name:generateContent", ); expect(url.toString()).to.not.include("key"); expect(url.toString()).to.not.include("alt=sse"); }); }); describe("constructRequest", () => { it("handles basic request", async () => { const request = await constructModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", {}, ); expect( (request.fetchOptions.headers as Headers).get("x-goog-api-client"), ).to.equal("genai-js/__PACKAGE_VERSION__"); expect( (request.fetchOptions.headers as Headers).get("x-goog-api-key"), ).to.equal("key"); expect( (request.fetchOptions.headers as Headers).get("Content-Type"), ).to.equal("application/json"); }); it("passes apiClient", async () => { const request = await constructModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { apiClient: "client/version", }, ); expect( (request.fetchOptions.headers as Headers).get("x-goog-api-client"), ).to.equal("client/version genai-js/__PACKAGE_VERSION__"); }); it("passes timeout", async () => { const request = await constructModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { timeout: 5000, }, ); expect(request.fetchOptions.signal).to.be.instanceOf(AbortSignal); }); it("passes custom headers", async () => { const request = await constructModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { customHeaders: new Headers({ customerHeader: "customerHeaderValue" }), }, ); expect( (request.fetchOptions.headers as Headers).get("customerHeader"), ).to.equal("customerHeaderValue"); }); it("passes custom x-goog-api-client header", async () => { await expect( constructModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { customHeaders: new Headers({ "x-goog-api-client": "client/version", }), }, ), ).to.be.rejectedWith(GoogleGenerativeAIRequestInputError); }); it("passes apiClient and custom x-goog-api-client header", async () => { await expect( constructModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { apiClient: "client/version", customHeaders: new Headers({ "x-goog-api-client": "client/version2", }), }, ), ).to.be.rejectedWith(GoogleGenerativeAIRequestInputError); }); }); describe("makeModelRequest", () => { it("no error", async () => { const fetchStub = stub().resolves({ ok: true, } as Response); const response = await makeModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", {}, fetchStub as typeof fetch, ); expect(fetchStub).to.be.calledWith(match.string, { method: "POST", headers: match.instanceOf(Headers), body: "", }); expect(response.ok).to.be.true; }); it("error with timeout", async () => { const fetchStub = stub().resolves({ ok: false, status: 500, statusText: "AbortError", } as Response); try { await makeModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { timeout: 0, }, fetchStub as typeof fetch, ); } catch (e) { expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( "AbortError", ); expect((e as GoogleGenerativeAIFetchError).message).to.include( "500 AbortError", ); } expect(fetchStub).to.be.calledOnce; }); it("Network error, no response.json()", async () => { const fetchStub = stub().resolves({ ok: false, status: 500, statusText: "Server Error", } as Response); try { await makeModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", {}, fetchStub as typeof fetch, ); } catch (e) { expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( "Server Error", ); expect((e as GoogleGenerativeAIFetchError).message).to.include( "500 Server Error", ); } expect(fetchStub).to.be.calledOnce; }); it("Network error, includes response.json()", async () => { const fetchStub = stub().resolves({ ok: false, status: 500, statusText: "Server Error", json: () => Promise.resolve({ error: { message: "extra info" } }), } as Response); try { await makeModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", {}, fetchStub as typeof fetch, ); } catch (e) { expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( "Server Error", ); expect((e as GoogleGenerativeAIFetchError).message).to.match( /500 Server Error.*extra info/, ); } expect(fetchStub).to.be.calledOnce; }); it("Network error, includes response.json() and details", async () => { const fetchStub = stub().resolves({ ok: false, status: 500, statusText: "Server Error", json: () => Promise.resolve({ error: { message: "extra info", details: [ { "@type": "type.googleapis.com/google.rpc.DebugInfo", detail: "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short", }, ], }, }), } as Response); try { await makeModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", {}, fetchStub as typeof fetch, ); } catch (e) { expect((e as GoogleGenerativeAIFetchError).status).to.equal(500); expect((e as GoogleGenerativeAIFetchError).statusText).to.equal( "Server Error", ); expect( (e as GoogleGenerativeAIFetchError).errorDetails[0].detail, ).to.equal( "[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short", ); expect((e as GoogleGenerativeAIFetchError).message).to.match( /500 Server Error.*extra info.*generic::invalid_argument/, ); } expect(fetchStub).to.be.calledOnce; }); it("has invalid custom header", async () => { const fetchStub = stub(); await expect( makeModelRequest( "model-name", Task.GENERATE_CONTENT, "key", true, "", { customHeaders: new Headers({ "x-goog-api-client": "client/version", }), }, fetchStub as typeof fetch, ), ).to.be.rejectedWith(GoogleGenerativeAIRequestInputError); }); }); });