pi-ui-extend 0.1.62 → 0.1.63

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.
@@ -63,8 +63,8 @@ export class ModelCommandActions {
63
63
  const parsed = parseScopedModelRef(modelRef);
64
64
  if (!parsed)
65
65
  throw new Error("Model must use provider/model[:thinking] format");
66
- runtime.services.modelRegistry.refresh();
67
- const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
66
+ await runtime.services.modelRuntime.reloadConfig();
67
+ const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
68
68
  if (!model)
69
69
  throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
70
70
  await this.runModelCommand(model);
@@ -97,8 +97,8 @@ export class ModelCommandActions {
97
97
  const parsed = parseScopedModelRef(modelRef);
98
98
  if (!parsed)
99
99
  throw new Error("Model must use provider/model[:thinking] format");
100
- runtime.services.modelRegistry.refresh();
101
- const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
100
+ await runtime.services.modelRuntime.reloadConfig();
101
+ const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
102
102
  if (!model)
103
103
  throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
104
104
  this.saveDefaultModel(modelRef);
@@ -118,8 +118,8 @@ export class ModelCommandActions {
118
118
  const parsed = parseScopedModelRef(modelRef);
119
119
  if (!parsed)
120
120
  throw new Error("Model must use provider/model[:thinking] format, or run /autocomplete with no arguments to disable");
121
- runtime.services.modelRegistry.refresh();
122
- const model = runtime.services.modelRegistry.find(parsed.provider, parsed.modelId);
121
+ await runtime.services.modelRuntime.reloadConfig();
122
+ const model = runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId);
123
123
  if (!model)
124
124
  throw new Error(`Model not found: ${parsed.provider}/${parsed.modelId}`);
125
125
  const saved = savePixAutocompleteModel(modelRef);
@@ -199,10 +199,10 @@ export class ModelCommandActions {
199
199
  const refs = value.split(/[,\s]+/).map((ref) => ref.trim()).filter(Boolean);
200
200
  const scopedModels = [];
201
201
  const invalidRefs = [];
202
- runtime.services.modelRegistry.refresh();
202
+ await runtime.services.modelRuntime.reloadConfig();
203
203
  for (const ref of refs) {
204
204
  const parsed = parseScopedModelRef(ref);
205
- const model = parsed ? runtime.services.modelRegistry.find(parsed.provider, parsed.modelId) : undefined;
205
+ const model = parsed ? runtime.services.modelRuntime.getModel(parsed.provider, parsed.modelId) : undefined;
206
206
  if (!parsed || !model) {
207
207
  invalidRefs.push(ref);
208
208
  continue;
@@ -12,7 +12,7 @@ import { checkPixUpdate, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsag
12
12
  import { createStartupInfoMessage } from "../cli/startup-info.js";
13
13
  import { getCompleteSessionStats } from "../session/session-stats.js";
14
14
  import { loadSessionTitleConfig } from "../../bundled-extensions/session-title/config.js";
15
- import { fallbackSessionTitleFromInput, firstUserMessageText, generateSessionTitle, sessionTitleModelRefs, } from "../../bundled-extensions/session-title/title-generation.js";
15
+ import { fallbackSessionTitleFromInput, firstUserMessageText, generateSessionTitleWithRuntime, sessionTitleModelRefs, } from "../../bundled-extensions/session-title/title-generation.js";
16
16
  export class SessionCommandActions {
17
17
  host;
18
18
  constructor(host) {
@@ -116,7 +116,7 @@ export class SessionCommandActions {
116
116
  if (config.enabled) {
117
117
  for (const modelRef of modelRefs) {
118
118
  for (let attempt = 0; attempt < config.generationAttempts; attempt++) {
119
- generatedName = await generateSessionTitle(firstPrompt.slice(0, config.maxInputChars).trim(), runtime.services.modelRegistry, config, modelRef, AbortSignal.timeout(config.timeoutMs));
119
+ generatedName = await generateSessionTitleWithRuntime(firstPrompt.slice(0, config.maxInputChars).trim(), runtime.services.modelRuntime, config, modelRef, AbortSignal.timeout(config.timeoutMs));
120
120
  if (generatedName)
121
121
  break;
122
122
  }
@@ -1,4 +1,3 @@
1
- import { streamSimple } from "@earendil-works/pi-ai/compat";
2
1
  import { isRecord } from "../guards.js";
3
2
  import { parseModelRef } from "../model/model-ref.js";
4
3
  const AUTOCOMPLETE_DEBOUNCE_MS = 350;
@@ -140,17 +139,14 @@ export class AppAutocompleteController {
140
139
  }
141
140
  export async function completeInputWithPi(runtime, draft, config, signal) {
142
141
  const parsedModel = parseModelRef(config.modelRef);
143
- const registry = runtime.services.modelRegistry;
144
- let model = registry.find(parsedModel.provider, parsedModel.modelId);
142
+ const modelRuntime = runtime.services.modelRuntime;
143
+ let model = modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
145
144
  if (!model) {
146
- registry.refresh();
147
- model = registry.find(parsedModel.provider, parsedModel.modelId);
145
+ await modelRuntime.reloadConfig();
146
+ model = modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
148
147
  }
149
148
  if (!model)
150
149
  throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
151
- const auth = await registry.getApiKeyAndHeaders(model);
152
- if (!auth.ok)
153
- throw new Error(auth.error);
154
150
  const timeoutMs = numberInRange(config.timeoutMs, AUTOCOMPLETE_TIMEOUT_MS, 250, 10_000);
155
151
  const maxTokens = numberInRange(config.maxTokens, AUTOCOMPLETE_MAX_TOKENS, 8, 256);
156
152
  const maxPromptTokens = numberInRange(config.maxPromptTokens, AUTOCOMPLETE_MAX_PROMPT_TOKENS, 256, 16_000);
@@ -165,12 +161,10 @@ export async function completeInputWithPi(runtime, draft, config, signal) {
165
161
  let output = "";
166
162
  let streamError;
167
163
  try {
168
- const stream = streamSimple(requestModel, {
164
+ const stream = modelRuntime.streamSimple(requestModel, {
169
165
  systemPrompt: AUTOCOMPLETE_SYSTEM_PROMPT,
170
166
  messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
171
167
  }, {
172
- ...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
173
- ...(auth.headers === undefined ? {} : { headers: auth.headers }),
174
168
  cacheRetention: "none",
175
169
  maxRetryDelayMs: 0,
176
170
  maxRetries: 0,
@@ -127,9 +127,8 @@ async function enhancePromptWithPi(runtime, draft, config) {
127
127
  const services = await promptEnhancerPiDeps.createAgentSessionServices({
128
128
  cwd: runtime.cwd,
129
129
  agentDir: runtime.services.agentDir,
130
- authStorage: runtime.services.authStorage,
131
130
  settingsManager: runtime.services.settingsManager,
132
- modelRegistry: runtime.services.modelRegistry,
131
+ modelRuntime: runtime.services.modelRuntime,
133
132
  resourceLoaderOptions: {
134
133
  noExtensions: true,
135
134
  noSkills: true,
@@ -139,10 +138,10 @@ async function enhancePromptWithPi(runtime, draft, config) {
139
138
  systemPrompt: PROMPT_ENHANCER_SYSTEM_PROMPT,
140
139
  },
141
140
  });
142
- services.modelRegistry.refresh();
143
- const model = services.modelRegistry.find(parsedModel.provider, parsedModel.modelId);
141
+ await services.modelRuntime.reloadConfig();
142
+ const model = services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
144
143
  if (!model) {
145
- throw new Error(modelNotFoundMessage(parsedModel.provider, parsedModel.modelId, services.modelRegistry.getAll()));
144
+ throw new Error(modelNotFoundMessage(parsedModel.provider, parsedModel.modelId, services.modelRuntime.getModels()));
146
145
  }
147
146
  const { session } = await promptEnhancerPiDeps.createAgentSessionFromServices({
148
147
  services,
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { AuthStorage } from "@earendil-works/pi-coding-agent";
5
+ import { ModelRuntime, readStoredCredential } from "@earendil-works/pi-coding-agent";
6
6
  import { formatCompactProgressBar } from "../../context-progress-bar.js";
7
7
  import { APP_ICONS } from "../icons.js";
8
8
  const OPENAI_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
@@ -222,17 +222,27 @@ async function queryOpenAIAccountUsage(now) {
222
222
  async function refreshOpenAICodexAuth() {
223
223
  // Delegate to pi core so refresh-token rotation is persisted under the same
224
224
  // cross-process auth.json lock used by model requests.
225
- const authStorage = AuthStorage.create(getPiAuthPath());
226
- const access = await authStorage.getApiKey("openai-codex", { includeFallback: false });
227
- const credential = authStorage.get("openai-codex");
225
+ const modelRuntime = await ModelRuntime.create({
226
+ authPath: getPiAuthPath(),
227
+ allowModelNetwork: false,
228
+ });
229
+ let resolvedAuth;
230
+ try {
231
+ resolvedAuth = await modelRuntime.getAuth("openai-codex");
232
+ }
233
+ catch (error) {
234
+ throw new Error("OpenAI Codex OAuth token refresh failed", { cause: error });
235
+ }
236
+ const credential = readStoredCredential("openai-codex", getPiAuthPath());
237
+ const access = resolvedAuth?.auth.apiKey ?? credential?.access;
228
238
  if (!access || credential?.type !== "oauth" || !credential.access || isExpired(credential)) {
229
239
  throw new Error("OpenAI Codex OAuth token refresh failed");
230
240
  }
231
241
  return {
232
242
  type: "oauth",
233
243
  access: credential.access,
234
- refresh: credential.refresh,
235
- expires: credential.expires,
244
+ ...(credential.refresh === undefined ? {} : { refresh: credential.refresh }),
245
+ ...(credential.expires === undefined ? {} : { expires: credential.expires }),
236
246
  };
237
247
  }
238
248
  async function readOpenAIAuth() {
@@ -173,16 +173,15 @@ export class AppMenuItemsController {
173
173
  return current?.provider === model.provider && current.id === model.id;
174
174
  }
175
175
  resolveScopedModelRefs(modelRefs) {
176
- const registry = this.host.runtime()?.services.modelRegistry;
177
- if (!registry)
176
+ const modelRuntime = this.host.runtime()?.services.modelRuntime;
177
+ if (!modelRuntime)
178
178
  return [];
179
- registry.refresh();
180
179
  const scopedModels = [];
181
180
  for (const modelRef of modelRefs) {
182
181
  const parsed = parseScopedModelRef(modelRef);
183
182
  if (!parsed)
184
183
  continue;
185
- const model = registry.find(parsed.provider, parsed.modelId);
184
+ const model = modelRuntime.getModel(parsed.provider, parsed.modelId);
186
185
  if (!model)
187
186
  continue;
188
187
  scopedModels.push({
@@ -197,10 +196,9 @@ export class AppMenuItemsController {
197
196
  const scopedModels = session?.scopedModels.length ? session.scopedModels : this.getFavoriteScopedModels();
198
197
  if (!scopedModels.length)
199
198
  return [];
200
- const registry = this.host.runtime()?.services.modelRegistry;
201
- registry?.refresh();
199
+ const modelRuntime = this.host.runtime()?.services.modelRuntime;
202
200
  return scopedModels.map((scoped) => {
203
- const refreshed = registry?.find(scoped.model.provider, scoped.model.id);
201
+ const refreshed = modelRuntime?.getModel(scoped.model.provider, scoped.model.id);
204
202
  return (refreshed ?? scoped.model);
205
203
  });
206
204
  }
@@ -239,8 +239,8 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
239
239
  config,
240
240
  ...(runtimeOptions.eventBus === undefined ? {} : { eventBus: runtimeOptions.eventBus }),
241
241
  });
242
- services.modelRegistry.refresh();
243
- const model = parsedModel ? services.modelRegistry.find(parsedModel.provider, parsedModel.modelId) : undefined;
242
+ await services.modelRuntime.reloadConfig();
243
+ const model = parsedModel ? services.modelRuntime.getModel(parsedModel.provider, parsedModel.modelId) : undefined;
244
244
  if (parsedModel && !model) {
245
245
  throw new Error(`Model not found: ${parsedModel.provider}/${parsedModel.modelId}`);
246
246
  }
@@ -250,7 +250,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
250
250
  const scoped = parseScopedModelRef(modelRef);
251
251
  if (!scoped)
252
252
  return [];
253
- const scopedModel = services.modelRegistry.find(scoped.provider, scoped.modelId);
253
+ const scopedModel = services.modelRuntime.getModel(scoped.provider, scoped.modelId);
254
254
  if (!scopedModel)
255
255
  return [];
256
256
  return [
@@ -1,5 +1,6 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- export { fallbackSessionTitleFromInput, generateSessionTitle, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
2
+ export { generateSessionTitle } from "./title-generation-compat.js";
3
+ export { fallbackSessionTitleFromInput, generateSessionTitleWithRuntime, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
3
4
  export declare function firstUserMessageText(ctx: ExtensionContext): string | undefined;
4
5
  export declare function buildForkTitleInput(parentTitle: string | undefined, forkPrompt: string): string;
5
6
  export default function sessionTitle(pi: ExtensionAPI): void;
@@ -1,8 +1,10 @@
1
1
  import { SessionManager } from "@earendil-works/pi-coding-agent";
2
2
  import { resolve } from "node:path";
3
3
  import { loadSessionTitleConfig } from "./config.js";
4
- import { fallbackSessionTitleFromInput, firstUserMessageText as firstUserMessageTextFromEntries, generateSessionTitle, sessionTitleModelRefs, } from "./title-generation.js";
5
- export { fallbackSessionTitleFromInput, generateSessionTitle, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
4
+ import { fallbackSessionTitleFromInput, firstUserMessageText as firstUserMessageTextFromEntries, sessionTitleModelRefs, } from "./title-generation.js";
5
+ import { generateSessionTitle } from "./title-generation-compat.js";
6
+ export { generateSessionTitle } from "./title-generation-compat.js";
7
+ export { fallbackSessionTitleFromInput, generateSessionTitleWithRuntime, sessionTitleModelRefs, sanitizeSessionTitle } from "./title-generation.js";
6
8
  const DEFAULT_TERMINAL_TITLE = "pi";
7
9
  function isStaleExtensionContextError(error) {
8
10
  if (!(error instanceof Error))
@@ -0,0 +1,17 @@
1
+ import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import type { SessionTitleConfig } from "./config.js";
3
+ type TitleModelRegistry = {
4
+ find(provider: string, modelId: string): Model<Api> | undefined;
5
+ getApiKeyAndHeaders(model: Model<Api>): Promise<{
6
+ ok: true;
7
+ apiKey?: string;
8
+ headers?: Record<string, string>;
9
+ env?: Record<string, string>;
10
+ } | {
11
+ ok: false;
12
+ error: string;
13
+ }>;
14
+ };
15
+ /** Extension-side title generation through Pi's public ModelRegistry facade. */
16
+ export declare function generateSessionTitle(input: string, modelRegistry: TitleModelRegistry, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
17
+ export {};
@@ -0,0 +1,40 @@
1
+ import { complete } from "@earendil-works/pi-ai/compat";
2
+ import { buildTitlePrompt, parseTitleModelRef, sanitizeSessionTitle, TITLE_SYSTEM_PROMPT, titleResponseText, } from "./title-generation.js";
3
+ /** Extension-side title generation through Pi's public ModelRegistry facade. */
4
+ export async function generateSessionTitle(input, modelRegistry, config, modelRef, signal, onWarning) {
5
+ const parsedModel = parseTitleModelRef(modelRef);
6
+ if (!parsedModel) {
7
+ onWarning?.(`Invalid session-title model: ${modelRef}`);
8
+ return undefined;
9
+ }
10
+ const model = modelRegistry.find(parsedModel.provider, parsedModel.modelId);
11
+ if (!model) {
12
+ onWarning?.(`Session-title model not found: ${modelRef}`);
13
+ return undefined;
14
+ }
15
+ const auth = await modelRegistry.getApiKeyAndHeaders(model);
16
+ if (auth.ok === false) {
17
+ onWarning?.(auth.error);
18
+ return undefined;
19
+ }
20
+ const response = await complete(model, {
21
+ systemPrompt: TITLE_SYSTEM_PROMPT,
22
+ messages: [
23
+ {
24
+ role: "user",
25
+ content: [{ type: "text", text: buildTitlePrompt(input, config.maxTitleChars) }],
26
+ timestamp: Date.now(),
27
+ },
28
+ ],
29
+ }, {
30
+ ...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
31
+ ...(auth.headers === undefined ? {} : { headers: auth.headers }),
32
+ ...(auth.env === undefined ? {} : { env: auth.env }),
33
+ cacheRetention: "none",
34
+ maxRetries: config.maxRetries,
35
+ maxTokens: config.maxTokens,
36
+ signal,
37
+ timeoutMs: config.timeoutMs,
38
+ });
39
+ return sanitizeSessionTitle(titleResponseText(response), config.maxTitleChars);
40
+ }
@@ -1,4 +1,4 @@
1
- import type { Api, Model } from "@earendil-works/pi-ai";
1
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
2
2
  import type { SessionTitleConfig } from "./config.js";
3
3
  type SessionEntryLike = {
4
4
  type?: string;
@@ -7,20 +7,23 @@ type SessionEntryLike = {
7
7
  content?: unknown;
8
8
  };
9
9
  };
10
- type TitleModelRegistry = {
11
- find(provider: string, modelId: string): Model<Api> | undefined;
12
- getApiKeyAndHeaders(model: Model<Api>): Promise<{
13
- ok: true;
14
- apiKey?: string;
15
- headers?: Record<string, string>;
16
- } | {
17
- ok: false;
18
- error: string;
19
- }>;
20
- };
10
+ type TitleModelRuntime = Pick<ModelRuntime, "getModel" | "completeSimple">;
11
+ export declare const TITLE_SYSTEM_PROMPT: string;
12
+ export declare function parseTitleModelRef(modelRef: string): {
13
+ provider: string;
14
+ modelId: string;
15
+ } | undefined;
21
16
  export declare function firstUserMessageText(entries: readonly SessionEntryLike[]): string | undefined;
22
17
  export declare function fallbackSessionTitleFromInput(input: string, maxTitleChars: number): string | undefined;
18
+ export declare function buildTitlePrompt(input: string, maxTitleChars: number): string;
19
+ export declare function titleResponseText(response: {
20
+ content: Array<{
21
+ type: string;
22
+ text?: string;
23
+ }>;
24
+ }): string;
23
25
  export declare function sanitizeSessionTitle(raw: string, maxTitleChars: number): string | undefined;
24
26
  export declare function sessionTitleModelRefs(config: SessionTitleConfig): string[];
25
- export declare function generateSessionTitle(input: string, modelRegistry: TitleModelRegistry, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
27
+ /** Host-side title generation through the canonical SDK ModelRuntime. */
28
+ export declare function generateSessionTitleWithRuntime(input: string, modelRuntime: TitleModelRuntime, config: SessionTitleConfig, modelRef: string, signal: AbortSignal, onWarning?: (message: string) => void): Promise<string | undefined>;
26
29
  export {};
@@ -1,11 +1,10 @@
1
- import { complete } from "@earendil-works/pi-ai/compat";
2
- const TITLE_SYSTEM_PROMPT = [
1
+ export const TITLE_SYSTEM_PROMPT = [
3
2
  "You name Pi coding-agent sessions from the user's first request.",
4
3
  "Return only one concise title, without quotes, markdown, emoji, or explanations.",
5
4
  "Use the same language as the task when possible.",
6
5
  "Keep it specific, 3-7 words, and under the requested character limit.",
7
6
  ].join("\n");
8
- function parseModelRef(modelRef) {
7
+ export function parseTitleModelRef(modelRef) {
9
8
  const trimmed = modelRef.trim();
10
9
  const slash = trimmed.indexOf("/");
11
10
  if (slash <= 0 || slash === trimmed.length - 1)
@@ -58,7 +57,7 @@ export function fallbackSessionTitleFromInput(input, maxTitleChars) {
58
57
  const candidate = selected.join(" ");
59
58
  return sanitizeSessionTitle(candidate || normalized, maxTitleChars);
60
59
  }
61
- function buildTitlePrompt(input, maxTitleChars) {
60
+ export function buildTitlePrompt(input, maxTitleChars) {
62
61
  return [
63
62
  `Generate a session title under ${maxTitleChars} characters for this session.`,
64
63
  "If a parent session title is provided, use it only as context and focus on the new request.",
@@ -69,7 +68,7 @@ function buildTitlePrompt(input, maxTitleChars) {
69
68
  "</session_context>",
70
69
  ].join("\n");
71
70
  }
72
- function responseText(response) {
71
+ export function titleResponseText(response) {
73
72
  return response.content
74
73
  .filter((block) => block.type === "text" && typeof block.text === "string")
75
74
  .map((block) => block.text)
@@ -103,23 +102,19 @@ export function sessionTitleModelRefs(config) {
103
102
  .filter(Boolean)
104
103
  .filter((modelRef, index, refs) => refs.indexOf(modelRef) === index);
105
104
  }
106
- export async function generateSessionTitle(input, modelRegistry, config, modelRef, signal, onWarning) {
107
- const parsedModel = parseModelRef(modelRef);
105
+ /** Host-side title generation through the canonical SDK ModelRuntime. */
106
+ export async function generateSessionTitleWithRuntime(input, modelRuntime, config, modelRef, signal, onWarning) {
107
+ const parsedModel = parseTitleModelRef(modelRef);
108
108
  if (!parsedModel) {
109
109
  onWarning?.(`Invalid session-title model: ${modelRef}`);
110
110
  return undefined;
111
111
  }
112
- const model = modelRegistry.find(parsedModel.provider, parsedModel.modelId);
112
+ const model = modelRuntime.getModel(parsedModel.provider, parsedModel.modelId);
113
113
  if (!model) {
114
114
  onWarning?.(`Session-title model not found: ${modelRef}`);
115
115
  return undefined;
116
116
  }
117
- const auth = await modelRegistry.getApiKeyAndHeaders(model);
118
- if (auth.ok === false) {
119
- onWarning?.(auth.error);
120
- return undefined;
121
- }
122
- const response = await complete(model, {
117
+ const response = await modelRuntime.completeSimple(model, {
123
118
  systemPrompt: TITLE_SYSTEM_PROMPT,
124
119
  messages: [
125
120
  {
@@ -129,13 +124,11 @@ export async function generateSessionTitle(input, modelRegistry, config, modelRe
129
124
  },
130
125
  ],
131
126
  }, {
132
- ...(auth.apiKey === undefined ? {} : { apiKey: auth.apiKey }),
133
- ...(auth.headers === undefined ? {} : { headers: auth.headers }),
134
127
  cacheRetention: "none",
135
128
  maxRetries: config.maxRetries,
136
129
  maxTokens: config.maxTokens,
137
130
  signal,
138
131
  timeoutMs: config.timeoutMs,
139
132
  });
140
- return sanitizeSessionTitle(responseText(response), config.maxTitleChars);
133
+ return sanitizeSessionTitle(titleResponseText(response), config.maxTitleChars);
141
134
  }
@@ -33,8 +33,9 @@
33
33
  "test:locate-benchmark-e2e": "PI_LOCATE_BENCH_E2E=1 PI_LOCATE_BENCH_MODEL=zai/glm-5-turbo bun test test/locate-benchmark-e2e.test.ts",
34
34
  "test:tool-selection-e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=30 test/tool-selection-e2e.test.ts",
35
35
  "test:e2e": "TOOL_SELECTION_E2E=1 TOOL_SELECTION_E2E_MODEL=zai/glm-5-turbo ASYNC_SUBAGENTS_E2E=1 ASYNC_SUBAGENTS_DEBUG_LOGS=1 ASYNC_SUBAGENTS_MODEL=zai/glm-5-turbo bun test --concurrent --max-concurrency=5 test/tool-selection-e2e.test.ts test/async-subagents/e2e.test.ts test/async-subagents/selection-e2e.test.ts test/todo-persistence-e2e.test.ts",
36
+ "typecheck": "tsc -p tsconfig.source.json",
36
37
  "typecheck:async-subagents": "bash scripts/typecheck-source.sh",
37
- "check": "npm run smoke"
38
+ "check": "npm run typecheck && npm test && npm run smoke"
38
39
  },
39
40
  "dependencies": {
40
41
  "jsonc-parser": "^3.3.1",
@@ -42,9 +43,9 @@
42
43
  "vscode-languageserver-protocol": "^3.17.5"
43
44
  },
44
45
  "peerDependencies": {
45
- "@earendil-works/pi-ai": "0.80.7",
46
- "@earendil-works/pi-coding-agent": "0.80.7",
47
- "@earendil-works/pi-tui": "0.80.7",
46
+ "@earendil-works/pi-ai": "0.80.9",
47
+ "@earendil-works/pi-coding-agent": "0.80.9",
48
+ "@earendil-works/pi-tui": "0.80.9",
48
49
  "typebox": "*"
49
50
  },
50
51
  "devDependencies": {
@@ -1,5 +1,5 @@
1
- import { complete } from "@earendil-works/pi-ai/compat";
2
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { completeWithModelRegistry, type ModelCompletionRegistry } from "../../model-completion.js";
3
3
  import type { AgentTask } from "./types.js";
4
4
  import {
5
5
  currentModelRef,
@@ -11,10 +11,10 @@ import {
11
11
 
12
12
  export interface SubagentRoutingContext {
13
13
  model?: unknown;
14
- modelRegistry?: {
14
+ modelRegistry?: ModelCompletionRegistry & {
15
15
  find(provider: string, modelId: string): Model<Api> | undefined;
16
16
  getApiKeyAndHeaders(model: Model<Api>): Promise<
17
- | { ok?: true; apiKey?: string; headers?: Record<string, string> }
17
+ | { ok?: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
18
18
  | { ok: false; error: string }
19
19
  >;
20
20
  };
@@ -67,7 +67,8 @@ export async function routeSubagentTasks(
67
67
  for (const candidate of candidates) {
68
68
  if (signal?.aborted) throw new Error("Aborted");
69
69
  try {
70
- response = await complete(
70
+ response = await completeWithModelRegistry(
71
+ ctx.modelRegistry,
71
72
  candidate.model,
72
73
  {
73
74
  systemPrompt: ROUTER_SYSTEM_PROMPT,
@@ -82,6 +83,7 @@ export async function routeSubagentTasks(
82
83
  {
83
84
  apiKey: candidate.apiKey,
84
85
  headers: candidate.headers,
86
+ env: candidate.env,
85
87
  cacheRetention: "none",
86
88
  maxRetries: routing.maxRetries,
87
89
  maxTokens: routing.maxTokens,
@@ -179,14 +181,16 @@ interface RoutingCandidate {
179
181
  model: Model<Api>;
180
182
  apiKey?: string;
181
183
  headers?: Record<string, string>;
184
+ env?: Record<string, string>;
182
185
  }
183
186
 
184
- type RoutingResponse = Awaited<ReturnType<typeof complete>>;
187
+ type RoutingResponse = Awaited<ReturnType<typeof completeWithModelRegistry>>;
185
188
 
186
189
  async function resolveModelRef(ctx: SubagentRoutingContext, modelRef: string): Promise<{
187
190
  model: Model<Api>;
188
191
  apiKey?: string;
189
192
  headers?: Record<string, string>;
193
+ env?: Record<string, string>;
190
194
  } | undefined> {
191
195
  const parsed = parseModelRef(modelRef);
192
196
  if (!parsed || !ctx.modelRegistry) return undefined;
@@ -194,7 +198,7 @@ async function resolveModelRef(ctx: SubagentRoutingContext, modelRef: string): P
194
198
  if (!model) return undefined;
195
199
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
196
200
  if (auth.ok === false) return undefined;
197
- return { model, apiKey: auth.apiKey, headers: auth.headers };
201
+ return { model, apiKey: auth.apiKey, headers: auth.headers, env: auth.env };
198
202
  }
199
203
 
200
204
  function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
@@ -1,15 +1,15 @@
1
- import { complete } from "@earendil-works/pi-ai/compat";
2
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { completeWithModelRegistry, type ModelCompletionRegistry } from "../../model-completion.js";
3
3
  import { currentModelRef, resolveSubagentRoutingConfig, type SubagentConfig } from "./config.js";
4
4
 
5
5
  export type UltraworkAutoDecision = "ultrawork" | "hint" | "none";
6
6
 
7
7
  export interface UltraworkAutoContext {
8
8
  model?: unknown;
9
- modelRegistry?: {
9
+ modelRegistry?: ModelCompletionRegistry & {
10
10
  find(provider: string, modelId: string): Model<Api> | undefined;
11
11
  getApiKeyAndHeaders(model: Model<Api>): Promise<
12
- | { ok?: true; apiKey?: string; headers?: Record<string, string> }
12
+ | { ok?: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
13
13
  | { ok: false; error: string }
14
14
  >;
15
15
  };
@@ -63,7 +63,8 @@ export async function decideUltraworkAuto(
63
63
  const resolved = await resolveClassifierModel(ctx, routing.model);
64
64
  if (!resolved) return "none";
65
65
 
66
- const response = await complete(
66
+ const response = await completeWithModelRegistry(
67
+ ctx.modelRegistry,
67
68
  resolved.model,
68
69
  {
69
70
  systemPrompt: CLASSIFIER_SYSTEM_PROMPT,
@@ -78,6 +79,7 @@ export async function decideUltraworkAuto(
78
79
  {
79
80
  apiKey: resolved.apiKey,
80
81
  headers: resolved.headers,
82
+ env: resolved.env,
81
83
  cacheRetention: "none",
82
84
  maxRetries: routing.maxRetries,
83
85
  maxTokens: Math.min(routing.maxTokens, 32),
@@ -130,6 +132,7 @@ async function resolveClassifierModel(ctx: UltraworkAutoContext, modelRef: strin
130
132
  model: Model<Api>;
131
133
  apiKey?: string;
132
134
  headers?: Record<string, string>;
135
+ env?: Record<string, string>;
133
136
  } | undefined> {
134
137
  const configured = await resolveModelRef(ctx, modelRef);
135
138
  if (configured) return configured;
@@ -141,6 +144,7 @@ async function resolveModelRef(ctx: UltraworkAutoContext, modelRef: string): Pro
141
144
  model: Model<Api>;
142
145
  apiKey?: string;
143
146
  headers?: Record<string, string>;
147
+ env?: Record<string, string>;
144
148
  } | undefined> {
145
149
  const parsed = parseModelRef(modelRef);
146
150
  if (!parsed || !ctx.modelRegistry) return undefined;
@@ -148,7 +152,7 @@ async function resolveModelRef(ctx: UltraworkAutoContext, modelRef: string): Pro
148
152
  if (!model) return undefined;
149
153
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
150
154
  if (auth.ok === false) return undefined;
151
- return { model, apiKey: auth.apiKey, headers: auth.headers };
155
+ return { model, apiKey: auth.apiKey, headers: auth.headers, env: auth.env };
152
156
  }
153
157
 
154
158
  function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
@@ -1,11 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { complete } from "@earendil-works/pi-ai/compat";
4
3
  import type { Api, AssistantMessage, ImageContent, Model, TextContent } from "@earendil-works/pi-ai";
5
4
  import { Type } from "typebox";
6
5
 
7
6
  import { loadPiToolsSuiteConfig } from "../config.js";
8
7
  import { ignoreStaleExtensionContextError } from "../context-usage.js";
8
+ import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js";
9
9
 
10
10
  type ExtensionAPI = any;
11
11
 
@@ -25,9 +25,11 @@ type LookupDetails = {
25
25
  };
26
26
 
27
27
  type ResolvedLookupModel = {
28
+ modelRegistry: ModelCompletionRegistry;
28
29
  model: Model<Api>;
29
30
  apiKey?: string;
30
31
  headers?: Record<string, string>;
32
+ env?: Record<string, string>;
31
33
  };
32
34
 
33
35
  const SILENT_PROMPT_MARKER_START = "<glm_silent_mode>";
@@ -504,7 +506,8 @@ function createLookupTool() {
504
506
  const promptText = buildLookupPrompt(params, recentContext, images.length, pathImages.warnings);
505
507
 
506
508
  try {
507
- const response = await complete(
509
+ const response = await completeWithModelRegistry(
510
+ resolved.modelRegistry,
508
511
  resolved.model,
509
512
  {
510
513
  systemPrompt: LOOKUP_SYSTEM_PROMPT,
@@ -519,6 +522,7 @@ function createLookupTool() {
519
522
  {
520
523
  apiKey: resolved.apiKey,
521
524
  headers: resolved.headers,
525
+ env: resolved.env,
522
526
  cacheRetention: "none",
523
527
  maxRetries: 1,
524
528
  maxTokens: DEFAULT_LOOKUP_MAX_TOKENS,
@@ -765,9 +769,20 @@ async function resolveLookupModel(ctx: unknown, modelRef: string): Promise<Resol
765
769
  if (!isRecord(registry) || typeof registry.find !== "function" || typeof registry.getApiKeyAndHeaders !== "function") return undefined;
766
770
  const model = registry.find(parsed.provider, parsed.modelId) as Model<Api> | undefined;
767
771
  if (!model) return undefined;
768
- const auth = await registry.getApiKeyAndHeaders(model) as { ok?: true; apiKey?: string; headers?: Record<string, string> } | { ok: false; error: string };
772
+ const auth = await registry.getApiKeyAndHeaders(model) as {
773
+ ok?: true;
774
+ apiKey?: string;
775
+ headers?: Record<string, string>;
776
+ env?: Record<string, string>;
777
+ } | { ok: false; error: string };
769
778
  if (auth.ok === false) return undefined;
770
- return { model, apiKey: auth.apiKey, headers: auth.headers };
779
+ return {
780
+ modelRegistry: registry as ModelCompletionRegistry,
781
+ model,
782
+ apiKey: auth.apiKey,
783
+ headers: auth.headers,
784
+ env: auth.env,
785
+ };
771
786
  }
772
787
 
773
788
  function parseModelRef(modelRef: string): { provider: string; modelId: string } | undefined {
@@ -13,8 +13,8 @@
13
13
  // automatic fallback to the programmatic digest on any failure/timeout.
14
14
  // ---------------------------------------------------------------------------
15
15
 
16
- import { complete } from "@earendil-works/pi-ai/compat"
17
16
  import type { Model, Api } from "@earendil-works/pi-ai"
17
+ import { completeWithModelRegistry, type ModelCompletionRegistry } from "../model-completion.js"
18
18
  import type { DcpState } from "./state.js"
19
19
  import type { DcpConfig } from "./config.js"
20
20
  import type { CompressionCandidate } from "./pruner-types.js"
@@ -134,6 +134,14 @@ export interface ModelSummaryResult {
134
134
  attempts: ModelSummaryAttempt[]
135
135
  }
136
136
 
137
+ type ModelSummaryRegistry = ModelCompletionRegistry & {
138
+ find(provider: string, modelId: string): Model<Api> | undefined
139
+ getApiKeyAndHeaders(model: Model<Api>): Promise<
140
+ | { ok: true; apiKey?: string; headers?: Record<string, string>; env?: Record<string, string> }
141
+ | { ok: false; error: string }
142
+ >
143
+ }
144
+
137
145
  /**
138
146
  * Try to produce a model-generated summary by calling each model in
139
147
  * `modelRefs` in order. On success returns `{ text, usedModelRef, attempts }`;
@@ -146,7 +154,7 @@ export interface ModelSummaryResult {
146
154
  */
147
155
  export async function generateModelSummary(
148
156
  modelRefs: string[],
149
- modelRegistry: any,
157
+ modelRegistry: ModelSummaryRegistry | undefined,
150
158
  signal: AbortSignal | undefined,
151
159
  topic: string,
152
160
  messagesInRange: any[],
@@ -178,7 +186,7 @@ export async function generateModelSummary(
178
186
  continue
179
187
  }
180
188
 
181
- let auth
189
+ let auth: Awaited<ReturnType<ModelSummaryRegistry["getApiKeyAndHeaders"]>>
182
190
  try {
183
191
  auth = await modelRegistry.getApiKeyAndHeaders(model)
184
192
  } catch (error) {
@@ -186,7 +194,7 @@ export async function generateModelSummary(
186
194
  attempts.push({ ref, outcome: "no-auth", error: error instanceof Error ? error.message : String(error) })
187
195
  continue
188
196
  }
189
- if (!auth?.ok || !auth.apiKey) {
197
+ if (auth.ok === false) {
190
198
  attempts.push({ ref, outcome: "no-auth" })
191
199
  continue
192
200
  }
@@ -202,7 +210,8 @@ export async function generateModelSummary(
202
210
  }
203
211
 
204
212
  try {
205
- const result = await complete(
213
+ const result = await completeWithModelRegistry(
214
+ modelRegistry,
206
215
  model,
207
216
  { systemPrompt: SUMMARIZER_SYSTEM_PROMPT, messages: [{ role: "user", content: userPrompt, timestamp: Date.now() }] },
208
217
  {
@@ -109,9 +109,10 @@ function collectNudgeStats(ctx: ExtensionCommandContext, state: DcpState): DcpNu
109
109
  else stats.upgraded++
110
110
  stats.byType[data.type]++
111
111
  const createdAt = typeof data.createdAt === "number" ? data.createdAt : undefined
112
- const contextPercent = typeof data.contextPercent === "number" || data.contextPercent === null
113
- ? data.contextPercent
114
- : undefined
112
+ const rawContextPercent = data.contextPercent
113
+ let contextPercent: number | null | undefined
114
+ if (typeof rawContextPercent === "number") contextPercent = rawContextPercent
115
+ else if (rawContextPercent === null) contextPercent = null
115
116
  if (!stats.last || (createdAt ?? 0) >= (stats.last.createdAt ?? 0)) {
116
117
  stats.last = { type: data.type, event, createdAt, contextPercent }
117
118
  }
@@ -0,0 +1,37 @@
1
+ import type {
2
+ Api,
3
+ AssistantMessage,
4
+ AssistantMessageEventStream,
5
+ Context,
6
+ Model,
7
+ SimpleStreamOptions,
8
+ } from "@earendil-works/pi-ai";
9
+ import { completeSimple } from "@earendil-works/pi-ai/compat";
10
+
11
+ type RegisteredProviderConfig = {
12
+ streamSimple?: (
13
+ model: Model<Api>,
14
+ context: Context,
15
+ options?: SimpleStreamOptions,
16
+ ) => AssistantMessageEventStream;
17
+ };
18
+
19
+ export type ModelCompletionRegistry = {
20
+ getRegisteredProviderConfig?(providerId: string): RegisteredProviderConfig | undefined;
21
+ };
22
+
23
+ /**
24
+ * Complete through an extension provider's registered stream when available.
25
+ * Pi 0.80.8+ no longer copies extension streams into pi-ai's global compat
26
+ * registry, so falling back to compat is valid only for built-in APIs.
27
+ */
28
+ export async function completeWithModelRegistry(
29
+ modelRegistry: ModelCompletionRegistry | undefined,
30
+ model: Model<Api>,
31
+ context: Context,
32
+ options?: SimpleStreamOptions,
33
+ ): Promise<AssistantMessage> {
34
+ const providerConfig = modelRegistry?.getRegisteredProviderConfig?.(model.provider);
35
+ if (providerConfig?.streamSimple) return providerConfig.streamSimple(model, context, options).result();
36
+ return completeSimple(model, context, options);
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,9 +65,9 @@
65
65
  "prepublishOnly": "npm run check && npm run build:pix && npm run generate-schemas"
66
66
  },
67
67
  "dependencies": {
68
- "@earendil-works/pi-ai": "0.80.7",
69
- "@earendil-works/pi-coding-agent": "0.80.7",
70
- "@earendil-works/pi-tui": "0.80.7",
68
+ "@earendil-works/pi-ai": "0.80.9",
69
+ "@earendil-works/pi-coding-agent": "0.80.9",
70
+ "@earendil-works/pi-tui": "0.80.9",
71
71
  "@mariozechner/clipboard": "^0.3.9",
72
72
  "jsonc-parser": "3.3.1",
73
73
  "typebox": "1.1.38",