@infrarix/locopilot 1.1.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/LICENSE +21 -0
- package/README.md +239 -0
- package/dist/api/index.js +79 -0
- package/dist/api/index.js.map +1 -0
- package/dist/api/middleware/rateLimiter.js +27 -0
- package/dist/api/middleware/rateLimiter.js.map +1 -0
- package/dist/api/routes/chat.js +75 -0
- package/dist/api/routes/chat.js.map +1 -0
- package/dist/api/routes/completions.js +72 -0
- package/dist/api/routes/completions.js.map +1 -0
- package/dist/api/routes/health.js +52 -0
- package/dist/api/routes/health.js.map +1 -0
- package/dist/api/routes/models.js +50 -0
- package/dist/api/routes/models.js.map +1 -0
- package/dist/api/routes/training.js +10 -0
- package/dist/api/routes/training.js.map +1 -0
- package/dist/api/services/localRouter.js +201 -0
- package/dist/api/services/localRouter.js.map +1 -0
- package/dist/api/services/localStubs.js +28 -0
- package/dist/api/services/localStubs.js.map +1 -0
- package/dist/api/services/ollama.js +22 -0
- package/dist/api/services/ollama.js.map +1 -0
- package/dist/api/types.js +3 -0
- package/dist/api/types.js.map +1 -0
- package/dist/api/utils/sse.js +78 -0
- package/dist/api/utils/sse.js.map +1 -0
- package/dist/cli/commands/doctor.js +230 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/expose.js +98 -0
- package/dist/cli/commands/expose.js.map +1 -0
- package/dist/cli/commands/init.js +340 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/login.js +116 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/logout.js +38 -0
- package/dist/cli/commands/logout.js.map +1 -0
- package/dist/cli/commands/logs.js +95 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/models.js +106 -0
- package/dist/cli/commands/models.js.map +1 -0
- package/dist/cli/commands/start.js +132 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/train.js +211 -0
- package/dist/cli/commands/train.js.map +1 -0
- package/dist/cli/commands/usage.js +43 -0
- package/dist/cli/commands/usage.js.map +1 -0
- package/dist/cli/commands/whoami.js +54 -0
- package/dist/cli/commands/whoami.js.map +1 -0
- package/dist/cli/index.js +49 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils/banner.js +177 -0
- package/dist/cli/utils/banner.js.map +1 -0
- package/dist/cli/utils/paths.js +37 -0
- package/dist/cli/utils/paths.js.map +1 -0
- package/dist/cloud/client.js +157 -0
- package/dist/cloud/client.js.map +1 -0
- package/dist/shared/constants.js +39 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/crypto.js +26 -0
- package/dist/shared/crypto.js.map +1 -0
- package/dist/shared/db/pool.js +83 -0
- package/dist/shared/db/pool.js.map +1 -0
- package/dist/shared/errors.js +59 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/index.js +24 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/ndjson.js +39 -0
- package/dist/shared/ndjson.js.map +1 -0
- package/dist/shared/runtime/ollama/index.js +55 -0
- package/dist/shared/runtime/ollama/index.js.map +1 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/training/adapters/axolotl.js +83 -0
- package/dist/training/adapters/axolotl.js.map +1 -0
- package/dist/training/adapters/axolotl_runner.py +38 -0
- package/dist/training/adapters/mlx.js +57 -0
- package/dist/training/adapters/mlx.js.map +1 -0
- package/dist/training/adapters/mlx_runner.py +175 -0
- package/dist/training/adapters/unsloth.js +57 -0
- package/dist/training/adapters/unsloth.js.map +1 -0
- package/dist/training/adapters/unsloth_runner.py +116 -0
- package/dist/training/index.js +47 -0
- package/dist/training/index.js.map +1 -0
- package/dist/training/types.js +18 -0
- package/dist/training/types.js.map +1 -0
- package/dist/training/validator.js +67 -0
- package/dist/training/validator.js.map +1 -0
- package/dist/worker/executor.js +98 -0
- package/dist/worker/executor.js.map +1 -0
- package/dist/worker/handlers.js +197 -0
- package/dist/worker/handlers.js.map +1 -0
- package/dist/worker/index.js +45 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker/logStore.js +24 -0
- package/dist/worker/logStore.js.map +1 -0
- package/dist/worker/types.js +3 -0
- package/dist/worker/types.js.map +1 -0
- package/dist/worker/worker.js +12 -0
- package/dist/worker/worker.js.map +1 -0
- package/package.json +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 LocoPilot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# 🚀 LocoPilot
|
|
2
|
+
|
|
3
|
+
> Local-first, OpenAI-compatible AI platform.
|
|
4
|
+
> Run models locally in seconds, optionally scale with cloud GPUs — all through one CLI + one API.
|
|
5
|
+
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# ✨ What is LocoPilot?
|
|
11
|
+
|
|
12
|
+
LocoPilot lets you:
|
|
13
|
+
|
|
14
|
+
- ⚡ Run LLMs locally via Ollama (zero config)
|
|
15
|
+
- 🔄 Auto-fallback to remote GPU (Pro)
|
|
16
|
+
- 🧠 Fine-tune models locally or in the cloud
|
|
17
|
+
- 🌐 Expose APIs publicly in one command
|
|
18
|
+
|
|
19
|
+
👉 Works like OpenAI — just change your base URL.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# 🧩 Open Source vs Pro
|
|
24
|
+
|
|
25
|
+
## 🟢 Open Source (this repo)
|
|
26
|
+
|
|
27
|
+
| Component | Description |
|
|
28
|
+
| --------------------- | ---------------------------------------------------------------------------- |
|
|
29
|
+
| `apps/cli` | CLI — `init`, `start`, `models`, `train`, `expose`, `login` |
|
|
30
|
+
| `apps/api` | Fastify API — OpenAI-compatible endpoints |
|
|
31
|
+
| `core/runtime/ollama` | Local inference wrapper |
|
|
32
|
+
| `core/training` | Basic local training: MLX on Apple Silicon, Unsloth/Axolotl on Linux/Windows |
|
|
33
|
+
| `db/` | SQLite schema (free tier) |
|
|
34
|
+
| `utils/` | Shared helpers |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🔵 Pro (Cloud — not shipped as code)
|
|
39
|
+
|
|
40
|
+
Powered by **LocoPilot Cloud**:
|
|
41
|
+
|
|
42
|
+
- ☁️ Remote GPU (via RunPod)
|
|
43
|
+
- ⚡ Faster training (10–50x)
|
|
44
|
+
- 🌐 Public API (Cloudflare tunnel)
|
|
45
|
+
- 📊 Usage tracking + billing
|
|
46
|
+
- 🔐 Auth + API key management
|
|
47
|
+
|
|
48
|
+
👉 No private packages. Everything runs via cloud APIs.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# ⚡ Quickstart (Free Tier — 60 seconds)
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# 1. Install CLI
|
|
56
|
+
npm install -g @infrarix/locopilot
|
|
57
|
+
|
|
58
|
+
# 2. Initialize (auto-installs Ollama if missing)
|
|
59
|
+
locopilot init
|
|
60
|
+
|
|
61
|
+
# 3. Start local API
|
|
62
|
+
locopilot start
|
|
63
|
+
|
|
64
|
+
# 4. Pull a model
|
|
65
|
+
locopilot models pull llama3
|
|
66
|
+
|
|
67
|
+
# 5. Test inference
|
|
68
|
+
curl http://localhost:8080/v1/chat/completions \
|
|
69
|
+
-H "Content-Type: application/json" \
|
|
70
|
+
-d '{"model":"llama3","messages":[{"role":"user","content":"Hello"}]}'
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
👉 No Docker. No setup. Works offline.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
# 🔓 Unlock Pro Features
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Login (OAuth or API key)
|
|
81
|
+
locopilot login
|
|
82
|
+
|
|
83
|
+
# Expose your API publicly
|
|
84
|
+
locopilot expose
|
|
85
|
+
|
|
86
|
+
# Use cloud GPU automatically when needed
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
# 🏗 Architecture
|
|
92
|
+
|
|
93
|
+
## 🟢 Free Tier (Local Mode)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
CLI → Local API → Ollama
|
|
97
|
+
→ SQLite
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- Runs fully offline
|
|
101
|
+
- No account required
|
|
102
|
+
- No external dependencies
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🔵 Pro Tier (Hybrid Mode)
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
CLI → Local API
|
|
110
|
+
│
|
|
111
|
+
└──→ LocoPilot Cloud
|
|
112
|
+
├── GPU (RunPod)
|
|
113
|
+
├── Tunnel (Cloudflare)
|
|
114
|
+
├── Auth
|
|
115
|
+
└── Usage tracking
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
# 🧠 Training
|
|
121
|
+
|
|
122
|
+
## Free (Local)
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
locopilot train --config config.json
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Runs locally
|
|
129
|
+
- Basic configs
|
|
130
|
+
- Limited by your hardware
|
|
131
|
+
- On Apple Silicon Macs, free-tier training automatically uses MLX (`mlx-lm`, Metal-optimized). On Linux/Windows, Unsloth/Axolotl are used.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Pro (Cloud)
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
locopilot train --cloud
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- Runs on GPU
|
|
142
|
+
- Faster + better results
|
|
143
|
+
- No setup required
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
# 🧰 CLI Reference
|
|
148
|
+
|
|
149
|
+
| Command | Description |
|
|
150
|
+
| --------------------------------- | -------------------------------------------- |
|
|
151
|
+
| `locopilot init` | Setup environment, install Ollama if missing |
|
|
152
|
+
| `locopilot start` | Start local API server |
|
|
153
|
+
| `locopilot login` | Authenticate for Pro features |
|
|
154
|
+
| `locopilot models pull <model>` | Pull model locally |
|
|
155
|
+
| `locopilot models list` | List available models |
|
|
156
|
+
| `locopilot expose` | Get public API URL |
|
|
157
|
+
| `locopilot train --config <file>` | Local training |
|
|
158
|
+
| `locopilot train --cloud` | Cloud training (Pro) |
|
|
159
|
+
| `locopilot logs` | View logs |
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
# 📦 Requirements
|
|
164
|
+
|
|
165
|
+
### Free Tier
|
|
166
|
+
|
|
167
|
+
- Node.js 20+
|
|
168
|
+
- Internet (for install only)
|
|
169
|
+
- No Docker required
|
|
170
|
+
|
|
171
|
+
### Pro Features
|
|
172
|
+
|
|
173
|
+
- LocoPilot account
|
|
174
|
+
- Internet connection
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
# 🔐 Security
|
|
179
|
+
|
|
180
|
+
- No API keys required for local mode
|
|
181
|
+
- Pro tokens stored in:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
~/.locopilot/config.json
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- No cloud secrets stored locally
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
# 💡 Philosophy
|
|
192
|
+
|
|
193
|
+
> **Free = capability**
|
|
194
|
+
> **Pro = speed, scale, convenience**
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
# 🧑💻 Contributing
|
|
199
|
+
|
|
200
|
+
We welcome contributions!
|
|
201
|
+
|
|
202
|
+
- Improve CLI / API
|
|
203
|
+
- Add integrations
|
|
204
|
+
- Improve docs
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
git clone https://github.com/locopilot/locopilot
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
# 📄 License
|
|
213
|
+
|
|
214
|
+
MIT — see [LICENSE](LICENSE)
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
# 🚀 Roadmap
|
|
219
|
+
|
|
220
|
+
- [ ] Plugin SDK
|
|
221
|
+
- [ ] GUI dashboard (Pro)
|
|
222
|
+
- [ ] Multi-model routing
|
|
223
|
+
- [ ] Enterprise features
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
# 🌐 Learn More
|
|
228
|
+
|
|
229
|
+
👉 [https://locopilot.dev](https://locopilot.dev)
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
If you want next level:
|
|
234
|
+
|
|
235
|
+
- I can also generate **landing page copy**
|
|
236
|
+
- or **GitHub repo structure + badges**
|
|
237
|
+
- or **launch strategy (HN/Product Hunt)**
|
|
238
|
+
|
|
239
|
+
Just tell me 👍
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
7
|
+
const shared_1 = require("../shared");
|
|
8
|
+
const localRouter_1 = require("./services/localRouter");
|
|
9
|
+
const localStubs_1 = require("./services/localStubs");
|
|
10
|
+
const client_1 = require("../cloud/client");
|
|
11
|
+
const chat_1 = __importDefault(require("./routes/chat"));
|
|
12
|
+
const models_1 = __importDefault(require("./routes/models"));
|
|
13
|
+
const completions_1 = __importDefault(require("./routes/completions"));
|
|
14
|
+
const health_1 = __importDefault(require("./routes/health"));
|
|
15
|
+
const training_1 = __importDefault(require("./routes/training"));
|
|
16
|
+
const rateLimiter_1 = require("./middleware/rateLimiter");
|
|
17
|
+
const PUBLIC_ROUTES = new Set(['/v1/models', '/v1/locopilot/health']);
|
|
18
|
+
async function build() {
|
|
19
|
+
const app = (0, fastify_1.default)({
|
|
20
|
+
logger: true,
|
|
21
|
+
disableRequestLogging: false,
|
|
22
|
+
});
|
|
23
|
+
const router = await (0, localRouter_1.createLocalRouter)();
|
|
24
|
+
const auth = await (0, localStubs_1.createAuthMiddleware)();
|
|
25
|
+
const tracker = await (0, localStubs_1.createUsageTracker)();
|
|
26
|
+
app.decorate('router', router);
|
|
27
|
+
app.decorate('auth', auth);
|
|
28
|
+
app.decorate('tracker', tracker);
|
|
29
|
+
await app.register(require('@fastify/cors'));
|
|
30
|
+
app.setErrorHandler((error, req, reply) => {
|
|
31
|
+
const statusCode = error.statusCode ?? 500;
|
|
32
|
+
if (error instanceof shared_1.AppError) {
|
|
33
|
+
return reply.code(statusCode).send(error.toJSON());
|
|
34
|
+
}
|
|
35
|
+
if (error.validation) {
|
|
36
|
+
return reply.code(400).send({ error: error.message });
|
|
37
|
+
}
|
|
38
|
+
req.log.error(error);
|
|
39
|
+
return reply.code(statusCode).send({
|
|
40
|
+
error: statusCode === 500 ? 'Internal server error' : error.message,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
app.addHook('onRequest', async (req, _reply) => {
|
|
44
|
+
if (PUBLIC_ROUTES.has(req.url) || PUBLIC_ROUTES.has(req.routeOptions?.url ?? ''))
|
|
45
|
+
return;
|
|
46
|
+
if (req.method === 'OPTIONS')
|
|
47
|
+
return;
|
|
48
|
+
const apiKey = await auth.validate(req.headers.authorization);
|
|
49
|
+
req.apiKey = apiKey ?? undefined;
|
|
50
|
+
});
|
|
51
|
+
app.addHook('preHandler', rateLimiter_1.rateLimiter);
|
|
52
|
+
await app.register(chat_1.default, { prefix: '/v1' });
|
|
53
|
+
await app.register(models_1.default, { prefix: '/v1' });
|
|
54
|
+
await app.register(completions_1.default, { prefix: '/v1' });
|
|
55
|
+
await app.register(health_1.default, { prefix: '/v1/locopilot' });
|
|
56
|
+
await app.register(training_1.default, { prefix: '/v1/locopilot' });
|
|
57
|
+
return app;
|
|
58
|
+
}
|
|
59
|
+
const pro = (0, client_1.isProUser)();
|
|
60
|
+
console.log(pro ? '🐌 Starting in Pro Tier mode (Cloud-augmented)' : '🐌 Starting in Free Tier mode (Local SQLite)');
|
|
61
|
+
build()
|
|
62
|
+
.then((app) => {
|
|
63
|
+
const port = parseInt(process.env.API_PORT ?? '') || 8080;
|
|
64
|
+
return app.listen({ port, host: '0.0.0.0' });
|
|
65
|
+
})
|
|
66
|
+
.then(() => {
|
|
67
|
+
console.log(`🐌 LocoPilot API running on port ${parseInt(process.env.API_PORT ?? '') || 8080}`);
|
|
68
|
+
})
|
|
69
|
+
.catch(async (err) => {
|
|
70
|
+
console.error('Failed to start LocoPilot API:', err);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
});
|
|
73
|
+
process.on('SIGTERM', () => {
|
|
74
|
+
process.exit(0);
|
|
75
|
+
});
|
|
76
|
+
process.on('SIGINT', () => {
|
|
77
|
+
process.exit(0);
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.rateLimiter = rateLimiter;
|
|
4
|
+
const shared_1 = require("../../shared");
|
|
5
|
+
const windows = new Map();
|
|
6
|
+
async function rateLimiter(req, reply) {
|
|
7
|
+
const { apiKey } = req;
|
|
8
|
+
if (!apiKey)
|
|
9
|
+
return;
|
|
10
|
+
const limit = apiKey.rate_limit_rpm || shared_1.DEFAULT_RATE_LIMIT_RPM;
|
|
11
|
+
const windowMs = (apiKey.rate_window_seconds || shared_1.DEFAULT_RATE_WINDOW_SECONDS) * 1000;
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
let entry = windows.get(apiKey.id);
|
|
14
|
+
if (!entry || now >= entry.resetAt) {
|
|
15
|
+
entry = { count: 0, resetAt: now + windowMs };
|
|
16
|
+
windows.set(apiKey.id, entry);
|
|
17
|
+
}
|
|
18
|
+
entry.count++;
|
|
19
|
+
const retryAfterSeconds = Math.ceil((entry.resetAt - now) / 1000);
|
|
20
|
+
reply.header('X-RateLimit-Limit', limit);
|
|
21
|
+
reply.header('X-RateLimit-Remaining', Math.max(0, limit - entry.count));
|
|
22
|
+
reply.header('X-RateLimit-Reset', Math.floor(entry.resetAt / 1000));
|
|
23
|
+
if (entry.count > limit) {
|
|
24
|
+
throw new shared_1.RateLimitError(retryAfterSeconds);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=rateLimiter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rateLimiter.js","sourceRoot":"","sources":["../../../src/api/middleware/rateLimiter.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAcb,kCAyBC;AApCD,yCAAgH;AAShH,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;AAExC,KAAK,UAAU,WAAW,CAAC,GAAmB,EAAE,KAAmB;IACxE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAwB,CAAC;IAC5C,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,KAAK,GAAG,MAAM,CAAC,cAAc,IAAI,+BAAsB,CAAC;IAC9D,MAAM,QAAQ,GAAG,CAAC,MAAM,CAAC,mBAAmB,IAAI,oCAA2B,CAAC,GAAG,IAAI,CAAC;IACpF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAEvB,IAAI,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,IAAI,GAAG,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QACnC,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,QAAQ,EAAE,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,KAAK,EAAE,CAAC;IAEd,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAElE,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;IACzC,KAAK,CAAC,MAAM,CAAC,uBAAuB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACxE,KAAK,CAAC,MAAM,CAAC,mBAAmB,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;IAEpE,IAAI,KAAK,CAAC,KAAK,GAAG,KAAK,EAAE,CAAC;QACxB,MAAM,IAAI,uBAAc,CAAC,iBAAiB,CAAC,CAAC;IAC9C,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = chatRoute;
|
|
4
|
+
const sse_1 = require("../utils/sse");
|
|
5
|
+
const shared_1 = require("../../shared");
|
|
6
|
+
const sseHelpers = {
|
|
7
|
+
writeSSE: sse_1.writeSSE,
|
|
8
|
+
endSSE: sse_1.endSSE,
|
|
9
|
+
writeSSEError: sse_1.writeSSEError,
|
|
10
|
+
buildChunk: sse_1.buildChunk,
|
|
11
|
+
buildCompletion: sse_1.buildCompletion,
|
|
12
|
+
generateChatId: sse_1.generateChatId,
|
|
13
|
+
estimateTokens: sse_1.estimateTokens,
|
|
14
|
+
};
|
|
15
|
+
async function chatRoute(fastify) {
|
|
16
|
+
fastify.post('/chat/completions', {
|
|
17
|
+
schema: {
|
|
18
|
+
body: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
required: ['model', 'messages'],
|
|
21
|
+
properties: {
|
|
22
|
+
model: { type: 'string', minLength: 1, maxLength: 256 },
|
|
23
|
+
messages: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
minItems: 1,
|
|
26
|
+
maxItems: 500,
|
|
27
|
+
items: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
required: ['role', 'content'],
|
|
30
|
+
properties: {
|
|
31
|
+
role: { type: 'string', enum: ['system', 'user', 'assistant'] },
|
|
32
|
+
content: { type: 'string', maxLength: 131072 }, // 128 KB per message
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
stream: { type: 'boolean', default: false },
|
|
37
|
+
temperature: { type: 'number', minimum: 0, maximum: 2 },
|
|
38
|
+
max_tokens: { type: 'integer', minimum: 1, maximum: 65536 },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
}, async (req, reply) => {
|
|
43
|
+
const { model, stream: isStreaming } = req.body;
|
|
44
|
+
const apiKey = req.apiKey; // may be undefined for Free tier
|
|
45
|
+
const isProUser = apiKey !== undefined;
|
|
46
|
+
const provider = await fastify.router.resolve(model, isProUser);
|
|
47
|
+
if (provider === shared_1.PROVIDERS.NOT_FOUND) {
|
|
48
|
+
return reply.code(404).send({
|
|
49
|
+
error: 'model_not_found',
|
|
50
|
+
message: `Model '${model}' is not available locally. Upgrade to Pro for remote GPU access.`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
reply.hijack();
|
|
54
|
+
const abortController = new AbortController();
|
|
55
|
+
req.raw.on('close', () => {
|
|
56
|
+
abortController.abort();
|
|
57
|
+
});
|
|
58
|
+
if (isStreaming) {
|
|
59
|
+
(0, sse_1.setSSEHeaders)(reply.raw);
|
|
60
|
+
reply.raw.writeHead(200);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const { usage } = await fastify.router.stream(provider, req.body, reply.raw, sseHelpers, abortController.signal, req.headers.authorization);
|
|
64
|
+
if (isProUser && apiKey) {
|
|
65
|
+
fastify.tracker
|
|
66
|
+
.record({ apiKeyId: apiKey.id, ...usage })
|
|
67
|
+
.catch((err) => req.log.error({ err }, 'Usage tracking failed'));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
req.log.error({ err }, isStreaming ? 'Streaming error' : 'Completion error');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../../src/api/routes/chat.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AA0Bb,4BA4EC;AAnGD,sCASsB;AACtB,yCAAyC;AAGzC,MAAM,UAAU,GAAG;IACjB,QAAQ,EAAR,cAAQ;IACR,MAAM,EAAN,YAAM;IACN,aAAa,EAAb,mBAAa;IACb,UAAU,EAAV,gBAAU;IACV,eAAe,EAAf,qBAAe;IACf,cAAc,EAAd,oBAAc;IACd,cAAc,EAAd,oBAAc;CACf,CAAC;AAEa,KAAK,UAAU,SAAS,CAAC,OAAwB;IAC9D,OAAO,CAAC,IAAI,CACV,mBAAmB,EACnB;QACE,MAAM,EAAE;YACN,IAAI,EAAE;gBACJ,IAAI,EAAE,QAAQ;gBACd,QAAQ,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC;gBAC/B,UAAU,EAAE;oBACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE;oBACvD,QAAQ,EAAE;wBACR,IAAI,EAAE,OAAO;wBACb,QAAQ,EAAE,CAAC;wBACX,QAAQ,EAAE,GAAG;wBACb,KAAK,EAAE;4BACL,IAAI,EAAE,QAAQ;4BACd,QAAQ,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC;4BAC7B,UAAU,EAAE;gCACV,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE;gCAC/D,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,qBAAqB;6BACtE;yBACF;qBACF;oBACD,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,KAAK,EAAE;oBAC3C,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE;oBACvD,UAAU,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE;iBAC5D;aACF;SACF;KACF,EACD,KAAK,EAAE,GAA8C,EAAE,KAAmB,EAAE,EAAE;QAC5E,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;QAChD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,iCAAiC;QAC5D,MAAM,SAAS,GAAG,MAAM,KAAK,SAAS,CAAC;QAEvC,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAEhE,IAAI,QAAQ,KAAK,kBAAS,CAAC,SAAS,EAAE,CAAC;YACrC,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,KAAK,EAAE,iBAAiB;gBACxB,OAAO,EAAE,UAAU,KAAK,mEAAmE;aAC5F,CAAC,CAAC;QACL,CAAC;QAED,KAAK,CAAC,MAAM,EAAE,CAAC;QAEf,MAAM,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;QAC9C,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACvB,eAAe,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;QAEH,IAAI,WAAW,EAAE,CAAC;YAChB,IAAA,mBAAa,EAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACzB,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC3B,CAAC;QAED,IAAI,CAAC;YACH,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,MAAM,CAC3C,QAAQ,EACR,GAAG,CAAC,IAAI,EACR,KAAK,CAAC,GAAG,EACT,UAAU,EACV,eAAe,CAAC,MAAM,EACtB,GAAG,CAAC,OAAO,CAAC,aAAa,CAC1B,CAAC;YAEF,IAAI,SAAS,IAAI,MAAM,EAAE,CAAC;gBACxB,OAAO,CAAC,OAAO;qBACZ,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC;qBACzC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,uBAAuB,CAAC,CAAC,CAAC;YACrE,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC;QAC/E,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = completionsRoute;
|
|
4
|
+
const sse_1 = require("../utils/sse");
|
|
5
|
+
const shared_1 = require("../../shared");
|
|
6
|
+
const sseHelpers = {
|
|
7
|
+
writeSSE: sse_1.writeSSE,
|
|
8
|
+
endSSE: sse_1.endSSE,
|
|
9
|
+
writeSSEError: sse_1.writeSSEError,
|
|
10
|
+
buildChunk: sse_1.buildChunk,
|
|
11
|
+
buildCompletion: sse_1.buildCompletion,
|
|
12
|
+
generateChatId: sse_1.generateChatId,
|
|
13
|
+
estimateTokens: sse_1.estimateTokens,
|
|
14
|
+
};
|
|
15
|
+
async function completionsRoute(fastify) {
|
|
16
|
+
fastify.post('/completions', {
|
|
17
|
+
schema: {
|
|
18
|
+
body: {
|
|
19
|
+
type: 'object',
|
|
20
|
+
required: ['model', 'prompt'],
|
|
21
|
+
properties: {
|
|
22
|
+
model: { type: 'string', minLength: 1, maxLength: 256 },
|
|
23
|
+
prompt: { type: 'string', minLength: 1, maxLength: 131072 },
|
|
24
|
+
stream: { type: 'boolean', default: false },
|
|
25
|
+
max_tokens: { type: 'integer', minimum: 1, maximum: 65536 },
|
|
26
|
+
temperature: { type: 'number', minimum: 0, maximum: 2 },
|
|
27
|
+
stop: {},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}, async (req, reply) => {
|
|
32
|
+
const { model, prompt, stream: isStreaming, max_tokens, temperature, stop } = req.body;
|
|
33
|
+
const apiKey = req.apiKey;
|
|
34
|
+
const isProUser = apiKey !== undefined;
|
|
35
|
+
const chatBody = {
|
|
36
|
+
model,
|
|
37
|
+
messages: [{ role: 'user', content: prompt }],
|
|
38
|
+
stream: isStreaming ?? false,
|
|
39
|
+
...(max_tokens !== undefined && { max_tokens }),
|
|
40
|
+
...(temperature !== undefined && { temperature }),
|
|
41
|
+
...(stop !== undefined && { stop }),
|
|
42
|
+
};
|
|
43
|
+
const provider = await fastify.router.resolve(model, isProUser);
|
|
44
|
+
if (provider === shared_1.PROVIDERS.NOT_FOUND) {
|
|
45
|
+
return reply.code(404).send({
|
|
46
|
+
error: 'model_not_found',
|
|
47
|
+
message: `Model '${model}' is not available locally. Upgrade to Pro for remote GPU access.`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
reply.hijack();
|
|
51
|
+
const abortController = new AbortController();
|
|
52
|
+
req.raw.on('close', () => {
|
|
53
|
+
abortController.abort();
|
|
54
|
+
});
|
|
55
|
+
if (isStreaming) {
|
|
56
|
+
(0, sse_1.setSSEHeaders)(reply.raw);
|
|
57
|
+
reply.raw.writeHead(200);
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const { usage } = await fastify.router.stream(provider, chatBody, reply.raw, sseHelpers, abortController.signal, req.headers.authorization);
|
|
61
|
+
if (isProUser && apiKey) {
|
|
62
|
+
fastify.tracker
|
|
63
|
+
.record({ apiKeyId: apiKey.id, ...usage })
|
|
64
|
+
.catch((err) => req.log.error({ err }, 'Usage tracking failed'));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
req.log.error({ err }, 'Legacy completions error');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=completions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"completions.js","sourceRoot":"","sources":["../../../src/api/routes/completions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = healthRoute;
|
|
4
|
+
const ollama_1 = require("../services/ollama");
|
|
5
|
+
const shared_1 = require("../../shared");
|
|
6
|
+
const client_1 = require("../../cloud/client");
|
|
7
|
+
async function timedCheck(fn) {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
await fn();
|
|
10
|
+
return Date.now() - start;
|
|
11
|
+
}
|
|
12
|
+
async function checkSQLite() {
|
|
13
|
+
await (0, shared_1.query)('SELECT 1');
|
|
14
|
+
}
|
|
15
|
+
async function healthRoute(fastify) {
|
|
16
|
+
fastify.get('/health', async (_req, reply) => {
|
|
17
|
+
const pro = (0, client_1.isProUser)();
|
|
18
|
+
const checks = [
|
|
19
|
+
['ollama', ollama_1.checkOllama],
|
|
20
|
+
['sqlite', checkSQLite],
|
|
21
|
+
];
|
|
22
|
+
if (pro) {
|
|
23
|
+
const token = (0, client_1.getCloudToken)();
|
|
24
|
+
checks.push([
|
|
25
|
+
'cloud',
|
|
26
|
+
() => (0, client_1.callCloudHealth)(`Bearer ${token}`).then((r) => {
|
|
27
|
+
if (!r.ok)
|
|
28
|
+
throw new Error(`Cloud health returned ${r.status}`);
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
const results = await Promise.allSettled(checks.map(([, fn]) => timedCheck(fn)));
|
|
33
|
+
const services = {};
|
|
34
|
+
let allOk = true;
|
|
35
|
+
results.forEach((result, i) => {
|
|
36
|
+
const name = checks[i][0];
|
|
37
|
+
const ok = result.status === 'fulfilled';
|
|
38
|
+
if (!ok)
|
|
39
|
+
allOk = false;
|
|
40
|
+
services[name] = ok
|
|
41
|
+
? { ok: true, responseMs: result.value }
|
|
42
|
+
: { ok: false, error: result.reason?.message };
|
|
43
|
+
});
|
|
44
|
+
const response = {
|
|
45
|
+
status: allOk ? 'ok' : 'degraded',
|
|
46
|
+
mode: pro ? 'pro' : 'free',
|
|
47
|
+
services,
|
|
48
|
+
};
|
|
49
|
+
return reply.send(response);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../../src/api/routes/health.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAkBb,8BA0CC;AAzDD,+CAAiD;AACjD,yCAAqC;AACrC,+CAA+E;AAG/E,KAAK,UAAU,UAAU,CAAC,EAAuB;IAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,EAAE,EAAE,CAAC;IACX,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;AAC5B,CAAC;AAED,KAAK,UAAU,WAAW;IACxB,MAAM,IAAA,cAAK,EAAC,UAAU,CAAC,CAAC;AAC1B,CAAC;AAEc,KAAK,UAAU,WAAW,CAAC,OAAwB;IAChE,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,IAAoB,EAAE,KAAmB,EAAE,EAAE;QACzE,MAAM,GAAG,GAAG,IAAA,kBAAS,GAAE,CAAC;QAExB,MAAM,MAAM,GAAyC;YACnD,CAAC,QAAQ,EAAE,oBAAW,CAAC;YACvB,CAAC,QAAQ,EAAE,WAAW,CAAC;SACxB,CAAC;QAEF,IAAI,GAAG,EAAE,CAAC;YACR,MAAM,KAAK,GAAG,IAAA,sBAAa,GAAG,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC;gBACV,OAAO;gBACP,GAAG,EAAE,CACH,IAAA,wBAAe,EAAC,UAAU,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;oBAC5C,IAAI,CAAC,CAAC,CAAC,EAAE;wBAAE,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;gBAClE,CAAC,CAAC;aACL,CAAC,CAAC;QACL,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAEjF,MAAM,QAAQ,GAAkC,EAAE,CAAC;QACnD,IAAI,KAAK,GAAG,IAAI,CAAC;QAEjB,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YAC5B,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1B,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC;YACzC,IAAI,CAAC,EAAE;gBAAE,KAAK,GAAG,KAAK,CAAC;YACvB,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE;gBACjB,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,CAAC,KAAK,EAAE;gBACxC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAG,MAAgC,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;QAC9E,CAAC,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAmB;YAC/B,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU;YACjC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM;YAC1B,QAAQ;SACT,CAAC;QAEF,OAAO,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = modelsRoute;
|
|
4
|
+
const ollama_1 = require("../services/ollama");
|
|
5
|
+
const client_1 = require("../../cloud/client");
|
|
6
|
+
async function fetchCloudModels(token, log) {
|
|
7
|
+
try {
|
|
8
|
+
const res = await (0, client_1.callCloudModels)(`Bearer ${token}`);
|
|
9
|
+
if (!res.ok) {
|
|
10
|
+
log.warn({ status: res.status }, 'cloud /api/models returned non-OK; pro users see local-only list');
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const body = (await res.json());
|
|
14
|
+
return Array.isArray(body.data) ? body.data : [];
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
log.warn({ err: err.message }, 'cloud /api/models unreachable; pro users see local-only list');
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function modelsRoute(fastify) {
|
|
22
|
+
fastify.get('/models', async (req, reply) => {
|
|
23
|
+
let localModels = [];
|
|
24
|
+
try {
|
|
25
|
+
localModels = await (0, ollama_1.getLocalModels)();
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
req.log.warn({ err }, 'Ollama unavailable — local model list will be empty');
|
|
29
|
+
}
|
|
30
|
+
const localData = localModels.map((m) => ({
|
|
31
|
+
id: m.name,
|
|
32
|
+
object: 'model',
|
|
33
|
+
created: Math.floor(Date.now() / 1000),
|
|
34
|
+
owned_by: 'local',
|
|
35
|
+
}));
|
|
36
|
+
// Free tier: local-only. Pro tier: merge cloud-managed remote model manifest.
|
|
37
|
+
if (!(0, client_1.isProUser)()) {
|
|
38
|
+
return reply.send({ object: 'list', data: localData });
|
|
39
|
+
}
|
|
40
|
+
const token = (0, client_1.getCloudToken)();
|
|
41
|
+
if (!token)
|
|
42
|
+
return reply.send({ object: 'list', data: localData });
|
|
43
|
+
const cloudData = await fetchCloudModels(token, req.log);
|
|
44
|
+
return reply.send({
|
|
45
|
+
object: 'list',
|
|
46
|
+
data: [...localData, ...cloudData],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=models.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"models.js","sourceRoot":"","sources":["../../../src/api/routes/models.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAwBb,8BA+BC;AApDD,+CAAoD;AACpD,+CAA+E;AAK/E,KAAK,UAAU,gBAAgB,CAAC,KAAa,EAAE,GAA6C;IAC1F,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,IAAA,wBAAe,EAAC,UAAU,KAAK,EAAE,CAAC,CAAC;QACrD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,kEAAkE,CAAC,CAAC;YACrG,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAwB,CAAC;QACvD,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IACnD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAG,GAAa,CAAC,OAAO,EAAE,EAAE,8DAA8D,CAAC,CAAC;QAC1G,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAEc,KAAK,UAAU,WAAW,CAAC,OAAwB;IAChE,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,GAAmB,EAAE,KAAmB,EAAE,EAAE;QACxE,IAAI,WAAW,GAA4B,EAAE,CAAC;QAC9C,IAAI,CAAC;YACH,WAAW,GAAG,MAAM,IAAA,uBAAc,GAAE,CAAC;QACvC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,qDAAqD,CAAC,CAAC;QAC/E,CAAC;QAED,MAAM,SAAS,GAAiB,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACtD,EAAE,EAAE,CAAC,CAAC,IAAI;YACV,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;YACtC,QAAQ,EAAE,OAAO;SAClB,CAAC,CAAC,CAAC;QAEJ,8EAA8E;QAC9E,IAAI,CAAC,IAAA,kBAAS,GAAE,EAAE,CAAC;YACjB,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QACzD,CAAC;QAED,MAAM,KAAK,GAAG,IAAA,sBAAa,GAAE,CAAC;QAC9B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QAEnE,MAAM,SAAS,GAAG,MAAM,gBAAgB,CAAC,KAAK,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAEzD,OAAO,KAAK,CAAC,IAAI,CAAC;YAChB,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,CAAC,GAAG,SAAS,EAAE,GAAG,SAAS,CAAC;SACnC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = trainingRoutes;
|
|
4
|
+
const worker_1 = require("../../worker");
|
|
5
|
+
async function trainingRoutes(fastify) {
|
|
6
|
+
fastify.post('/training/jobs', worker_1.trainingHandlers.create);
|
|
7
|
+
fastify.get('/training/jobs/:id', worker_1.trainingHandlers.getStatus);
|
|
8
|
+
fastify.get('/training/jobs/:id/logs', worker_1.trainingHandlers.streamLogs);
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=training.js.map
|