Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@forwardimpact/libllm

Package Overview
Dependencies
Maintainers
1
Versions
11
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@forwardimpact/libllm - npm Package Compare versions

Comparing version
0.1.80
to
0.1.82
+460
test/libllm-api.test.js
import { test, describe, beforeEach, mock } from "node:test";
import assert from "node:assert";
import { LlmApi, DEFAULT_BASE_URL } from "../index.js";
import { Retry } from "@forwardimpact/libutil";
const EMBEDDING_BASE_URL = "http://localhost:8090";
describe("LlmApi", () => {
let mockFetch;
let llmApi;
let retry;
beforeEach(() => {
mockFetch = mock.fn();
retry = new Retry();
llmApi = new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
retry,
mockFetch,
);
});
test("creates LlmApi with token and model", () => {
assert.ok(llmApi instanceof LlmApi);
});
test("createCompletions makes correct API call", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [{ message: { role: "assistant", content: "Hello" } }],
usage: { total_tokens: 10 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const messages = [{ role: "user", content: "Hello" }];
const tools = undefined;
const temperature = 0.5;
const max_tokens = 100;
const result = await llmApi.createCompletions(
messages,
tools,
temperature,
max_tokens,
);
assert.strictEqual(mockFetch.mock.callCount(), 1);
const [url, options] = mockFetch.mock.calls[0].arguments;
assert.strictEqual(url, `${DEFAULT_BASE_URL}/chat/completions`);
assert.strictEqual(options.method, "POST");
assert.ok(options.headers.Authorization.includes("test-token"));
assert.strictEqual(result.id, "test-id");
});
test("createCompletions uses default model when not specified", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [],
usage: { total_tokens: 10 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const messages = [{ role: "user", content: "Hello" }];
await llmApi.createCompletions(messages);
const [, options] = mockFetch.mock.calls[0].arguments;
const body = JSON.parse(options.body);
assert.strictEqual(body.model, "gpt-4");
});
test("createCompletions throws error on HTTP error", async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: "Not Found",
text: mock.fn(() => Promise.resolve("Error details")),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const messages = [{ role: "user", content: "Hello" }];
await assert.rejects(() => llmApi.createCompletions(messages), {
message: /HTTP 404: Not Found/,
});
});
test("createCompletions throws error immediately on non-retryable HTTP error", async () => {
const errorResponse = {
ok: false,
status: 400,
statusText: "Bad Request",
text: mock.fn(() => Promise.resolve("Invalid request details")),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(errorResponse));
const messages = [{ role: "user", content: "Hello" }];
await assert.rejects(() => llmApi.createCompletions(messages), {
message: /HTTP 400: Bad Request/,
});
assert.strictEqual(mockFetch.mock.callCount(), 1);
});
test("createCompletions fixes multi_tool_use.parallel hallucination", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: "Planning to call tools...",
tool_calls: [
{
id: "call_abc123",
type: "function",
function: {
name: "multi_tool_use.parallel",
arguments: JSON.stringify({
tool_uses: [
{
recipient_name: "functions.get_ontology",
parameters: {},
},
{
recipient_name: "functions.get_subjects",
parameters: { type: "schema:Person" },
},
],
}),
},
},
],
},
},
],
usage: { total_tokens: 100 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const messages = [{ role: "user", content: "Query the graph" }];
const result = await llmApi.createCompletions({ messages });
assert.strictEqual(result.choices[0].message.tool_calls.length, 2);
const call0 = result.choices[0].message.tool_calls[0];
assert.strictEqual(call0.function.name, "get_ontology");
assert.strictEqual(call0.id, "call_abc123_0");
assert.deepStrictEqual(JSON.parse(call0.function.arguments), {});
const call1 = result.choices[0].message.tool_calls[1];
assert.strictEqual(call1.function.name, "get_subjects");
assert.strictEqual(call1.id, "call_abc123_1");
assert.deepStrictEqual(JSON.parse(call1.function.arguments), {
type: "schema:Person",
});
});
test("createCompletions fixes parallel hallucination (short form)", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: "",
tool_calls: [
{
id: "call_xyz",
type: "function",
function: {
name: "parallel",
arguments: JSON.stringify({
tool_uses: [
{
recipient_name: "search_content",
parameters: { query: "test" },
},
],
}),
},
},
],
},
},
],
usage: { total_tokens: 50 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const result = await llmApi.createCompletions({
messages: [{ role: "user", content: "Search" }],
});
assert.strictEqual(result.choices[0].message.tool_calls.length, 1);
assert.strictEqual(
result.choices[0].message.tool_calls[0].function.name,
"search_content",
);
});
test("createCompletions preserves normal tool calls", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: "",
tool_calls: [
{
id: "call_normal",
type: "function",
function: {
name: "search_content",
arguments: '{"query":"test"}',
},
},
],
},
},
],
usage: { total_tokens: 30 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const result = await llmApi.createCompletions({
messages: [{ role: "user", content: "Test" }],
});
assert.strictEqual(result.choices[0].message.tool_calls.length, 1);
assert.strictEqual(
result.choices[0].message.tool_calls[0].function.name,
"search_content",
);
assert.strictEqual(
result.choices[0].message.tool_calls[0].id,
"call_normal",
);
});
test("createEmbeddings makes correct TEI API call", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve([
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
]),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const texts = ["Hello", "World"];
const result = await llmApi.createEmbeddings(texts);
assert.strictEqual(mockFetch.mock.callCount(), 1);
const [url, options] = mockFetch.mock.calls[0].arguments;
assert.strictEqual(url, `${EMBEDDING_BASE_URL}/embed`);
assert.strictEqual(options.method, "POST");
const body = JSON.parse(options.body);
assert.deepStrictEqual(body.inputs, texts);
assert.strictEqual(body.model, undefined);
assert.strictEqual(options.headers.Authorization, undefined);
assert.strictEqual(options.headers["Content-Type"], "application/json");
assert.strictEqual(result.data.length, 2);
assert.deepStrictEqual(result.data[0].embedding, [0.1, 0.2, 0.3]);
assert.deepStrictEqual(result.data[1].embedding, [0.4, 0.5, 0.6]);
assert.strictEqual(result.model, "bge-small-en-v1.5");
});
test("createEmbeddings retries on 429 status", async () => {
const retryResponse = {
ok: false,
status: 429,
statusText: "Too Many Requests",
};
const successResponse = {
ok: true,
json: mock.fn(() => Promise.resolve([[0.1, 0.2, 0.3]])),
};
let callCount = 0;
mockFetch.mock.mockImplementation(() => {
callCount++;
if (callCount === 1) {
return Promise.resolve(retryResponse);
} else {
return Promise.resolve(successResponse);
}
});
const texts = ["Hello"];
const result = await llmApi.createEmbeddings(texts);
assert(mockFetch.mock.callCount() >= 2);
assert.strictEqual(result.data.length, 1);
});
test("createEmbeddings throws error immediately on non-retryable HTTP error", async () => {
const errorResponse = {
ok: false,
status: 400,
statusText: "Bad Request",
text: mock.fn(() => Promise.resolve("Invalid request details")),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(errorResponse));
const texts = ["Hello"];
await assert.rejects(() => llmApi.createEmbeddings(texts), {
message: /HTTP 400: Bad Request/,
});
assert.strictEqual(mockFetch.mock.callCount(), 1);
});
test("LlmApi falls back to baseUrl when embeddingBaseUrl is null", () => {
const teiMockFetch = mock.fn();
const teiRetry = new Retry();
const llm = new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
null,
teiRetry,
teiMockFetch,
);
assert.ok(llm instanceof LlmApi);
});
test("createEmbeddings uses OpenAI-compatible format when embeddingBaseUrl matches baseUrl", async () => {
const oaiMockFetch = mock.fn();
const oaiRetry = new Retry();
const oaiLlm = new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
null,
oaiRetry,
oaiMockFetch,
);
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
object: "list",
data: [{ object: "embedding", index: 0, embedding: [0.1, 0.2, 0.3] }],
model: "text-embedding-ada-002",
usage: { prompt_tokens: 5, completion_tokens: 0, total_tokens: 5 },
}),
),
};
oaiMockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const result = await oaiLlm.createEmbeddings(["Hello"]);
assert.strictEqual(oaiMockFetch.mock.callCount(), 1);
const [url, options] = oaiMockFetch.mock.calls[0].arguments;
assert.ok(url.endsWith("/embeddings"));
assert.ok(!url.endsWith("/embed"));
assert.strictEqual(options.method, "POST");
const body = JSON.parse(options.body);
assert.deepStrictEqual(body.input, ["Hello"]);
assert.strictEqual(body.model, "gpt-4");
assert.strictEqual(result.data.length, 1);
assert.deepStrictEqual(result.data[0].embedding, [0.1, 0.2, 0.3]);
assert.strictEqual(result.model, "text-embedding-ada-002");
});
test("listModels makes correct API call", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
data: [
{ id: "gpt-4", object: "model" },
{ id: "gpt-3.5-turbo", object: "model" },
],
}),
),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
const result = await llmApi.listModels();
assert.strictEqual(mockFetch.mock.callCount(), 1);
const [url, options] = mockFetch.mock.calls[0].arguments;
assert.strictEqual(
url,
DEFAULT_BASE_URL.replace("/inference", "/catalog/models"),
);
assert.strictEqual(options.method, "GET");
assert.strictEqual(result.data.length, 2);
assert.strictEqual(result.data[0].id, "gpt-4");
assert.strictEqual(result.data[1].id, "gpt-3.5-turbo");
});
test("listModels throws error on HTTP error", async () => {
const mockResponse = {
ok: false,
status: 401,
statusText: "Unauthorized",
text: mock.fn(() => Promise.resolve("Auth error details")),
};
mockFetch.mock.mockImplementationOnce(() => Promise.resolve(mockResponse));
await assert.rejects(() => llmApi.listModels(), {
message: /HTTP 401: Unauthorized/,
});
});
});
import { test, describe, beforeEach, mock } from "node:test";
import assert from "node:assert";
import { LlmApi, DEFAULT_BASE_URL } from "../index.js";
import { Retry } from "@forwardimpact/libutil";
const EMBEDDING_BASE_URL = "http://localhost:8090";
describe("LlmApi instance methods", () => {
let llmApi;
let retry;
beforeEach(() => {
const mockFetch = mock.fn();
retry = new Retry();
llmApi = new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
retry,
mockFetch,
);
});
test("countTokens returns token count for text", () => {
const text = "Hello, world!";
const count = llmApi.countTokens(text);
assert.strictEqual(typeof count, "number");
assert(count > 0);
});
test("countTokens handles empty text", () => {
const count = llmApi.countTokens("");
assert.strictEqual(count, 0);
});
test("countTokens handles longer text", () => {
const shortText = "Hello";
const longText =
"Hello, this is a much longer text that should have more tokens";
const shortCount = llmApi.countTokens(shortText);
const longCount = llmApi.countTokens(longText);
assert(longCount > shortCount);
});
});
describe("Proxy Support", () => {
test("createLlmApi creates LlmApi instance with default fetch", async () => {
const { createLlmApi, LlmApi, DEFAULT_BASE_URL } =
await import("../index.js");
const llm = createLlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
);
assert.ok(llm instanceof LlmApi);
});
test("createLlmApi works without embeddingBaseUrl", async () => {
const { createLlmApi, LlmApi, DEFAULT_BASE_URL } =
await import("../index.js");
const llm = createLlmApi("test-token", "gpt-4", DEFAULT_BASE_URL);
assert.ok(llm instanceof LlmApi);
});
test("createLlmApi works when HTTPS_PROXY environment variable is set", async () => {
const originalProxy = process.env.HTTPS_PROXY;
process.env.HTTPS_PROXY = "http://proxy.example.com:3128";
try {
const { createLlmApi, LlmApi, DEFAULT_BASE_URL } =
await import("../index.js");
const llm = createLlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
);
assert.ok(llm instanceof LlmApi);
} finally {
if (originalProxy) {
process.env.HTTPS_PROXY = originalProxy;
} else {
delete process.env.HTTPS_PROXY;
}
}
});
});
+57
-10

