pi-codebuddy-sdk 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-codebuddy-sdk",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Pi extension that uses CodeBuddy (via Agent SDK) as a model provider and adds an AskCodebuddy tool.",
5
5
  "author": "slahser",
6
6
  "license": "MIT",
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
@@ -124,6 +124,10 @@ const SDK_TO_PI_TOOL_NAME: Record<string, string> = {
124
124
  let MODELS: PiModel[] = buildModels(FALLBACK_MODELS);
125
125
  let providerSettings: NonNullable<Config["provider"]> = {};
126
126
 
127
+ // Last-known-good model list, kept in a shared global so a module reload
128
+ // during session resume can recover it when SDK discovery fails.
129
+ const MODELS_CACHE_KEY = Symbol.for("codebuddy-sdk:modelsCache");
130
+
127
131
  type ContentBlockParam =
128
132
  | { type: "text"; text: string }
129
133
  | { type: "image"; source: { type: "base64"; media_type: string; data: string } };
@@ -877,7 +881,18 @@ function processStreamEvent(
877
881
  }
878
882
 
879
883
  if (event?.type === "content_block_delta") {
880
- const index = c.turnBlocks.findIndex((b: any) => b.index === event.index);
884
+ // Match the most-recently-started block at this content_block index.
885
+ // A later tool_use can reuse an index already held by an earlier
886
+ // block (seen in real streams); first-match lookup would append the
887
+ // later block's deltas onto the earlier one, concatenating two JSON
888
+ // payloads and silently dropping one tool's arguments. Reverse scan
889
+ // routes deltas to the active (last-started) block. (Stop events
890
+ // keep first-match: they run in start order and each deletes its
891
+ // index, so the next stop finds the correct block.)
892
+ let index = -1;
893
+ for (let i = c.turnBlocks.length - 1; i >= 0; i--) {
894
+ if (c.turnBlocks[i].index === event.index) { index = i; break; }
895
+ }
881
896
  const block = c.turnBlocks[index];
882
897
  if (!block) return;
883
898
  if (event.delta?.type === "text_delta" && block.type === "text") {
@@ -1572,7 +1587,15 @@ async function discoverModels(pi: ExtensionAPI): Promise<void> {
1572
1587
  const supported = await q.supportedModels();
1573
1588
  await q.return().catch(() => {});
1574
1589
  if (!supported.length) return;
1575
- MODELS = buildModels(rawModelsFromSdk(supported as any));
1590
+ MODELS = buildModels(
1591
+ rawModelsFromSdk(supported as any),
1592
+ providerSettings.modelOverrides ? undefined : providerSettings,
1593
+ providerSettings.modelOverrides,
1594
+ );
1595
+ // Persist the successful discovery globally so a later session
1596
+ // resume (module reload) can recover the real model list even if
1597
+ // discovery fails that time (e.g. SDK subprocess gate is busy).
1598
+ (globalThis as Record<symbol, any>)[MODELS_CACHE_KEY] = 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
- debug("discoverModels: failed, using fallback models", err);
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 = (globalThis as Record<symbol, any>)[MODELS_CACHE_KEY] as PiModel[] | undefined;
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,15 @@ 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
+ const cached = (globalThis as Record<symbol, any>)[MODELS_CACHE_KEY] as PiModel[] | undefined;
1653
+ if (cached && cached.length) {
1654
+ MODELS = cached;
1655
+ debug(`default: recovered ${MODELS.length} cached models from prior discovery`);
1656
+ } else {
1657
+ MODELS = buildModels(FALLBACK_MODELS, providerSettings, providerSettings.modelOverrides);
1658
+ }
1605
1659
  const registeredModels = MODELS;
1606
1660
 
1607
1661
  discoverInFlight = discoverModels(pi);
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 function buildModels(models: PiModel[]): PiModel[] {
59
- return models.map((m) => ({
60
- ...m,
61
- cost: m.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
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 {