@jz92/ai-provider 0.3.2 → 0.4.0
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/README.md +168 -190
- package/dist/index.cjs +31 -24
- package/dist/index.js +31 -24
- package/package.json +2 -6
- package/scripts/postinstall.js +4 -8
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @jz92/ai-provider
|
|
2
2
|
|
|
3
3
|
A zero-config AI routing layer for Node.js and Next.js projects.
|
|
4
4
|
|
|
5
|
-
Import one function — get Ollama locally and
|
|
5
|
+
Import one function — get Ollama locally and any cloud provider in production, automatically, based on `NODE_ENV`. No provider-switching logic in your feature code, ever.
|
|
6
|
+
|
|
7
|
+
| Environment | Provider | Model | Cost |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| `development` | Ollama (local) | `qwen2.5-coder:14b` | $0 |
|
|
10
|
+
| `test` / CI | Anthropic | `claude-haiku-4-5` | ~$0.001/req |
|
|
11
|
+
| `production` | Anthropic | `claude-sonnet-4-6` | ~$0.03/req |
|
|
6
12
|
|
|
7
13
|
---
|
|
8
14
|
|
|
@@ -25,155 +31,164 @@ You bring Ollama. This package talks to it.
|
|
|
25
31
|
|
|
26
32
|
---
|
|
27
33
|
|
|
28
|
-
##
|
|
34
|
+
## Quick setup for a Next.js project
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
### 1. Install production deps
|
|
31
37
|
|
|
32
38
|
```bash
|
|
33
|
-
|
|
34
|
-
brew install ollama
|
|
35
|
-
|
|
36
|
-
# Start as a background service
|
|
37
|
-
brew services start ollama
|
|
38
|
-
|
|
39
|
-
# Pull a model (one-time, ~9GB)
|
|
40
|
-
ollama pull qwen2.5-coder:14b
|
|
39
|
+
npm install @jz92/ai-provider @ai-sdk/anthropic ai zod
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
### 2. Install local dev dep (Ollama)
|
|
43
|
+
|
|
44
44
|
```bash
|
|
45
|
-
|
|
45
|
+
npm install ollama-ai-provider --save-dev --legacy-peer-deps
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
> **Why `--legacy-peer-deps`?**
|
|
49
|
+
> `ollama-ai-provider@1.2.0` has a peer dependency conflict with `zod@4`.
|
|
50
|
+
> This flag is required until `ollama-ai-provider` releases a `zod@4` compatible version.
|
|
51
|
+
> This is an upstream issue and **does not affect production** — `devDependencies` are
|
|
52
|
+
> never installed on Vercel or AWS.
|
|
53
|
+
> Track the upstream issue: [ollama-ai-provider on GitHub](https://github.com/sgomez/ollama-ai-provider)
|
|
54
|
+
|
|
55
|
+
### 3. Your `package.json` should look like this
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@jz92/ai-provider": "^0.3.2",
|
|
61
|
+
"@ai-sdk/anthropic": "^4.0.0",
|
|
62
|
+
"ai": "^7.0.0",
|
|
63
|
+
"zod": "^4.0.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"ollama-ai-provider": "^1.2.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
This ensures:
|
|
72
|
+
- **Vercel / AWS** — `ollama-ai-provider` is never installed, no conflict, clean build
|
|
73
|
+
- **Local dev** — `ollama-ai-provider` installed, Ollama runs free at `localhost:11434`
|
|
51
74
|
|
|
52
|
-
|
|
75
|
+
### 4. Set up Ollama locally (first time only)
|
|
53
76
|
|
|
54
77
|
```bash
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
# Install Ollama
|
|
79
|
+
brew install ollama
|
|
57
80
|
|
|
58
|
-
|
|
81
|
+
# Start as a background service
|
|
82
|
+
brew services start ollama
|
|
59
83
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
65
|
-
npm install ollama-ai-provider
|
|
66
|
-
|
|
67
|
-
# Cloud — install only the provider(s) you use
|
|
68
|
-
npm install @ai-sdk/anthropic # → ANTHROPIC_API_KEY
|
|
69
|
-
npm install @ai-sdk/openai # → OPENAI_API_KEY
|
|
70
|
-
npm install @ai-sdk/google # → GOOGLE_GENERATIVE_AI_API_KEY
|
|
71
|
-
npm install @ai-sdk/groq # → GROQ_API_KEY
|
|
72
|
-
npm install @ai-sdk/mistral # → MISTRAL_API_KEY
|
|
84
|
+
# Pull the default model (~9GB)
|
|
85
|
+
ollama pull qwen2.5-coder:14b
|
|
86
|
+
|
|
87
|
+
# Verify
|
|
88
|
+
curl http://localhost:11434 # → Ollama is running
|
|
73
89
|
```
|
|
74
90
|
|
|
75
|
-
|
|
91
|
+
### 5. Set environment variables
|
|
76
92
|
|
|
77
|
-
|
|
93
|
+
```bash
|
|
94
|
+
# .env.development (commit the .example, not the real file)
|
|
95
|
+
NODE_ENV=development
|
|
96
|
+
OLLAMA_BASE_URL=http://localhost:11434
|
|
97
|
+
OLLAMA_MODEL=qwen2.5-coder:14b
|
|
98
|
+
AI_LOG_USAGE=true
|
|
78
99
|
|
|
79
|
-
|
|
100
|
+
# .env.production (set as secrets in Vercel / AWS — never commit)
|
|
101
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
102
|
+
```
|
|
80
103
|
|
|
81
|
-
|
|
104
|
+
On Vercel, set `ANTHROPIC_API_KEY` in **Project Settings → Environment Variables**.
|
|
105
|
+
`NODE_ENV=production` is set automatically.
|
|
106
|
+
|
|
107
|
+
### 6. Use it
|
|
82
108
|
|
|
83
109
|
```typescript
|
|
84
|
-
import { generateStructured, generatePlainText } from '@
|
|
110
|
+
import { generateStructured, generatePlainText } from '@jz92/ai-provider'
|
|
85
111
|
import { z } from 'zod'
|
|
86
112
|
|
|
87
|
-
// Structured output — returns validated, typed JSON
|
|
88
113
|
const result = await generateStructured({
|
|
89
114
|
systemPrompt: 'Extract data. Respond in JSON only.',
|
|
90
|
-
prompt:
|
|
115
|
+
prompt: userInput,
|
|
91
116
|
schema: z.object({ name: z.string(), city: z.string() }),
|
|
92
|
-
cacheKey: `extract:${
|
|
117
|
+
cacheKey: `extract:${userInput}`,
|
|
93
118
|
})
|
|
94
119
|
|
|
95
|
-
console.log(result.data) // {
|
|
120
|
+
console.log(result.data) // { name: 'Alex', city: 'London' }
|
|
96
121
|
console.log(result.provider) // 'ollama' locally · 'anthropic' in prod
|
|
97
122
|
console.log(result.fromCache) // true on cache hit
|
|
98
|
-
|
|
99
|
-
// Plain text output
|
|
100
|
-
const result = await generatePlainText({
|
|
101
|
-
systemPrompt: 'You are a helpful assistant.',
|
|
102
|
-
prompt: 'Summarise this in one sentence...',
|
|
103
|
-
})
|
|
104
123
|
```
|
|
105
124
|
|
|
106
125
|
Your code is identical in every environment. The provider switches automatically.
|
|
107
126
|
|
|
108
127
|
---
|
|
109
128
|
|
|
110
|
-
##
|
|
129
|
+
## Switching cloud providers
|
|
111
130
|
|
|
112
|
-
|
|
113
|
-
|---|---|---|---|
|
|
114
|
-
| `development` | Ollama (local) | `qwen2.5-coder:14b` | $0 |
|
|
115
|
-
| `test` / CI | Anthropic | `claude-haiku-4-5` | ~$0.001/req |
|
|
116
|
-
| `production` | Anthropic | `claude-sonnet-4-6` | ~$0.03/req |
|
|
131
|
+
Switching from Anthropic to OpenAI (or any other provider) is one env var and one package:
|
|
117
132
|
|
|
118
|
-
|
|
133
|
+
```bash
|
|
134
|
+
npm install @ai-sdk/openai
|
|
135
|
+
```
|
|
119
136
|
|
|
120
137
|
```bash
|
|
121
|
-
#
|
|
122
|
-
AI_PROVIDER=openai
|
|
138
|
+
# .env.production
|
|
139
|
+
AI_PROVIDER=openai
|
|
140
|
+
OPENAI_API_KEY=sk-...
|
|
141
|
+
```
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
AI_MODEL=gpt-4o npm run dev
|
|
143
|
+
No code changes. Your `generateStructured()` calls are unchanged.
|
|
126
144
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
145
|
+
### Supported providers
|
|
146
|
+
|
|
147
|
+
| Provider | Package | Env var |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| Anthropic (default) | `@ai-sdk/anthropic` | `ANTHROPIC_API_KEY` |
|
|
150
|
+
| OpenAI | `@ai-sdk/openai` | `OPENAI_API_KEY` |
|
|
151
|
+
| Google Gemini | `@ai-sdk/google` | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
152
|
+
| Groq | `@ai-sdk/groq` | `GROQ_API_KEY` |
|
|
153
|
+
| Mistral | `@ai-sdk/mistral` | `MISTRAL_API_KEY` |
|
|
154
|
+
| Ollama (local) | `ollama-ai-provider` | — |
|
|
130
155
|
|
|
131
156
|
---
|
|
132
157
|
|
|
133
|
-
##
|
|
158
|
+
## Usage
|
|
134
159
|
|
|
135
|
-
|
|
160
|
+
### `generateStructured` — typed JSON output
|
|
136
161
|
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
```typescript
|
|
163
|
+
import { generateStructured } from '@jz92/ai-provider'
|
|
164
|
+
import { z } from 'zod'
|
|
139
165
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
const result = await generateStructured({
|
|
167
|
+
systemPrompt: 'You are a data extraction assistant. Respond in JSON only.',
|
|
168
|
+
prompt: 'Extract name and city from: "Hi I am Alex from London"',
|
|
169
|
+
schema: z.object({ name: z.string(), city: z.string() }),
|
|
170
|
+
cacheKey: 'extract:alex', // optional — repeat calls skip the API
|
|
171
|
+
maxInputTokens: 4000, // optional — throws if exceeded
|
|
172
|
+
})
|
|
147
173
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
console.log(result.data) // { name: 'Alex', city: 'London' }
|
|
175
|
+
console.log(result.provider) // 'ollama' | 'anthropic' | 'openai' ...
|
|
176
|
+
console.log(result.fromCache) // true if served from response cache
|
|
177
|
+
console.log(result.usage) // { inputTokens, outputTokens, cachedTokens }
|
|
152
178
|
```
|
|
153
|
-
`NODE_ENV=production` is set automatically by Vercel. Done.
|
|
154
179
|
|
|
155
|
-
###
|
|
156
|
-
```bash
|
|
157
|
-
ANTHROPIC_API_KEY=sk-ant-...
|
|
158
|
-
NODE_ENV=production
|
|
159
|
-
```
|
|
180
|
+
### `generatePlainText` — unstructured text output
|
|
160
181
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
env:
|
|
164
|
-
NODE_ENV: test
|
|
165
|
-
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
166
|
-
```
|
|
182
|
+
```typescript
|
|
183
|
+
import { generatePlainText } from '@jz92/ai-provider'
|
|
167
184
|
|
|
168
|
-
|
|
185
|
+
const result = await generatePlainText({
|
|
186
|
+
systemPrompt: 'You are a helpful assistant.',
|
|
187
|
+
prompt: 'Summarise this in one sentence...',
|
|
188
|
+
})
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
| Anthropic | `ANTHROPIC_API_KEY` |
|
|
173
|
-
| OpenAI | `OPENAI_API_KEY` |
|
|
174
|
-
| Google | `GOOGLE_GENERATIVE_AI_API_KEY` |
|
|
175
|
-
| Groq | `GROQ_API_KEY` |
|
|
176
|
-
| Mistral | `MISTRAL_API_KEY` |
|
|
190
|
+
console.log(result.data) // the text response
|
|
191
|
+
```
|
|
177
192
|
|
|
178
193
|
---
|
|
179
194
|
|
|
@@ -193,107 +208,30 @@ env:
|
|
|
193
208
|
|
|
194
209
|
---
|
|
195
210
|
|
|
196
|
-
## Architecture
|
|
197
|
-
|
|
198
|
-
```mermaid
|
|
199
|
-
flowchart TD
|
|
200
|
-
A["Your feature code\ngenerateStructured() · generatePlainText()"]
|
|
201
|
-
B["Gateway\ncache · token guard · retry · timeout"]
|
|
202
|
-
C["Provider resolver\nreads NODE_ENV + overrides"]
|
|
203
|
-
|
|
204
|
-
D["development\nNODE_ENV=development"]
|
|
205
|
-
E["test / CI\nNODE_ENV=test"]
|
|
206
|
-
F["production\nNODE_ENV=production"]
|
|
207
|
-
|
|
208
|
-
G["Ollama\nlocalhost:11434 · free"]
|
|
209
|
-
H["Anthropic · OpenAI\nGoogle · Groq · Mistral"]
|
|
210
|
-
|
|
211
|
-
A --> B --> C
|
|
212
|
-
C --> D --> G
|
|
213
|
-
C --> E --> H
|
|
214
|
-
C --> F --> H
|
|
215
|
-
|
|
216
|
-
style A fill:#F1EFE8,stroke:#5F5E5A
|
|
217
|
-
style B fill:#EEEDFE,stroke:#534AB7
|
|
218
|
-
style C fill:#EEEDFE,stroke:#534AB7
|
|
219
|
-
style D fill:#E1F5EE,stroke:#0F6E56
|
|
220
|
-
style E fill:#FAEEDA,stroke:#854F0B
|
|
221
|
-
style F fill:#FAECE7,stroke:#993C1D
|
|
222
|
-
style G fill:#E1F5EE,stroke:#0F6E56
|
|
223
|
-
style H fill:#FAECE7,stroke:#993C1D
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
---
|
|
227
|
-
|
|
228
211
|
## What's included in the gateway
|
|
229
212
|
|
|
230
213
|
Every request passes through the gateway regardless of provider:
|
|
231
214
|
|
|
232
|
-
- **Response cache** — same `cacheKey` skips the API entirely. Bounded at 500 entries, 5 min TTL.
|
|
233
|
-
- **Token budget guard** —
|
|
234
|
-
- **Smart retry** — retries only transient errors (
|
|
235
|
-
- **Hard timeout** — 60s for Ollama
|
|
236
|
-
- **Prompt caching** — automatically enabled for Anthropic in production.
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## Custom Ollama model variants
|
|
241
|
-
|
|
242
|
-
You can bake your system prompt into a named local model using an Ollama `Modelfile`. This mirrors what prompt caching does in production — the stable context is paid once, not on every request.
|
|
243
|
-
|
|
244
|
-
```dockerfile
|
|
245
|
-
# modelfiles/Modelfile.my-feature
|
|
246
|
-
FROM qwen2.5-coder:14b
|
|
247
|
-
|
|
248
|
-
SYSTEM """
|
|
249
|
-
Your stable system prompt here.
|
|
250
|
-
Respond only in JSON.
|
|
251
|
-
"""
|
|
252
|
-
|
|
253
|
-
PARAMETER temperature 0.1
|
|
254
|
-
PARAMETER num_predict 1024
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
```bash
|
|
258
|
-
ollama create my-feature -f modelfiles/Modelfile.my-feature
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
```bash
|
|
262
|
-
# .env.development
|
|
263
|
-
OLLAMA_MODEL=my-feature
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
A `Modelfile.template` is included at `node_modules/@jithin/ai-provider/modelfiles-template/Modelfile.template`.
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
## Security
|
|
271
|
-
|
|
272
|
-
This package reads API keys from environment variables and passes them directly to the provider SDK over HTTPS. Keys are never logged, stored, or transmitted by this package.
|
|
273
|
-
|
|
274
|
-
Your responsibilities as a consumer:
|
|
275
|
-
|
|
276
|
-
- Never commit `.env` or `.env.local` — add both to `.gitignore`
|
|
277
|
-
- Never log `process.env` in application code
|
|
278
|
-
- Use `.env.example` with placeholder values for documentation
|
|
279
|
-
- Use deployment secrets (Vercel dashboard / AWS Secrets Manager) in production
|
|
280
|
-
- Rotate keys immediately if accidentally exposed
|
|
215
|
+
- **Response cache** — same `cacheKey` skips the API entirely. Bounded at 500 entries, 5 min TTL.
|
|
216
|
+
- **Token budget guard** — throws before the API call if input exceeds `maxInputTokens`.
|
|
217
|
+
- **Smart retry** — retries only transient errors (429, 500, timeout). Never retries auth or billing failures.
|
|
218
|
+
- **Hard timeout** — 60s for Ollama, 30s for cloud. Override with `AI_TIMEOUT_MS`.
|
|
219
|
+
- **Prompt caching** — automatically enabled for Anthropic in production. Reduces input costs by ~90% on repeat calls.
|
|
220
|
+
- **Usage logging** — formatted terminal output in development showing provider, model, and token counts.
|
|
281
221
|
|
|
282
222
|
---
|
|
283
223
|
|
|
284
224
|
## Error handling
|
|
285
225
|
|
|
286
|
-
The package throws `AIProviderError` with a typed `code` and a clear actionable message. You never see raw SDK errors.
|
|
287
|
-
|
|
288
226
|
```typescript
|
|
289
|
-
import { generateStructured, AIProviderError } from '@
|
|
227
|
+
import { generateStructured, AIProviderError } from '@jz92/ai-provider'
|
|
290
228
|
|
|
291
229
|
try {
|
|
292
230
|
const result = await generateStructured({ ... })
|
|
293
231
|
} catch (err) {
|
|
294
232
|
if (err instanceof AIProviderError) {
|
|
295
|
-
console.error(err.code) // 'AUTH_ERROR' | '
|
|
296
|
-
console.error(err.message) //
|
|
233
|
+
console.error(err.code) // 'AUTH_ERROR' | 'RATE_LIMIT' | 'TIMEOUT' | etc.
|
|
234
|
+
console.error(err.message) // actionable message with exact steps to fix
|
|
297
235
|
}
|
|
298
236
|
}
|
|
299
237
|
```
|
|
@@ -342,6 +280,49 @@ try {
|
|
|
342
280
|
|
|
343
281
|
---
|
|
344
282
|
|
|
283
|
+
## Architecture
|
|
284
|
+
|
|
285
|
+
```mermaid
|
|
286
|
+
flowchart TD
|
|
287
|
+
A["Your feature code\ngenerateStructured() · generatePlainText()"]
|
|
288
|
+
B["Gateway\ncache · token guard · retry · timeout"]
|
|
289
|
+
C["Provider resolver\nreads NODE_ENV + overrides"]
|
|
290
|
+
|
|
291
|
+
D["development\nNODE_ENV=development"]
|
|
292
|
+
E["test / CI\nNODE_ENV=test"]
|
|
293
|
+
F["production\nNODE_ENV=production"]
|
|
294
|
+
|
|
295
|
+
G["Ollama\nlocalhost:11434 · free"]
|
|
296
|
+
H["Anthropic · OpenAI\nGoogle · Groq · Mistral"]
|
|
297
|
+
|
|
298
|
+
A --> B --> C
|
|
299
|
+
C --> D --> G
|
|
300
|
+
C --> E --> H
|
|
301
|
+
C --> F --> H
|
|
302
|
+
|
|
303
|
+
style A fill:#F1EFE8,stroke:#5F5E5A
|
|
304
|
+
style B fill:#EEEDFE,stroke:#534AB7
|
|
305
|
+
style C fill:#EEEDFE,stroke:#534AB7
|
|
306
|
+
style D fill:#E1F5EE,stroke:#0F6E56
|
|
307
|
+
style E fill:#FAEEDA,stroke:#854F0B
|
|
308
|
+
style F fill:#FAECE7,stroke:#993C1D
|
|
309
|
+
style G fill:#E1F5EE,stroke:#0F6E56
|
|
310
|
+
style H fill:#FAECE7,stroke:#993C1D
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## Security
|
|
316
|
+
|
|
317
|
+
This package reads API keys from environment variables and passes them directly to the provider SDK over HTTPS. Keys are never logged, stored, or transmitted by this package.
|
|
318
|
+
|
|
319
|
+
- Never commit `.env` or `.env.local` — add both to `.gitignore`
|
|
320
|
+
- Never log `process.env` in application code
|
|
321
|
+
- Use deployment secrets (Vercel / AWS Secrets Manager) in production
|
|
322
|
+
- Rotate keys immediately if accidentally exposed
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
345
326
|
## Running tests
|
|
346
327
|
|
|
347
328
|
```bash
|
|
@@ -349,19 +330,16 @@ try {
|
|
|
349
330
|
npm test
|
|
350
331
|
```
|
|
351
332
|
|
|
352
|
-
Expected:
|
|
333
|
+
Expected: 27 passed.
|
|
353
334
|
|
|
354
335
|
---
|
|
355
336
|
|
|
356
|
-
##
|
|
337
|
+
## Reference implementation
|
|
357
338
|
|
|
358
|
-
|
|
359
|
-
npm run build
|
|
360
|
-
npm publish --access public
|
|
361
|
-
```
|
|
339
|
+
See [portfolio-lab](https://github.com/jithinjohnzachariah92/portfolio-lab) for a working Next.js project using this package across multiple AI-powered features.
|
|
362
340
|
|
|
363
341
|
---
|
|
364
342
|
|
|
365
343
|
## Repo
|
|
366
344
|
|
|
367
|
-
[github.com/jithinjohnzachariah92/ai-provider](https://github.com/jithinjohnzachariah92/ai-provider)
|
|
345
|
+
[github.com/jithinjohnzachariah92/ai-provider](https://github.com/jithinjohnzachariah92/ai-provider) · [npmjs.com/package/@jz92/ai-provider](https://www.npmjs.com/package/@jz92/ai-provider)
|
package/dist/index.cjs
CHANGED
|
@@ -227,13 +227,17 @@ async function buildModel(config) {
|
|
|
227
227
|
async function buildOllamaModel(config) {
|
|
228
228
|
await assertOllamaReachable(config.baseURL ?? "http://localhost:11434");
|
|
229
229
|
try {
|
|
230
|
-
const {
|
|
231
|
-
const ollama =
|
|
230
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
231
|
+
const ollama = createOpenAI({
|
|
232
|
+
baseURL: `${config.baseURL}/v1`,
|
|
233
|
+
apiKey: "ollama"
|
|
234
|
+
// required by the client but not validated by Ollama
|
|
235
|
+
});
|
|
232
236
|
return ollama(config.model);
|
|
233
237
|
} catch (err) {
|
|
234
238
|
if (isModuleNotFound(err)) {
|
|
235
239
|
throw new AIProviderError(
|
|
236
|
-
"[ai-provider]
|
|
240
|
+
"[ai-provider] @ai-sdk/openai is not installed.\nRun: npm install @ai-sdk/openai",
|
|
237
241
|
"UNKNOWN",
|
|
238
242
|
"ollama"
|
|
239
243
|
);
|
|
@@ -413,12 +417,13 @@ async function generateStructured(options) {
|
|
|
413
417
|
}
|
|
414
418
|
guardTokenBudget(options.systemPrompt + options.prompt, options.maxInputTokens ?? 8e3);
|
|
415
419
|
const model = await buildModel(config);
|
|
416
|
-
const messages = buildMessages(config, options.systemPrompt, options.prompt);
|
|
420
|
+
const { system, messages } = buildMessages(config, options.systemPrompt, options.prompt);
|
|
417
421
|
const timeout = TIMEOUT_MS[config.provider];
|
|
418
422
|
const result = await withRetry(
|
|
419
423
|
() => withTimeout(
|
|
420
424
|
() => (0, import_ai.generateText)({
|
|
421
425
|
model,
|
|
426
|
+
system,
|
|
422
427
|
messages,
|
|
423
428
|
output: import_ai.Output.object({ schema: options.schema }),
|
|
424
429
|
maxOutputTokens: config.maxTokens
|
|
@@ -437,11 +442,11 @@ async function generatePlainText(options) {
|
|
|
437
442
|
}
|
|
438
443
|
guardTokenBudget(options.systemPrompt + options.prompt, options.maxInputTokens ?? 8e3);
|
|
439
444
|
const model = await buildModel(config);
|
|
440
|
-
const messages = buildMessages(config, options.systemPrompt, options.prompt);
|
|
445
|
+
const { system, messages } = buildMessages(config, options.systemPrompt, options.prompt);
|
|
441
446
|
const timeout = TIMEOUT_MS[config.provider];
|
|
442
447
|
const result = await withRetry(
|
|
443
448
|
() => withTimeout(
|
|
444
|
-
() => (0, import_ai.generateText)({ model, messages, maxOutputTokens: config.maxTokens }),
|
|
449
|
+
() => (0, import_ai.generateText)({ model, system, messages, maxOutputTokens: config.maxTokens }),
|
|
445
450
|
timeout
|
|
446
451
|
),
|
|
447
452
|
config.provider
|
|
@@ -450,26 +455,28 @@ async function generatePlainText(options) {
|
|
|
450
455
|
}
|
|
451
456
|
function buildMessages(config, systemPrompt, userPrompt) {
|
|
452
457
|
if (config.usePromptCache) {
|
|
453
|
-
return
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
458
|
+
return {
|
|
459
|
+
system: systemPrompt,
|
|
460
|
+
messages: [
|
|
461
|
+
{
|
|
462
|
+
role: "user",
|
|
463
|
+
content: [
|
|
464
|
+
{
|
|
465
|
+
type: "text",
|
|
466
|
+
text: userPrompt,
|
|
467
|
+
providerOptions: {
|
|
468
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
469
|
+
}
|
|
463
470
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
471
|
+
]
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
};
|
|
468
475
|
}
|
|
469
|
-
return
|
|
470
|
-
|
|
471
|
-
{ role: "user", content: userPrompt }
|
|
472
|
-
|
|
476
|
+
return {
|
|
477
|
+
system: systemPrompt,
|
|
478
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
479
|
+
};
|
|
473
480
|
}
|
|
474
481
|
function buildResponse(data, usage, config, cacheKey) {
|
|
475
482
|
if (cacheKey) responseCache.set(cacheKey, data);
|
package/dist/index.js
CHANGED
|
@@ -187,13 +187,17 @@ async function buildModel(config) {
|
|
|
187
187
|
async function buildOllamaModel(config) {
|
|
188
188
|
await assertOllamaReachable(config.baseURL ?? "http://localhost:11434");
|
|
189
189
|
try {
|
|
190
|
-
const {
|
|
191
|
-
const ollama =
|
|
190
|
+
const { createOpenAI } = await import("@ai-sdk/openai");
|
|
191
|
+
const ollama = createOpenAI({
|
|
192
|
+
baseURL: `${config.baseURL}/v1`,
|
|
193
|
+
apiKey: "ollama"
|
|
194
|
+
// required by the client but not validated by Ollama
|
|
195
|
+
});
|
|
192
196
|
return ollama(config.model);
|
|
193
197
|
} catch (err) {
|
|
194
198
|
if (isModuleNotFound(err)) {
|
|
195
199
|
throw new AIProviderError(
|
|
196
|
-
"[ai-provider]
|
|
200
|
+
"[ai-provider] @ai-sdk/openai is not installed.\nRun: npm install @ai-sdk/openai",
|
|
197
201
|
"UNKNOWN",
|
|
198
202
|
"ollama"
|
|
199
203
|
);
|
|
@@ -373,12 +377,13 @@ async function generateStructured(options) {
|
|
|
373
377
|
}
|
|
374
378
|
guardTokenBudget(options.systemPrompt + options.prompt, options.maxInputTokens ?? 8e3);
|
|
375
379
|
const model = await buildModel(config);
|
|
376
|
-
const messages = buildMessages(config, options.systemPrompt, options.prompt);
|
|
380
|
+
const { system, messages } = buildMessages(config, options.systemPrompt, options.prompt);
|
|
377
381
|
const timeout = TIMEOUT_MS[config.provider];
|
|
378
382
|
const result = await withRetry(
|
|
379
383
|
() => withTimeout(
|
|
380
384
|
() => generateText({
|
|
381
385
|
model,
|
|
386
|
+
system,
|
|
382
387
|
messages,
|
|
383
388
|
output: Output.object({ schema: options.schema }),
|
|
384
389
|
maxOutputTokens: config.maxTokens
|
|
@@ -397,11 +402,11 @@ async function generatePlainText(options) {
|
|
|
397
402
|
}
|
|
398
403
|
guardTokenBudget(options.systemPrompt + options.prompt, options.maxInputTokens ?? 8e3);
|
|
399
404
|
const model = await buildModel(config);
|
|
400
|
-
const messages = buildMessages(config, options.systemPrompt, options.prompt);
|
|
405
|
+
const { system, messages } = buildMessages(config, options.systemPrompt, options.prompt);
|
|
401
406
|
const timeout = TIMEOUT_MS[config.provider];
|
|
402
407
|
const result = await withRetry(
|
|
403
408
|
() => withTimeout(
|
|
404
|
-
() => generateText({ model, messages, maxOutputTokens: config.maxTokens }),
|
|
409
|
+
() => generateText({ model, system, messages, maxOutputTokens: config.maxTokens }),
|
|
405
410
|
timeout
|
|
406
411
|
),
|
|
407
412
|
config.provider
|
|
@@ -410,26 +415,28 @@ async function generatePlainText(options) {
|
|
|
410
415
|
}
|
|
411
416
|
function buildMessages(config, systemPrompt, userPrompt) {
|
|
412
417
|
if (config.usePromptCache) {
|
|
413
|
-
return
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
418
|
+
return {
|
|
419
|
+
system: systemPrompt,
|
|
420
|
+
messages: [
|
|
421
|
+
{
|
|
422
|
+
role: "user",
|
|
423
|
+
content: [
|
|
424
|
+
{
|
|
425
|
+
type: "text",
|
|
426
|
+
text: userPrompt,
|
|
427
|
+
providerOptions: {
|
|
428
|
+
anthropic: { cacheControl: { type: "ephemeral" } }
|
|
429
|
+
}
|
|
423
430
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
]
|
|
434
|
+
};
|
|
428
435
|
}
|
|
429
|
-
return
|
|
430
|
-
|
|
431
|
-
{ role: "user", content: userPrompt }
|
|
432
|
-
|
|
436
|
+
return {
|
|
437
|
+
system: systemPrompt,
|
|
438
|
+
messages: [{ role: "user", content: userPrompt }]
|
|
439
|
+
};
|
|
433
440
|
}
|
|
434
441
|
function buildResponse(data, usage, config, cacheKey) {
|
|
435
442
|
if (cacheKey) responseCache.set(cacheKey, data);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jz92/ai-provider",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Environment-aware AI provider for portfolio projects. Local dev → Ollama, deployed → any cloud provider.",
|
|
5
5
|
"author": "jz92",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
"@ai-sdk/openai": ">=3.0.0",
|
|
38
38
|
"@ai-sdk/google": ">=3.0.0",
|
|
39
39
|
"@ai-sdk/groq": ">=3.0.0",
|
|
40
|
-
"@ai-sdk/mistral": ">=3.0.0"
|
|
41
|
-
"ollama-ai-provider": ">=1.0.0"
|
|
40
|
+
"@ai-sdk/mistral": ">=3.0.0"
|
|
42
41
|
},
|
|
43
42
|
"peerDependenciesMeta": {
|
|
44
43
|
"@ai-sdk/anthropic": {
|
|
@@ -55,9 +54,6 @@
|
|
|
55
54
|
},
|
|
56
55
|
"@ai-sdk/mistral": {
|
|
57
56
|
"optional": true
|
|
58
|
-
},
|
|
59
|
-
"ollama-ai-provider": {
|
|
60
|
-
"optional": true
|
|
61
57
|
}
|
|
62
58
|
},
|
|
63
59
|
"devDependencies": {
|
package/scripts/postinstall.js
CHANGED
|
@@ -6,7 +6,6 @@ const cyan = '\x1b[36m'
|
|
|
6
6
|
const yellow = '\x1b[33m'
|
|
7
7
|
const green = '\x1b[32m'
|
|
8
8
|
const dim = '\x1b[2m'
|
|
9
|
-
const red = '\x1b[31m'
|
|
10
9
|
|
|
11
10
|
console.log(`
|
|
12
11
|
${bold}@jz92/ai-provider${reset} installed successfully.
|
|
@@ -23,13 +22,10 @@ ${bold}Cloud providers — install only what you use:${reset}
|
|
|
23
22
|
${cyan}npm install @ai-sdk/groq${reset} ${dim}→ GROQ_API_KEY https://console.groq.com/keys${reset}
|
|
24
23
|
${cyan}npm install @ai-sdk/mistral${reset} ${dim}→ MISTRAL_API_KEY https://console.mistral.ai${reset}
|
|
25
24
|
|
|
26
|
-
${bold}Local dev with Ollama
|
|
27
|
-
${
|
|
28
|
-
${dim}
|
|
29
|
-
|
|
30
|
-
${red}Note:${reset} ollama-ai-provider@1.2.0 conflicts with zod@4.
|
|
31
|
-
${dim}--legacy-peer-deps is required until ollama-ai-provider updates.${reset}
|
|
32
|
-
${dim}This does not affect production builds — devDependencies are excluded on Vercel/AWS.${reset}
|
|
25
|
+
${bold}Local dev with Ollama:${reset}
|
|
26
|
+
${dim}Ollama uses @ai-sdk/openai pointed at localhost:11434/v1${reset}
|
|
27
|
+
${dim}No extra package needed — just install Ollama and pull a model:${reset}
|
|
28
|
+
${cyan}brew install ollama && ollama pull qwen2.5-coder:14b${reset}
|
|
33
29
|
|
|
34
30
|
${bold}Set NODE_ENV in your .env:${reset}
|
|
35
31
|
${dim}development${reset} → Ollama (free, local)
|