# unlimited-ai

**English** | [日本語](README-ja.md)

Fast, minimal Node.js wrapper for the [Voids API](https://voids.top/) AI chat completions.

> [!Note]
> The Voids API is not affiliated with this package. For API issues, do not open GitHub issues here.

```sh
npm install unlimited-ai
```

## unlimited-ai is back!

Rewritten in TypeScript. `axios` replaced with `ky`.  
Conversation history is now supported via conversation IDs — previously, every call was stateless.

<details>

<summary>See other changes...</summary>

`allModels()` renamed to `models()`. Curated model list removed. `searchModels()` no longer takes an `all` parameter.

</details>

> [!Caution]
>
> v6.x and below are no longer supported.  
> Some functions no longer work or have changed. Please update to v7 or later.

---

## Quick start

```ts
// ESM
import { AI } from 'unlimited-ai';
// CJS
const { AI } = require('unlimited-ai');

const ai = new AI({ model: 'gpt-4o', system: 'You are a helpful assistant.' });
console.log(await ai.ask('Hello!'));
```

Or with the functional API:

```ts
// ESM
import { generate, ask, models } from 'unlimited-ai';
// CJS
const { generate, ask, models } = require('unlimited-ai');

const list = await models();
console.log(list); // ['gpt-4o', 'gemini-1.5-flash', ...]

const reply = await generate('gpt-4o', [{ role: 'user', content: 'Hello!' }]);
console.log(reply);
```

---

## `new AI(init?)`

```ts
new AI(init?: {
  model?: string;
  system?: string;
  messages?: Message[];
})
```

Creates a new AI instance.

**`init.model`** `string` *(optional)*  
Model ID to use (e.g. `'gpt-4o'`).

**`init.system`** `string` *(optional)*  
System prompt. Prepended to every request as a `system` message.

**`init.messages`** `Message[]` *(optional)*  
Initial static context. These messages are prepended to every request.

---

### Examples

**Multi-turn with `useConversation`**

```ts
// ESM
import { AI } from 'unlimited-ai';
// CJS
const { AI } = require('unlimited-ai');

const ai = new AI({ model: 'gpt-4o', system: 'You are a helpful assistant.' });

ai.useConversation('session-1');
console.log(await ai.ask('What is TypeScript?'));
console.log(await ai.ask('What about its type system?')); // full history sent
```

**Per-user histories (inline IDs)**

```ts
// ESM
import { AI } from 'unlimited-ai';
// CJS
const { AI } = require('unlimited-ai');

const ai = new AI({ model: 'gpt-4o', system: 'You are a helpful assistant.' });

await ai.ask('Hello!', 'user-alice');
await ai.ask('Hi there!', 'user-bob');
await ai.ask('Remember me?', 'user-alice'); // alice's history is restored
```

**Stateless**

```ts
const reply = await ai.ask('What is 2 + 2?');
// Sends static context + this prompt only. Nothing is saved.
```

**Streaming**

```ts
// ESM
import { AI } from 'unlimited-ai';
// CJS
const { AI } = require('unlimited-ai');

const ai = new AI({ model: 'gpt-4o' });

ai.useConversation('session-1');
for await (const chunk of ai.stream('Tell me a joke.')) {
  process.stdout.write(chunk);
}
// The full reply is appended to session-1 automatically.
```

**Persisting conversations across restarts**

```ts
// ESM
import { AI } from 'unlimited-ai';
import { readFileSync, writeFileSync } from 'node:fs';
// CJS
const { AI } = require('unlimited-ai');
const { readFileSync, writeFileSync } = require('node:fs');

const ai = new AI({ model: 'gpt-4o' });

// Restore saved conversations on startup
try {
  const saved = JSON.parse(readFileSync('conversations.json', 'utf-8'));
  ai.importConversations(saved);
} catch { /* no saved file yet */ }

ai.useConversation('session-1');
console.log(await ai.ask('Hello!'));

// Save all conversations before shutting down
writeFileSync('conversations.json', JSON.stringify(ai.exportConversations()));
```

**Traditional (manual history)**

```ts
// ESM
import { AI } from 'unlimited-ai';
// CJS
const { AI } = require('unlimited-ai');

const ai = new AI();
const reply = await ai
  .setModel('gpt-4o')
  .addMessage({ role: 'system', content: 'You are a helpful assistant.' })
  .addMessage({ role: 'user', content: 'Hello!' })
  .generate();

console.log(reply);
```

---

### Static context

Static context messages are prepended to every request. Useful for system prompts and few-shot examples. All methods return `this` and are chainable.

---

#### `setModel(model, search?)`

```ts
setModel(model: string, search?: boolean): this
```

Sets the model for subsequent requests.

**`model`** `string`  
Model ID (e.g. `'gpt-4o'`).

**`search`** `boolean` *(optional, default: `false`)*  
When `true`, runs a fuzzy search via `searchModels` and uses the closest matching model ID.

---

#### `setSystem(content)`

```ts
setSystem(content: string): this
```

Sets or replaces the system prompt. If a `system` message already exists in the static context, it is replaced in-place; otherwise one is prepended.

**`content`** `string`  
System prompt text.

---

#### `setMessages(messages)`

```ts
setMessages(messages: Message[]): this
```

Replaces the entire static context array.

**`messages`** `Message[]`  
New static context. Overwrites the previous value entirely.

---

#### `addMessage(message)`

```ts
addMessage(message: Message): this
```

Appends a single message to the static context.

**`message`** `Message`  
Message to append (`{ role, content }`).

---

#### `removeMessage(index)`

```ts
removeMessage(index: number): this
```

Removes a message from the static context by index. Throws `RangeError` if the index is out of bounds.

**`index`** `number`  
Zero-based index of the message to remove.

---

#### `clearMessages()`

```ts
clearMessages(): this
```

Removes all messages from the static context.

---

#### `getFormat()`

```ts
getFormat(): { model: string; messages: Message[] }
```

Returns a snapshot of the current model and static context. The returned `messages` array is a shallow copy — mutating it does not affect the instance.

---

#### `generate(raw?)`

```ts
generate(raw?: false): Promise<string>
generate(raw: true): Promise<CompletionResponse>
```

Sends the current static context (`this.messages`) as-is and returns the reply.

**`raw`** `boolean` *(optional, default: `false`)*  
When `true`, returns the full `CompletionResponse` object instead of just the reply string.

---

### Conversations

Per-conversation history is managed automatically and kept separate from the static context. All methods return `this` and are chainable unless noted otherwise.

---

#### `useConversation(id)`

```ts
useConversation(id: string | null): this
```

Sets the active conversation for subsequent `ask` and `stream` calls. Pass `null` to return to stateless mode.

**`id`** `string | null`  
Conversation ID to activate. Any non-empty string is valid (UUID, snowflake, username, etc.). When a new ID is used, an empty history is created automatically.

---

#### `ask(prompt, id?)`

```ts
ask(prompt: string, id?: string): Promise<string>
```

Sends a message and returns the reply.

- If `id` is provided, or an active conversation is set via `useConversation`, the user message and reply are appended to that conversation's history.
- If no ID is active, the call is stateless — only the static context and this prompt are sent, and nothing is saved.

**`prompt`** `string`  
The user message to send.

**`id`** `string` *(optional)*  
Conversation ID. Overrides the active conversation set by `useConversation` for this call only.

**Returns** `Promise<string>`  
The assistant's reply.

---

#### `stream(prompt, id?)`

```ts
stream(prompt: string, id?: string): AsyncGenerator<string>
```

Same as `ask`, but yields the reply as chunks as they arrive. After the generator is exhausted, the full assembled reply is saved to the conversation history (if an ID is active).

**`prompt`** `string`  
The user message to send.

**`id`** `string` *(optional)*  
Conversation ID. Overrides the active conversation for this call only.

**Yields** `string`  
Successive text chunks of the reply.

---

#### `listConversations()`

```ts
listConversations(): string[]
```

Returns an array of all known conversation IDs.

---

#### `getConversationMessages(id)`

```ts
getConversationMessages(id: string): Message[]
```

Returns a copy of the message history for the given conversation. Mutating the returned array does not affect the stored history.

**`id`** `string`  
Conversation ID.

---

#### `clearConversation(id)`

```ts
clearConversation(id: string): this
```

Empties the message history for the given conversation without removing the conversation itself.

**`id`** `string`  
Conversation ID.

---

#### `deleteConversation(id)`

```ts
deleteConversation(id: string): this
```

Removes the conversation and its history entirely.

**`id`** `string`  
Conversation ID.

---

#### `exportConversations(id?)`

```ts
exportConversations(id: string): Message[]
exportConversations(): ConversationStore
```

Exports conversation history as a plain, JSON-serializable value.

- When `id` is provided, returns a copy of that conversation's `Message[]`.
- When called without arguments, returns a `ConversationStore` (a plain object keyed by conversation ID) containing copies of all conversations.

**`id`** `string` *(optional)*  
Conversation ID. When omitted, all conversations are exported.

---

#### `importConversations(data, replace?)`

```ts
importConversations(data: ConversationStore, replace?: boolean): this
```

Loads conversations from a plain object (e.g. parsed from JSON). Useful for restoring history after a restart.

**`data`** `ConversationStore`  
Plain object mapping conversation IDs to `Message[]` arrays.

**`replace`** `boolean` *(optional, default: `false`)*  
When `false` (default), incoming conversations are merged with existing ones. When `true`, all existing conversations are cleared before importing.

---

## Functional API

### `generate(model, messages, raw?)`

```ts
generate(model: string, messages: Message[], raw?: false): Promise<string>
generate(model: string, messages: Message[], raw: true): Promise<CompletionResponse>
```

```ts
// ESM
import { generate } from 'unlimited-ai';
// CJS
const { generate } = require('unlimited-ai');

const reply = await generate('gpt-4o', [{ role: 'user', content: 'Hello!' }]);

// Full response object
const raw = await generate('gpt-4o', messages, true);
console.log(raw.choices[0]?.message.content);
```

Sends a chat completion request and returns the result.

**`model`** `string`  
Model ID (e.g. `'gpt-4o'`).

**`messages`** `Message[]`  
Conversation messages to send.

**`raw`** `boolean` *(optional, default: `false`)*  
When `true`, returns the full `CompletionResponse` object instead of just the reply string.

---

### `ask(model, prompt, system?)`

```ts
ask(model: string, prompt: string, system?: string): Promise<string>
```

```ts
// ESM
import { ask } from 'unlimited-ai';
// CJS
const { ask } = require('unlimited-ai');

const reply = await ask('gpt-4o', 'Hello!', 'You are a helpful assistant.');
console.log(reply);
```

One-shot helper that sends a single user message and returns the reply.

**`model`** `string`  
Model ID.

**`prompt`** `string`  
The user message to send.

**`system`** `string` *(optional)*  
System prompt. If provided, it is prepended as a `system` message.

---

### `stream(model, messages)`

```ts
stream(model: string, messages: Message[]): AsyncGenerator<string>
```

```ts
// ESM
import { stream } from 'unlimited-ai';
// CJS
const { stream } = require('unlimited-ai');

for await (const chunk of stream('gpt-4o', [{ role: 'user', content: 'Hello!' }])) {
  process.stdout.write(chunk);
}
```

Sends a chat completion request with streaming enabled and yields reply chunks as they arrive.

**`model`** `string`  
Model ID.

**`messages`** `Message[]`  
Conversation messages to send.

**Yields** `string`  
Successive text chunks of the reply.

---

### `models()`

```ts
models(): Promise<string[]>
```

```ts
// ESM
import { models } from 'unlimited-ai';
// CJS
const { models } = require('unlimited-ai');

const list = await models();
// ['gpt-4o', 'gpt-4-turbo', 'gemini-1.5-flash', ...]
```

Fetches the list of available model IDs from the API.

---

### `searchModels(word)`

```ts
searchModels(word: string): Promise<string[]>
```

```ts
// ESM
import { searchModels, generate } from 'unlimited-ai';
// CJS
const { searchModels, generate } = require('unlimited-ai');

const [model] = await searchModels('gpt-4');
const reply = await generate(model, [{ role: 'user', content: 'Hi!' }]);
```

Fuzzy-searches available model IDs by keyword and returns the closest matches.

**`word`** `string`  
Search keyword.

**Returns** `Promise<string[]>`  
Matching model IDs, ordered by similarity.

---

### `config`

```ts
// ESM
import { config } from 'unlimited-ai';
// CJS
const { config } = require('unlimited-ai');
```

```ts
console.log(config.API_URL);    // https://api.voids.top/v1/chat/completions
console.log(config.MODELS_URL); // https://api.voids.top/v1/models
```

A read-only object with the API endpoint URLs used internally.

**`config.API_URL`** `string`  
Chat completions endpoint. Default: `https://api.voids.top/v1/chat/completions`

**`config.MODELS_URL`** `string`  
Models list endpoint. Default: `https://api.voids.top/v1/models`

---

## Types

```ts
type Role = 'system' | 'user' | 'assistant';

interface Message {
  role: Role;
  content: string;
}

type ConversationStore = Record<string, Message[]>;

interface CompletionResponse {
  id: string;
  object: string;
  created: number;
  model: string;
  choices: Array<{
    index: number;
    message: { role: string; content: string };
    finish_reason: string;
  }>;
  usage: { prompt_tokens: number; completion_tokens: number; total_tokens: number };
}

interface StreamChunk {
  id: string;
  object: string;
  created: number;
  model: string;
  choices: Array<{
    index: number;
    delta: { content?: string; role?: string };
    finish_reason: string | null;
  }>;
}
```

---

## Support

[![Discord](https://discordapp.com/api/guilds/1369635074395344998/widget.png?style=banner2)](https://discord.gg/upSpdDgDha)
