pi-orchestrator-config 4.3.9 → 4.3.10

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.
@@ -810,6 +810,15 @@ function bodyAlreadySigned(text: string): boolean {
810
810
  return /\*Assisted-by:/.test(text) || /(?:^|\n|\\n)Assisted-by:\s/.test(text);
811
811
  }
812
812
 
813
+ /** Unresolved model placeholders are stale template footers, not signatures. */
814
+ function replaceUnresolvedModelFooters(payload: string): string {
815
+ const unresolvedFooter = /(?:\n|\\n){2}---(?:\n|\\n)\*Assisted-by: PI \((?:\$PI_MODEL|\$\{PI_MODEL\})\)\*/g;
816
+ const replaced = payload.replace(unresolvedFooter, "");
817
+ if (replaced !== payload)
818
+ enfLog.debug("injectGhBodySignature replaced unresolved PI_MODEL footer");
819
+ return replaced;
820
+ }
821
+
813
822
  /** Index of the matching closer for a quote at `openIdx`, honoring POSIX escapes. */
814
823
  function matchingQuoteIndex(s: string, openIdx: number): number | null {
815
824
  const q = s[openIdx];
@@ -888,10 +897,6 @@ export function injectGhBodySignature(command: string, signature: string): strin
888
897
  enfLog.debug("injectGhBodySignature skip not a gh body command");
889
898
  return command;
890
899
  }
891
- if (bodyAlreadySigned(command)) {
892
- enfLog.debug("injectGhBodySignature skip already present");
893
- return command;
894
- }
895
900
  const footer = commentSignatureFooter(signature);
896
901
  const span = findLastQuotedBodySpan(command);
897
902
  if (!span) {
@@ -899,6 +904,19 @@ export function injectGhBodySignature(command: string, signature: string): strin
899
904
  return command;
900
905
  }
901
906
 
907
+ const payload = command.slice(span.open + 1, span.close);
908
+ const normalizedPayload = replaceUnresolvedModelFooters(payload);
909
+ if (normalizedPayload !== payload) {
910
+ // Re-parse positions after removal so command-substitution heredocs still
911
+ // receive their footer before the closing delimiter.
912
+ const normalizedCommand = command.slice(0, span.open + 1) + normalizedPayload + command.slice(span.close);
913
+ return injectGhBodySignature(normalizedCommand, signature);
914
+ }
915
+ if (bodyAlreadySigned(payload)) {
916
+ enfLog.debug("injectGhBodySignature skip already present in body");
917
+ return command;
918
+ }
919
+
902
920
  const last = lastHeredocInSpan(command, span.open, span.close);
903
921
  if (last) {
904
922
  const delim = last[2];
@@ -911,7 +929,6 @@ export function injectGhBodySignature(command: string, signature: string): strin
911
929
  }
912
930
  }
913
931
 
