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 +21 -0
- package/README.md +177 -0
- package/custom-models.json +24 -0
- package/index.ts +319 -0
- package/models.json +246 -0
- package/package.json +48 -0
- package/patch.json +54 -0
- package/scripts/update-models.js +330 -0
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
|
+
[](https://github.com/earendil-works/pi-coding-agent)
|
|
10
|
+
[](./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();
|