pi-baseten-provider 1.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,177 @@
1
+ <div align="center">
2
+
3
+ # 🅱️ pi-baseten-provider
4
+
5
+ **DeepSeek, Kimi, GLM & more through [Baseten](https://baseten.co/)**
6
+
7
+ _A [pi](https://github.com/earendil-works/pi-coding-agent) provider extension for Baseten's OpenAI-compatible Model API._
8
+
9
+ [![pi extension](https://img.shields.io/badge/pi-extension-blueviolet)](https://github.com/earendil-works/pi-coding-agent)
10
+ [![license](https://img.shields.io/badge/license-MIT-blue)](./LICENSE)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## Features
17
+
18
+ - **8+ AI Models** including DeepSeek V3.1, Kimi K2.5/K2.6, GLM 4.7/5, MiniMax M2.5, Nemotron Super, and GPT OSS 120B
19
+ - **OpenAI-Compatible API** — Just change the base URL and API key
20
+ - **Cost Tracking** — Per-model pricing for budget management
21
+ - **Reasoning Models** — Extended thinking via `reasoning_content` field
22
+ - **Vision Support** — Image input on Kimi K2.5 and Kimi K2.6
23
+ - **Reasoning Effort** — Control reasoning depth on GPT OSS 120B
24
+
25
+ ## Installation
26
+
27
+ ### Option 1: Using `pi install` (Recommended)
28
+
29
+ Install directly from GitHub:
30
+
31
+ ```bash
32
+ pi install https://github.com/monotykamary/pi-baseten-provider
33
+ ```
34
+
35
+ Then set your API key and run pi:
36
+ ```bash
37
+ # Recommended: add to auth.json
38
+ # See Authentication section below
39
+
40
+ # Or set as environment variable
41
+ export BASETEN_API_KEY=your-api-key-here
42
+
43
+ pi
44
+ ```
45
+
46
+ ### Option 2: Manual Clone
47
+
48
+ 1. Clone this repository:
49
+ ```bash
50
+ git clone https://github.com/monotykamary/pi-baseten-provider.git
51
+ cd pi-baseten-provider
52
+ ```
53
+
54
+ 2. Set your Baseten API key:
55
+ ```bash
56
+ # Recommended: add to auth.json
57
+ # See Authentication section below
58
+
59
+ # Or set as environment variable
60
+ export BASETEN_API_KEY=your-api-key-here
61
+ ```
62
+
63
+ 3. Run pi with the extension:
64
+ ```bash
65
+ pi -e /path/to/pi-baseten-provider
66
+ ```
67
+
68
+ ## Available Models
69
+
70
+ | Model | Context | Vision | Reasoning | Input $/M | Output $/M |
71
+ |-------|---------|--------|-----------|-----------|------------|
72
+ | DeepSeek V4 Pro | 131K | ❌ | ✅ | $1.74 | $3.48 |
73
+ | GLM 4.7 | 200K | ❌ | ✅ | $0.60 | $2.20 |
74
+ | GLM 5 | 203K | ❌ | ✅ | $0.95 | $3.15 |
75
+ | GLM 5.1 | 203K | ❌ | ❌ | $1.30 | $4.30 |
76
+ | GLM 5.2 | 262K | ❌ | ✅ | $1.40 | $4.40 |
77
+ | Kimi K2.5 | 262K | ✅ | ✅ | $0.60 | $3.00 |
78
+ | Kimi K2.6 | 262K | ✅ | ✅ | $0.60 | $3.00 |
79
+ | Kimi K2.7 Code | 262K | ❌ | ✅ | $0.95 | $4.00 |
80
+ | Nemotron Super | 203K | ❌ | ✅ | $0.30 | $0.75 |
81
+ | Nemotron Ultra | 203K | ❌ | ✅ | $0.60 | $2.40 |
82
+ | OpenAI GPT 120B | 128K | ❌ | ✅ | $0.10 | $0.50 |
83
+
84
+ *Costs are per million tokens. Prices subject to change — check [baseten.co/pricing](https://www.baseten.co/pricing/) for current pricing.*
85
+
86
+ ## Usage
87
+
88
+ After loading the extension, use the `/model` command in pi to select your preferred model:
89
+
90
+ ```
91
+ /model baseten deepseek-ai/DeepSeek-V3.1
92
+ ```
93
+
94
+ Or start pi directly with a Baseten model:
95
+
96
+ ```bash
97
+ pi --provider baseten --model deepseek-ai/DeepSeek-V3.1
98
+ ```
99
+
100
+ ## Authentication
101
+
102
+ The Baseten API key can be configured in multiple ways (resolved in this order):
103
+
104
+ 1. **`auth.json`** (recommended) — Add to `~/.pi/agent/auth.json`:
105
+ ```json
106
+ { "baseten": { "type": "api_key", "key": "your-api-key" } }
107
+ ```
108
+ The `key` field supports literal values, env var names, and shell commands (prefix with `!`). See [pi's auth file docs](https://github.com/badlogic/pi-mono) for details.
109
+ 2. **Runtime override** — Use the `--api-key` CLI flag
110
+ 3. **Environment variable** — Set `BASETEN_API_KEY`
111
+
112
+ Get your API key at [app.baseten.co/settings/api_keys](https://app.baseten.co/settings/api_keys).
113
+
114
+ ## Environment Variables
115
+
116
+ | Variable | Required | Description |
117
+ |----------|----------|-------------|
118
+ | `BASETEN_API_KEY` | No | Your Baseten API key (fallback if not in auth.json) |
119
+
120
+ ## Configuration
121
+
122
+ Add to your pi configuration for automatic loading:
123
+
124
+ ```json
125
+ {
126
+ "extensions": [
127
+ "/path/to/pi-baseten-provider"
128
+ ]
129
+ }
130
+ ```
131
+
132
+ ### Compat Settings
133
+
134
+ Baseten's API follows the OpenAI Chat Completions API:
135
+
136
+ - **`supportsDeveloperRole: true`** — All models. Baseten supports the `developer` role.
137
+ - **`maxTokensField: "max_completion_tokens"`** — All models. Baseten uses `max_completion_tokens`.
138
+ - **`thinkingFormat: "qwen-chat-template"`** — Kimi K2.5/K2.6 and GLM 4.7/5. Reasoning is opt-in on Baseten via `chat_template_kwargs.enable_thinking`. Pi enables this automatically when you set a thinking level (Shift+Tab).
139
+ - **`thinkingFormat: "openai"`** — DeepSeek V3.1, MiniMax M2.5, Nemotron Super. Returns thinking in `reasoning_content` field by default.
140
+ - **`supportsReasoningEffort: true`** — GPT OSS 120B. Supports `reasoning_effort` parameter.
141
+ - **`supportsStore: false`** — All models. Baseten doesn't support the `store` parameter.
142
+
143
+ ### Patch Overrides
144
+
145
+ The `patch.json` file contains overrides that are applied on top of `models.json` data. This is useful for:
146
+ - Marking models as reasoning-capable when the API features list doesn't include it
147
+ - Filling in pricing for models where the API returns empty values (e.g. new/unlisted models)
148
+ - Adding compat settings that the API doesn't provide
149
+ - Setting `thinkingFormat: "qwen-chat-template"` for models that require `chat_template_kwargs` to enable reasoning (Kimi K2.5/K2.6, GLM 4.7/5)
150
+
151
+ ### Custom Models
152
+
153
+ The `custom-models.json` file contains full model definitions for models that need manual curation beyond what the API provides, or models not yet available from the API. These are merged in after patch application, taking precedence for matching IDs.
154
+
155
+ Merge order: `[live|cache|embedded] → patch.json → custom-models.json`
156
+
157
+ ## Updating Models
158
+
159
+ Run the update script to fetch the latest models from Baseten's API:
160
+
161
+ ```bash
162
+ export BASETEN_API_KEY=your-api-key
163
+ node scripts/update-models.js
164
+ ```
165
+
166
+ This will:
167
+ 1. Fetch models from `https://inference.baseten.co/v1/models`
168
+ 2. Convert per-token pricing to per-million-tokens
169
+ 3. Preserve existing model data (pricing, compat) for known models
170
+ 4. Apply overrides from `patch.json`
171
+ 5. Update `models.json` and the README model table
172
+
173
+ A GitHub Actions workflow runs this daily and creates a PR if models have changed.
174
+
175
+ ## License
176
+
177
+ MIT
@@ -0,0 +1,24 @@
1
+ [
2
+ {
3
+ "id": "deepseek-ai/DeepSeek-V4-Pro",
4
+ "name": "DeepSeek V4 Pro",
5
+ "reasoning": true,
6
+ "input": [
7
+ "text"
8
+ ],
9
+ "cost": {
10
+ "input": 1.74,
11
+ "output": 3.48,
12
+ "cacheRead": 0.15,
13
+ "cacheWrite": 0
14
+ },
15
+ "contextWindow": 131000,
16
+ "maxTokens": 131000,
17
+ "compat": {
18
+ "supportsDeveloperRole": true,
19
+ "supportsStore": false,
20
+ "maxTokensField": "max_completion_tokens",
21
+ "thinkingFormat": "openai"
22
+ }
23
+ }
24
+ ]
package/index.ts ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * Baseten Provider Extension
3
+ *
4
+ * Registers Baseten as a custom provider using the openai-completions API.
5
+ * Base URL: https://inference.baseten.co/v1
6
+ *
7
+ * Model resolution strategy: Stale-While-Revalidate
8
+ * 1. Serve stale immediately: disk cache → embedded models.json (zero-latency)
9
+ * 2. Revalidate in background: live API /models → merge with embedded → cache → hot-swap
10
+ * 3. patch.json + custom-models.json applied on top of whichever source won
11
+ *
12
+ * Merge order: [live|cache|embedded] → apply patch.json → merge custom-models.json
13
+ *
14
+ * Usage:
15
+ * # Option 1: Store in auth.json (recommended)
16
+ * # Add to ~/.pi/agent/auth.json:
17
+ * # "baseten": { "type": "api_key", "key": "your-api-key" }
18
+ *
19
+ * # Option 2: Set as environment variable
20
+ * export BASETEN_API_KEY=your-api-key
21
+ *
22
+ * # Run pi with the extension
23
+ * pi -e /path/to/pi-baseten-provider
24
+ *
25
+ * Then use /model to select from available models
26
+ */
27
+
28
+ import type { ExtensionAPI, ModelRegistry } from "@earendil-works/pi-coding-agent";
29
+ import modelsData from "./models.json" with { type: "json" };
30
+ import customModelsData from "./custom-models.json" with { type: "json" };
31
+ import patchData from "./patch.json" with { type: "json" };
32
+ import fs from "fs";
33
+ import os from "os";
34
+ import path from "path";
35
+
36
+ // ─── Types ────────────────────────────────────────────────────────────────────
37
+
38
+ interface JsonModel {
39
+ id: string;
40
+ name: string;
41
+ reasoning: boolean;
42
+ input: string[];
43
+ cost: {
44
+ input: number;
45
+ output: number;
46
+ cacheRead: number;
47
+ cacheWrite: number;
48
+ };
49
+ contextWindow: number;
50
+ maxTokens: number;
51
+ thinkingLevelMap?: {
52
+ minimal?: string | null;
53
+ low?: string | null;
54
+ medium?: string | null;
55
+ high?: string | null;
56
+ xhigh?: string | null;
57
+ };
58
+ compat?: {
59
+ supportsDeveloperRole?: boolean;
60
+ supportsStore?: boolean;
61
+ maxTokensField?: "max_completion_tokens" | "max_tokens";
62
+ thinkingFormat?: "openai" | "zai" | "qwen" | "qwen-chat-template";
63
+ supportsReasoningEffort?: boolean;
64
+ };
65
+ }
66
+
67
+ interface PatchEntry {
68
+ name?: string;
69
+ reasoning?: boolean;
70
+ input?: string[];
71
+ cost?: {
72
+ input?: number;
73
+ output?: number;
74
+ cacheRead?: number;
75
+ cacheWrite?: number;
76
+ };
77
+ contextWindow?: number;
78
+ maxTokens?: number;
79
+ thinkingLevelMap?: Record<string, string | null>;
80
+ compat?: Record<string, unknown>;
81
+ }
82
+
83
+ type PatchData = Record<string, PatchEntry>;
84
+
85
+ // ─── Patch Application ────────────────────────────────────────────────────────
86
+
87
+ function applyPatch(model: JsonModel, patch: PatchEntry): JsonModel {
88
+ const result = { ...model };
89
+
90
+ if (patch.name !== undefined) result.name = patch.name;
91
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
92
+ if (patch.input !== undefined) result.input = patch.input;
93
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
94
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
95
+ if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = patch.thinkingLevelMap as JsonModel["thinkingLevelMap"];
96
+
97
+ if (patch.cost) {
98
+ result.cost = {
99
+ input: patch.cost.input ?? result.cost.input,
100
+ output: patch.cost.output ?? result.cost.output,
101
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
102
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
103
+ };
104
+ }
105
+ if (patch.compat) {
106
+ result.compat = { ...(result.compat || {}), ...patch.compat };
107
+ }
108
+
109
+ if (!result.reasoning && result.compat?.thinkingFormat) {
110
+ delete result.compat.thinkingFormat;
111
+ }
112
+ if (!result.reasoning && result.thinkingLevelMap) {
113
+ delete result.thinkingLevelMap;
114
+ }
115
+ if (result.compat && Object.keys(result.compat).length === 0) {
116
+ delete result.compat;
117
+ }
118
+
119
+ return result;
120
+ }
121
+
122
+ /** Full pipeline: base models → patch → custom → result */
123
+ function buildModels(base: JsonModel[], custom: JsonModel[], patch: PatchData): JsonModel[] {
124
+ const modelMap = new Map<string, JsonModel>();
125
+
126
+ for (const model of base) {
127
+ modelMap.set(model.id, model);
128
+ }
129
+
130
+ for (const [id, patchEntry] of Object.entries(patch)) {
131
+ const existing = modelMap.get(id);
132
+ if (existing) {
133
+ modelMap.set(id, applyPatch(existing, patchEntry));
134
+ }
135
+ }
136
+
137
+ for (const model of custom) {
138
+ const existing = modelMap.get(model.id);
139
+ const patchEntry = patch[model.id];
140
+ if (existing && patchEntry) {
141
+ modelMap.set(model.id, applyPatch(model, patchEntry));
142
+ } else if (existing) {
143
+ modelMap.set(model.id, model);
144
+ } else if (patchEntry) {
145
+ modelMap.set(model.id, applyPatch(model, patchEntry));
146
+ } else {
147
+ modelMap.set(model.id, model);
148
+ }
149
+ }
150
+
151
+ return Array.from(modelMap.values());
152
+ }
153
+
154
+ // ─── Stale-While-Revalidate Model Sync ────────────────────────────────────────
155
+
156
+ const PROVIDER_ID = "baseten";
157
+ const BASE_URL = "https://inference.baseten.co/v1";
158
+ const MODELS_URL = `${BASE_URL}/models`;
159
+ const CACHE_DIR = path.join(os.homedir(), ".pi", "agent", "cache");
160
+ const CACHE_PATH = path.join(CACHE_DIR, `${PROVIDER_ID}-models.json`);
161
+ const LIVE_FETCH_TIMEOUT_MS = 8000;
162
+
163
+ /** Transform a model from the Baseten /v1/models API. Baseten returns rich metadata. */
164
+ function transformApiModel(apiModel: any): JsonModel | null {
165
+ const features: string[] = apiModel.supported_features || [];
166
+ const hasVision = (apiModel.input_modalities || []).includes("image");
167
+ const pricing = apiModel.pricing || {};
168
+ const toPerM = (v: any) => (typeof v === "string" ? parseFloat(v) : (v || 0)) * 1_000_000;
169
+ const model: JsonModel = {
170
+ id: apiModel.id,
171
+ name: apiModel.name || apiModel.id,
172
+ reasoning: features.includes("reasoning"),
173
+ input: hasVision ? ["text", "image"] : ["text"],
174
+ cost: {
175
+ input: toPerM(pricing.prompt),
176
+ output: toPerM(pricing.completion),
177
+ cacheRead: toPerM(pricing.cache_prompt),
178
+ cacheWrite: 0,
179
+ },
180
+ contextWindow: apiModel.context_length || 131072,
181
+ maxTokens: apiModel.max_completion_tokens || 131072,
182
+ };
183
+ if (features.includes("reasoning_effort")) {
184
+ model.compat = { ...model.compat, supportsReasoningEffort: true };
185
+ }
186
+ return model;
187
+ }
188
+
189
+ async function fetchLiveModels(apiKey: string, signal?: AbortSignal): Promise<JsonModel[] | null> {
190
+ try {
191
+ const response = await fetch(MODELS_URL, {
192
+ headers: { Authorization: `Bearer ${apiKey}` },
193
+ signal: signal ? AbortSignal.any([AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS), signal]) : AbortSignal.timeout(LIVE_FETCH_TIMEOUT_MS),
194
+ });
195
+ if (!response.ok) return null;
196
+ const data = await response.json();
197
+ const apiModels = Array.isArray(data) ? data : (data.data || []);
198
+ if (!Array.isArray(apiModels) || apiModels.length === 0) return null;
199
+ return apiModels.map(transformApiModel).filter((m): m is JsonModel => m !== null);
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ function loadCachedModels(): JsonModel[] | null {
206
+ try {
207
+ const data = JSON.parse(fs.readFileSync(CACHE_PATH, "utf8"));
208
+ return Array.isArray(data) ? data : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function cacheModels(models: JsonModel[]): void {
215
+ try {
216
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
217
+ fs.writeFileSync(CACHE_PATH, JSON.stringify(models, null, 2) + "\n");
218
+ } catch {
219
+ // Cache write failure is non-fatal
220
+ }
221
+ }
222
+
223
+ function mergeWithEmbedded(liveModels: JsonModel[], embeddedModels: JsonModel[]): JsonModel[] {
224
+ const embeddedMap = new Map(embeddedModels.map(m => [m.id, m]));
225
+ const seen = new Set<string>();
226
+ const result: JsonModel[] = [];
227
+ for (const liveModel of liveModels) {
228
+ const embedded = embeddedMap.get(liveModel.id);
229
+ seen.add(liveModel.id);
230
+ if (embedded) {
231
+ result.push({
232
+ ...liveModel,
233
+ ...embedded,
234
+ contextWindow: liveModel.contextWindow || embedded.contextWindow,
235
+ });
236
+ } else {
237
+ result.push(liveModel);
238
+ }
239
+ }
240
+ // Append any embedded models that the live API didn't return
241
+ for (const em of embeddedModels) {
242
+ if (!seen.has(em.id)) {
243
+ result.push(em);
244
+ }
245
+ }
246
+ return result;
247
+ }
248
+
249
+ function loadStaleModels(embeddedModels: JsonModel[]): JsonModel[] {
250
+ const cached = loadCachedModels();
251
+ if (!cached || cached.length === 0) return embeddedModels;
252
+
253
+ // Merge embedded models that are missing from cache (newly added models)
254
+ const cachedMap = new Map(cached.map(m => [m.id, m]));
255
+ for (const em of embeddedModels) {
256
+ if (!cachedMap.has(em.id)) {
257
+ cached.push(em);
258
+ }
259
+ }
260
+ return cached;
261
+ }
262
+
263
+ async function revalidateModels(apiKey: string | undefined, embeddedModels: JsonModel[], signal?: AbortSignal): Promise<JsonModel[] | null> {
264
+ if (!apiKey) return null;
265
+ const liveModels = await fetchLiveModels(apiKey, signal);
266
+ if (!liveModels || liveModels.length === 0) return null;
267
+ const merged = mergeWithEmbedded(liveModels, embeddedModels);
268
+ cacheModels(merged);
269
+ return merged;
270
+ }
271
+
272
+ // ─── API Key Resolution (via ModelRegistry) ────────────────────────────────────
273
+
274
+ let cachedApiKey: string | undefined;
275
+ let revalidateAbort: AbortController | null = null;
276
+
277
+ async function resolveApiKey(modelRegistry: ModelRegistry): Promise<void> {
278
+ cachedApiKey = await modelRegistry.getApiKeyForProvider("baseten") ?? undefined;
279
+ }
280
+
281
+ // ─── Extension Entry Point ────────────────────────────────────────────────────
282
+
283
+ export default function (pi: ExtensionAPI) {
284
+ const embeddedModels = modelsData as JsonModel[];
285
+ const customModels = customModelsData as JsonModel[];
286
+ const patches = patchData as PatchData;
287
+
288
+ const staleBase = loadStaleModels(embeddedModels);
289
+ const staleModels = buildModels(staleBase, customModels, patches);
290
+
291
+ pi.registerProvider("baseten", {
292
+ baseUrl: BASE_URL,
293
+ apiKey: "$BASETEN_API_KEY",
294
+ api: "openai-completions",
295
+ models: staleModels,
296
+ });
297
+
298
+ pi.on("session_start", async (_event, ctx) => {
299
+ revalidateAbort?.abort();
300
+ revalidateAbort = new AbortController();
301
+ const signal = revalidateAbort.signal;
302
+ resolveApiKey(ctx.modelRegistry).then(() => {
303
+ revalidateModels(cachedApiKey, embeddedModels, signal).then((freshBase) => {
304
+ if (freshBase && !signal.aborted) {
305
+ pi.registerProvider("baseten", {
306
+ baseUrl: BASE_URL,
307
+ apiKey: "$BASETEN_API_KEY",
308
+ api: "openai-completions",
309
+ models: buildModels(freshBase, customModels, patches),
310
+ });
311
+ }
312
+ });
313
+ });
314
+ });
315
+
316
+ pi.on("session_shutdown", () => {
317
+ revalidateAbort?.abort();
318
+ });
319
+ }
package/models.json ADDED
@@ -0,0 +1,246 @@
1
+ [
2
+ {
3
+ "id": "deepseek-ai/DeepSeek-V4-Pro",
4
+ "name": "Deepseek V4 Pro",
5
+ "reasoning": true,
6
+ "input": [
7
+ "text"
8
+ ],
9
+ "cost": {
10
+ "input": 1.74,
11
+ "output": 3.48,
12
+ "cacheRead": 0,
13
+ "cacheWrite": 0
14
+ },
15
+ "contextWindow": 131000,
16
+ "maxTokens": 131000,
17
+ "compat": {
18
+ "supportsDeveloperRole": true,
19
+ "supportsStore": false,
20
+ "maxTokensField": "max_completion_tokens",
21
+ "thinkingFormat": "openai"
22
+ }
23
+ },
24
+ {
25
+ "id": "zai-org/GLM-4.7",
26
+ "name": "GLM 4.7",
27
+ "reasoning": false,
28
+ "input": [
29
+ "text"
30
+ ],
31
+ "cost": {
32
+ "input": 0.6,
33
+ "output": 2.2,
34
+ "cacheRead": 0,
35
+ "cacheWrite": 0
36
+ },
37
+ "contextWindow": 200000,
38
+ "maxTokens": 200000,
39
+ "compat": {
40
+ "supportsDeveloperRole": true,
41
+ "supportsStore": false,
42
+ "maxTokensField": "max_completion_tokens",
43
+ "thinkingFormat": "openai"
44
+ }
45
+ },
46
+ {
47
+ "id": "zai-org/GLM-5",
48
+ "name": "GLM 5",
49
+ "reasoning": false,
50
+ "input": [
51
+ "text"
52
+ ],
53
+ "cost": {
54
+ "input": 0.95,
55
+ "output": 3.15,
56
+ "cacheRead": 0,
57
+ "cacheWrite": 0
58
+ },
59
+ "contextWindow": 202800,
60
+ "maxTokens": 202800,
61
+ "compat": {
62
+ "supportsDeveloperRole": true,
63
+ "supportsStore": false,
64
+ "maxTokensField": "max_completion_tokens",
65
+ "thinkingFormat": "openai"
66
+ }
67
+ },
68
+ {
69
+ "id": "zai-org/GLM-5.1",
70
+ "name": "GLM 5.1",
71
+ "reasoning": false,
72
+ "input": [
73
+ "text"
74
+ ],
75
+ "cost": {
76
+ "input": 1.3,
77
+ "output": 4.3,
78
+ "cacheRead": 0,
79
+ "cacheWrite": 0
80
+ },
81
+ "contextWindow": 202800,
82
+ "maxTokens": 202800,
83
+ "compat": {
84
+ "supportsDeveloperRole": true,
85
+ "supportsStore": false,
86
+ "maxTokensField": "max_completion_tokens"
87
+ }
88
+ },
89
+ {
90
+ "id": "zai-org/GLM-5.2",
91
+ "name": "GLM 5.2",
92
+ "reasoning": true,
93
+ "input": [
94
+ "text"
95
+ ],
96
+ "cost": {
97
+ "input": 1.4,
98
+ "output": 4.4,
99
+ "cacheRead": 0,
100
+ "cacheWrite": 0
101
+ },
102
+ "contextWindow": 262144,
103
+ "maxTokens": 262144,
104
+ "compat": {
105
+ "supportsDeveloperRole": true,
106
+ "supportsStore": false,
107
+ "maxTokensField": "max_completion_tokens",
108
+ "thinkingFormat": "openai"
109
+ }
110
+ },
111
+ {
112
+ "id": "moonshotai/Kimi-K2.5",
113
+ "name": "Kimi K2.5",
114
+ "reasoning": false,
115
+ "input": [
116
+ "text",
117
+ "image"
118
+ ],
119
+ "cost": {
120
+ "input": 0.6,
121
+ "output": 3,
122
+ "cacheRead": 0,
123
+ "cacheWrite": 0
124
+ },
125
+ "contextWindow": 262000,
126
+ "maxTokens": 262000,
127
+ "compat": {
128
+ "supportsDeveloperRole": true,
129
+ "supportsStore": false,
130
+ "maxTokensField": "max_completion_tokens",
131
+ "thinkingFormat": "openai"
132
+ }
133
+ },
134
+ {
135
+ "id": "moonshotai/Kimi-K2.6",
136
+ "name": "Kimi K2.6",
137
+ "reasoning": true,
138
+ "input": [
139
+ "text",
140
+ "image"
141
+ ],
142
+ "cost": {
143
+ "input": 0.95,
144
+ "output": 4,
145
+ "cacheRead": 0,
146
+ "cacheWrite": 0
147
+ },
148
+ "contextWindow": 262000,
149
+ "maxTokens": 262000,
150
+ "compat": {
151
+ "supportsDeveloperRole": true,
152
+ "supportsStore": false,
153
+ "maxTokensField": "max_completion_tokens",
154
+ "thinkingFormat": "qwen-chat-template"
155
+ }
156
+ },
157
+ {
158
+ "id": "moonshotai/Kimi-K2.7-Code",
159
+ "name": "Kimi K2.7 Code",
160
+ "reasoning": true,
161
+ "input": [
162
+ "text"
163
+ ],
164
+ "cost": {
165
+ "input": 0.95,
166
+ "output": 4,
167
+ "cacheRead": 0,
168
+ "cacheWrite": 0
169
+ },
170
+ "contextWindow": 262000,
171
+ "maxTokens": 262000,
172
+ "compat": {
173
+ "supportsDeveloperRole": true,
174
+ "supportsStore": false,
175
+ "maxTokensField": "max_completion_tokens",
176
+ "thinkingFormat": "openai"
177
+ }
178
+ },
179
+ {
180
+ "id": "nvidia/Nemotron-120B-A12B",
181
+ "name": "Nemotron Super",
182
+ "reasoning": true,
183
+ "input": [
184
+ "text"
185
+ ],
186
+ "cost": {
187
+ "input": 0.3,
188
+ "output": 0.75,
189
+ "cacheRead": 0,
190
+ "cacheWrite": 0
191
+ },
192
+ "contextWindow": 202800,
193
+ "maxTokens": 202800,
194
+ "compat": {
195
+ "supportsDeveloperRole": true,
196
+ "supportsStore": false,
197
+ "maxTokensField": "max_completion_tokens",
198
+ "thinkingFormat": "openai"
199
+ }
200
+ },
201
+ {
202
+ "id": "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B",
203
+ "name": "Nemotron Ultra",
204
+ "reasoning": true,
205
+ "input": [
206
+ "text"
207
+ ],
208
+ "cost": {
209
+ "input": 0.6,
210
+ "output": 2.4,
211
+ "cacheRead": 0,
212
+ "cacheWrite": 0
213
+ },
214
+ "contextWindow": 202800,
215
+ "maxTokens": 202800,
216
+ "compat": {
217
+ "supportsDeveloperRole": true,
218
+ "supportsStore": false,
219
+ "maxTokensField": "max_completion_tokens",
220
+ "thinkingFormat": "openai"
221
+ }
222
+ },
223
+ {
224
+ "id": "openai/gpt-oss-120b",
225
+ "name": "OpenAI GPT 120B",
226
+ "reasoning": true,
227
+ "input": [
228
+ "text"
229
+ ],
230
+ "cost": {
231
+ "input": 0.1,
232
+ "output": 0.5,
233
+ "cacheRead": 0,
234
+ "cacheWrite": 0
235
+ },
236
+ "contextWindow": 128072,
237
+ "maxTokens": 128072,
238
+ "compat": {
239
+ "supportsDeveloperRole": true,
240
+ "supportsStore": false,
241
+ "maxTokensField": "max_completion_tokens",
242
+ "thinkingFormat": "openai",
243
+ "supportsReasoningEffort": true
244
+ }
245
+ }
246
+ ]
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "pi-baseten-provider",
3
+ "version": "1.0.1",
4
+ "description": "Baseten provider extension for pi - Access DeepSeek, Kimi, GLM, MiniMax, Nemotron, and GPT-OSS models through the Baseten Model API",
5
+ "author": "monotykamary",
6
+ "homepage": "https://github.com/monotykamary/pi-baseten-provider#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/monotykamary/pi-baseten-provider.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/monotykamary/pi-baseten-provider/issues"
13
+ },
14
+ "type": "module",
15
+ "main": "index.ts",
16
+ "keywords": [
17
+ "pi",
18
+ "extension",
19
+ "provider",
20
+ "baseten",
21
+ "ai",
22
+ "llm",
23
+ "deepseek",
24
+ "kimi",
25
+ "glm",
26
+ "minimax",
27
+ "nemotron"
28
+ ],
29
+ "license": "MIT",
30
+ "files": [
31
+ "index.ts",
32
+ "models.json",
33
+ "custom-models.json",
34
+ "patch.json",
35
+ "scripts/update-models.js"
36
+ ],
37
+ "pi": {
38
+ "extensions": [
39
+ "./index.ts"
40
+ ]
41
+ },
42
+ "scripts": {
43
+ "clean": "echo 'nothing to clean'",
44
+ "build": "echo 'nothing to build'",
45
+ "check": "echo 'nothing to check'",
46
+ "update-models": "node scripts/update-models.js"
47
+ }
48
+ }
package/patch.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "moonshotai/Kimi-K2.6": {
3
+ "reasoning": true,
4
+ "cost": {
5
+ "input": 0.6,
6
+ "output": 3.0
7
+ },
8
+ "compat": {
9
+ "thinkingFormat": "qwen-chat-template"
10
+ }
11
+ },
12
+ "moonshotai/Kimi-K2.5": {
13
+ "reasoning": true,
14
+ "compat": {
15
+ "thinkingFormat": "qwen-chat-template"
16
+ }
17
+ },
18
+ "zai-org/GLM-4.7": {
19
+ "reasoning": true,
20
+ "compat": {
21
+ "thinkingFormat": "qwen-chat-template"
22
+ }
23
+ },
24
+ "zai-org/GLM-5": {
25
+ "reasoning": true,
26
+ "compat": {
27
+ "thinkingFormat": "qwen-chat-template"
28
+ }
29
+ },
30
+ "zai-org/GLM-5.2": {
31
+ "compat": {
32
+ "thinkingFormat": "qwen-chat-template"
33
+ },
34
+ "thinkingLevelMap": {
35
+ "minimal": "minimal",
36
+ "low": "low",
37
+ "medium": "medium",
38
+ "high": "high",
39
+ "xhigh": "max"
40
+ }
41
+ },
42
+ "moonshotai/Kimi-K2.7-Code": {
43
+ "thinkingLevelMap": {
44
+ "minimal": null,
45
+ "low": "low",
46
+ "medium": "medium",
47
+ "high": "high",
48
+ "xhigh": null
49
+ }
50
+ },
51
+ "nvidia/Nemotron-120B-A12B": {
52
+ "reasoning": true
53
+ }
54
+ }
@@ -0,0 +1,330 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Update Baseten models from API
4
+ *
5
+ * Fetches models from https://inference.baseten.co/v1/models and updates:
6
+ * - models.json: Provider model definitions (enriched with pricing & compat)
7
+ * - README.md: Model table in the Available Models section
8
+ *
9
+ * The Baseten /v1/models API returns model info including pricing per token,
10
+ * context lengths, and supported features. Pricing is converted from per-token
11
+ * to per-million-tokens for pi.
12
+ *
13
+ * models.json is the source of truth for curated specs — the script preserves
14
+ * existing data and only adds new models with API-derived defaults.
15
+ * Curate models.json manually after new model discovery.
16
+ *
17
+ * patch.json and custom-models.json are applied at runtime by the provider.
18
+ * They are NOT baked into models.json, but ARE used to generate the README table.
19
+ *
20
+ * Requires BASETEN_API_KEY environment variable.
21
+ */
22
+
23
+ import fs from 'fs';
24
+ import path from 'path';
25
+ import { fileURLToPath } from 'url';
26
+
27
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
28
+
29
+ const MODELS_API_URL = 'https://inference.baseten.co/v1/models';
30
+ const MODELS_JSON_PATH = path.join(__dirname, '..', 'models.json');
31
+ const PATCH_JSON_PATH = path.join(__dirname, '..', 'patch.json');
32
+ const CUSTOM_MODELS_JSON_PATH = path.join(__dirname, '..', 'custom-models.json');
33
+ const README_PATH = path.join(__dirname, '..', 'README.md');
34
+
35
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
36
+
37
+ function loadJson(filePath) {
38
+ try {
39
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+
45
+ function saveJson(filePath, data) {
46
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
47
+ console.log(`✓ Saved ${path.basename(filePath)}`);
48
+ }
49
+
50
+ // Convert per-token pricing from API to per-million-tokens
51
+ function toPerMillion(val) {
52
+ if (val === '' || val === null || val === undefined) return null;
53
+ return Math.round(parseFloat(val) * 1_000_000 * 100) / 100;
54
+ }
55
+
56
+ // ─── API fetch ───────────────────────────────────────────────────────────────
57
+
58
+ async function fetchModels() {
59
+ const apiKey = process.env.BASETEN_API_KEY;
60
+ if (!apiKey) {
61
+ throw new Error('BASETEN_API_KEY environment variable is required');
62
+ }
63
+
64
+ console.log(`Fetching models from ${MODELS_API_URL}...`);
65
+ const response = await fetch(MODELS_API_URL, {
66
+ headers: { 'Authorization': `Api-Key ${apiKey}` },
67
+ });
68
+
69
+ if (!response.ok) {
70
+ throw new Error(`API error: ${response.status} ${response.statusText}`);
71
+ }
72
+
73
+ const data = await response.json();
74
+ const models = data.data || [];
75
+ console.log(`✓ Fetched ${models.length} models from API`);
76
+ return models;
77
+ }
78
+
79
+ // ─── Transform API model → models.json entry ────────────────────────────────
80
+
81
+ function transformApiModel(apiModel, existingModelsMap) {
82
+ const id = apiModel.id;
83
+
84
+ // Preserve existing curated data (pricing, reasoning, compat, etc.)
85
+ if (existingModelsMap[id]) {
86
+ const existing = { ...existingModelsMap[id] };
87
+ // Update context window from API if changed
88
+ if (apiModel.context_length) {
89
+ existing.contextWindow = apiModel.context_length;
90
+ }
91
+ // Update max output tokens from API
92
+ if (apiModel.max_completion_tokens) {
93
+ existing.maxTokens = apiModel.max_completion_tokens;
94
+ }
95
+ // Update features from API
96
+ const features = apiModel.supported_features || [];
97
+ existing.reasoning = features.includes('reasoning') ?? existing.reasoning;
98
+ if (features.includes('vision') && !existing.input.includes('image')) {
99
+ existing.input = ['text', 'image'];
100
+ }
101
+ // Update pricing from API
102
+ const pricing = apiModel.pricing || {};
103
+ const inputCost = toPerMillion(pricing.prompt);
104
+ const outputCost = toPerMillion(pricing.completion);
105
+ if (inputCost !== null && inputCost > 0) existing.cost.input = inputCost;
106
+ if (outputCost !== null && outputCost > 0) existing.cost.output = outputCost;
107
+ return existing;
108
+ }
109
+
110
+ // New model — build from API data + sensible defaults
111
+ const features = apiModel.supported_features || [];
112
+ const pricing = apiModel.pricing || {};
113
+ const hasReasoning = features.includes('reasoning');
114
+ const hasVision = features.includes('vision');
115
+
116
+ const inputTypes = ['text'];
117
+ if (hasVision) inputTypes.push('image');
118
+
119
+ // Convert pricing from per-token to per-million
120
+ let inputCost = toPerMillion(pricing.prompt) || 0;
121
+ let outputCost = toPerMillion(pricing.completion) || 0;
122
+
123
+ const model = {
124
+ id,
125
+ name: apiModel.name || generateDisplayName(id),
126
+ reasoning: hasReasoning,
127
+ input: inputTypes,
128
+ cost: {
129
+ input: inputCost,
130
+ output: outputCost,
131
+ cacheRead: 0,
132
+ cacheWrite: 0,
133
+ },
134
+ contextWindow: apiModel.context_length || 131072,
135
+ maxTokens: apiModel.max_completion_tokens || apiModel.context_length || 131072,
136
+ };
137
+
138
+ // Add compat
139
+ const compat = {
140
+ supportsDeveloperRole: true,
141
+ supportsStore: false,
142
+ maxTokensField: 'max_completion_tokens',
143
+ };
144
+
145
+ if (hasReasoning) {
146
+ compat.thinkingFormat = 'openai';
147
+ }
148
+ if (features.includes('reasoning_effort')) {
149
+ compat.supportsReasoningEffort = true;
150
+ }
151
+
152
+ model.compat = compat;
153
+
154
+ return model;
155
+ }
156
+
157
+ function generateDisplayName(id) {
158
+ // Fallback: prettify the ID
159
+ return id
160
+ .split('/')
161
+ .pop()
162
+ .replace(/[-_]/g, ' ')
163
+ .replace(/\b\w/g, c => c.toUpperCase());
164
+ }
165
+
166
+ // ─── Patch & Custom Models ──────────────────────────────────────────────────
167
+
168
+ function applyPatch(model, patch) {
169
+ const result = { ...model };
170
+ if (patch.name !== undefined) result.name = patch.name;
171
+ if (patch.reasoning !== undefined) result.reasoning = patch.reasoning;
172
+ if (patch.input !== undefined) result.input = patch.input;
173
+ if (patch.contextWindow !== undefined) result.contextWindow = patch.contextWindow;
174
+ if (patch.maxTokens !== undefined) result.maxTokens = patch.maxTokens;
175
+ if (patch.thinkingLevelMap !== undefined) result.thinkingLevelMap = patch.thinkingLevelMap;
176
+ if (patch.cost) {
177
+ result.cost = {
178
+ input: patch.cost.input ?? result.cost.input,
179
+ output: patch.cost.output ?? result.cost.output,
180
+ cacheRead: patch.cost.cacheRead ?? result.cost.cacheRead,
181
+ cacheWrite: patch.cost.cacheWrite ?? result.cost.cacheWrite,
182
+ };
183
+ }
184
+ if (patch.compat) {
185
+ result.compat = { ...(result.compat || {}), ...patch.compat };
186
+ }
187
+ if (!result.reasoning && result.compat?.thinkingFormat) {
188
+ delete result.compat.thinkingFormat;
189
+ }
190
+ if (!result.reasoning && result.thinkingLevelMap) {
191
+ delete result.thinkingLevelMap;
192
+ }
193
+ if (result.compat && Object.keys(result.compat).length === 0) {
194
+ delete result.compat;
195
+ }
196
+ return result;
197
+ }
198
+
199
+ /** Full pipeline: base models → patch → custom → result */
200
+ function buildModels(baseModels, customModels, patchData) {
201
+ const modelMap = new Map();
202
+ for (const model of baseModels) {
203
+ modelMap.set(model.id, model);
204
+ }
205
+ for (const [id, patchEntry] of Object.entries(patchData)) {
206
+ const existing = modelMap.get(id);
207
+ if (existing) {
208
+ modelMap.set(id, applyPatch(existing, patchEntry));
209
+ }
210
+ }
211
+ for (const model of customModels) {
212
+ const existing = modelMap.get(model.id);
213
+ const patchEntry = patchData[model.id];
214
+ if (existing && patchEntry) {
215
+ modelMap.set(model.id, applyPatch(model, patchEntry));
216
+ } else if (existing) {
217
+ modelMap.set(model.id, model);
218
+ } else if (patchEntry) {
219
+ modelMap.set(model.id, applyPatch(model, patchEntry));
220
+ } else {
221
+ modelMap.set(model.id, model);
222
+ }
223
+ }
224
+ return Array.from(modelMap.values());
225
+ }
226
+
227
+ // ─── README generation ──────────────────────────────────────────────────────
228
+
229
+ function formatContext(n) {
230
+ if (n >= 1000000) return `${(n / 1000000).toFixed(1)}M`;
231
+ if (n >= 1000) return `${Math.round(n / 1000)}K`;
232
+ return n.toString();
233
+ }
234
+
235
+ function formatCost(cost) {
236
+ if (cost === 0) return 'Free';
237
+ if (cost === null || cost === undefined) return '-';
238
+ return `$${cost.toFixed(2)}`;
239
+ }
240
+
241
+ function generateReadmeTable(models) {
242
+ const lines = [
243
+ '| Model | Context | Vision | Reasoning | Input $/M | Output $/M |',
244
+ '|-------|---------|--------|-----------|-----------|------------|',
245
+ ];
246
+
247
+ for (const model of models) {
248
+ const context = formatContext(model.contextWindow);
249
+ const vision = model.input.includes('image') ? '✅' : '❌';
250
+ const reasoning = model.reasoning ? '✅' : '❌';
251
+ const inputCost = formatCost(model.cost.input);
252
+ const outputCost = formatCost(model.cost.output);
253
+
254
+ lines.push(`| ${model.name} | ${context} | ${vision} | ${reasoning} | ${inputCost} | ${outputCost} |`);
255
+ }
256
+
257
+ return lines.join('\n');
258
+ }
259
+
260
+ function updateReadme(models) {
261
+ let readme = fs.readFileSync(README_PATH, 'utf8');
262
+ const newTable = generateReadmeTable(models);
263
+
264
+ const tableRegex = /(## Available Models\n\n)\| Model \|[^\n]+\|\n\|[-| ]+\|(\n\|[^\n]+\|)*\n*/;
265
+
266
+ if (tableRegex.test(readme)) {
267
+ readme = readme.replace(tableRegex, (match, header) => `${header}${newTable}\n\n`);
268
+ fs.writeFileSync(README_PATH, readme);
269
+ console.log('✓ Updated README.md');
270
+ } else {
271
+ console.warn('⚠ Could not find model table in "## Available Models" section');
272
+ }
273
+ }
274
+
275
+ // ─── Main ────────────────────────────────────────────────────────────────────
276
+
277
+ async function main() {
278
+ try {
279
+ const apiModels = await fetchModels();
280
+
281
+ // Load existing models.json — source of truth for curated specs
282
+ const existingModels = loadJson(MODELS_JSON_PATH);
283
+ const existingModelsMap = {};
284
+ for (const m of (Array.isArray(existingModels) ? existingModels : [])) {
285
+ existingModelsMap[m.id] = m;
286
+ }
287
+
288
+ // Transform API models, preserving existing data where available
289
+ let models = apiModels.map(m =>
290
+ transformApiModel(m, existingModelsMap)
291
+ );
292
+
293
+ // Live API is authoritative — models absent from API are removed
294
+ // (embedded data is already used for enrichment in transformApiModel)
295
+
296
+ // Sort by model name
297
+ models.sort((a, b) => a.name.localeCompare(b.name));
298
+
299
+ // Save models.json (pure API output, no patch/custom baked in)
300
+ saveJson(MODELS_JSON_PATH, models);
301
+
302
+ // Build full model list for README: base → patch → custom
303
+ const patchData = loadJson(PATCH_JSON_PATH);
304
+ const customModels = loadJson(CUSTOM_MODELS_JSON_PATH);
305
+ const readmeModels = buildModels(models, Array.isArray(customModels) ? customModels : [], patchData);
306
+ readmeModels.sort((a, b) => a.name.localeCompare(b.name));
307
+
308
+ // Update README
309
+ updateReadme(readmeModels);
310
+
311
+ // Summary
312
+ const newIds = new Set(models.map(m => m.id));
313
+ const oldIds = new Set(Object.keys(existingModelsMap));
314
+ const added = [...newIds].filter(id => !oldIds.has(id));
315
+ const removed = [...oldIds].filter(id => !newIds.has(id));
316
+
317
+ console.log('\n--- Summary ---');
318
+ console.log(`Total models: ${models.length}`);
319
+ console.log(`Reasoning models: ${models.filter(m => m.reasoning).length}`);
320
+ console.log(`Vision models: ${models.filter(m => m.input.includes('image')).length}`);
321
+ if (added.length > 0) console.log(`New models: ${added.join(', ')} — curate models.json manually`);
322
+ if (removed.length > 0) console.log(`Removed models: ${removed.join(', ')}`);
323
+
324
+ } catch (error) {
325
+ console.error('Error:', error.message);
326
+ process.exit(1);
327
+ }
328
+ }
329
+
330
+ main();