@@ -40,2 +40,3 @@ import { readFile } from "node:fs/promises";

#embeddingBaseURL;
#useTeiEmbeddings;
#headers;

@@ -52,3 +53,3 @@ #fetch;

* @param {string} baseUrl - Base URL for the LLM API
* @param {string} embeddingBaseUrl - Base URL for TEI embeddings (required)
* @param {string} embeddingBaseUrl - Base URL for embeddings (TEI endpoint or OpenAI-compatible)
* @param {import("@forwardimpact/libutil").Retry} retry - Retry instance for handling transient errors

@@ -70,3 +71,2 @@ * @param {(url: string, options?: object) => Promise<Response>} fetchFn - HTTP client function (defaults to fetch if not provided)

if (!baseUrl) throw new Error("baseUrl is required");
if (!embeddingBaseUrl) throw new Error("embeddingBaseUrl is required");
if (!retry) throw new Error("retry is required");

@@ -80,3 +80,6 @@ if (typeof fetchFn !== "function")

this.#baseURL = normalizeBaseUrl(baseUrl);
this.#embeddingBaseURL = embeddingBaseUrl;
this.#embeddingBaseURL = embeddingBaseUrl || this.#baseURL;
this.#useTeiEmbeddings =
!!embeddingBaseUrl &&
normalizeBaseUrl(embeddingBaseUrl) !== this.#baseURL;
this.#headers = {

