@pi-archimedes/session-name 2.11.0 → 2.11.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 +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +50 -9
- package/src/index.ts +27 -14
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ After installing Pi, choose one installation command above, then `cd` into your
|
|
|
41
41
|
| Setting | Type | Default | Description |
|
|
42
42
|
|---------|------|---------|-------------|
|
|
43
43
|
| `model` | string | _(current model)_ | Model used for title generation (e.g. `openai/gpt-4o-mini`). Canonical `provider/id`, bare IDs, and thinking-suffix forms are all resolved. Empty = current model. |
|
|
44
|
-
| `reasoning` | string \| null | `minimal` | Thinking level for the title request: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`.
|
|
44
|
+
| `reasoning` | string \| null | `minimal` | Thinking level for the title request: `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `off`. Resolved to the nearest level the model supports (warns on change); the option is omitted only when that resolves to `"off"`. `off`/`null` therefore request no thinking but can still send a level on models that exclude it — use `"minimal"` for bounded-small everywhere. Case and stray spaces are tolerated (`" Minimal "` works). Anything unrecognised is ignored in favour of the default and logged. Only supported levels reach the provider: the setting resolves through pi's clamp to the nearest level the model supports. |
|
|
45
45
|
|
|
46
46
|
`model` is also editable in the `/archimedes` panel (**Model for naming** — Enter to type, blank to go back to the current model); `reasoning` is JSON-only.
|
|
47
47
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/session-name",
|
|
3
|
-
"version": "2.11.
|
|
3
|
+
"version": "2.11.1",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/danielcherubini/pi-archimedes.git"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
".": "./src/index.ts"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@pi-archimedes/core": "2.11.
|
|
21
|
+
"@pi-archimedes/core": "2.11.1"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"@earendil-works/pi-coding-agent": ">=0.86.0",
|
package/src/index.test.ts
CHANGED
|
@@ -40,7 +40,9 @@ function createMockCtx(options: {
|
|
|
40
40
|
},
|
|
41
41
|
];
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// Reasoning model without a level map: every named level passes clamp
|
|
44
|
+
// through, so existing assertions on forwarded levels keep holding.
|
|
45
|
+
const defaultModel = { provider: "anthropic", id: "claude-3-5-sonnet", reasoning: true };
|
|
44
46
|
|
|
45
47
|
return {
|
|
46
48
|
sessionManager: {
|
|
@@ -132,8 +134,10 @@ describe("generateTitle", () => {
|
|
|
132
134
|
// anything. A non-level must never reach the provider — see the pin at the
|
|
133
135
|
// end of this block for what that costs.
|
|
134
136
|
describe("reasoning validation", () => {
|
|
135
|
-
it("passes every
|
|
136
|
-
|
|
137
|
+
it("passes every supported thinking level through untouched", async () => {
|
|
138
|
+
// The mock model has no level map, like the real title model — so only
|
|
139
|
+
// minimal..high pass through; xhigh/max step down (covered below).
|
|
140
|
+
for (const reasoning of ["minimal", "low", "medium", "high"]) {
|
|
137
141
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
138
142
|
const ctx = createMockCtx({});
|
|
139
143
|
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
@@ -143,7 +147,19 @@ describe("generateTitle", () => {
|
|
|
143
147
|
}
|
|
144
148
|
});
|
|
145
149
|
|
|
146
|
-
it.each(["
|
|
150
|
+
it.each([["xhigh", "high"], ["MAX", "high"]])("clamps %s to nearest supported %s, warning", async (raw, clamped) => {
|
|
151
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
152
|
+
const ctx = createMockCtx({});
|
|
153
|
+
clearDiag();
|
|
154
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
155
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe(clamped);
|
|
156
|
+
expect(diag).toHaveBeenCalledWith(
|
|
157
|
+
"session-name",
|
|
158
|
+
expect.stringContaining(`using "${clamped}"`),
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it.each(["Minimal", " high "])("tolerates casing and padding: %s", async (raw) => {
|
|
147
163
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
148
164
|
const ctx = createMockCtx({});
|
|
149
165
|
clearDiag();
|
|
@@ -154,7 +170,7 @@ describe("generateTitle", () => {
|
|
|
154
170
|
expect(diag).not.toHaveBeenCalled();
|
|
155
171
|
});
|
|
156
172
|
|
|
157
|
-
it.each([["ultra"], ["
|
|
173
|
+
it.each([["ultra"], [""], [42], [true], [{}], [[]]])(
|
|
158
174
|
"never forwards a non-level: %s",
|
|
159
175
|
async (raw) => {
|
|
160
176
|
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: raw });
|
|
@@ -174,8 +190,9 @@ describe("generateTitle", () => {
|
|
|
174
190
|
},
|
|
175
191
|
);
|
|
176
192
|
|
|
177
|
-
it("
|
|
178
|
-
|
|
193
|
+
it.each([["off"], [null]])("omits the key for %s on models that take it", async (reasoning) => {
|
|
194
|
+
// Default mock has no level map, so "off" is supported and omitted silently.
|
|
195
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning });
|
|
179
196
|
const ctx = createMockCtx({});
|
|
180
197
|
clearDiag();
|
|
181
198
|
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
@@ -183,6 +200,20 @@ describe("generateTitle", () => {
|
|
|
183
200
|
expect(diag).not.toHaveBeenCalled();
|
|
184
201
|
});
|
|
185
202
|
|
|
203
|
+
it("clamps off to minimal with a warning where the model excludes it", async () => {
|
|
204
|
+
// Muse-spark shape: thinking model whose map excludes "off" (null).
|
|
205
|
+
const model = { provider: "op", id: "spark", reasoning: true, thinkingLevelMap: { off: null } };
|
|
206
|
+
vi.mocked(loadConfig).mockReturnValueOnce({ reasoning: "off" });
|
|
207
|
+
const ctx = createMockCtx({ model });
|
|
208
|
+
clearDiag();
|
|
209
|
+
await generateTitle(createMockPi() as any, ctx as any, vi.fn(), vi.fn());
|
|
210
|
+
expect((ctx.modelRegistry.streamSimple as any).mock.calls[0][2].reasoning).toBe("minimal");
|
|
211
|
+
expect(diag).toHaveBeenCalledWith(
|
|
212
|
+
"session-name",
|
|
213
|
+
expect.stringContaining('using "minimal"'),
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
186
217
|
it("keeps the default when the setting is absent entirely, and says nothing", async () => {
|
|
187
218
|
// loadConfig merges the package default, so an absent key arrives as
|
|
188
219
|
// "minimal" — the common path must stay silent. A regression that warned
|
|
@@ -202,6 +233,13 @@ describe("generateTitle", () => {
|
|
|
202
233
|
expect(diag).not.toHaveBeenCalled();
|
|
203
234
|
});
|
|
204
235
|
|
|
236
|
+
it("accepts off for the call site to clamp per model", () => {
|
|
237
|
+
clearDiag();
|
|
238
|
+
expect(resolveTitleReasoning("off")).toBe("off");
|
|
239
|
+
expect(resolveTitleReasoning(" Off ")).toBe("off");
|
|
240
|
+
expect(diag).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
205
243
|
// Pin the invariant against the arithmetic that makes this dangerous. Of
|
|
206
244
|
// pi-ai's adapters that export a streamSimple, three never call
|
|
207
245
|
// `clampThinkingLevel` — anthropic-messages, bedrock-converse-stream, and
|
|
@@ -221,15 +259,18 @@ describe("generateTitle", () => {
|
|
|
221
259
|
for (const level of levels) {
|
|
222
260
|
expect(thinkingBudgetForLevel(level)).toBeTypeOf("number");
|
|
223
261
|
}
|
|
224
|
-
const junk: unknown[] = ["ultra", "MINIMAL", "
|
|
262
|
+
const junk: unknown[] = ["ultra", "MINIMAL", "", "Max ", 42, true, {}, []];
|
|
225
263
|
for (const value of junk) {
|
|
226
264
|
expect(thinkingBudgetForLevel(value as never)).toBeUndefined();
|
|
227
265
|
}
|
|
228
266
|
|
|
229
|
-
// Whatever the setting holds, the value
|
|
267
|
+
// Whatever the setting holds, the value reaching clamp is a real level
|
|
268
|
+
// or "off" — never raw input. "off" is resolved per model at the call
|
|
269
|
+
// site (nearest supported, warn on change), never forwarded verbatim.
|
|
230
270
|
for (const raw of [...levels, ...junk, null, undefined]) {
|
|
231
271
|
const resolved = resolveTitleReasoning(raw);
|
|
232
272
|
if (resolved === null) continue; // omitted → no level requested
|
|
273
|
+
if (resolved === "off") continue; // clamped per model at the call site
|
|
233
274
|
expect(levels).toContain(resolved);
|
|
234
275
|
expect(thinkingBudgetForLevel(resolved)).toBeTypeOf("number");
|
|
235
276
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ModelThinkingLevel, ThinkingLevel } from "@earendil-works/pi-ai";
|
|
3
4
|
import { loadConfig } from "@pi-archimedes/core/settings-io";
|
|
4
5
|
import { diag, describeError } from "@pi-archimedes/core/log";
|
|
5
6
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
@@ -10,7 +11,7 @@ export interface SessionNameSettings {
|
|
|
10
11
|
// suite-managed by meta's plugin gate (archimedes.sessionName.enabled — see ADR 0012); session-name never reads this
|
|
11
12
|
enabled?: boolean | undefined;
|
|
12
13
|
model?: string | undefined;
|
|
13
|
-
reasoning?: ThinkingLevel | null | undefined;
|
|
14
|
+
reasoning?: ThinkingLevel | "off" | null | undefined;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const DEFAULT_SESSION_NAME_CONFIG: SessionNameSettings = {
|
|
@@ -20,14 +21,16 @@ const DEFAULT_SESSION_NAME_CONFIG: SessionNameSettings = {
|
|
|
20
21
|
|
|
21
22
|
const NAMESPACE = "archimedes.sessionName";
|
|
22
23
|
|
|
23
|
-
/** The thinking levels we accept — pi-ai's set,
|
|
24
|
-
|
|
24
|
+
/** The thinking levels we accept — pi-ai's set, plus `off` (resolved per
|
|
25
|
+
* model at the call site: omitting is the only portable spelling of none). */
|
|
26
|
+
const REASONING_LEVELS: readonly (ThinkingLevel | "off")[] = [
|
|
25
27
|
"minimal",
|
|
26
28
|
"low",
|
|
27
29
|
"medium",
|
|
28
30
|
"high",
|
|
29
31
|
"xhigh",
|
|
30
32
|
"max",
|
|
33
|
+
"off",
|
|
31
34
|
];
|
|
32
35
|
|
|
33
36
|
/**
|
|
@@ -46,14 +49,15 @@ const REASONING_LEVELS: readonly ThinkingLevel[] = [
|
|
|
46
49
|
* budget-based Claude it instead misses the four-key budget table, and the
|
|
47
50
|
* resulting `undefined` reaches the provider as an invalid `max_tokens`.
|
|
48
51
|
*
|
|
49
|
-
* Outcomes: a real level (casing/padding tolerated) is
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the typo. `"off"` is
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* Outcomes: a real level (casing/padding tolerated) is returned for the call
|
|
53
|
+
* site to resolve per model; `null` means omit the option entirely; anything
|
|
54
|
+
* else — including absent — is treated like an absent setting and gets the
|
|
55
|
+
* default, with a log line so the user can find the typo. `"off"` is
|
|
56
|
+
* accepted but never forwarded raw: it resolves through pi's
|
|
57
|
+
* `clampThinkingLevel` like every other level (nearest supported, warn on
|
|
58
|
+
* change), because unclamped providers map unknown levels to maximum effort.
|
|
55
59
|
*/
|
|
56
|
-
export function resolveTitleReasoning(raw: unknown): ThinkingLevel | null {
|
|
60
|
+
export function resolveTitleReasoning(raw: unknown): ThinkingLevel | "off" | null {
|
|
57
61
|
if (raw === null) return null;
|
|
58
62
|
if (typeof raw === "string") {
|
|
59
63
|
const value = raw.trim().toLowerCase();
|
|
@@ -206,8 +210,17 @@ export async function generateTitle(
|
|
|
206
210
|
if (!ctx.modelRegistry.hasConfiguredAuth(model)) return;
|
|
207
211
|
|
|
208
212
|
// 5. Stream simple with built-in auth resolution.
|
|
209
|
-
// Reasoning is resolved, not forwarded raw: see
|
|
210
|
-
|
|
213
|
+
// Reasoning is resolved per model, not forwarded raw: see
|
|
214
|
+
// resolveTitleReasoning(). `clampThinkingLevel` maps to the nearest level
|
|
215
|
+
// the model supports (warn on change); only an `off` result omits the
|
|
216
|
+
// option, which is the single portable spelling of none.
|
|
217
|
+
const resolved = resolveTitleReasoning(settings.reasoning);
|
|
218
|
+
const requested: ModelThinkingLevel = resolved === null ? "off" : (resolved ?? "minimal");
|
|
219
|
+
const clamped = clampThinkingLevel(model, requested);
|
|
220
|
+
if (clamped !== requested) {
|
|
221
|
+
diag("session-name", `[archimedes] session-name: reasoning "${requested}" is not supported — using "${clamped}"`);
|
|
222
|
+
}
|
|
223
|
+
const reasoning = clamped === "off" ? undefined : clamped;
|
|
211
224
|
const stream = ctx.modelRegistry.streamSimple(
|
|
212
225
|
model,
|
|
213
226
|
{
|
|
@@ -220,7 +233,7 @@ export async function generateTitle(
|
|
|
220
233
|
],
|
|
221
234
|
},
|
|
222
235
|
{
|
|
223
|
-
...(reasoning !==
|
|
236
|
+
...(reasoning !== undefined ? { reasoning } : {}),
|
|
224
237
|
cacheRetention: "none",
|
|
225
238
|
sessionId: crypto.randomUUID(),
|
|
226
239
|
},
|