pi-codebuddy-sdk 0.3.1 → 0.4.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/README.md +25 -0
- package/package.json +1 -1
- package/src/config.ts +11 -0
- package/src/index.ts +58 -3
- package/src/models-cache.ts +48 -0
- package/src/models.ts +43 -5
package/README.md
CHANGED
|
@@ -102,6 +102,25 @@ File: `~/.pi/agent/codebuddy-sdk.json` or `.pi/codebuddy-sdk.json` in a project.
|
|
|
102
102
|
| `provider.appendSystemPrompt` | `true` | Use Pi's system prompt instead of CodeBuddy's default identity |
|
|
103
103
|
| `provider.strictMcpConfig` | `true` | Use only Pi-bridged MCP tools |
|
|
104
104
|
| `provider.pathToCodebuddyCode` | auto | Path to `codebuddy` when it is **not** on `PATH` |
|
|
105
|
+
| `provider.contextWindow` | estimated | Override context window (tokens) for **all** models (e.g. `300000`) |
|
|
106
|
+
| `provider.maxTokens` | estimated | Override max output tokens for **all** models (e.g. `32768`) |
|
|
107
|
+
| `provider.modelOverrides` | — | Per-model overrides, keyed by model id or substring (see below) |
|
|
108
|
+
|
|
109
|
+
`modelOverrides` lets you fine-tune individual models; the longest matching
|
|
110
|
+
key wins and beats the global `contextWindow` / `maxTokens`:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"provider": {
|
|
115
|
+
"modelOverrides": {
|
|
116
|
+
"gpt-5": { "contextWindow": 1048576, "maxTokens": 64000, "images": false },
|
|
117
|
+
"claude": { "reasoning": true }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Supported per-model keys: `contextWindow`, `maxTokens`, `reasoning`, `images`.
|
|
105
124
|
|
|
106
125
|
## Privacy
|
|
107
126
|
|
|
@@ -117,6 +136,12 @@ export CODEBUDDY_SDK_DEBUG=1
|
|
|
117
136
|
|
|
118
137
|
Default log: `~/.pi/agent/codebuddy-sdk.log`. See [CONTRIBUTING.md](CONTRIBUTING.md) for maintainer details.
|
|
119
138
|
|
|
139
|
+
**After `pi -r`, the previously selected codebuddy model is gone from `/model`?**
|
|
140
|
+
The last-known-good model list is cached to `~/.pi/agent/codebuddy-sdk-models.json` on every
|
|
141
|
+
successful discovery, so a new pi process can re-register it synchronously at module load —
|
|
142
|
+
even when SDK discovery is still in flight (new process, gate busy, or stale auth). If the
|
|
143
|
+
model is still missing, share the debug log (`CODEBUDDY_SDK_DEBUG=1`) and the cache file.
|
|
144
|
+
|
|
120
145
|
## Development
|
|
121
146
|
|
|
122
147
|
Maintainers: [CONTRIBUTING.md](CONTRIBUTING.md).
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -27,6 +27,17 @@ export interface Config {
|
|
|
27
27
|
settingSources?: SettingSource[];
|
|
28
28
|
strictMcpConfig?: boolean;
|
|
29
29
|
pathToCodebuddyCode?: string;
|
|
30
|
+
/** Override the estimated context window (tokens) for ALL models. */
|
|
31
|
+
contextWindow?: number;
|
|
32
|
+
/** Override the estimated max output tokens for ALL models. */
|
|
33
|
+
maxTokens?: number;
|
|
34
|
+
/** Per-model overrides keyed by model id (or id substring). */
|
|
35
|
+
modelOverrides?: Record<string, {
|
|
36
|
+
contextWindow?: number;
|
|
37
|
+
maxTokens?: number;
|
|
38
|
+
reasoning?: boolean;
|
|
39
|
+
images?: boolean;
|
|
40
|
+
}>;
|
|
30
41
|
};
|
|
31
42
|
}
|
|
32
43
|
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { homedir } from "os";
|
|
|
10
10
|
import { dirname, join } from "path";
|
|
11
11
|
import { PROVIDER_ID, messageContentToText, convertPiMessages } from "./convert.js";
|
|
12
12
|
import { buildModels, codebuddyModelId, FALLBACK_MODELS, rawModelsFromSdk, resolveModel as _resolveModel, type PiModel } from "./models.js";
|
|
13
|
+
import { readModelsCache, writeModelsCache } from "./models-cache.js";
|
|
13
14
|
import { MCP_SERVER_NAME, MCP_TOOL_PREFIX, buildCodebuddySystemPrompt } from "./skills.js";
|
|
14
15
|
import { verifyWrittenSession as _verifyWrittenSession } from "./session-verify.js";
|
|
15
16
|
import { extractAllToolResults as _extractAllToolResults, type McpResult } from "./extract-tool-results.js";
|
|
@@ -877,7 +878,18 @@ function processStreamEvent(
|
|
|
877
878
|
}
|
|
878
879
|
|
|
879
880
|
if (event?.type === "content_block_delta") {
|
|
880
|
-
|
|
881
|
+
// Match the most-recently-started block at this content_block index.
|
|
882
|
+
// A later tool_use can reuse an index already held by an earlier
|
|
883
|
+
// block (seen in real streams); first-match lookup would append the
|
|
884
|
+
// later block's deltas onto the earlier one, concatenating two JSON
|
|
885
|
+
// payloads and silently dropping one tool's arguments. Reverse scan
|
|
886
|
+
// routes deltas to the active (last-started) block. (Stop events
|
|
887
|
+
// keep first-match: they run in start order and each deletes its
|
|
888
|
+
// index, so the next stop finds the correct block.)
|
|
889
|
+
let index = -1;
|
|
890
|
+
for (let i = c.turnBlocks.length - 1; i >= 0; i--) {
|
|
891
|
+
if (c.turnBlocks[i].index === event.index) { index = i; break; }
|
|
892
|
+
}
|
|
881
893
|
const block = c.turnBlocks[index];
|
|
882
894
|
if (!block) return;
|
|
883
895
|
if (event.delta?.type === "text_delta" && block.type === "text") {
|
|
@@ -1572,7 +1584,18 @@ async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
|
1572
1584
|
const supported = await q.supportedModels();
|
|
1573
1585
|
await q.return().catch(() => {});
|
|
1574
1586
|
if (!supported.length) return;
|
|
1575
|
-
MODELS = buildModels(
|
|
1587
|
+
MODELS = buildModels(
|
|
1588
|
+
rawModelsFromSdk(supported as any),
|
|
1589
|
+
providerSettings.modelOverrides ? undefined : providerSettings,
|
|
1590
|
+
providerSettings.modelOverrides,
|
|
1591
|
+
);
|
|
1592
|
+
// Persist the successful discovery globally so a later session
|
|
1593
|
+
// resume (module reload) can recover the real model list even if
|
|
1594
|
+
// discovery fails that time (e.g. SDK subprocess gate is busy).
|
|
1595
|
+
// Written to disk so a NEW pi process (`pi -r` after exit) can also
|
|
1596
|
+
// recover it synchronously at module load — the in-process global
|
|
1597
|
+
// alone dies with the old process.
|
|
1598
|
+
writeModelsCache(MODELS);
|
|
1576
1599
|
const g = globalThis as Record<symbol, any>;
|
|
1577
1600
|
const streamFn = g[ACTIVE_STREAM_SIMPLE_KEY] ?? streamCodebuddySdk;
|
|
1578
1601
|
pi.registerProvider(PROVIDER_ID, {
|
|
@@ -1586,7 +1609,29 @@ async function discoverModels(pi: ExtensionAPI): Promise<void> {
|
|
|
1586
1609
|
modelsDiscovered = true;
|
|
1587
1610
|
debug(`discoverModels: registered ${MODELS.length} models from SDK`);
|
|
1588
1611
|
} catch (err) {
|
|
1589
|
-
|
|
1612
|
+
// Resume/reload: discovery may fail (SDK subprocess gate busy,
|
|
1613
|
+
// stale auth, transient CLI error). Recover the last known-good
|
|
1614
|
+
// model list instead of collapsing to the single fallback model —
|
|
1615
|
+
// otherwise the previously selected codebuddy model disappears
|
|
1616
|
+
// from /model after `pi -r` and the user must re-pick it.
|
|
1617
|
+
const cached = readModelsCache();
|
|
1618
|
+
if (cached && cached.length) {
|
|
1619
|
+
MODELS = cached;
|
|
1620
|
+
debug(`discoverModels: failed, recovered ${MODELS.length} cached models`, err);
|
|
1621
|
+
const g = globalThis as Record<symbol, any>;
|
|
1622
|
+
const streamFn = g[ACTIVE_STREAM_SIMPLE_KEY] ?? streamCodebuddySdk;
|
|
1623
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
1624
|
+
name: "CodeBuddy",
|
|
1625
|
+
baseUrl: PROVIDER_ID,
|
|
1626
|
+
apiKey: "not-used",
|
|
1627
|
+
api: "codebuddy-sdk",
|
|
1628
|
+
models: MODELS as any,
|
|
1629
|
+
streamSimple: streamFn as any,
|
|
1630
|
+
});
|
|
1631
|
+
modelsDiscovered = true;
|
|
1632
|
+
} else {
|
|
1633
|
+
debug("discoverModels: failed, using fallback models", err);
|
|
1634
|
+
}
|
|
1590
1635
|
}
|
|
1591
1636
|
});
|
|
1592
1637
|
}
|
|
@@ -1602,6 +1647,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1602
1647
|
const config = loadConfig(process.cwd());
|
|
1603
1648
|
debug("loadConfig:", JSON.stringify(config));
|
|
1604
1649
|
providerSettings = config.provider ?? {};
|
|
1650
|
+
// Apply config overrides to the fallback models immediately, and recover
|
|
1651
|
+
// a previously cached model list across module reloads (session resume)
|
|
1652
|
+
// or across processes (`pi -r` after exit).
|
|
1653
|
+
const cached = readModelsCache();
|
|
1654
|
+
if (cached && cached.length) {
|
|
1655
|
+
MODELS = cached;
|
|
1656
|
+
debug(`default: recovered ${MODELS.length} cached models from prior discovery`);
|
|
1657
|
+
} else {
|
|
1658
|
+
MODELS = buildModels(FALLBACK_MODELS, providerSettings, providerSettings.modelOverrides);
|
|
1659
|
+
}
|
|
1605
1660
|
const registeredModels = MODELS;
|
|
1606
1661
|
|
|
1607
1662
|
discoverInFlight = discoverModels(pi);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Persisted last-known-good model list.
|
|
2
|
+
//
|
|
3
|
+
// `pi -r` after exiting pi spawns a NEW process: the Symbol.for in-process
|
|
4
|
+
// cache from v0.4.0 dies with the old process, so a session-resume model
|
|
5
|
+
// resolution that runs while SDK discovery is still in flight only sees the
|
|
6
|
+
// fallback list and the previously selected codebuddy model drops out of
|
|
7
|
+
// /model (user must re-pick it). Persisting the list to disk lets the new
|
|
8
|
+
// process register the full model list synchronously at module load.
|
|
9
|
+
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
13
|
+
import type { PiModel } from "./models.js";
|
|
14
|
+
|
|
15
|
+
export const MODELS_CACHE_KEY = Symbol.for("codebuddy-sdk:modelsCache");
|
|
16
|
+
|
|
17
|
+
const defaultCacheFile = () => join(homedir(), ".pi", "agent", "codebuddy-sdk-models.json");
|
|
18
|
+
|
|
19
|
+
// Injectable for tests.
|
|
20
|
+
const cacheFile = () => process.env.CODEBUDDY_SDK_MODELS_CACHE_PATH || defaultCacheFile();
|
|
21
|
+
|
|
22
|
+
export function readModelsCache(): PiModel[] | undefined {
|
|
23
|
+
// In-process shared cache first (module reloads within a process).
|
|
24
|
+
const inProcess = (globalThis as Record<symbol, unknown>)[MODELS_CACHE_KEY] as PiModel[] | undefined;
|
|
25
|
+
if (inProcess?.length) return inProcess;
|
|
26
|
+
// Disk cache second (new process — `pi -r` after exiting pi).
|
|
27
|
+
try {
|
|
28
|
+
const parsed: unknown = JSON.parse(readFileSync(cacheFile(), "utf8"));
|
|
29
|
+
if (Array.isArray(parsed) && parsed.length) return parsed as PiModel[];
|
|
30
|
+
} catch {
|
|
31
|
+
// No cache yet, or corrupt file — caller falls back.
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function writeModelsCache(models: PiModel[]): void {
|
|
37
|
+
if (!models.length) return;
|
|
38
|
+
(globalThis as Record<symbol, unknown>)[MODELS_CACHE_KEY] = models;
|
|
39
|
+
try {
|
|
40
|
+
const path = cacheFile();
|
|
41
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
42
|
+
const tmp = `${path}.tmp`;
|
|
43
|
+
writeFileSync(tmp, JSON.stringify(models));
|
|
44
|
+
renameSync(tmp, path);
|
|
45
|
+
} catch {
|
|
46
|
+
// Cache is best-effort; discovery retries next load.
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/models.ts
CHANGED
|
@@ -55,11 +55,49 @@ export const FALLBACK_MODELS: PiModel[] = [
|
|
|
55
55
|
{ id: "hy3-preview-agent-ioa", name: "Hunyuan 3 Preview", reasoning: true, input: ["text"], contextWindow: DEFAULT_CONTEXT, maxTokens: DEFAULT_MAX_TOKENS, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } },
|
|
56
56
|
];
|
|
57
57
|
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
export type ModelOverrides = {
|
|
59
|
+
contextWindow?: number;
|
|
60
|
+
maxTokens?: number;
|
|
61
|
+
reasoning?: boolean;
|
|
62
|
+
images?: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Apply config-driven overrides on top of the estimated model metadata.
|
|
67
|
+
* `globalOverrides` applies to every model; `perModel` is keyed by model id
|
|
68
|
+
* (matched by exact id first, then case-insensitive substring, longest key
|
|
69
|
+
* wins) and beats the global defaults.
|
|
70
|
+
*/
|
|
71
|
+
export function buildModels(
|
|
72
|
+
models: PiModel[],
|
|
73
|
+
globalOverrides?: ModelOverrides,
|
|
74
|
+
perModel?: Record<string, ModelOverrides>,
|
|
75
|
+
): PiModel[] {
|
|
76
|
+
const sortedKeys = Object.keys(perModel ?? {}).sort((a, b) => b.length - a.length);
|
|
77
|
+
const match = (id: string): ModelOverrides | undefined => {
|
|
78
|
+
const lower = id.toLowerCase();
|
|
79
|
+
for (const key of sortedKeys) {
|
|
80
|
+
const k = key.toLowerCase();
|
|
81
|
+
if (id === key || (lower.includes(k) && k.length > 0)) return perModel![key];
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
return models.map((m) => {
|
|
86
|
+
const o = { ...globalOverrides, ...match(m.id) };
|
|
87
|
+
const contextWindow = o.contextWindow ?? m.contextWindow;
|
|
88
|
+
const maxTokens = o.maxTokens ?? m.maxTokens;
|
|
89
|
+
const input: ("text" | "image")[] = o.images === undefined
|
|
90
|
+
? m.input
|
|
91
|
+
: (o.images ? ["text", "image"] as const : ["text"] as const);
|
|
92
|
+
return {
|
|
93
|
+
...m,
|
|
94
|
+
contextWindow,
|
|
95
|
+
maxTokens,
|
|
96
|
+
reasoning: o.reasoning ?? m.reasoning,
|
|
97
|
+
input,
|
|
98
|
+
cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
99
|
+
};
|
|
100
|
+
});
|
|
63
101
|
}
|
|
64
102
|
|
|
65
103
|
export function codebuddyModelId(model: { id: string }): string {
|