@luanpoppe/ai 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -1
- package/README.md +795 -0
- package/dist/@types/embedding-model-names.d.ts +11 -0
- package/dist/@types/embedding-model-names.d.ts.map +1 -0
- package/dist/@types/embedding-model-names.js +3 -0
- package/dist/@types/embedding-model-names.js.map +1 -0
- package/dist/@types/model-names.d.ts +21 -5
- package/dist/@types/model-names.d.ts.map +1 -1
- package/dist/@types/openrouter-provider.d.ts +25 -0
- package/dist/@types/openrouter-provider.d.ts.map +1 -0
- package/dist/@types/openrouter-provider.js +3 -0
- package/dist/@types/openrouter-provider.js.map +1 -0
- package/dist/ai.d.ts +7 -0
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +56 -5
- package/dist/ai.js.map +1 -1
- package/dist/audio/gemini-transcription.d.ts +4 -0
- package/dist/audio/gemini-transcription.d.ts.map +1 -0
- package/dist/audio/gemini-transcription.js +37 -0
- package/dist/audio/gemini-transcription.js.map +1 -0
- package/dist/audio/index.d.ts +33 -0
- package/dist/audio/index.d.ts.map +1 -0
- package/dist/audio/index.js +43 -0
- package/dist/audio/index.js.map +1 -0
- package/dist/audio/openrouter-client.d.ts +2 -0
- package/dist/audio/openrouter-client.d.ts.map +1 -0
- package/dist/audio/openrouter-client.js +7 -0
- package/dist/audio/openrouter-client.js.map +1 -0
- package/dist/audio/speech-openai.d.ts +4 -0
- package/dist/audio/speech-openai.d.ts.map +1 -0
- package/dist/audio/speech-openai.js +65 -0
- package/dist/audio/speech-openai.js.map +1 -0
- package/dist/audio/speech-openrouter.d.ts +4 -0
- package/dist/audio/speech-openrouter.d.ts.map +1 -0
- package/dist/audio/speech-openrouter.js +89 -0
- package/dist/audio/speech-openrouter.js.map +1 -0
- package/dist/audio/transcription-openai.d.ts +11 -0
- package/dist/audio/transcription-openai.d.ts.map +1 -0
- package/dist/audio/transcription-openai.js +204 -0
- package/dist/audio/transcription-openai.js.map +1 -0
- package/dist/audio/transcription-openrouter.d.ts +4 -0
- package/dist/audio/transcription-openrouter.d.ts.map +1 -0
- package/dist/audio/transcription-openrouter.js +44 -0
- package/dist/audio/transcription-openrouter.js.map +1 -0
- package/dist/audio/types.d.ts +92 -0
- package/dist/audio/types.d.ts.map +1 -0
- package/dist/audio/types.js +6 -0
- package/dist/audio/types.js.map +1 -0
- package/dist/audio/utils.d.ts +5 -0
- package/dist/audio/utils.d.ts.map +1 -0
- package/dist/audio/utils.js +25 -0
- package/dist/audio/utils.js.map +1 -0
- package/dist/embeddings/embed-gemini.d.ts +3 -0
- package/dist/embeddings/embed-gemini.d.ts.map +1 -0
- package/dist/embeddings/embed-gemini.js +38 -0
- package/dist/embeddings/embed-gemini.js.map +1 -0
- package/dist/embeddings/embed-openai.d.ts +3 -0
- package/dist/embeddings/embed-openai.d.ts.map +1 -0
- package/dist/embeddings/embed-openai.js +44 -0
- package/dist/embeddings/embed-openai.js.map +1 -0
- package/dist/embeddings/embed-openrouter.d.ts +3 -0
- package/dist/embeddings/embed-openrouter.d.ts.map +1 -0
- package/dist/embeddings/embed-openrouter.js +47 -0
- package/dist/embeddings/embed-openrouter.js.map +1 -0
- package/dist/embeddings/index.d.ts +19 -0
- package/dist/embeddings/index.d.ts.map +1 -0
- package/dist/embeddings/index.js +36 -0
- package/dist/embeddings/index.js.map +1 -0
- package/dist/embeddings/route.d.ts +12 -0
- package/dist/embeddings/route.d.ts.map +1 -0
- package/dist/embeddings/route.js +42 -0
- package/dist/embeddings/route.js.map +1 -0
- package/dist/embeddings/types.d.ts +37 -0
- package/dist/embeddings/types.d.ts.map +1 -0
- package/dist/embeddings/types.js +6 -0
- package/dist/embeddings/types.js.map +1 -0
- package/dist/embeddings/utils.d.ts +6 -0
- package/dist/embeddings/utils.d.ts.map +1 -0
- package/dist/embeddings/utils.js +25 -0
- package/dist/embeddings/utils.js.map +1 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/langchain/audio-transcription.d.ts +8 -13
- package/dist/langchain/audio-transcription.d.ts.map +1 -1
- package/dist/langchain/audio-transcription.js +8 -92
- package/dist/langchain/audio-transcription.js.map +1 -1
- package/dist/langchain/messages.d.ts +6 -1
- package/dist/langchain/messages.d.ts.map +1 -1
- package/dist/langchain/messages.js +35 -7
- package/dist/langchain/messages.js.map +1 -1
- package/dist/langchain/models.d.ts +38 -0
- package/dist/langchain/models.d.ts.map +1 -1
- package/dist/langchain/models.js +63 -4
- package/dist/langchain/models.js.map +1 -1
- package/dist/langchain/structured-output.d.ts +9 -0
- package/dist/langchain/structured-output.d.ts.map +1 -0
- package/dist/langchain/structured-output.js +70 -0
- package/dist/langchain/structured-output.js.map +1 -0
- package/dist/langchain/tools.d.ts +1 -1
- package/dist/utils/openrouter-client.d.ts +4 -0
- package/dist/utils/openrouter-client.d.ts.map +1 -0
- package/dist/utils/openrouter-client.js +16 -0
- package/dist/utils/openrouter-client.js.map +1 -0
- package/docs/LANGCHAIN_WHISPER_WINDOWS_BUG.md +2 -0
- package/package.json +4 -4
- package/src/@types/embedding-model-names.ts +36 -0
- package/src/@types/model-names.ts +104 -6
- package/src/@types/openrouter-provider.ts +27 -0
- package/src/ai.ts +89 -10
- package/src/audio/gemini-transcription.ts +47 -0
- package/src/audio/index.ts +101 -0
- package/src/audio/openrouter-client.ts +4 -0
- package/src/audio/speech-openai.ts +79 -0
- package/src/audio/speech-openrouter.ts +121 -0
- package/src/audio/transcription-openai.ts +259 -0
- package/src/audio/transcription-openrouter.ts +59 -0
- package/src/audio/types.ts +127 -0
- package/src/audio/utils.ts +20 -0
- package/src/embeddings/embed-gemini.ts +54 -0
- package/src/embeddings/embed-openai.ts +53 -0
- package/src/embeddings/embed-openrouter.ts +64 -0
- package/src/embeddings/index.ts +83 -0
- package/src/embeddings/route.ts +62 -0
- package/src/embeddings/types.ts +53 -0
- package/src/embeddings/utils.ts +21 -0
- package/src/index.ts +52 -5
- package/src/langchain/audio-transcription.ts +19 -90
- package/src/langchain/messages.ts +84 -12
- package/src/langchain/models.ts +130 -3
- package/src/langchain/structured-output.ts +77 -0
- package/src/utils/openrouter-client.ts +10 -0
- package/tests/e2e/README.md +53 -0
- package/tests/e2e/ai-audio-multiprovider.test.ts +118 -0
- package/tests/e2e/ai-deepseek-structured-output.test.ts +133 -0
- package/tests/e2e/ai-embeddings.test.ts +87 -0
- package/tests/e2e/ai-local-models.test.ts +65 -0
- package/tests/unit/audio/index.test.ts +17 -0
- package/tests/unit/audio/speech-openai.test.ts +74 -0
- package/tests/unit/audio/speech-openrouter.test.ts +88 -0
- package/tests/unit/audio/transcription-openai.test.ts +130 -0
- package/tests/unit/audio/transcription-openrouter.test.ts +82 -0
- package/tests/unit/embeddings/embed-gemini.test.ts +52 -0
- package/tests/unit/embeddings/embed-openai.test.ts +68 -0
- package/tests/unit/embeddings/embed-openrouter.test.ts +62 -0
- package/tests/unit/embeddings/index.test.ts +55 -0
- package/tests/unit/index.test.ts +209 -0
- package/tests/unit/langchain/messages.test.ts +37 -0
- package/tests/unit/langchain/models.test.ts +223 -1
- package/tests/unit/langchain/structured-output.test.ts +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
# @luanpoppe/ai
|
|
2
|
+
|
|
3
|
+
TypeScript library for building LLM-powered applications with **OpenAI**, **Google Gemini**, **OpenRouter** (Anthropic, DeepSeek, Qwen, and more), and **local models** via **Ollama** or **OpenAI-compatible** servers (LM Studio, Ollama `/v1`, vLLM, etc.). Built on [LangChain](https://js.langchain.com/) agents and [LangGraph](https://langchain-ai.github.io/langgraph/) checkpointers.
|
|
4
|
+
|
|
5
|
+
It provides a single `AI` facade for chat, structured JSON output, conversation memory, multimodal messages, a dedicated **`AIAudio`** API for speech-to-text (STT) and text-to-speech (TTS), and **`AIEmbeddings`** for vector embeddings via OpenAI, Google Gemini, and OpenRouter.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Quick start](#quick-start)
|
|
13
|
+
- [Configuration](#configuration)
|
|
14
|
+
- [Chat completions (`AI.call`)](#chat-completions-aicall)
|
|
15
|
+
- [Structured output (`AI.callStructuredOutput`)](#structured-output-aicallstructuredoutput)
|
|
16
|
+
- [Model names](#model-names)
|
|
17
|
+
- [Local models (Ollama and LM Studio)](#local-models-ollama-and-lm-studio)
|
|
18
|
+
- [OpenRouter provider routing](#openrouter-provider-routing)
|
|
19
|
+
- [Retry and model fallback](#retry-and-model-fallback)
|
|
20
|
+
- [Messages (`AIMessages`)](#messages-aimessages)
|
|
21
|
+
- [Tools (`AITools`)](#tools-aitools)
|
|
22
|
+
- [Conversation memory (`AIMemory`)](#conversation-memory-aimemory)
|
|
23
|
+
- [Audio (`AIAudio`)](#audio-aiaudio)
|
|
24
|
+
- [Embeddings (`AIEmbeddings`)](#embeddings-aiembeddings)
|
|
25
|
+
- [Utilities](#utilities)
|
|
26
|
+
- [Advanced: raw LangChain agent](#advanced-raw-langchain-agent)
|
|
27
|
+
- [Deprecated exports](#deprecated-exports)
|
|
28
|
+
- [Development](#development)
|
|
29
|
+
- [License](#license)
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @luanpoppe/ai
|
|
37
|
+
# or
|
|
38
|
+
pnpm add @luanpoppe/ai
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Peer requirements:** Node.js 20+ and API keys for the cloud providers you use. Local models only need a running Ollama or OpenAI-compatible server (no cloud API key).
|
|
42
|
+
|
|
43
|
+
**Optional dependencies** (install only if you need that checkpointer):
|
|
44
|
+
|
|
45
|
+
| Package | Use case |
|
|
46
|
+
|---------|----------|
|
|
47
|
+
| `@langchain/langgraph-checkpoint-sqlite` | SQLite persistence |
|
|
48
|
+
| `@langchain/langgraph-checkpoint-postgres` | PostgreSQL persistence |
|
|
49
|
+
| `@langchain/langgraph-checkpoint-redis` | Redis persistence |
|
|
50
|
+
| `@langchain/langgraph-checkpoint-mongodb` + `mongodb` | MongoDB persistence |
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { AI, AIMessages } from "@luanpoppe/ai";
|
|
58
|
+
|
|
59
|
+
const ai = new AI({
|
|
60
|
+
openAIApiKey: process.env.OPENAI_API_KEY,
|
|
61
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
62
|
+
googleGeminiToken: process.env.GOOGLE_GEMINI_TOKEN,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const { text } = await ai.call({
|
|
66
|
+
aiModel: "gpt-4o",
|
|
67
|
+
messages: [AIMessages.human("Say hello in one sentence.")],
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(text);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Configuration
|
|
76
|
+
|
|
77
|
+
Pass API keys to the `AI` constructor. Keys are read per request based on the model prefix you choose.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const ai = new AI({
|
|
81
|
+
openAIApiKey: "sk-...", // models starting with gpt*
|
|
82
|
+
googleGeminiToken: "...", // models starting with gemini*
|
|
83
|
+
openRouterApiKey: "sk-or-...", // models starting with openrouter/*
|
|
84
|
+
aiModelsFallback: ["gpt-4o-mini"], // default fallback list (optional)
|
|
85
|
+
memory: { type: "memory" }, // optional — see AIMemory
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Constructor option | Description |
|
|
90
|
+
|--------------------|-------------|
|
|
91
|
+
| `openAIApiKey` | OpenAI API key |
|
|
92
|
+
| `googleGeminiToken` | Google AI / Gemini API key |
|
|
93
|
+
| `openRouterApiKey` | OpenRouter API key |
|
|
94
|
+
| `ollamaBaseUrl` | Ollama native API base URL for `ollama/*` models (default: `http://127.0.0.1:11434`) |
|
|
95
|
+
| `localBaseUrl` | OpenAI-compatible base URL for `local/*` models (e.g. `http://localhost:1234/v1` for LM Studio) |
|
|
96
|
+
| `localApiKey` | API key sent to the local server (many accept any value; default: `"not-needed"`) |
|
|
97
|
+
| `aiModelsFallback` | Default model list used when `aiModelsFallback` is omitted on `call` |
|
|
98
|
+
| `memory` | `MemoryConfig` or `AIMemory` instance for thread persistence |
|
|
99
|
+
| `checkpointer` | Custom LangGraph `BaseCheckpointSaver` (advanced) |
|
|
100
|
+
|
|
101
|
+
**Per-call override:** `modelConfig.baseUrl` overrides `ollamaBaseUrl` / `localBaseUrl` for that request. For Ollama, `modelConfig.numCtx` sets context window size.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Chat completions (`AI.call`)
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { AI, AIMessages } from "@luanpoppe/ai";
|
|
109
|
+
|
|
110
|
+
const { text, messages } = await ai.call({
|
|
111
|
+
aiModel: "openrouter/anthropic/claude-sonnet-4.6",
|
|
112
|
+
systemPrompt: "You are a helpful assistant.",
|
|
113
|
+
messages: [
|
|
114
|
+
AIMessages.human("What is 2 + 2?"),
|
|
115
|
+
],
|
|
116
|
+
modelConfig: {
|
|
117
|
+
temperature: 0.2,
|
|
118
|
+
maxTokens: 1024,
|
|
119
|
+
// OpenRouter-only:
|
|
120
|
+
openRouterProvider: { sort: "price" },
|
|
121
|
+
openRouterAllowAllProviders: true,
|
|
122
|
+
},
|
|
123
|
+
maxRetries: 3,
|
|
124
|
+
threadId: "user-123", // required when memory/checkpointer is enabled
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Parameters (`AICallParams`)
|
|
129
|
+
|
|
130
|
+
| Field | Description |
|
|
131
|
+
|-------|-------------|
|
|
132
|
+
| `aiModel` | Model id — see [Model names](#model-names) |
|
|
133
|
+
| `messages` | `SystemMessage`, `HumanMessage`, or `AIMessage` (from `AIMessages` or LangChain) |
|
|
134
|
+
| `systemPrompt` | Optional system instructions |
|
|
135
|
+
| `modelConfig` | `maxTokens`, `temperature`, `reasoningEffort` (OpenAI), `baseUrl`, `numCtx` (Ollama), OpenRouter provider options |
|
|
136
|
+
| `maxRetries` | Retries per model via LangChain `modelRetryMiddleware` (default: 3) |
|
|
137
|
+
| `aiModelsFallback` | Try these models if the primary fails after retries |
|
|
138
|
+
| `threadId` | Conversation id when using memory/checkpointer |
|
|
139
|
+
| `agent.tools` | LangChain tools |
|
|
140
|
+
| `agent.middleware` | Extra agent middleware |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Structured output (`AI.callStructuredOutput`)
|
|
145
|
+
|
|
146
|
+
Returns JSON validated with a **Zod** schema.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import z from "zod";
|
|
150
|
+
import { AI, AIMessages } from "@luanpoppe/ai";
|
|
151
|
+
|
|
152
|
+
const schema = z.object({
|
|
153
|
+
name: z.string(),
|
|
154
|
+
age: z.number(),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const { response } = await ai.callStructuredOutput({
|
|
158
|
+
aiModel: "gpt-4o",
|
|
159
|
+
outputSchema: schema,
|
|
160
|
+
messages: [AIMessages.human("Extract: João is 30 years old.")],
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// response: { name: "João", age: 30 }
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### OpenAI / OpenRouter (GPT)
|
|
167
|
+
|
|
168
|
+
Uses LangChain `responseFormat` with JSON schema. Optional fields (`.optional()`) are normalized to `.nullable()` for OpenAI compatibility.
|
|
169
|
+
|
|
170
|
+
### DeepSeek via OpenRouter
|
|
171
|
+
|
|
172
|
+
DeepSeek models do **not** support `json_schema`. The library automatically:
|
|
173
|
+
|
|
174
|
+
1. Sets `response_format: { type: "json_object" }` on OpenRouter
|
|
175
|
+
2. Merges the Zod schema into the system prompt
|
|
176
|
+
3. Parses JSON from the assistant message if `structuredResponse` is missing
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
await ai.callStructuredOutput({
|
|
180
|
+
aiModel: "openrouter/deepseek/deepseek-v4-flash",
|
|
181
|
+
outputSchema: schema,
|
|
182
|
+
messages: [AIMessages.human("Return JSON only.")],
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Local models (`ollama/*`, `local/*`)
|
|
187
|
+
|
|
188
|
+
Structured output uses LangChain `responseFormat` when the model supports it. Capability varies by model; see [Local models](#local-models-ollama-and-lm-studio).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Model names
|
|
193
|
+
|
|
194
|
+
`AIModelNames` is a TypeScript union for autocomplete and safety.
|
|
195
|
+
|
|
196
|
+
| Prefix | Provider | Example |
|
|
197
|
+
|--------|----------|---------|
|
|
198
|
+
| `gpt-*`, `o3`, `o4-mini` | OpenAI | `"gpt-4o"`, `"gpt-5.4"` |
|
|
199
|
+
| `gemini-*` | Google Gemini | `"gemini-2.5-flash"` |
|
|
200
|
+
| `openrouter/<provider>/<model>` | OpenRouter | `"openrouter/deepseek/deepseek-v4-flash"` |
|
|
201
|
+
| `ollama/<model>` | Ollama (native API) | `"ollama/llama3.2"`, `"ollama/qwen2.5:7b"` |
|
|
202
|
+
| `local/<model>` | OpenAI-compatible local server | `"local/meta-llama/Llama-3.2-3B-Instruct"` |
|
|
203
|
+
|
|
204
|
+
OpenRouter slug format: `openrouter/{provider}/{model}` — e.g. `openrouter/openai/gpt-4o`, `openrouter/anthropic/claude-sonnet-4.6`, `openrouter/google/gemini-2.5-flash`.
|
|
205
|
+
|
|
206
|
+
For `ollama/*` and `local/*`, the segment after the prefix must match the model name on your server (`ollama list` or LM Studio model id).
|
|
207
|
+
|
|
208
|
+
Use `AIModels` directly if you need low-level LangChain chat models:
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { AIModels } from "@luanpoppe/ai";
|
|
212
|
+
|
|
213
|
+
const model = AIModels.openrouter({
|
|
214
|
+
model: "deepseek/deepseek-v4-flash",
|
|
215
|
+
apiKey: process.env.OPENROUTER_API_KEY!,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const ollama = AIModels.ollama({
|
|
219
|
+
model: "llama3.2",
|
|
220
|
+
baseUrl: "http://127.0.0.1:11434",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const lmStudio = AIModels.openaiCompatible({
|
|
224
|
+
model: "your-model-id",
|
|
225
|
+
baseURL: "http://localhost:1234/v1",
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Local models (Ollama and LM Studio)
|
|
232
|
+
|
|
233
|
+
Run LLMs on your machine without cloud API keys. Two prefixes cover the common setups:
|
|
234
|
+
|
|
235
|
+
| Prefix | Server | Typical URL |
|
|
236
|
+
|--------|--------|-------------|
|
|
237
|
+
| `ollama/` | [Ollama](https://ollama.com/) native API | `http://127.0.0.1:11434` |
|
|
238
|
+
| `local/` | LM Studio, Ollama OpenAI mode (`/v1`), vLLM, LocalAI | `http://localhost:1234/v1` or `http://127.0.0.1:11434/v1` |
|
|
239
|
+
|
|
240
|
+
### Quick example
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const ai = new AI({
|
|
244
|
+
ollamaBaseUrl: "http://127.0.0.1:11434",
|
|
245
|
+
localBaseUrl: "http://localhost:1234/v1", // LM Studio local server
|
|
246
|
+
localApiKey: "lm-studio", // optional — many servers ignore this
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Ollama: ollama pull llama3.2 first
|
|
250
|
+
const { text: fromOllama } = await ai.call({
|
|
251
|
+
aiModel: "ollama/llama3.2",
|
|
252
|
+
messages: [AIMessages.human("Hello!")],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// LM Studio: start the local server and use the exact model id shown in the UI
|
|
256
|
+
const { text: fromLmStudio } = await ai.call({
|
|
257
|
+
aiModel: "local/your-model-id",
|
|
258
|
+
messages: [AIMessages.human("Hello!")],
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Ollama via OpenAI-compatible API
|
|
263
|
+
|
|
264
|
+
If you prefer a single integration style, point `localBaseUrl` at Ollama’s OpenAI endpoint and use the `local/` prefix:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const ai = new AI({
|
|
268
|
+
localBaseUrl: "http://127.0.0.1:11434/v1",
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await ai.call({
|
|
272
|
+
aiModel: "local/llama3.2",
|
|
273
|
+
messages: [AIMessages.human("Hello!")],
|
|
274
|
+
});
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Structured output and tools
|
|
278
|
+
|
|
279
|
+
- **`callStructuredOutput`:** support depends on the local model. Small models may need plain JSON in the reply; the library does not apply OpenAI schema normalization to `ollama/*` or `local/*`.
|
|
280
|
+
- **Tools (`agent.tools`):** requires a model with function calling (e.g. some Llama 3.1+ variants on Ollama).
|
|
281
|
+
- **Audio:** STT/TTS stays on `AIAudio` (cloud APIs). Local chat does not replace Whisper/TTS.
|
|
282
|
+
- **Fallback:** `aiModelsFallback` can mix local and cloud models, e.g. `["gpt-4o-mini"]` after `ollama/llama3.2`.
|
|
283
|
+
|
|
284
|
+
### Constants
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { DEFAULT_OLLAMA_BASE_URL, DEFAULT_LOCAL_API_KEY } from "@luanpoppe/ai";
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## OpenRouter provider routing
|
|
293
|
+
|
|
294
|
+
Control which upstream hosts OpenRouter uses ([docs](https://openrouter.ai/docs/guides/routing/provider-selection)).
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
await ai.call({
|
|
298
|
+
aiModel: "openrouter/deepseek/deepseek-v4-pro",
|
|
299
|
+
messages: [...],
|
|
300
|
+
modelConfig: {
|
|
301
|
+
// Custom routing (overrides defaults):
|
|
302
|
+
openRouterProvider: {
|
|
303
|
+
only: ["deepseek"],
|
|
304
|
+
sort: "price",
|
|
305
|
+
max_price: { prompt: 1, completion: 2 },
|
|
306
|
+
},
|
|
307
|
+
// Or allow OpenRouter’s default routing for all providers:
|
|
308
|
+
openRouterAllowAllProviders: true,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**Default behavior:** for models whose slug starts with `deepseek/`, the library applies `provider: { only: ["deepseek"] }` unless you pass `openRouterProvider` or `openRouterAllowAllProviders: true`.
|
|
314
|
+
|
|
315
|
+
`resolveOpenRouterProvider(model, config)` is exported if you build custom OpenRouter requests.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Retry and model fallback
|
|
320
|
+
|
|
321
|
+
1. **Retries:** `maxRetries` (default 3) with exponential backoff on the same model.
|
|
322
|
+
2. **Fallback:** if all retries fail, the next model in `aiModelsFallback` is tried.
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
await ai.call({
|
|
326
|
+
aiModel: "openrouter/deepseek/deepseek-v4-flash",
|
|
327
|
+
aiModelsFallback: [
|
|
328
|
+
"openrouter/openai/gpt-4o-mini",
|
|
329
|
+
"gpt-4o-mini",
|
|
330
|
+
],
|
|
331
|
+
messages: [...],
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Messages (`AIMessages`)
|
|
338
|
+
|
|
339
|
+
Helpers for LangChain message types.
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { AIMessages } from "@luanpoppe/ai";
|
|
343
|
+
|
|
344
|
+
AIMessages.system("You are a tutor.");
|
|
345
|
+
AIMessages.human("Explain recursion.");
|
|
346
|
+
AIMessages.ai("Recursion is...");
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Images (multimodal)
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
const msg = AIMessages.humanImage({
|
|
353
|
+
image: { buffer: imageBuffer, filename: "photo.jpg" },
|
|
354
|
+
text: "Describe this image.",
|
|
355
|
+
});
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Audio in chat
|
|
359
|
+
|
|
360
|
+
OpenAI and Anthropic chat APIs do not accept raw audio in the same way as Gemini. This library handles that for you:
|
|
361
|
+
|
|
362
|
+
| `provider` | Behavior |
|
|
363
|
+
|------------|----------|
|
|
364
|
+
| `"gemini"` | Native audio block (multimodal) |
|
|
365
|
+
| `"openai"` | Pre-transcribes with Whisper, sends text |
|
|
366
|
+
| `"openrouter"` | Pre-transcribes via OpenRouter STT, sends text |
|
|
367
|
+
| `"auto"` | OpenAI key → OpenAI; else OpenRouter key → OpenRouter; else Gemini |
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
const msg = await AIMessages.humanAudio({
|
|
371
|
+
audio: { buffer: audioBuffer, filename: "note.mp3" },
|
|
372
|
+
text: "Summarize this recording.",
|
|
373
|
+
provider: "openai",
|
|
374
|
+
openAIApiKey: process.env.OPENAI_API_KEY,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await ai.call({ aiModel: "gpt-4o", messages: [msg] });
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
> **Anthropic:** there is no native public STT/TTS API. Use STT first, then chat. The constant `ANTHROPIC_NO_NATIVE_AUDIO` documents this limitation.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Tools (`AITools`)
|
|
385
|
+
|
|
386
|
+
Wrap LangChain tools with Zod schemas:
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import z from "zod";
|
|
390
|
+
import { AITools } from "@luanpoppe/ai";
|
|
391
|
+
|
|
392
|
+
const tools = new AITools();
|
|
393
|
+
|
|
394
|
+
const getWeather = tools.createTool({
|
|
395
|
+
name: "get_weather",
|
|
396
|
+
description: "Get weather for a city",
|
|
397
|
+
schema: z.object({ city: z.string() }),
|
|
398
|
+
toolFunction: async ({ city }) => `Weather in ${city}: sunny`,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await ai.call({
|
|
402
|
+
aiModel: "gpt-4o",
|
|
403
|
+
messages: [AIMessages.human("Weather in Paris?")],
|
|
404
|
+
agent: { tools: [getWeather] },
|
|
405
|
+
});
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## Conversation memory (`AIMemory`)
|
|
411
|
+
|
|
412
|
+
Persist multi-turn conversations with LangGraph checkpointers.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
const ai = new AI({
|
|
416
|
+
openAIApiKey: process.env.OPENAI_API_KEY!,
|
|
417
|
+
memory: { type: "sqlite", connectionString: "./chat.db" },
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
await ai.call({
|
|
421
|
+
aiModel: "gpt-4o",
|
|
422
|
+
threadId: "session-42",
|
|
423
|
+
messages: [AIMessages.human("My name is Ana.")],
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
await ai.call({
|
|
427
|
+
aiModel: "gpt-4o",
|
|
428
|
+
threadId: "session-42",
|
|
429
|
+
messages: [AIMessages.human("What is my name?")],
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const { messages } = await ai.memory.getHistory("session-42");
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Checkpointer types
|
|
436
|
+
|
|
437
|
+
| `type` | Config |
|
|
438
|
+
|--------|--------|
|
|
439
|
+
| `"memory"` | In-memory (dev/tests) |
|
|
440
|
+
| `"sqlite"` | `{ connectionString: "./data.db" }` or `":memory:"` |
|
|
441
|
+
| `"postgres"` | `{ connectionString: "postgresql://..." }` |
|
|
442
|
+
| `"redis"` | `{ url: "redis://..." }` |
|
|
443
|
+
| `"mongodb"` | `{ url: "..." }` or `{ client: mongoClient }` |
|
|
444
|
+
|
|
445
|
+
When `memory` or `checkpointer` is set, **`threadId` is required** on every `call` / `callStructuredOutput`.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Audio (`AIAudio`)
|
|
450
|
+
|
|
451
|
+
Standalone STT/TTS APIs (not coupled to `AI.call`). Pass API keys as method arguments.
|
|
452
|
+
|
|
453
|
+
### OpenAI — speech-to-text
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { AIAudio } from "@luanpoppe/ai";
|
|
457
|
+
import fs from "fs";
|
|
458
|
+
|
|
459
|
+
const buffer = fs.readFileSync("meeting.mp3");
|
|
460
|
+
|
|
461
|
+
// Simple text
|
|
462
|
+
const text = await AIAudio.transcribeWithWhisper(
|
|
463
|
+
buffer,
|
|
464
|
+
{ languageIn2Digits: "pt" },
|
|
465
|
+
process.env.OPENAI_API_KEY,
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
// Rich response (words, segments, duration)
|
|
469
|
+
const detailed = await AIAudio.transcribeDetailedOpenAI(
|
|
470
|
+
buffer,
|
|
471
|
+
{
|
|
472
|
+
model: "whisper-1",
|
|
473
|
+
responseFormat: "verbose_json",
|
|
474
|
+
timestampGranularities: ["word", "segment"],
|
|
475
|
+
},
|
|
476
|
+
process.env.OPENAI_API_KEY,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Subtitles
|
|
480
|
+
const srt = await AIAudio.transcribeToSrtOpenAI(buffer, {}, apiKey);
|
|
481
|
+
const vtt = await AIAudio.transcribeToVttOpenAI(buffer, {}, apiKey);
|
|
482
|
+
|
|
483
|
+
// Speaker diarization
|
|
484
|
+
const diarized = await AIAudio.transcribeDiarizedOpenAI(buffer, {}, apiKey);
|
|
485
|
+
|
|
486
|
+
// Translate to English
|
|
487
|
+
const english = await AIAudio.translateOpenAI(buffer, {}, apiKey);
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Supported Whisper / transcription models include `whisper-1`, `gpt-4o-transcribe`, `gpt-4o-mini-transcribe`, and `gpt-4o-transcribe-diarize`.
|
|
491
|
+
|
|
492
|
+
### OpenAI — text-to-speech
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
const { audio, contentType } = await AIAudio.speakOpenAI(
|
|
496
|
+
"Hello!",
|
|
497
|
+
{
|
|
498
|
+
model: "gpt-4o-mini-tts",
|
|
499
|
+
voice: "nova",
|
|
500
|
+
instructions: "Speak warmly.", // gpt-4o-mini-tts* only
|
|
501
|
+
responseFormat: "mp3",
|
|
502
|
+
},
|
|
503
|
+
process.env.OPENAI_API_KEY,
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Streaming
|
|
507
|
+
const stream = await AIAudio.speakOpenAIStream("Hello!", { voice: "nova" }, apiKey);
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### OpenRouter — STT
|
|
511
|
+
|
|
512
|
+
Uses `POST https://openrouter.ai/api/v1/audio/transcriptions`.
|
|
513
|
+
|
|
514
|
+
```typescript
|
|
515
|
+
const { text, usage } = await AIAudio.transcribeOpenRouter(
|
|
516
|
+
buffer,
|
|
517
|
+
{
|
|
518
|
+
model: "openai/gpt-4o-mini-transcribe",
|
|
519
|
+
format: "mp3",
|
|
520
|
+
language: "pt",
|
|
521
|
+
openRouterProvider: { sort: "price" },
|
|
522
|
+
},
|
|
523
|
+
process.env.OPENROUTER_API_KEY,
|
|
524
|
+
);
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### OpenRouter — TTS
|
|
528
|
+
|
|
529
|
+
Uses `POST https://openrouter.ai/api/v1/audio/speech`.
|
|
530
|
+
|
|
531
|
+
Discover models:
|
|
532
|
+
|
|
533
|
+
```http
|
|
534
|
+
GET https://openrouter.ai/api/v1/models?output_modalities=speech
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Each model lists `supported_voices` in the response.
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// Gemini TTS (pcm only on OpenRouter)
|
|
541
|
+
const gemini = await AIAudio.speakOpenRouter(
|
|
542
|
+
"Summary of the day.",
|
|
543
|
+
{
|
|
544
|
+
model: "google/gemini-3.1-flash-tts-preview",
|
|
545
|
+
voice: "Kore",
|
|
546
|
+
// responseFormat defaults to "pcm" for gemini*TTS* models
|
|
547
|
+
},
|
|
548
|
+
process.env.OPENROUTER_API_KEY,
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
// Mistral TTS (mp3)
|
|
552
|
+
const mistral = await AIAudio.speakOpenRouter(
|
|
553
|
+
"Hello world.",
|
|
554
|
+
{
|
|
555
|
+
model: "mistralai/voxtral-mini-tts-2603",
|
|
556
|
+
voice: "en_paul_neutral",
|
|
557
|
+
responseFormat: "mp3",
|
|
558
|
+
},
|
|
559
|
+
process.env.OPENROUTER_API_KEY,
|
|
560
|
+
);
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
> Enable the required **providers** in your OpenRouter account settings if you get `No allowed providers are available`.
|
|
564
|
+
|
|
565
|
+
### Gemini — transcription helper
|
|
566
|
+
|
|
567
|
+
Multimodal transcription without `@google/genai` (uses LangChain + your Gemini token):
|
|
568
|
+
|
|
569
|
+
```typescript
|
|
570
|
+
const transcript = await AIAudio.transcribeWithGeminiPrompt(
|
|
571
|
+
buffer,
|
|
572
|
+
{
|
|
573
|
+
model: "gemini-2.5-flash",
|
|
574
|
+
prompt: "Transcribe with approximate [mm:ss] timestamps.",
|
|
575
|
+
googleGeminiToken: process.env.GOOGLE_GEMINI_TOKEN,
|
|
576
|
+
},
|
|
577
|
+
);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### `AIAudio` method reference
|
|
581
|
+
|
|
582
|
+
| Method | Description |
|
|
583
|
+
|--------|-------------|
|
|
584
|
+
| `transcribeWithWhisper` | OpenAI STT → plain text |
|
|
585
|
+
| `transcribeDetailedOpenAI` | OpenAI STT → detailed object |
|
|
586
|
+
| `transcribeToSrtOpenAI` / `transcribeToVttOpenAI` | Subtitle formats |
|
|
587
|
+
| `transcribeDiarizedOpenAI` | Speaker diarization |
|
|
588
|
+
| `translateOpenAI` | Translate audio to English |
|
|
589
|
+
| `transcribeFileWithWhisper` / `transcribeFileDetailedOpenAI` | File path variants |
|
|
590
|
+
| `transcribeOpenRouter` | OpenRouter STT |
|
|
591
|
+
| `speakOpenAI` / `speakOpenAIStream` | OpenAI TTS |
|
|
592
|
+
| `speakOpenRouter` / `speakOpenRouterStream` | OpenRouter TTS |
|
|
593
|
+
| `transcribeWithGeminiPrompt` | Gemini multimodal STT |
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Embeddings (`AIEmbeddings`)
|
|
598
|
+
|
|
599
|
+
Standalone embedding APIs (not coupled to `AI.call`). Use typed model names from `AIEmbeddingModelNames` (separate from chat `AIModelNames`).
|
|
600
|
+
|
|
601
|
+
> **Anthropic:** there is no public Anthropic embeddings API. The package exports `ANTHROPIC_NO_NATIVE_EMBEDDINGS`. Vectorize text with OpenAI, Gemini, or OpenRouter before using `openrouter/anthropic/*` chat models in RAG pipelines.
|
|
602
|
+
|
|
603
|
+
### TypeScript types
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
import {
|
|
607
|
+
AIEmbeddings,
|
|
608
|
+
type AIEmbeddingModelNames,
|
|
609
|
+
type EmbeddingResult,
|
|
610
|
+
type EmbedOptions,
|
|
611
|
+
type EmbedApiKeys,
|
|
612
|
+
ANTHROPIC_NO_NATIVE_EMBEDDINGS,
|
|
613
|
+
} from "@luanpoppe/ai";
|
|
614
|
+
|
|
615
|
+
const model: AIEmbeddingModelNames = "text-embedding-3-small";
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### OpenAI
|
|
619
|
+
|
|
620
|
+
```typescript
|
|
621
|
+
import { AIEmbeddings } from "@luanpoppe/ai";
|
|
622
|
+
|
|
623
|
+
const { embeddings, usage } = await AIEmbeddings.embedOpenAI(
|
|
624
|
+
["Document one.", "Document two."],
|
|
625
|
+
{ model: "text-embedding-3-small", dimensions: 512 },
|
|
626
|
+
process.env.OPENAI_API_KEY,
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
const queryVector = await AIEmbeddings.embedQuery(
|
|
630
|
+
"search query",
|
|
631
|
+
{ model: "text-embedding-3-large" },
|
|
632
|
+
{ openAIApiKey: process.env.OPENAI_API_KEY },
|
|
633
|
+
);
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
Supported direct models: `text-embedding-3-small`, `text-embedding-3-large`, `text-embedding-ada-002`.
|
|
637
|
+
|
|
638
|
+
Optional: `dimensions` (for `text-embedding-3-*`) and `encodingFormat: "float" | "base64"` (default: `"float"`).
|
|
639
|
+
|
|
640
|
+
### Google Gemini
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
const vectors = await AIEmbeddings.embedDocuments(
|
|
644
|
+
["chunk a", "chunk b"],
|
|
645
|
+
{
|
|
646
|
+
model: "gemini-embedding-001",
|
|
647
|
+
taskType: "RETRIEVAL_DOCUMENT",
|
|
648
|
+
dimensions: 768,
|
|
649
|
+
},
|
|
650
|
+
{ googleGeminiToken: process.env.GOOGLE_GEMINI_TOKEN },
|
|
651
|
+
);
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Models: `gemini-embedding-001`, `gemini-embedding-2-preview` (text via this API; multimodal inputs require the Google GenAI SDK directly).
|
|
655
|
+
|
|
656
|
+
For `gemini-embedding-001`, when `dimensions` is set below the native 3072 (e.g. `768`), the library truncates the vector and applies **L2 normalization** so cosine similarity remains meaningful.
|
|
657
|
+
|
|
658
|
+
### OpenRouter
|
|
659
|
+
|
|
660
|
+
OpenAI-compatible `POST /api/v1/embeddings` with provider routing.
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
const result = await AIEmbeddings.embedOpenRouter(
|
|
664
|
+
"Semantic search benchmark.",
|
|
665
|
+
{
|
|
666
|
+
model: "openrouter/openai/text-embedding-3-small",
|
|
667
|
+
openRouterAllowAllProviders: true,
|
|
668
|
+
},
|
|
669
|
+
process.env.OPENROUTER_API_KEY,
|
|
670
|
+
);
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
Discover models:
|
|
674
|
+
|
|
675
|
+
```http
|
|
676
|
+
GET https://openrouter.ai/api/v1/embeddings/models
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Typed OpenRouter examples: `openrouter/google/gemini-embedding-001`, `openrouter/qwen/qwen3-embedding-8b`, `openrouter/BAAI/bge-m3`.
|
|
680
|
+
|
|
681
|
+
Supports `dimensions`, `encodingFormat`, `openRouterProvider`, and `openRouterAllowAllProviders` (enable all providers in your OpenRouter account if routing fails).
|
|
682
|
+
|
|
683
|
+
### Routing helpers
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// Auto-route by model prefix (text-embedding-* | gemini-embedding-* | openrouter/*)
|
|
687
|
+
await AIEmbeddings.embed("text", {
|
|
688
|
+
model: "openrouter/openai/text-embedding-3-small",
|
|
689
|
+
}, {
|
|
690
|
+
openRouterApiKey: process.env.OPENROUTER_API_KEY,
|
|
691
|
+
});
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### `AIEmbeddings` method reference
|
|
695
|
+
|
|
696
|
+
| Method | Description |
|
|
697
|
+
|--------|-------------|
|
|
698
|
+
| `embedOpenAI` | OpenAI `embeddings.create` |
|
|
699
|
+
| `embedGemini` | LangChain `GoogleGenerativeAIEmbeddings` |
|
|
700
|
+
| `embedOpenRouter` | OpenRouter embeddings + `resolveOpenRouterProvider` |
|
|
701
|
+
| `embed` | Routes by model prefix |
|
|
702
|
+
| `embedDocuments` | Batch embed (default Gemini task: `RETRIEVAL_DOCUMENT`) |
|
|
703
|
+
| `embedQuery` | Single query vector (default Gemini task: `RETRIEVAL_QUERY`) |
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Utilities
|
|
708
|
+
|
|
709
|
+
```typescript
|
|
710
|
+
import { AudioUtils, ImageUtils } from "@luanpoppe/ai";
|
|
711
|
+
|
|
712
|
+
AudioUtils.bufferToBase64(buffer);
|
|
713
|
+
AudioUtils.detectAudioMimeType(buffer, "audio.mp3");
|
|
714
|
+
|
|
715
|
+
ImageUtils.bufferToBase64(buffer);
|
|
716
|
+
ImageUtils.detectImageMimeType(buffer, "photo.png");
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Advanced: raw LangChain agent
|
|
722
|
+
|
|
723
|
+
For full control over the LangGraph agent:
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
const { agent } = await ai.getRawAgent({
|
|
727
|
+
aiModel: "gpt-4o",
|
|
728
|
+
messages: [],
|
|
729
|
+
threadId: "t-1",
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const state = await agent.invoke(
|
|
733
|
+
{ messages: [AIMessages.human("Hi")] },
|
|
734
|
+
{ configurable: { thread_id: "t-1" } },
|
|
735
|
+
);
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
---
|
|
739
|
+
|
|
740
|
+
## Deprecated exports
|
|
741
|
+
|
|
742
|
+
These aliases remain for backward compatibility and will be removed in **v2.0.0**:
|
|
743
|
+
|
|
744
|
+
| Deprecated | Use instead |
|
|
745
|
+
|------------|-------------|
|
|
746
|
+
| `Langchain` | `AI` |
|
|
747
|
+
| `LangchainModels` | `AIModels` |
|
|
748
|
+
| `LangchainMessages` | `AIMessages` |
|
|
749
|
+
| `LangchainTools` | `AITools` |
|
|
750
|
+
| `AIAudioTranscription` | `AIAudio` |
|
|
751
|
+
|
|
752
|
+
---
|
|
753
|
+
|
|
754
|
+
## Development
|
|
755
|
+
|
|
756
|
+
From `packages/ai`:
|
|
757
|
+
|
|
758
|
+
```bash
|
|
759
|
+
pnpm install
|
|
760
|
+
pnpm build # compile to dist/
|
|
761
|
+
pnpm test:unit # unit tests (mocked APIs)
|
|
762
|
+
pnpm test:e2e # live API tests — requires .env keys
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Copy `.env.example` to `.env` and set:
|
|
766
|
+
|
|
767
|
+
- `OPENAI_API_KEY`
|
|
768
|
+
- `GOOGLE_GEMINI_TOKEN`
|
|
769
|
+
- `OPENROUTER_API_KEY`
|
|
770
|
+
|
|
771
|
+
Targeted E2E suites:
|
|
772
|
+
|
|
773
|
+
```bash
|
|
774
|
+
pnpm test:e2e -- ai-embeddings # OpenAI + Gemini + OpenRouter embeddings
|
|
775
|
+
pnpm test:e2e -- ai-audio-multiprovider # STT/TTS multiprovider
|
|
776
|
+
pnpm test:e2e -- ai-deepseek-structured-output
|
|
777
|
+
pnpm test:e2e -- ai-local-models # Ollama + LM Studio (optional)
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
For `ai-local-models`:
|
|
781
|
+
|
|
782
|
+
- `OLLAMA_E2E=1` with Ollama running (`ollama serve`, model pulled)
|
|
783
|
+
- `LOCAL_BASE_URL` + `LOCAL_MODEL_NAME` with LM Studio (or similar) server active
|
|
784
|
+
|
|
785
|
+
See `tests/e2e/README.md` for E2E details.
|
|
786
|
+
|
|
787
|
+
### Known issue (Windows + Whisper file upload)
|
|
788
|
+
|
|
789
|
+
If you hit file-upload errors with Whisper on Windows, see [docs/LANGCHAIN_WHISPER_WINDOWS_BUG.md](./docs/LANGCHAIN_WHISPER_WINDOWS_BUG.md). This package uses the OpenAI SDK `toFile` helper directly in `AIAudio` to avoid the LangChain loader bug.
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## License
|
|
794
|
+
|
|
795
|
+
ISC
|