@@ -151,3 +154,6 @@ Authorization: `Bearer ${token}`,

/**
* Creates embeddings using TEI (Text Embeddings Inference)
* Creates embeddings via TEI or OpenAI-compatible endpoint.
* Uses TEI format when EMBEDDING_BASE_URL is explicitly set to a
* different host; otherwise uses the OpenAI-compatible /embeddings
* endpoint on the LLM base URL.
* @param {string[]} input - Array of text strings to embed

@@ -157,2 +163,13 @@ * @returns {Promise<import("@forwardimpact/libtype").common.Embeddings>} Embeddings response

async createEmbeddings(input) {
if (this.#useTeiEmbeddings) {
return this.#createTeiEmbeddings(input);
}
return this.#createOpenAIEmbeddings(input);
}
/**
* TEI (Text Embeddings Inference) format: POST /embed
* @param {string[]} input
*/
async #createTeiEmbeddings(input) {
const response = await this.#retry.execute(() =>

@@ -183,2 +200,37 @@ this.#fetch(`${this.#embeddingBaseURL}/embed`, {

/**
* OpenAI-compatible format: POST /embeddings
* @param {string[]} input
*/
async #createOpenAIEmbeddings(input) {
const response = await this.#retry.execute(() =>
this.#fetch(`${this.#embeddingBaseURL}/embeddings`, {
method: "POST",
headers: this.#headers,
body: JSON.stringify({
input,
model: this.#model,
}),
}),
);
await this.#throwIfNotOk(response);
const json = await response.json();
return common.Embeddings.fromObject({
object: json.object || "list",
data: json.data.map((item) => ({
object: item.object || "embedding",
index: item.index,
embedding: item.embedding,
})),
model: json.model || this.#model,
usage: json.usage || {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0,
},
});
}
/**
* Lists models available to the current user

@@ -304,3 +356,3 @@ * @returns {Promise<object[]>} Array of available models

* @param {string} baseUrl - Base URL for the LLM API (required, e.g. https://models.github.ai/orgs/{org})
* @param {string} embeddingBaseUrl - Base URL for TEI embeddings (required)
* @param {string|null} embeddingBaseUrl - Base URL for embeddings (null falls back to baseUrl with OpenAI-compatible format)
* @param {number} [temperature] - Temperature for completions

@@ -325,7 +377,2 @@ * @param {(url: string, options?: object) => Promise<Response>} [fetchFn] - HTTP client function

}
if (!embeddingBaseUrl) {
throw new Error(
"embeddingBaseUrl is required. Set EMBEDDING_BASE_URL for TEI endpoint.",
);
}
const retry = createRetry();

@@ -332,0 +379,0 @@ return new LlmApi(

+1
-1
{
"name": "@forwardimpact/libllm",
"version": "0.1.80",
"version": "0.1.82",
"description": "LLM API client for OpenAI-compatible endpoints",

@@ -5,0 +5,0 @@ "license": "Apache-2.0",

import { test, describe, beforeEach, mock } from "node:test";
import assert from "node:assert";
// Module under test
import { LlmApi, DEFAULT_BASE_URL } from "../index.js";
import { Retry } from "@forwardimpact/libutil";
const EMBEDDING_BASE_URL = "http://localhost:8090";
describe("libllm", () => {
describe("LlmApi", () => {
let mockFetch;
let llmApi;
let retry;
beforeEach(() => {
mockFetch = mock.fn();
retry = new Retry();
llmApi = new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
retry,
mockFetch,
);
});
test("creates LlmApi with token and model", () => {
assert.ok(llmApi instanceof LlmApi);
});
test("createCompletions makes correct API call", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [{ message: { role: "assistant", content: "Hello" } }],
usage: { total_tokens: 10 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const messages = [{ role: "user", content: "Hello" }];
const tools = undefined;
const temperature = 0.5;
const max_tokens = 100;
const result = await llmApi.createCompletions(
messages,
tools,
temperature,
max_tokens,
);
assert.strictEqual(mockFetch.mock.callCount(), 1);
const [url, options] = mockFetch.mock.calls[0].arguments;
assert.strictEqual(url, `${DEFAULT_BASE_URL}/chat/completions`);
assert.strictEqual(options.method, "POST");
assert.ok(options.headers.Authorization.includes("test-token"));
assert.strictEqual(result.id, "test-id");
});
test("createCompletions uses default model when not specified", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [],
usage: { total_tokens: 10 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const messages = [{ role: "user", content: "Hello" }];
await llmApi.createCompletions(messages);
const [, options] = mockFetch.mock.calls[0].arguments;
const body = JSON.parse(options.body);
assert.strictEqual(body.model, "gpt-4");
});
test("createCompletions throws error on HTTP error", async () => {
const mockResponse = {
ok: false,
status: 404,
statusText: "Not Found",
text: mock.fn(() => Promise.resolve("Error details")),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const messages = [{ role: "user", content: "Hello" }];
await assert.rejects(() => llmApi.createCompletions(messages), {
message: /HTTP 404: Not Found/,
});
});
test("createCompletions throws error immediately on non-retryable HTTP error", async () => {
const errorResponse = {
ok: false,
status: 400,
statusText: "Bad Request",
text: mock.fn(() => Promise.resolve("Invalid request details")),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(errorResponse),
);
const messages = [{ role: "user", content: "Hello" }];
await assert.rejects(() => llmApi.createCompletions(messages), {
message: /HTTP 400: Bad Request/,
});
// Should not retry for non-retryable errors (like 400)
assert.strictEqual(mockFetch.mock.callCount(), 1);
});
test("createCompletions fixes multi_tool_use.parallel hallucination", async () => {
// Simulates the hallucinated multi_tool_use.parallel response from OpenAI
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: "Planning to call tools...",
tool_calls: [
{
id: "call_abc123",
type: "function",
function: {
name: "multi_tool_use.parallel",
arguments: JSON.stringify({
tool_uses: [
{
recipient_name: "functions.get_ontology",
parameters: {},
},
{
recipient_name: "functions.get_subjects",
parameters: { type: "schema:Person" },
},
],
}),
},
},
],
},
},
],
usage: { total_tokens: 100 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const messages = [{ role: "user", content: "Query the graph" }];
const result = await llmApi.createCompletions({ messages });
// Should have expanded to 2 tool calls
assert.strictEqual(result.choices[0].message.tool_calls.length, 2);
// First tool call should be get_ontology
const call0 = result.choices[0].message.tool_calls[0];
assert.strictEqual(call0.function.name, "get_ontology");
assert.strictEqual(call0.id, "call_abc123_0");
assert.deepStrictEqual(JSON.parse(call0.function.arguments), {});
// Second tool call should be get_subjects
const call1 = result.choices[0].message.tool_calls[1];
assert.strictEqual(call1.function.name, "get_subjects");
assert.strictEqual(call1.id, "call_abc123_1");
assert.deepStrictEqual(JSON.parse(call1.function.arguments), {
type: "schema:Person",
});
});
test("createCompletions fixes parallel hallucination (short form)", async () => {
// Also handles the short form "parallel" name
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: "",
tool_calls: [
{
id: "call_xyz",
type: "function",
function: {
name: "parallel",
arguments: JSON.stringify({
tool_uses: [
{
recipient_name: "search_content",
parameters: { query: "test" },
},
],
}),
},
},
],
},
},
],
usage: { total_tokens: 50 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const result = await llmApi.createCompletions({
messages: [{ role: "user", content: "Search" }],
});
assert.strictEqual(result.choices[0].message.tool_calls.length, 1);
assert.strictEqual(
result.choices[0].message.tool_calls[0].function.name,
"search_content",
);
});
test("createCompletions preserves normal tool calls", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
id: "test-id",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: "",
tool_calls: [
{
id: "call_normal",
type: "function",
function: {
name: "search_content",
arguments: '{"query":"test"}',
},
},
],
},
},
],
usage: { total_tokens: 30 },
}),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const result = await llmApi.createCompletions({
messages: [{ role: "user", content: "Test" }],
});
// Normal tool calls should pass through unchanged
assert.strictEqual(result.choices[0].message.tool_calls.length, 1);
assert.strictEqual(
result.choices[0].message.tool_calls[0].function.name,
"search_content",
);
assert.strictEqual(
result.choices[0].message.tool_calls[0].id,
"call_normal",
);
});
test("createEmbeddings makes correct TEI API call", async () => {
// TEI returns array of arrays: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve([
[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
]),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const texts = ["Hello", "World"];
const result = await llmApi.createEmbeddings(texts);
assert.strictEqual(mockFetch.mock.callCount(), 1);
const [url, options] = mockFetch.mock.calls[0].arguments;
assert.strictEqual(url, `${EMBEDDING_BASE_URL}/embed`);
assert.strictEqual(options.method, "POST");
// TEI uses { inputs: [...] } format
const body = JSON.parse(options.body);
assert.deepStrictEqual(body.inputs, texts);
assert.strictEqual(body.model, undefined); // TEI doesn't need model
// No authorization header for TEI
assert.strictEqual(options.headers.Authorization, undefined);
assert.strictEqual(options.headers["Content-Type"], "application/json");
// Result should be normalized to Embeddings format
assert.strictEqual(result.data.length, 2);
assert.deepStrictEqual(result.data[0].embedding, [0.1, 0.2, 0.3]);
assert.deepStrictEqual(result.data[1].embedding, [0.4, 0.5, 0.6]);
assert.strictEqual(result.model, "bge-small-en-v1.5");
});
test("createEmbeddings retries on 429 status", async () => {
const retryResponse = {
ok: false,
status: 429,
statusText: "Too Many Requests",
};
// TEI returns array of arrays
const successResponse = {
ok: true,
json: mock.fn(() => Promise.resolve([[0.1, 0.2, 0.3]])),
};
// Set up mock to return retry response first, then success
let callCount = 0;
mockFetch.mock.mockImplementation(() => {
callCount++;
if (callCount === 1) {
return Promise.resolve(retryResponse);
} else {
return Promise.resolve(successResponse);
}
});
const texts = ["Hello"];
const result = await llmApi.createEmbeddings(texts);
// Should retry once and then succeed
assert(mockFetch.mock.callCount() >= 2);
assert.strictEqual(result.data.length, 1);
});
test("createEmbeddings throws error immediately on non-retryable HTTP error", async () => {
const errorResponse = {
ok: false,
status: 400,
statusText: "Bad Request",
text: mock.fn(() => Promise.resolve("Invalid request details")),
};
// Mock all attempts to fail with non-retryable error (no retries)
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(errorResponse),
);
const texts = ["Hello"];
await assert.rejects(() => llmApi.createEmbeddings(texts), {
message: /HTTP 400: Bad Request/,
});
assert.strictEqual(mockFetch.mock.callCount(), 1); // No retries for non-retryable errors
});
test("LlmApi throws when embeddingBaseUrl is not provided", () => {
const teiMockFetch = mock.fn();
const teiRetry = new Retry();
assert.throws(
() =>
new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
null, // embeddingBaseUrl is required
teiRetry,
teiMockFetch,
),
{ message: /embeddingBaseUrl is required/ },
);
});
test("listModels makes correct API call", async () => {
const mockResponse = {
ok: true,
json: mock.fn(() =>
Promise.resolve({
data: [
{ id: "gpt-4", object: "model" },
{ id: "gpt-3.5-turbo", object: "model" },
],
}),
),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
const result = await llmApi.listModels();
assert.strictEqual(mockFetch.mock.callCount(), 1);
const [url, options] = mockFetch.mock.calls[0].arguments;
// listModels uses /catalog/models endpoint (not under /inference)
assert.strictEqual(
url,
DEFAULT_BASE_URL.replace("/inference", "/catalog/models"),
);
assert.strictEqual(options.method, "GET");
assert.strictEqual(result.data.length, 2);
assert.strictEqual(result.data[0].id, "gpt-4");
assert.strictEqual(result.data[1].id, "gpt-3.5-turbo");
});
test("listModels throws error on HTTP error", async () => {
const mockResponse = {
ok: false,
status: 401,
statusText: "Unauthorized",
text: mock.fn(() => Promise.resolve("Auth error details")),
};
mockFetch.mock.mockImplementationOnce(() =>
Promise.resolve(mockResponse),
);
await assert.rejects(() => llmApi.listModels(), {
message: /HTTP 401: Unauthorized/,
});
});
});
describe("LlmApi instance methods", () => {
let llmApi;
let retry;
beforeEach(() => {
const mockFetch = mock.fn();
retry = new Retry();
llmApi = new LlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
retry,
mockFetch,
);
});
test("countTokens returns token count for text", () => {
const text = "Hello, world!";
const count = llmApi.countTokens(text);
assert.strictEqual(typeof count, "number");
assert(count > 0);
});
test("countTokens handles empty text", () => {
const count = llmApi.countTokens("");
assert.strictEqual(count, 0);
});
test("countTokens handles longer text", () => {
const shortText = "Hello";
const longText =
"Hello, this is a much longer text that should have more tokens";
const shortCount = llmApi.countTokens(shortText);
const longCount = llmApi.countTokens(longText);
assert(longCount > shortCount);
});
});
describe("Proxy Support", () => {
test("createLlmApi creates LlmApi instance with default fetch", async () => {
// Import the function dynamically to test it
const { createLlmApi, LlmApi, DEFAULT_BASE_URL } =
await import("../index.js");
// Create an LLM instance (embeddingBaseUrl is now required)
const llm = createLlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
);
// Verify that the LLM was created successfully
assert.ok(llm instanceof LlmApi);
});
test("createLlmApi throws when embeddingBaseUrl is not provided", async () => {
const { createLlmApi, DEFAULT_BASE_URL } = await import("../index.js");
assert.throws(
() => createLlmApi("test-token", "gpt-4", DEFAULT_BASE_URL),
{ message: /embeddingBaseUrl is required/ },
);
});
test("createLlmApi works when HTTPS_PROXY environment variable is set", async () => {
// Set proxy environment variable for this test
const originalProxy = process.env.HTTPS_PROXY;
process.env.HTTPS_PROXY = "http://proxy.example.com:3128";
try {
// Import the function dynamically to test it
const { createLlmApi, LlmApi, DEFAULT_BASE_URL } =
await import("../index.js");
// Create an LLM instance with proxy environment
const llm = createLlmApi(
"test-token",
"gpt-4",
DEFAULT_BASE_URL,
EMBEDDING_BASE_URL,
);
// Verify that the LLM was created successfully
assert.ok(llm instanceof LlmApi);
} finally {
// Restore original environment
if (originalProxy) {
process.env.HTTPS_PROXY = originalProxy;
} else {
delete process.env.HTTPS_PROXY;
}
}
});
});
});