authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-10-11 00:47:45+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 17:47:45-05:00
log160b18c8667f8370f46708d273d368648316b200
tree754ce8729eee5a6b74d46ddf68680e7053fa103a
parent7e8071f1f1566220fb52cbfaaf97a2bc4e93a193
signature Signed by PGP key B5690EEEBB952194

fix(pi): keep thinking defaults specific to each model (#17835)


3 files changed, 62 insertions(+), 16 deletions(-)

docs/user/providers-pi.md+2-1
...@@ -36,7 +36,8 @@ update it through that manager instead. For example, a Nix profile installation...@@ -36,7 +36,8 @@ update it through that manager instead. For example, a Nix profile installation
36## What Carries Over36## What Carries Over
3737
38T3 Code discovers the models reported by Pi and exposes their supported thinking levels. The38T3 Code discovers the models reported by Pi and exposes their supported thinking levels. The
39thinking picker marks Pi's current configured level as the default without overriding it. Threads39thinking picker marks the discovered active model's configured level as its default without
40overriding it. Other models keep their Pi defaults when no level is selected. Threads
40use Pi's native session files for resume, rollback, and forks within the same Pi instance. Forks41use Pi's native session files for resume, rollback, and forks within the same Pi instance. Forks
41preserve the native conversation through the selected turn in the destination workspace.42preserve the native conversation through the selected turn in the destination workspace.
42Switching providers uses portable conversation context. Extension43Switching providers uses portable conversation context. Extension
packages/provider-pi/src/server/driver.test.ts+49-6
...@@ -47,7 +47,9 @@ const personalSkill = {...@@ -47,7 +47,9 @@ const personalSkill = {
47};47};
4848
49// Respond through the real stdio transport, with a distinct command catalog for each cwd.49// Respond through the real stdio transport, with a distinct command catalog for each cwd.
50const makePiSpawner = Effect.gen(function* () {50const makePiSpawner = Effect.fnUntraced(function* (
51 discovery: { readonly state?: unknown; readonly models?: ReadonlyArray<unknown> } = {},
52) {
51 const pendingCommand = yield* Deferred.make<void>();53 const pendingCommand = yield* Deferred.make<void>();
52 const launches: Array<ChildProcess.StandardCommand> = [];54 const launches: Array<ChildProcess.StandardCommand> = [];
53 const spawner = ChildProcessSpawner.make((command) =>55 const spawner = ChildProcessSpawner.make((command) =>
...@@ -92,8 +94,10 @@ const makePiSpawner = Effect.gen(function* () {...@@ -92,8 +94,10 @@ const makePiSpawner = Effect.gen(function* () {
92 ],94 ],
93 }95 }
94 : request.type === "get_available_models"96 : request.type === "get_available_models"
95 ? { models: [{ provider: "test", id: "model" }] }97 ? { models: discovery.models ?? [{ provider: "test", id: "model" }] }
96 : {};98 : request.type === "get_state"
99 ? (discovery.state ?? {})
100 : {};
97 return Queue.offer(101 return Queue.offer(
98 stdout,102 stdout,
99 encoder.encode(103 encoder.encode(
...@@ -125,7 +129,7 @@ const create = (config: Partial<PiSettings> = {}, enabled = true) =>...@@ -125,7 +129,7 @@ const create = (config: Partial<PiSettings> = {}, enabled = true) =>
125it.layer(layerTest)("PiDriver workspace discovery", (it) => {129it.layer(layerTest)("PiDriver workspace discovery", (it) => {
126 it.effect("keeps each workspace's skills and commands separate from the machine catalog", () =>130 it.effect("keeps each workspace's skills and commands separate from the machine catalog", () =>
127 Effect.gen(function* () {131 Effect.gen(function* () {
128 const { spawner, launches } = yield* makePiSpawner;132 const { spawner, launches } = yield* makePiSpawner();
129 const instance = yield* create({ launchArgs: '--approve --skill "extra skill"' }).pipe(133 const instance = yield* create({ launchArgs: '--approve --skill "extra skill"' }).pipe(
130 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),134 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
131 );135 );
...@@ -186,11 +190,50 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => {...@@ -186,11 +190,50 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => {
186 }).pipe(Effect.scoped),190 }).pipe(Effect.scoped),
187 );191 );
188192
193 it.effect.each([true, false])(
194 "limits discovered thinking defaults to the known active model, known=%s",
195 (known) =>
196 Effect.gen(function* () {
197 const { spawner } = yield* makePiSpawner({
198 state: {
199 thinkingLevel: "high",
200 ...(known ? { model: { provider: "test", id: "model" } } : {}),
201 },
202 models: [
203 { provider: "test", id: "model", reasoning: true },
204 { provider: "other", id: "model", reasoning: true },
205 { provider: "test", id: "other", reasoning: true },
206 ],
207 });
208 const instance = yield* create().pipe(
209 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
210 );
211 yield* instance.snapshot.refresh;
212 const snapshot = yield* instance.snapshot.getSnapshot;
213 assert.deepEqual(
214 snapshot.models.map((model) => [
215 model.slug,
216 (model.capabilities?.optionDescriptors ?? []).flatMap((descriptor) =>
217 descriptor.type === "select"
218 ? descriptor.options.filter((option) => option.isDefault).map((option) => option.id)
219 : [],
220 ),
221 ]),
222 [
223 ["default", []],
224 ["test/model", known ? ["high"] : []],
225 ["other/model", []],
226 ["test/other", []],
227 ],
228 );
229 }).pipe(Effect.scoped),
230 );
231
189 it.effect(232 it.effect(
190 "fails command discovery instead of returning an empty successful workspace catalog",233 "fails command discovery instead of returning an empty successful workspace catalog",
191 () =>234 () =>
192 Effect.gen(function* () {235 Effect.gen(function* () {
193 const { spawner } = yield* makePiSpawner;236 const { spawner } = yield* makePiSpawner();
194 const instance = yield* create().pipe(237 const instance = yield* create().pipe(
195 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),238 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
196 );239 );
...@@ -208,7 +251,7 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => {...@@ -208,7 +251,7 @@ it.layer(layerTest)("PiDriver workspace discovery", (it) => {
208251
209 it.effect("times out workspace discovery that needs interactive input", () =>252 it.effect("times out workspace discovery that needs interactive input", () =>
210 Effect.gen(function* () {253 Effect.gen(function* () {
211 const { spawner, pendingCommand } = yield* makePiSpawner;254 const { spawner, pendingCommand } = yield* makePiSpawner();
212 const instance = yield* create().pipe(255 const instance = yield* create().pipe(
213 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),256 Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),
214 );257 );
packages/provider-pi/src/server/status.ts+11-9
...@@ -101,12 +101,12 @@ function piModelsFromSettings(...@@ -101,12 +101,12 @@ function piModelsFromSettings(
101 );101 );
102}102}
103103
104function parseDiscoveredModels(104function parseDiscoveredModels(data: unknown, state: unknown): ReadonlyArray<ServerProviderModel> {
105 data: unknown,
106 defaultThinkingLevel: unknown,
107): ReadonlyArray<ServerProviderModel> {
108 const models = recordField(data, "models");105 const models = recordField(data, "models");
109 if (!Array.isArray(models)) return [];106 if (!Array.isArray(models)) return [];
107 const activeModel = recordField(state, "model");
108 const activeProvider = recordString(activeModel, "provider");
109 const activeModelId = recordString(activeModel, "id");
110 const seen = new Set<string>();110 const seen = new Set<string>();
111 const parsed: Array<ServerProviderModel> = [];111 const parsed: Array<ServerProviderModel> = [];
112 for (const model of models) {112 for (const model of models) {
...@@ -121,7 +121,12 @@ function parseDiscoveredModels(...@@ -121,7 +121,12 @@ function parseDiscoveredModels(
121 name: recordString(model, "name") ?? slug,121 name: recordString(model, "name") ?? slug,
122 subProvider: provider,122 subProvider: provider,
123 isCustom: false,123 isCustom: false,
124 capabilities: thinkingCapabilitiesForPiModel(model, defaultThinkingLevel),124 capabilities: thinkingCapabilitiesForPiModel(
125 model,
126 provider === activeProvider && id === activeModelId
127 ? recordString(state, "thinkingLevel")
128 : undefined,
129 ),
125 });130 });
126 }131 }
127 return parsed;132 return parsed;
...@@ -167,10 +172,7 @@ const discoverPiViaRpc = (...@@ -167,10 +172,7 @@ const discoverPiViaRpc = (
167 const commandsData = yield* connection172 const commandsData = yield* connection
168 .request({ type: "get_commands" })173 .request({ type: "get_commands" })
169 .pipe(Effect.orElseSucceed(() => undefined));174 .pipe(Effect.orElseSucceed(() => undefined));
170 const discoveredModels = parseDiscoveredModels(175 const discoveredModels = parseDiscoveredModels(modelsData, stateData);
171 modelsData,
172 recordString(stateData, "thinkingLevel"),
173 );
174 const { slashCommands, skills } = parsePiDiscoveredCommands(commandsData);176 const { slashCommands, skills } = parsePiDiscoveredCommands(commandsData);
175 return {177 return {
176 models: discoveredModels,178 models: discoveredModels,