@ilya-lesikov/pi-pi 0.6.0 → 0.8.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/3p/pi-ask-user/index.ts +1 -1
- package/3p/pi-plannotator/apps/pi-extension/README.md +6 -5
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +25 -10
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.code-review.test.ts +172 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +50 -12
- package/3p/pi-plannotator/apps/pi-extension/server/project.test.ts +45 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +7 -6
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +41 -25
- package/3p/pi-subagents/src/agent-manager.ts +34 -1
- package/3p/pi-subagents/src/agent-runner.ts +66 -33
- package/3p/pi-subagents/src/index.ts +3 -38
- package/3p/pi-subagents/src/types.ts +4 -0
- package/extensions/orchestrator/agents/advisor.ts +35 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/code-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/constraints.test.ts +44 -0
- package/extensions/orchestrator/agents/constraints.ts +3 -0
- package/extensions/orchestrator/agents/deep-debugger.ts +35 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +7 -7
- package/extensions/orchestrator/agents/planner.ts +9 -8
- package/extensions/orchestrator/agents/prompts.test.ts +120 -0
- package/extensions/orchestrator/agents/reviewer.ts +48 -0
- package/extensions/orchestrator/agents/task.ts +6 -20
- package/extensions/orchestrator/agents/tool-routing.ts +23 -1
- package/extensions/orchestrator/command-handlers.ts +1 -1
- package/extensions/orchestrator/config.ts +8 -4
- package/extensions/orchestrator/context.test.ts +54 -0
- package/extensions/orchestrator/context.ts +65 -2
- package/extensions/orchestrator/custom-footer.ts +5 -2
- package/extensions/orchestrator/doctor.test.ts +3 -1
- package/extensions/orchestrator/doctor.ts +40 -2
- package/extensions/orchestrator/event-handlers.test.ts +97 -1
- package/extensions/orchestrator/event-handlers.ts +222 -48
- package/extensions/orchestrator/flant-infra.test.ts +312 -0
- package/extensions/orchestrator/flant-infra.ts +407 -44
- package/extensions/orchestrator/index.ts +1 -1
- package/extensions/orchestrator/integration.test.ts +312 -18
- package/extensions/orchestrator/messages.test.ts +30 -0
- package/extensions/orchestrator/messages.ts +6 -0
- package/extensions/orchestrator/model-registry.test.ts +124 -13
- package/extensions/orchestrator/model-registry.ts +91 -33
- package/extensions/orchestrator/orchestrator.test.ts +113 -0
- package/extensions/orchestrator/orchestrator.ts +163 -3
- package/extensions/orchestrator/phases/brainstorm.ts +9 -20
- package/extensions/orchestrator/phases/implementation.test.ts +11 -0
- package/extensions/orchestrator/phases/implementation.ts +4 -6
- package/extensions/orchestrator/phases/planning.test.ts +16 -0
- package/extensions/orchestrator/phases/planning.ts +11 -4
- package/extensions/orchestrator/phases/review-task.ts +1 -4
- package/extensions/orchestrator/phases/review.test.ts +62 -0
- package/extensions/orchestrator/phases/review.ts +58 -15
- package/extensions/orchestrator/plannotator.ts +9 -6
- package/extensions/orchestrator/pp-menu.test.ts +74 -1
- package/extensions/orchestrator/pp-menu.ts +366 -94
- package/extensions/orchestrator/pp-state-tools.test.ts +192 -0
- package/extensions/orchestrator/pp-state-tools.ts +249 -0
- package/extensions/orchestrator/rate-limit-fallback-flow.test.ts +128 -0
- package/extensions/orchestrator/rate-limit-fallback.test.ts +98 -0
- package/extensions/orchestrator/rate-limit-fallback.ts +243 -0
- package/extensions/orchestrator/state.ts +41 -20
- package/extensions/orchestrator/test-helpers.ts +18 -3
- package/extensions/orchestrator/usage-tracker.test.ts +131 -3
- package/extensions/orchestrator/usage-tracker.ts +78 -11
- package/extensions/orchestrator/validate-artifacts.test.ts +20 -0
- package/extensions/orchestrator/validate-artifacts.ts +2 -2
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
3
|
vi.mock("./log.js", () => ({
|
|
4
4
|
getLogger: () => ({
|
|
@@ -7,10 +7,14 @@ vi.mock("./log.js", () => ({
|
|
|
7
7
|
}));
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
findLatestFamilyMatch,
|
|
10
11
|
getAllAliases,
|
|
11
12
|
getModelFamilies,
|
|
12
13
|
getModelInfo,
|
|
14
|
+
isSubscriptionFallbackActive,
|
|
13
15
|
resolveModel,
|
|
16
|
+
setSubscriptionFallbackActive,
|
|
17
|
+
toNonSubSpec,
|
|
14
18
|
updateRegistryFromAvailableModels,
|
|
15
19
|
} from "./model-registry.js";
|
|
16
20
|
|
|
@@ -19,20 +23,43 @@ describe("model-registry", () => {
|
|
|
19
23
|
updateRegistryFromAvailableModels([]);
|
|
20
24
|
});
|
|
21
25
|
|
|
22
|
-
it("resolveModel
|
|
23
|
-
expect(resolveModel("anthropic/claude-sonnet-latest")).toBe("anthropic/claude-sonnet-
|
|
24
|
-
expect(resolveModel("
|
|
26
|
+
it("resolveModel passes through native-latest aliases", () => {
|
|
27
|
+
expect(resolveModel("anthropic/claude-sonnet-latest")).toBe("anthropic/claude-sonnet-latest");
|
|
28
|
+
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-latest");
|
|
29
|
+
expect(resolveModel("anthropic/claude-haiku-latest")).toBe("anthropic/claude-haiku-latest");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("resolveModel returns input unchanged for non-native aliases without available models", () => {
|
|
33
|
+
expect(resolveModel("openai/gpt-mini-latest")).toBe("openai/gpt-mini-latest");
|
|
34
|
+
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-latest");
|
|
25
35
|
});
|
|
26
36
|
|
|
27
37
|
it("resolveModel passes through unknown aliases", () => {
|
|
28
38
|
expect(resolveModel("custom/provider-model")).toBe("custom/provider-model");
|
|
29
39
|
});
|
|
30
40
|
|
|
31
|
-
it("resolveModel resolves flant aliases", () => {
|
|
41
|
+
it("resolveModel resolves flant aliases after updateRegistry", () => {
|
|
42
|
+
updateRegistryFromAvailableModels(["claude-opus-4-6", "gemini-3.1-pro"]);
|
|
32
43
|
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-4-6");
|
|
33
44
|
expect(resolveModel("pp-flant-openai/gemini-pro-latest")).toBe("pp-flant-openai/gemini-3.1-pro");
|
|
34
45
|
});
|
|
35
46
|
|
|
47
|
+
it("getModelInfo detects personal-subscription Claude models", () => {
|
|
48
|
+
expect(getModelInfo("pp-flant-anthropic-sub/sub/claude-opus-4-8")).toMatchObject({ vendor: "anthropic", family: "opus", tier: "smart" });
|
|
49
|
+
expect(getModelInfo("pp-flant-anthropic-sub/sub/claude-sonnet-4-6")).toMatchObject({ vendor: "anthropic", family: "sonnet", tier: "regular" });
|
|
50
|
+
expect(getModelInfo("pp-flant-anthropic-sub/sub/claude-haiku-4-5")).toMatchObject({ vendor: "anthropic", family: "haiku", tier: "stupid" });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("resolveModel resolves subscription aliases to sub/ specs after updateRegistry", () => {
|
|
54
|
+
updateRegistryFromAvailableModels([
|
|
55
|
+
"pp-flant-anthropic-sub/sub/claude-opus-4-7",
|
|
56
|
+
"pp-flant-anthropic-sub/sub/claude-opus-4-8",
|
|
57
|
+
"pp-flant-anthropic-sub/sub/claude-haiku-4-5",
|
|
58
|
+
]);
|
|
59
|
+
expect(resolveModel("pp-flant-anthropic-sub/claude-opus-latest")).toBe("pp-flant-anthropic-sub/sub/claude-opus-4-8");
|
|
60
|
+
expect(resolveModel("pp-flant-anthropic-sub/claude-haiku-latest")).toBe("pp-flant-anthropic-sub/sub/claude-haiku-4-5");
|
|
61
|
+
});
|
|
62
|
+
|
|
36
63
|
it("getModelInfo detects all configured families", () => {
|
|
37
64
|
expect(getModelInfo("anthropic/claude-opus-4-6")).toMatchObject({ vendor: "anthropic", family: "opus", tier: "smart" });
|
|
38
65
|
expect(getModelInfo("anthropic/claude-sonnet-4-6")).toMatchObject({ vendor: "anthropic", family: "sonnet", tier: "regular" });
|
|
@@ -153,11 +180,11 @@ describe("model-registry", () => {
|
|
|
153
180
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-5.6");
|
|
154
181
|
});
|
|
155
182
|
|
|
156
|
-
it("updateRegistryFromAvailableModels keeps
|
|
183
|
+
it("updateRegistryFromAvailableModels keeps native-latest for missing families", () => {
|
|
157
184
|
updateRegistryFromAvailableModels(["openai/gpt-9.1"]);
|
|
158
185
|
|
|
159
186
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-9.1");
|
|
160
|
-
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-
|
|
187
|
+
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-latest");
|
|
161
188
|
});
|
|
162
189
|
|
|
163
190
|
it("updateRegistryFromAvailableModels ignores aliases ending with -latest", () => {
|
|
@@ -168,7 +195,7 @@ describe("model-registry", () => {
|
|
|
168
195
|
]);
|
|
169
196
|
|
|
170
197
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-5.4");
|
|
171
|
-
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-
|
|
198
|
+
expect(resolveModel("pp-flant-anthropic/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-latest");
|
|
172
199
|
});
|
|
173
200
|
|
|
174
201
|
it("updateRegistryFromAvailableModels handles empty input", () => {
|
|
@@ -176,14 +203,14 @@ describe("model-registry", () => {
|
|
|
176
203
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-9.9");
|
|
177
204
|
|
|
178
205
|
updateRegistryFromAvailableModels([]);
|
|
179
|
-
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-
|
|
206
|
+
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-latest");
|
|
180
207
|
});
|
|
181
208
|
|
|
182
209
|
it("updateRegistryFromAvailableModels ignores unknown bare ids", () => {
|
|
183
210
|
updateRegistryFromAvailableModels(["custom-model-1", "another-custom-model"]);
|
|
184
211
|
|
|
185
|
-
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-
|
|
186
|
-
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-
|
|
212
|
+
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-latest");
|
|
213
|
+
expect(resolveModel("anthropic/claude-opus-latest")).toBe("anthropic/claude-opus-latest");
|
|
187
214
|
});
|
|
188
215
|
|
|
189
216
|
it("updateRegistryFromAvailableModels chooses highest among flant openai versions", () => {
|
|
@@ -197,16 +224,25 @@ describe("model-registry", () => {
|
|
|
197
224
|
});
|
|
198
225
|
|
|
199
226
|
it("getAllAliases returns a copy", () => {
|
|
227
|
+
updateRegistryFromAvailableModels(["openai/gpt-5.4"]);
|
|
200
228
|
const aliases = getAllAliases();
|
|
201
229
|
aliases["openai/gpt-latest"] = "openai/gpt-0.0";
|
|
202
230
|
|
|
203
231
|
expect(resolveModel("openai/gpt-latest")).toBe("openai/gpt-5.4");
|
|
204
232
|
});
|
|
205
233
|
|
|
206
|
-
it("getAllAliases contains
|
|
234
|
+
it("getAllAliases contains native-latest identity mappings", () => {
|
|
235
|
+
const aliases = getAllAliases();
|
|
236
|
+
|
|
237
|
+
expect(aliases["anthropic/claude-opus-latest"]).toBe("anthropic/claude-opus-latest");
|
|
238
|
+
expect(aliases["anthropic/claude-sonnet-latest"]).toBe("anthropic/claude-sonnet-latest");
|
|
239
|
+
expect(aliases["anthropic/claude-haiku-latest"]).toBe("anthropic/claude-haiku-latest");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("getAllAliases contains resolved aliases after updateRegistry", () => {
|
|
243
|
+
updateRegistryFromAvailableModels(["grok-4"]);
|
|
207
244
|
const aliases = getAllAliases();
|
|
208
245
|
|
|
209
|
-
expect(aliases["anthropic/claude-opus-latest"]).toBe("anthropic/claude-opus-4-6");
|
|
210
246
|
expect(aliases["pp-flant-openai/grok-latest"]).toBe("pp-flant-openai/grok-4");
|
|
211
247
|
});
|
|
212
248
|
|
|
@@ -235,4 +271,79 @@ describe("model-registry", () => {
|
|
|
235
271
|
expect(families.find((f) => f.family === "haiku")).toMatchObject({ vendor: "anthropic", tier: "stupid" });
|
|
236
272
|
expect(families.find((f) => f.family === "gemini-pro")).toMatchObject({ vendor: "google", tier: "regular" });
|
|
237
273
|
});
|
|
274
|
+
|
|
275
|
+
describe("findLatestFamilyMatch", () => {
|
|
276
|
+
it("returns latest versioned model for a native-latest alias", () => {
|
|
277
|
+
const available = [
|
|
278
|
+
"anthropic/claude-opus-4-0-20250514",
|
|
279
|
+
"anthropic/claude-opus-4-6",
|
|
280
|
+
"anthropic/claude-sonnet-4-6",
|
|
281
|
+
];
|
|
282
|
+
expect(findLatestFamilyMatch("anthropic/claude-opus-latest", available)).toBe("anthropic/claude-opus-4-6");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("returns null for unknown model spec", () => {
|
|
286
|
+
expect(findLatestFamilyMatch("custom/unknown-model", ["anthropic/claude-opus-4-6"])).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("returns null when no candidates match the provider", () => {
|
|
290
|
+
expect(findLatestFamilyMatch("anthropic/claude-opus-latest", ["openai/gpt-5.4"])).toBeNull();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("returns null for spec without provider", () => {
|
|
294
|
+
expect(findLatestFamilyMatch("claude-opus-latest", ["anthropic/claude-opus-4-6"])).toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("returns the only candidate when there is one", () => {
|
|
298
|
+
expect(findLatestFamilyMatch("openai/gpt-latest", ["openai/gpt-5.4"])).toBe("openai/gpt-5.4");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("returns null for empty available list", () => {
|
|
302
|
+
expect(findLatestFamilyMatch("anthropic/claude-opus-latest", [])).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("subscription fallback rewrite", () => {
|
|
307
|
+
beforeEach(() => setSubscriptionFallbackActive(false));
|
|
308
|
+
afterEach(() => setSubscriptionFallbackActive(false));
|
|
309
|
+
|
|
310
|
+
it("toNonSubSpec rewrites provider-prefixed sub specs", () => {
|
|
311
|
+
expect(toNonSubSpec("pp-flant-anthropic-sub/sub/claude-opus-4-8")).toBe("pp-flant-anthropic/claude-opus-4-8");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("toNonSubSpec rewrites bare sub/ ids", () => {
|
|
315
|
+
expect(toNonSubSpec("sub/claude-haiku-4-5")).toBe("pp-flant-anthropic/claude-haiku-4-5");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("toNonSubSpec leaves non-subscription specs unchanged", () => {
|
|
319
|
+
expect(toNonSubSpec("pp-flant-anthropic/claude-opus-4-8")).toBe("pp-flant-anthropic/claude-opus-4-8");
|
|
320
|
+
expect(toNonSubSpec("openai/gpt-5.4")).toBe("openai/gpt-5.4");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("resolveModel leaves sub specs unchanged while fallback inactive", () => {
|
|
324
|
+
expect(isSubscriptionFallbackActive()).toBe(false);
|
|
325
|
+
expect(resolveModel("pp-flant-anthropic-sub/sub/claude-opus-4-8")).toBe("pp-flant-anthropic-sub/sub/claude-opus-4-8");
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("resolveModel rewrites sub specs to non-sub while fallback active", () => {
|
|
329
|
+
setSubscriptionFallbackActive(true);
|
|
330
|
+
expect(isSubscriptionFallbackActive()).toBe(true);
|
|
331
|
+
expect(resolveModel("pp-flant-anthropic-sub/sub/claude-opus-4-8")).toBe("pp-flant-anthropic/claude-opus-4-8");
|
|
332
|
+
expect(resolveModel("sub/claude-haiku-4-5")).toBe("pp-flant-anthropic/claude-haiku-4-5");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("resolveModel does not touch non-sub specs while fallback active", () => {
|
|
336
|
+
setSubscriptionFallbackActive(true);
|
|
337
|
+
expect(resolveModel("pp-flant-openai/gpt-5-4")).toBe("pp-flant-openai/gpt-5-4");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("resolveModel rewrites a subscription alias after registry update while fallback active", () => {
|
|
341
|
+
updateRegistryFromAvailableModels([
|
|
342
|
+
"pp-flant-anthropic-sub/sub/claude-opus-4-7",
|
|
343
|
+
"pp-flant-anthropic-sub/sub/claude-opus-4-8",
|
|
344
|
+
]);
|
|
345
|
+
setSubscriptionFallbackActive(true);
|
|
346
|
+
expect(resolveModel("pp-flant-anthropic-sub/claude-opus-latest")).toBe("pp-flant-anthropic/claude-opus-4-8");
|
|
347
|
+
});
|
|
348
|
+
});
|
|
238
349
|
});
|
|
@@ -11,7 +11,7 @@ export interface ModelInfo {
|
|
|
11
11
|
displayName: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
type ProviderPrefix = "anthropic" | "openai" | "google" | "deepseek" | "x-ai" | "qwen" | "pp-flant-anthropic" | "pp-flant-openai";
|
|
14
|
+
type ProviderPrefix = "anthropic" | "openai" | "google" | "deepseek" | "x-ai" | "qwen" | "pp-flant-anthropic" | "pp-flant-anthropic-sub" | "pp-flant-openai";
|
|
15
15
|
type KnownVendor = "anthropic" | "openai" | "google" | "deepseek" | "xai" | "qwen";
|
|
16
16
|
type KnownFamily = "opus" | "sonnet" | "haiku" | "gpt" | "gpt-mini" | "gemini-pro" | "gemini-flash" | "deepseek" | "grok" | "qwen";
|
|
17
17
|
type KnownTier = "stupid" | "regular" | "smart" | "xsmart";
|
|
@@ -24,6 +24,7 @@ export interface ModelFamilyDefinition {
|
|
|
24
24
|
patterns: RegExp[];
|
|
25
25
|
aliasTemplate: string;
|
|
26
26
|
providers: ProviderPrefix[];
|
|
27
|
+
nativeLatestProviders?: ProviderPrefix[];
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface ModelFamilyInfo {
|
|
@@ -41,27 +42,30 @@ export const MODEL_FAMILIES: ModelFamilyDefinition[] = [
|
|
|
41
42
|
family: "opus",
|
|
42
43
|
tier: "smart",
|
|
43
44
|
displayName: "Claude Opus",
|
|
44
|
-
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-opus-[a-z0-9.-]+$/],
|
|
45
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-opus-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-opus-[a-z0-9.-]+$/],
|
|
45
46
|
aliasTemplate: "claude-opus-latest",
|
|
46
|
-
providers: ["anthropic", "pp-flant-anthropic"],
|
|
47
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
48
|
+
nativeLatestProviders: ["anthropic"],
|
|
47
49
|
},
|
|
48
50
|
{
|
|
49
51
|
vendor: "anthropic",
|
|
50
52
|
family: "sonnet",
|
|
51
53
|
tier: "regular",
|
|
52
54
|
displayName: "Claude Sonnet",
|
|
53
|
-
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-sonnet-[a-z0-9.-]+$/],
|
|
55
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-sonnet-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-sonnet-[a-z0-9.-]+$/],
|
|
54
56
|
aliasTemplate: "claude-sonnet-latest",
|
|
55
|
-
providers: ["anthropic", "pp-flant-anthropic"],
|
|
57
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
58
|
+
nativeLatestProviders: ["anthropic"],
|
|
56
59
|
},
|
|
57
60
|
{
|
|
58
61
|
vendor: "anthropic",
|
|
59
62
|
family: "haiku",
|
|
60
63
|
tier: "stupid",
|
|
61
64
|
displayName: "Claude Haiku",
|
|
62
|
-
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-haiku-[a-z0-9.-]+$/],
|
|
65
|
+
patterns: [/^(anthropic|pp-flant-anthropic)\/claude-haiku-[a-z0-9.-]+$/, /^pp-flant-anthropic-sub\/sub\/claude-haiku-[a-z0-9.-]+$/],
|
|
63
66
|
aliasTemplate: "claude-haiku-latest",
|
|
64
|
-
providers: ["anthropic", "pp-flant-anthropic"],
|
|
67
|
+
providers: ["anthropic", "pp-flant-anthropic", "pp-flant-anthropic-sub"],
|
|
68
|
+
nativeLatestProviders: ["anthropic"],
|
|
65
69
|
},
|
|
66
70
|
{
|
|
67
71
|
vendor: "openai",
|
|
@@ -137,30 +141,20 @@ export const MODEL_FAMILIES: ModelFamilyDefinition[] = [
|
|
|
137
141
|
},
|
|
138
142
|
];
|
|
139
143
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
"pp-flant-anthropic/claude-sonnet-latest": "pp-flant-anthropic/claude-sonnet-4-6",
|
|
153
|
-
"pp-flant-anthropic/claude-haiku-latest": "pp-flant-anthropic/claude-haiku-3-5",
|
|
154
|
-
"pp-flant-openai/gpt-latest": "pp-flant-openai/gpt-5.4",
|
|
155
|
-
"pp-flant-openai/gpt-mini-latest": "pp-flant-openai/gpt-5.4-mini",
|
|
156
|
-
"pp-flant-openai/gemini-pro-latest": "pp-flant-openai/gemini-3.1-pro",
|
|
157
|
-
"pp-flant-openai/gemini-flash-latest": "pp-flant-openai/gemini-3.1-flash",
|
|
158
|
-
"pp-flant-openai/deepseek-latest": "pp-flant-openai/deepseek-v3",
|
|
159
|
-
"pp-flant-openai/grok-latest": "pp-flant-openai/grok-4",
|
|
160
|
-
"pp-flant-openai/qwen-latest": "pp-flant-openai/qwen3-coder",
|
|
161
|
-
};
|
|
144
|
+
function buildNativeLatestAliases(): Record<string, string> {
|
|
145
|
+
const map: Record<string, string> = {};
|
|
146
|
+
for (const family of MODEL_FAMILIES) {
|
|
147
|
+
const nativeSet = new Set(family.nativeLatestProviders ?? []);
|
|
148
|
+
for (const provider of family.providers) {
|
|
149
|
+
if (!nativeSet.has(provider)) continue;
|
|
150
|
+
const alias = toAlias(provider, family.aliasTemplate);
|
|
151
|
+
map[alias] = alias;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return map;
|
|
155
|
+
}
|
|
162
156
|
|
|
163
|
-
let aliasMap: Record<string, string> =
|
|
157
|
+
let aliasMap: Record<string, string> = buildNativeLatestAliases();
|
|
164
158
|
|
|
165
159
|
function pickLatest(models: string[]): string | null {
|
|
166
160
|
if (models.length === 0) return null;
|
|
@@ -213,12 +207,54 @@ function findFamily(modelId: string): ModelFamilyDefinition | null {
|
|
|
213
207
|
}
|
|
214
208
|
|
|
215
209
|
import { getLogger } from "./log.js";
|
|
210
|
+
import { SUB_MODEL_PREFIX, SUB_PROVIDER } from "./flant-infra.js";
|
|
211
|
+
|
|
212
|
+
// Session-scoped subscription fallback. When active (set after the user accepts
|
|
213
|
+
// a sub-429 fallback), every model spec resolved through resolveModel that is
|
|
214
|
+
// subscription-routed (`pp-flant-anthropic-sub/sub/<m>` or a bare `sub/<m>`) is
|
|
215
|
+
// rewritten to the regular per-token form `pp-flant-anthropic/<m>`. This is the
|
|
216
|
+
// single lever every resolution site funnels through (main phase switchModel,
|
|
217
|
+
// subagent tool_call input.model, planner/reviewer specs, registerAgents), so a
|
|
218
|
+
// one-shot switch cannot be undone by a later phase/subagent re-resolving a
|
|
219
|
+
// `sub/` spec. Cleared when the user switches back or the task ends.
|
|
220
|
+
let subscriptionFallbackActive = false;
|
|
221
|
+
|
|
222
|
+
export function setSubscriptionFallbackActive(active: boolean): void {
|
|
223
|
+
subscriptionFallbackActive = active;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isSubscriptionFallbackActive(): boolean {
|
|
227
|
+
return subscriptionFallbackActive;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Rewrite a subscription-routed spec to its regular per-token equivalent.
|
|
231
|
+
// Handles both `pp-flant-anthropic-sub/sub/<m>` and a bare `sub/<m>` id.
|
|
232
|
+
// Non-subscription specs pass through unchanged.
|
|
233
|
+
export function toNonSubSpec(spec: string): string {
|
|
234
|
+
if (spec.startsWith(`${SUB_PROVIDER}/${SUB_MODEL_PREFIX}`)) {
|
|
235
|
+
return `pp-flant-anthropic/${spec.slice(`${SUB_PROVIDER}/${SUB_MODEL_PREFIX}`.length)}`;
|
|
236
|
+
}
|
|
237
|
+
if (spec.startsWith(`${SUB_PROVIDER}/`)) {
|
|
238
|
+
return `pp-flant-anthropic/${spec.slice(`${SUB_PROVIDER}/`.length)}`;
|
|
239
|
+
}
|
|
240
|
+
if (spec.startsWith(SUB_MODEL_PREFIX)) {
|
|
241
|
+
return `pp-flant-anthropic/${spec.slice(SUB_MODEL_PREFIX.length)}`;
|
|
242
|
+
}
|
|
243
|
+
return spec;
|
|
244
|
+
}
|
|
216
245
|
|
|
217
246
|
export function resolveModel(aliasOrId: string): string {
|
|
218
|
-
|
|
247
|
+
let resolved = aliasMap[aliasOrId] ?? aliasOrId;
|
|
219
248
|
if (resolved !== aliasOrId) {
|
|
220
249
|
getLogger().debug({ s: "model", alias: aliasOrId, resolved }, "resolved model alias");
|
|
221
250
|
}
|
|
251
|
+
if (subscriptionFallbackActive) {
|
|
252
|
+
const rewritten = toNonSubSpec(resolved);
|
|
253
|
+
if (rewritten !== resolved) {
|
|
254
|
+
getLogger().debug({ s: "model", from: resolved, to: rewritten }, "subscription fallback rewrite");
|
|
255
|
+
resolved = rewritten;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
222
258
|
return resolved;
|
|
223
259
|
}
|
|
224
260
|
|
|
@@ -244,11 +280,13 @@ export function getModelInfo(modelId: string): ModelInfo {
|
|
|
244
280
|
export function updateRegistryFromAvailableModels(availableModels: string[]): void {
|
|
245
281
|
const log = getLogger();
|
|
246
282
|
const normalizedModels = collectNormalizedModels(availableModels).filter((modelId) => !modelId.endsWith("-latest"));
|
|
247
|
-
const nextAliasMap: Record<string, string> =
|
|
283
|
+
const nextAliasMap: Record<string, string> = buildNativeLatestAliases();
|
|
248
284
|
|
|
249
285
|
let updatedCount = 0;
|
|
250
286
|
for (const family of MODEL_FAMILIES) {
|
|
287
|
+
const nativeSet = new Set(family.nativeLatestProviders ?? []);
|
|
251
288
|
for (const provider of family.providers) {
|
|
289
|
+
if (nativeSet.has(provider)) continue;
|
|
252
290
|
const alias = toAlias(provider, family.aliasTemplate);
|
|
253
291
|
const candidates = normalizedModels.filter((modelId) => {
|
|
254
292
|
if (!modelId.startsWith(`${provider}/`)) return false;
|
|
@@ -257,7 +295,7 @@ export function updateRegistryFromAvailableModels(availableModels: string[]): vo
|
|
|
257
295
|
const latest = pickLatest(candidates);
|
|
258
296
|
if (latest) {
|
|
259
297
|
nextAliasMap[alias] = latest;
|
|
260
|
-
|
|
298
|
+
updatedCount++;
|
|
261
299
|
}
|
|
262
300
|
}
|
|
263
301
|
}
|
|
@@ -270,6 +308,26 @@ export function getAllAliases(): Record<string, string> {
|
|
|
270
308
|
return { ...aliasMap };
|
|
271
309
|
}
|
|
272
310
|
|
|
311
|
+
export function findLatestFamilyMatch(
|
|
312
|
+
modelSpec: string,
|
|
313
|
+
availableSpecs: string[],
|
|
314
|
+
): string | null {
|
|
315
|
+
const family = findFamily(modelSpec);
|
|
316
|
+
if (!family) return null;
|
|
317
|
+
|
|
318
|
+
const slashIdx = modelSpec.indexOf("/");
|
|
319
|
+
if (slashIdx === -1) return null;
|
|
320
|
+
const requestedProvider = modelSpec.substring(0, slashIdx).trim().toLowerCase();
|
|
321
|
+
|
|
322
|
+
const candidates = availableSpecs.filter((spec) => {
|
|
323
|
+
const specLower = spec.toLowerCase();
|
|
324
|
+
if (!specLower.startsWith(`${requestedProvider}/`)) return false;
|
|
325
|
+
return family.patterns.some((p) => p.test(specLower));
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return pickLatest(candidates);
|
|
329
|
+
}
|
|
330
|
+
|
|
273
331
|
export function getModelFamilies(): ModelFamilyInfo[] {
|
|
274
332
|
return MODEL_FAMILIES.map((family) => ({
|
|
275
333
|
vendor: family.vendor,
|
|
@@ -123,6 +123,119 @@ describe("Orchestrator.safeSendUserMessage", () => {
|
|
|
123
123
|
});
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
describe("Orchestrator.cancelPendingRetry", () => {
|
|
127
|
+
it("clears the pending timer, disarms the ESC interrupt, and resets the counter", () => {
|
|
128
|
+
vi.useFakeTimers();
|
|
129
|
+
const send = vi.fn();
|
|
130
|
+
const orchestrator = new Orchestrator(makePi({ sendUserMessage: send }));
|
|
131
|
+
const unsub = vi.fn();
|
|
132
|
+
orchestrator.errorRetryCount = 3;
|
|
133
|
+
orchestrator.pendingRetryEscUnsub = unsub;
|
|
134
|
+
orchestrator.pendingRetryTimer = setTimeout(() => send("fired"), 1000) as any;
|
|
135
|
+
|
|
136
|
+
orchestrator.cancelPendingRetry();
|
|
137
|
+
|
|
138
|
+
expect(orchestrator.pendingRetryTimer).toBeNull();
|
|
139
|
+
expect(orchestrator.pendingRetryEscUnsub).toBeNull();
|
|
140
|
+
expect(orchestrator.errorRetryCount).toBe(0);
|
|
141
|
+
expect(unsub).toHaveBeenCalledTimes(1);
|
|
142
|
+
vi.advanceTimersByTime(2000);
|
|
143
|
+
expect(send).not.toHaveBeenCalled();
|
|
144
|
+
vi.useRealTimers();
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("Orchestrator.armRetryEscInterrupt", () => {
|
|
149
|
+
function makeCtxWithTerminal() {
|
|
150
|
+
let handler: ((data: string) => any) | null = null;
|
|
151
|
+
const notify = vi.fn();
|
|
152
|
+
const ctx = {
|
|
153
|
+
ui: {
|
|
154
|
+
notify,
|
|
155
|
+
onTerminalInput: (h: (data: string) => any) => {
|
|
156
|
+
handler = h;
|
|
157
|
+
return () => { handler = null; };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
return { ctx, notify, feed: (data: string) => handler?.(data) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
it("cancels the pending retry on a bare ESC only", () => {
|
|
165
|
+
vi.useFakeTimers();
|
|
166
|
+
const orchestrator = new Orchestrator(makePi());
|
|
167
|
+
const { ctx, notify, feed } = makeCtxWithTerminal();
|
|
168
|
+
orchestrator.pendingRetryTimer = setTimeout(() => {}, 10000) as any;
|
|
169
|
+
orchestrator.armRetryEscInterrupt(ctx as any);
|
|
170
|
+
|
|
171
|
+
// Arrow key (ESC-prefixed sequence) must NOT cancel and must NOT be consumed.
|
|
172
|
+
const arrow = feed("\x1b[A");
|
|
173
|
+
expect(arrow).toBeUndefined();
|
|
174
|
+
expect(orchestrator.pendingRetryTimer).not.toBeNull();
|
|
175
|
+
|
|
176
|
+
// Bare ESC cancels and consumes.
|
|
177
|
+
const esc = feed("\x1b");
|
|
178
|
+
expect(esc).toEqual({ consume: true });
|
|
179
|
+
expect(orchestrator.pendingRetryTimer).toBeNull();
|
|
180
|
+
expect(notify).toHaveBeenCalled();
|
|
181
|
+
vi.useRealTimers();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("is a no-op when no retry timer is armed", () => {
|
|
185
|
+
const orchestrator = new Orchestrator(makePi());
|
|
186
|
+
const { ctx, feed } = makeCtxWithTerminal();
|
|
187
|
+
orchestrator.armRetryEscInterrupt(ctx as any);
|
|
188
|
+
expect(feed("\x1b")).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("Orchestrator.sendUserMessageWhenIdle", () => {
|
|
193
|
+
it("sends immediately when idle", () => {
|
|
194
|
+
const send = vi.fn();
|
|
195
|
+
const orchestrator = new Orchestrator(makePi({ sendUserMessage: send }));
|
|
196
|
+
orchestrator.active = makeActiveTask(null);
|
|
197
|
+
orchestrator.activeTaskToken = 7;
|
|
198
|
+
orchestrator.lastCtx = { isIdle: () => true };
|
|
199
|
+
|
|
200
|
+
orchestrator.sendUserMessageWhenIdle("[PI-PI] go", 7);
|
|
201
|
+
expect(send).toHaveBeenCalledWith("[PI-PI] go", { deliverAs: "followUp" });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("defers while busy, then sends once idle", async () => {
|
|
205
|
+
vi.useFakeTimers();
|
|
206
|
+
const send = vi.fn();
|
|
207
|
+
const orchestrator = new Orchestrator(makePi({ sendUserMessage: send }));
|
|
208
|
+
orchestrator.active = makeActiveTask(null);
|
|
209
|
+
orchestrator.activeTaskToken = 1;
|
|
210
|
+
let idle = false;
|
|
211
|
+
orchestrator.lastCtx = { isIdle: () => idle };
|
|
212
|
+
|
|
213
|
+
orchestrator.sendUserMessageWhenIdle("[PI-PI] go", 1);
|
|
214
|
+
expect(send).not.toHaveBeenCalled();
|
|
215
|
+
await vi.advanceTimersByTimeAsync(3000);
|
|
216
|
+
expect(send).not.toHaveBeenCalled();
|
|
217
|
+
idle = true;
|
|
218
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
219
|
+
expect(send).toHaveBeenCalledWith("[PI-PI] go", { deliverAs: "followUp" });
|
|
220
|
+
vi.useRealTimers();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("drops (does not send) when the task token changes", async () => {
|
|
224
|
+
vi.useFakeTimers();
|
|
225
|
+
const send = vi.fn();
|
|
226
|
+
const orchestrator = new Orchestrator(makePi({ sendUserMessage: send }));
|
|
227
|
+
orchestrator.active = makeActiveTask(null);
|
|
228
|
+
orchestrator.activeTaskToken = 1;
|
|
229
|
+
orchestrator.lastCtx = { isIdle: () => false };
|
|
230
|
+
|
|
231
|
+
orchestrator.sendUserMessageWhenIdle("[PI-PI] go", 1);
|
|
232
|
+
orchestrator.activeTaskToken = 2; // task switched
|
|
233
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
234
|
+
expect(send).not.toHaveBeenCalled();
|
|
235
|
+
vi.useRealTimers();
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
126
239
|
describe("Orchestrator.switchModel thinking level", () => {
|
|
127
240
|
function makeCtx() {
|
|
128
241
|
return {
|