914
- const payload = command.slice(span.open + 1, span.close);
915
932
  if (payload.startsWith("$(")) {
916
933
  enfLog.warn("injectGhBodySignature --body command subst without heredoc match");
917
934
  return command;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pidiff-ui",
3
3
  "private": true,
4
- "version": "4.3.9",
4
+ "version": "4.3.10",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -27,7 +27,6 @@ import type {
27
27
  StreamOptions,
28
28
  } from "@earendil-works/pi-ai";
29
29
  import { createLogger } from "../shared/logger.js";
30
- import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
31
30
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
32
31
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
33
32
  import { asStringArray, getSetting } from "../orchestrator/project-settings.js";
@@ -80,12 +79,35 @@ const DISCOVERY_TIMEOUT_MS = 30_000;
80
79
 
81
80
  const registry = new ProviderDriverRegistry();
82
81
 
82
+ let createAssistantMessageEventStream: () => AssistantMessageEventStream;
83
+
83
84
  /** Track which instances are CLI vs ACPX for configured gates. */
84
85
  const cliInstances = new Map<string, ProviderInstance>();
85
86
  const acpxInstances = new Map<string, ProviderInstance>();
86
87
 
87
88
  let projectCwd = "";
88
89
 
90
+ interface ProviderDiscoverySummary {
91
+ summary: string;
92
+ }
93
+
94
+ /** Render durable discovery entries without adding a TUI package dependency. */
95
+ function providerDiscoverySummaryComponent(
96
+ summary: string,
97
+ theme: { fg: (color: "muted", text: string) => string },
98
+ ) {
99
+ log.debug("rendering CLI/ACPX discovery summary", { summary });
100
+ return {
101
+ render: (_width: number): string[] => {
102
+ log.debug("rendered CLI/ACPX discovery summary");
103
+ return [theme.fg("muted", summary)];
104
+ },
105
+ invalidate: (): void => {
106
+ log.debug("invalidated CLI/ACPX discovery summary");
107
+ },
108
+ };
109
+ }
110
+
89
111
  // ---------------------------------------------------------------------------
90
112
  // Configured Gates
91
113
  // ---------------------------------------------------------------------------
@@ -294,7 +316,17 @@ log.debug("providers module loaded");
294
316
 
295
317
  const cwdHookBound = new WeakSet<object>();
296
318
 
297
- export default async function (pi: ExtensionAPI) {
319
+ export default async function (
320
+ pi: ExtensionAPI,
321
+ testOptions?: { providerSummaryParts?: readonly string[] },
322
+ ) {
323
+ if (typeof pi.registerEntryRenderer === "function") {
324
+ pi.registerEntryRenderer<ProviderDiscoverySummary>(
325
+ "provider-discovery-summary",
326
+ (entry, _options, theme) => providerDiscoverySummaryComponent(entry.data?.summary ?? "", theme),
327
+ );
328
+ }
329
+
298
330
  // pi --help / --version — skip discovery
299
331
  if (isPiMetaInvocation()) return;
300
332
 
@@ -319,6 +351,9 @@ export default async function (pi: ExtensionAPI) {
319
351
  return;
320
352
  }
321
353
  markProvidersInitialized();
354
+ if (!testOptions?.providerSummaryParts) {
355
+ ({ createAssistantMessageEventStream } = await import("@earendil-works/pi-ai"));
356
+ }
322
357
  log.debug(
323
358
  "providers factory: proceeding after reset / first load — re-entering discovery",
324
359
  );
@@ -548,15 +583,23 @@ export default async function (pi: ExtensionAPI) {
548
583
 
549
584
  // Show discovery summary on session start; restore saved default model when
550
585
  // findInitialModel raced hasConfiguredAuth false → wrong initial model (#753).
551
- const providerSummaryParts: string[] = [];
552
- for (const [agent, inst] of cliInstances) {
553
- const count = inst.snapshot.getSnapshot().models.length;
554
- providerSummaryParts.push(`cli-${agent} (${count})`);
555
- }
556
- for (const [agent, inst] of acpxInstances) {
557
- const count = inst.snapshot.getSnapshot().models.length;
558
- providerSummaryParts.push(`acpx-${agent} (${count})`);
559
- }
586
+ const providerSummaryParts = testOptions?.providerSummaryParts
587
+ ? [...testOptions.providerSummaryParts]
588
+ : (() => {
589
+ const parts: string[] = [];
590
+ for (const [agent, inst] of cliInstances) {
591
+ const count = inst.snapshot.getSnapshot().models.length;
592
+ parts.push(`cli-${agent} (${count})`);
593
+ }
594
+ for (const [agent, inst] of acpxInstances) {
595
+ const count = inst.snapshot.getSnapshot().models.length;
596
+ parts.push(`acpx-${agent} (${count})`);
597
+ }
598
+ log.debug("built CLI/ACPX provider discovery summary", {
599
+ providerCount: parts.length,
600
+ });
601
+ return parts;
602
+ })();
560
603
 
561
604
  // Fire-and-forget restore so session_start is not blocked by retries (#753).
562
605
  // Omit registeredProviders: cli/acpx-only lists falsely fail-fast native defaults.
@@ -578,15 +621,18 @@ export default async function (pi: ExtensionAPI) {
578
621
 
579
622
  pi.on("session_start", (event, ctx) => {
580
623
  const reason = typeof event?.reason === "string" ? event.reason : "";
581
- if (providerSummaryParts.length > 0 && ctx.hasUI) {
624
+ const summary = `Providers: ${providerSummaryParts.join(", ")}`;
625
+ // Entries survive reload/resume. Only append for a newly started session;
626
+ // its persisted entry renders on later lifecycle events without duplication.
627
+ if (providerSummaryParts.length > 0 && (reason === "startup" || reason === "new")) {
582
628
  try {
583
- ctx.ui.notify(`Providers: ${providerSummaryParts.join(", ")}`, "info");
629
+ pi.appendEntry<ProviderDiscoverySummary>("provider-discovery-summary", { summary });
630
+ log.info("persisted CLI/ACPX discovery summary", { reason, summary });
584
631
  } catch (err) {
585
- log.debug(
586
- "providers notify skipped",
587
- err instanceof Error ? err.message : String(err),
588
- );
632
+ log.warn("provider discovery summary append failed", err instanceof Error ? err.name : typeof err);
589
633
  }
634
+ } else if (providerSummaryParts.length > 0) {
635
+ log.debug("provider discovery summary retained", { reason: reason || "unknown" });
590
636
  }
591
637
  applyCliAcpxThinking(
592
638
  ctx.model
@@ -30,6 +30,10 @@ export interface OpenAiCompatibleModelRecord {
30
30
  cost?: unknown;
31
31
  contextWindow?: unknown;
32
32
  maxTokens?: unknown;
33
+ /** LiteLLM's generic input capacity alias. */
34
+ max_input_tokens?: unknown;
35
+ /** LiteLLM's generic output capacity alias. */
36
+ max_output_tokens?: unknown;
33
37
  }
34
38
 
35
39
  /** Pi's models.json defaults for omitted static-model metadata. */
@@ -461,6 +465,7 @@ export function materializeOpenAiCompatibleModels(
461
465
  baseUrl: string,
462
466
  providerId: string,
463
467
  ): Model<Api>[] {
468
+ log.debug("materializing OpenAI-compatible models", { providerId, recordCount: records.length });
464
469
  const models: Model<Api>[] = [];
465
470
  const seenIds = new Set<string>();
466
471
  for (const record of records) {
@@ -477,8 +482,21 @@ export function materializeOpenAiCompatibleModels(
477
482
  : PI_STATIC_MODEL_DEFAULTS.reasoning,
478
483
  input: materializeInput(record.input),
479
484
  cost: materializeCost(record.cost),
480
- contextWindow: positiveFiniteNumber(record.contextWindow, PI_STATIC_MODEL_DEFAULTS.contextWindow),
481
- maxTokens: positiveFiniteNumber(record.maxTokens, PI_STATIC_MODEL_DEFAULTS.maxTokens),
485
+ // Native Pi metadata wins. LiteLLM commonly exposes only generic input
486
+ // and output capacities, whose combined capacity is Pi's context window.
487
+ contextWindow: positiveFiniteNumber(
488
+ record.contextWindow,
489
+ (() => {
490
+ const input = positiveFiniteNumber(record.max_input_tokens, 0);
491
+ const output = positiveFiniteNumber(record.max_output_tokens, 0);
492
+ const capacity = input > 0 && output > 0 ? input + output : 0;
493
+ return positiveFiniteNumber(capacity, PI_STATIC_MODEL_DEFAULTS.contextWindow);
494
+ })(),
495
+ ),
496
+ maxTokens: positiveFiniteNumber(
497
+ record.maxTokens,
498
+ positiveFiniteNumber(record.max_output_tokens, PI_STATIC_MODEL_DEFAULTS.maxTokens),
499
+ ),
482
500
  });
483
501
  }
484
502
  return models;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-orchestrator-config",
3
- "version": "4.3.9",
3
+ "version": "4.3.10",
4
4
  "description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
5
5
  "keywords": [
6
6
  "pi-package"