pi-extension-wandb 0.0.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +88 -0
  3. package/index.ts +176 -0
  4. package/package.json +52 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kiran Gadhave
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,88 @@
1
+ # pi-extension-wandb
2
+
3
+ A [pi coding agent](https://github.com/earendil-works/pi) extension that adds
4
+ [Weights & Biases Inference](https://wandb.ai/site/inference/) as a model
5
+ provider. Model list is discovered dynamically from W&B's OpenAI-compatible
6
+ `/v1/models` endpoint, so new models show up without code changes.
7
+
8
+ ## Install
9
+
10
+ Get an API key from <https://wandb.ai/authorize>, then:
11
+
12
+ ```bash
13
+ export WANDB_API_KEY=... # required
14
+ export WANDB_PROJECT=team/project # optional, recommended
15
+
16
+ pi install https://github.com/kirangadhave/pi-extension-wandb
17
+ ```
18
+
19
+ Verify:
20
+
21
+ ```bash
22
+ pi --list-models | grep wandb
23
+ ```
24
+
25
+ ## Use
26
+
27
+ ```bash
28
+ pi --provider wandb --model deepseek-ai/DeepSeek-V3.1 "hello"
29
+ ```
30
+
31
+ ## Configuration
32
+
33
+ | Env var | Required | Notes |
34
+ | --- | --- | --- |
35
+ | `WANDB_API_KEY` | yes | From <https://wandb.ai/authorize>. If unset, the provider is silently skipped (no `wandb/*` models appear in `--list-models`). |
36
+ | `WANDB_PROJECT` | no\* | Sent as the `OpenAI-Project` header. Format: `team/project`. **If you belong to multiple W&B teams, set this explicitly** &mdash; otherwise W&B picks a default team for attribution, which can land usage on the wrong team. Single-team users can safely omit. |
37
+ | `WANDB_DEBUG` | no | `1` enables stderr logging from the extension (fetch errors, cache hits, etc.). |
38
+ | `WANDB_NO_CACHE` | no | `1` bypasses the model-list cache and fetches fresh on every pi startup. |
39
+
40
+ ## How it works
41
+
42
+ On every pi startup the extension:
43
+
44
+ 1. Reads `~/.cache/pi-extension-wandb/models.json` if it's fresh (< 1 hour old).
45
+ 2. Otherwise fetches `https://api.inference.wandb.ai/v1/models` (5s timeout)
46
+ and writes the result to the cache.
47
+ 3. Registers each returned model under the `wandb` provider with
48
+ `api: "openai-completions"`.
49
+
50
+ If the fetch fails and there is no usable cache, the provider is not
51
+ registered &mdash; `pi --list-models` will not show wandb models, and pi falls
52
+ back to its other configured providers cleanly.
53
+
54
+ ## Known limitations
55
+
56
+ - **Context windows are guesses.** W&B's `/v1/models` doesn't expose limits.
57
+ The extension hardcodes known windows for a handful of popular models
58
+ (`KNOWN_CONTEXT_WINDOWS` in `index.ts`); everything else gets a default of
59
+ 128K. PRs to extend the map are welcome.
60
+ - **Reasoning detection is heuristic.** Substring match against the model id
61
+ (`REASONING_MODEL_PATTERNS`). Edit the list for new families.
62
+ - **No cost tracking.** All models report zero cost. W&B publishes per-token
63
+ pricing &mdash; PRs to encode it are welcome.
64
+ - **No provider-specific `compat` flags.** Some reasoning models (Qwen3
65
+ thinking, gpt-oss, DeepSeek-R1) may need provider-specific quirks for
66
+ thinking mode. If you hit a problem, open an issue with the model id and
67
+ the exact failure.
68
+
69
+ ## Contributing
70
+
71
+ PRs welcome. The only file you typically need to touch is `index.ts`:
72
+
73
+ - `KNOWN_CONTEXT_WINDOWS` &mdash; add accurate context sizes for new models.
74
+ - `REASONING_MODEL_PATTERNS` &mdash; add model family substrings that support
75
+ reasoning/thinking.
76
+
77
+ For editor type-checking, after cloning:
78
+
79
+ ```bash
80
+ pnpm install
81
+ pnpm check
82
+ ```
83
+
84
+ pi loads `index.ts` directly at runtime (no build step).
85
+
86
+ ## License
87
+
88
+ MIT &mdash; see [LICENSE](./LICENSE).
package/index.ts ADDED
@@ -0,0 +1,176 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+
6
+ const BASE_URL = "https://api.inference.wandb.ai/v1";
7
+ const CACHE_DIR = join(homedir(), ".cache", "pi-extension-wandb");
8
+ const CACHE_FILE = join(CACHE_DIR, "models.json");
9
+ const CACHE_TTL_MS = 60 * 60 * 1000;
10
+ const FETCH_TIMEOUT_MS = 5_000;
11
+
12
+ // Substring matches against model id (case-insensitive). Add new reasoning
13
+ // families here; PRs welcome.
14
+ const REASONING_MODEL_PATTERNS = [
15
+ "deepseek-r1",
16
+ "deepseek-v3.1",
17
+ "deepseek-v3.2",
18
+ "gpt-oss",
19
+ "qwen3",
20
+ "glm-4.5",
21
+ "glm-5",
22
+ ];
23
+
24
+ // W&B's /v1/models doesn't return context-window metadata. Hardcode the ones
25
+ // we know; unknown ids fall back to DEFAULT_CONTEXT_WINDOW. PRs welcome.
26
+ const KNOWN_CONTEXT_WINDOWS: Record<string, number> = {
27
+ "deepseek-ai/DeepSeek-V3.1": 128_000,
28
+ "deepseek-ai/DeepSeek-R1-0528": 161_000,
29
+ "meta-llama/Llama-3.1-8B-Instruct": 131_072,
30
+ "meta-llama/Llama-3.3-70B-Instruct": 131_072,
31
+ "meta-llama/Llama-4-Scout-17B-16E-Instruct": 131_072,
32
+ "moonshotai/Kimi-K2-Instruct": 131_072,
33
+ "openai/gpt-oss-20b": 131_072,
34
+ "openai/gpt-oss-120b": 131_072,
35
+ "Qwen/Qwen3-235B-A22B-Instruct-2507": 262_144,
36
+ "Qwen/Qwen3-Coder-480B-A35B-Instruct": 262_144,
37
+ "zai-org/GLM-4.5": 131_072,
38
+ };
39
+
40
+ const DEFAULT_CONTEXT_WINDOW = 131_072;
41
+ const DEFAULT_MAX_TOKENS = 16_384;
42
+
43
+ type ApiModel = {
44
+ id: string;
45
+ name?: string;
46
+ };
47
+
48
+ type CachedModels = {
49
+ fetchedAt: number;
50
+ data: ApiModel[];
51
+ };
52
+
53
+ const isReasoning = (id: string): boolean => {
54
+ const lower = id.toLowerCase();
55
+ return REASONING_MODEL_PATTERNS.some((p) => lower.includes(p));
56
+ };
57
+
58
+ const prettyName = (id: string): string => {
59
+ if (!id.includes("/")) return id;
60
+ const [vendor, ...rest] = id.split("/");
61
+ return `${vendor} · ${rest.join("/")}`;
62
+ };
63
+
64
+ const log = (msg: string): void => {
65
+ if (process.env.WANDB_DEBUG === "1") {
66
+ console.error(`[pi-extension-wandb] ${msg}`);
67
+ }
68
+ };
69
+
70
+ async function fetchModels(apiKey: string, project?: string): Promise<ApiModel[]> {
71
+ const headers: Record<string, string> = {
72
+ Authorization: `Bearer ${apiKey}`,
73
+ Accept: "application/json",
74
+ };
75
+ if (project) headers["OpenAI-Project"] = project;
76
+
77
+ const controller = new AbortController();
78
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
79
+ try {
80
+ const res = await fetch(`${BASE_URL}/models`, { headers, signal: controller.signal });
81
+ if (!res.ok) {
82
+ throw new Error(`HTTP ${res.status}: ${await res.text()}`);
83
+ }
84
+ const payload = (await res.json()) as { data?: ApiModel[] };
85
+ const data = payload.data ?? [];
86
+ if (data.length === 0) throw new Error("empty model list");
87
+ return data;
88
+ } finally {
89
+ clearTimeout(timer);
90
+ }
91
+ }
92
+
93
+ function readCache(): ApiModel[] | null {
94
+ try {
95
+ if (!existsSync(CACHE_FILE)) return null;
96
+ const cached = JSON.parse(readFileSync(CACHE_FILE, "utf8")) as CachedModels;
97
+ return cached.data;
98
+ } catch (err) {
99
+ log(`cache read failed: ${(err as Error).message}`);
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function isCacheFresh(): boolean {
105
+ try {
106
+ if (!existsSync(CACHE_FILE)) return false;
107
+ const cached = JSON.parse(readFileSync(CACHE_FILE, "utf8")) as CachedModels;
108
+ return Date.now() - cached.fetchedAt < CACHE_TTL_MS;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ function writeCache(data: ApiModel[]): void {
115
+ try {
116
+ mkdirSync(CACHE_DIR, { recursive: true });
117
+ const cached: CachedModels = { fetchedAt: Date.now(), data };
118
+ writeFileSync(CACHE_FILE, JSON.stringify(cached));
119
+ } catch (err) {
120
+ log(`cache write failed: ${(err as Error).message}`);
121
+ }
122
+ }
123
+
124
+ export default async function (pi: ExtensionAPI): Promise<void> {
125
+ const apiKey = process.env.WANDB_API_KEY;
126
+ const project = process.env.WANDB_PROJECT;
127
+ const skipCache = process.env.WANDB_NO_CACHE === "1";
128
+
129
+ if (!apiKey) {
130
+ log("WANDB_API_KEY not set; provider not registered. See README.");
131
+ return;
132
+ }
133
+
134
+ let models: ApiModel[] | null = null;
135
+ if (!skipCache && isCacheFresh()) {
136
+ models = readCache();
137
+ if (models) log(`using fresh cached model list (${models.length} models)`);
138
+ }
139
+
140
+ if (!models) {
141
+ try {
142
+ models = await fetchModels(apiKey, project);
143
+ writeCache(models);
144
+ log(`fetched ${models.length} models from /v1/models`);
145
+ } catch (err) {
146
+ log(`fetch failed: ${(err as Error).message}`);
147
+ models = readCache();
148
+ if (models) {
149
+ log(`using stale cached model list (${models.length} models)`);
150
+ } else {
151
+ log("no cached models available; provider not registered.");
152
+ return;
153
+ }
154
+ }
155
+ }
156
+
157
+ const headers: Record<string, string> = {};
158
+ if (project) headers["OpenAI-Project"] = project;
159
+
160
+ pi.registerProvider("wandb", {
161
+ name: "Weights & Biases Inference",
162
+ baseUrl: BASE_URL,
163
+ apiKey: "WANDB_API_KEY",
164
+ api: "openai-completions",
165
+ headers,
166
+ models: models.map((m) => ({
167
+ id: m.id,
168
+ name: m.name ?? prettyName(m.id),
169
+ reasoning: isReasoning(m.id),
170
+ input: ["text"],
171
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
172
+ contextWindow: KNOWN_CONTEXT_WINDOWS[m.id] ?? DEFAULT_CONTEXT_WINDOW,
173
+ maxTokens: DEFAULT_MAX_TOKENS,
174
+ })),
175
+ });
176
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "pi-extension-wandb",
3
+ "version": "0.0.0",
4
+ "description": "pi coding agent extension that adds Weights & Biases Inference as a model provider.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "author": "Kiran Gadhave",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/kirangadhave/pi-extension-wandb.git"
17
+ },
18
+ "homepage": "https://github.com/kirangadhave/pi-extension-wandb",
19
+ "bugs": {
20
+ "url": "https://github.com/kirangadhave/pi-extension-wandb/issues"
21
+ },
22
+ "keywords": [
23
+ "pi",
24
+ "pi-extension",
25
+ "pi-coding-agent",
26
+ "wandb",
27
+ "weights-and-biases",
28
+ "inference",
29
+ "llm",
30
+ "provider"
31
+ ],
32
+ "files": [
33
+ "index.ts",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "scripts": {
38
+ "clean": "echo 'nothing to clean'",
39
+ "build": "echo 'nothing to build'",
40
+ "check": "tsc --noEmit"
41
+ },
42
+ "pi": {
43
+ "extensions": [
44
+ "./index.ts"
45
+ ]
46
+ },
47
+ "devDependencies": {
48
+ "@earendil-works/pi-coding-agent": "^0.75.0",
49
+ "@types/node": "^22.0.0",
50
+ "typescript": "^5.5.0"
51
+ }
52
+ }