@oh-my-pi/pi-coding-agent 17.1.2 → 17.1.4
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/CHANGELOG.md +63 -0
- package/dist/cli.js +3806 -3798
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +1 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +8 -0
- package/dist/types/cli/update-cli.d.ts +41 -0
- package/dist/types/cli/usage-cli.d.ts +4 -2
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +19 -0
- package/dist/types/config/settings-schema.d.ts +30 -7
- package/dist/types/cursor.d.ts +47 -1
- package/dist/types/eval/js/process-entry.d.ts +2 -1
- package/dist/types/eval/js/worker-core.d.ts +4 -1
- package/dist/types/extensibility/extensions/runner.d.ts +1 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-tui-shim.d.ts +15 -7
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +9 -0
- package/dist/types/modes/components/custom-editor.d.ts +12 -8
- package/dist/types/plan-mode/approved-plan.d.ts +3 -2
- package/dist/types/secrets/obfuscator.d.ts +7 -31
- package/dist/types/session/agent-session.d.ts +19 -4
- package/dist/types/session/prewalk.d.ts +2 -1
- package/dist/types/session/session-advisors.d.ts +8 -0
- package/dist/types/session/session-metadata.d.ts +29 -0
- package/dist/types/session/turn-recovery.d.ts +5 -5
- package/dist/types/stt/sherpa-runtime.d.ts +38 -0
- package/dist/types/thinking.d.ts +24 -0
- package/dist/types/tools/auto-generated-guard.d.ts +5 -2
- package/dist/types/tools/bash.d.ts +5 -4
- package/dist/types/tools/computer/exposure.d.ts +8 -0
- package/dist/types/tools/computer/supervisor.d.ts +1 -1
- package/dist/types/tools/computer/worker-entry.d.ts +1 -1
- package/dist/types/tools/computer/worker.d.ts +1 -1
- package/dist/types/tools/computer.d.ts +6 -0
- package/dist/types/tools/memory-render.d.ts +1 -4
- package/dist/types/tools/shell-tokenize.d.ts +14 -0
- package/dist/types/tools/todo.d.ts +7 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +80 -10
- package/src/advisor/runtime.ts +27 -1
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +111 -0
- package/src/cli/auth-gateway-cli.ts +63 -16
- package/src/cli/config-cli.ts +25 -7
- package/src/cli/update-cli.ts +229 -29
- package/src/cli/usage-cli.ts +144 -15
- package/src/cli.ts +21 -15
- package/src/commands/update.ts +6 -0
- package/src/config/__tests__/model-registry.test.ts +42 -7
- package/src/config/model-registry.ts +67 -4
- package/src/config/settings-schema.ts +39 -8
- package/src/config/settings.ts +24 -2
- package/src/cursor.ts +153 -0
- package/src/dap/session.ts +70 -11
- package/src/edit/hashline/filesystem.ts +1 -1
- package/src/edit/modes/patch.ts +1 -1
- package/src/eval/__tests__/js-context-manager.test.ts +9 -1
- package/src/eval/__tests__/process-entry-import.test.ts +111 -1
- package/src/eval/js/process-entry.ts +9 -5
- package/src/eval/js/worker-core.ts +6 -2
- package/src/exec/bash-executor.ts +4 -2
- package/src/extensibility/extensions/runner.ts +23 -8
- package/src/extensibility/legacy-pi-ai-shim.ts +9 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +14 -2
- package/src/extensibility/legacy-pi-tui-shim.ts +33 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/main.ts +22 -1
- package/src/modes/acp/acp-agent.ts +6 -0
- package/src/modes/components/assistant-message.ts +88 -11
- package/src/modes/components/chat-transcript-builder.ts +2 -0
- package/src/modes/components/custom-editor.test.ts +170 -0
- package/src/modes/components/custom-editor.ts +79 -29
- package/src/modes/components/settings-defs.ts +5 -1
- package/src/modes/controllers/event-controller.ts +96 -4
- package/src/modes/controllers/selector-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +34 -8
- package/src/modes/utils/context-usage.ts +19 -2
- package/src/modes/utils/interactive-context-helpers.ts +1 -0
- package/src/plan-mode/approved-plan.ts +18 -10
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/sdk.ts +4 -0
- package/src/secrets/obfuscator.ts +36 -126
- package/src/session/agent-session.ts +69 -60
- package/src/session/prewalk.ts +8 -3
- package/src/session/session-advisors.ts +67 -3
- package/src/session/session-metadata.ts +53 -0
- package/src/session/session-provider-boundary.ts +0 -1
- package/src/session/session-tools.ts +35 -1
- package/src/session/stream-guards.ts +1 -1
- package/src/session/turn-recovery.ts +36 -19
- package/src/slash-commands/builtin-registry.ts +49 -7
- package/src/stt/asr-worker.ts +2 -37
- package/src/stt/sherpa-runtime.ts +71 -0
- package/src/task/executor.ts +6 -5
- package/src/thinking.ts +39 -0
- package/src/tools/auto-generated-guard.ts +18 -5
- package/src/tools/bash.ts +43 -15
- package/src/tools/computer/exposure.ts +38 -0
- package/src/tools/computer/supervisor.ts +61 -13
- package/src/tools/computer/worker-entry.ts +28 -19
- package/src/tools/computer/worker.ts +3 -7
- package/src/tools/computer.ts +65 -10
- package/src/tools/gh-cache-invalidation.ts +2 -82
- package/src/tools/memory-render.ts +11 -2
- package/src/tools/render-utils.ts +8 -3
- package/src/tools/shell-tokenize.ts +83 -0
- package/src/tools/todo.ts +44 -17
- package/src/tools/write.ts +1 -1
package/src/cli/usage-cli.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* always covers the full credential pool.
|
|
9
9
|
*/
|
|
10
10
|
import {
|
|
11
|
+
ANTHROPIC_OAUTH_GRANT_TTL_MS,
|
|
11
12
|
type AuthStorage,
|
|
13
|
+
type DisabledCredentialSummary,
|
|
12
14
|
resolveUsedFraction,
|
|
13
15
|
type UsageHistoryEntry,
|
|
14
16
|
type UsageLimit,
|
|
@@ -44,6 +46,8 @@ export interface UsageAccountIdentity {
|
|
|
44
46
|
/** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
|
|
45
47
|
orgId?: string;
|
|
46
48
|
orgName?: string;
|
|
49
|
+
/** Epoch ms of the interactive login that minted the OAuth grant (see `OAuthCredentials.authorizedAt`). */
|
|
50
|
+
authorizedAt?: number;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
|
@@ -126,7 +130,11 @@ function findDistinguishingInfix(value: string, peers: string[]): string | undef
|
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
/** Every identity string the output could surface — input for {@link buildRedactionMap}. */
|
|
129
|
-
function collectIdentityStrings(
|
|
133
|
+
function collectIdentityStrings(
|
|
134
|
+
reports: UsageReport[],
|
|
135
|
+
accounts: UsageAccountIdentity[],
|
|
136
|
+
disabled: DisabledCredentialSummary[] = [],
|
|
137
|
+
): string[] {
|
|
130
138
|
const values: string[] = [];
|
|
131
139
|
const add = (value: unknown): void => {
|
|
132
140
|
if (typeof value === "string" && value) values.push(value);
|
|
@@ -152,6 +160,12 @@ function collectIdentityStrings(reports: UsageReport[], accounts: UsageAccountId
|
|
|
152
160
|
add(account.orgName);
|
|
153
161
|
add(account.enterpriseUrl);
|
|
154
162
|
}
|
|
163
|
+
for (const summary of disabled) {
|
|
164
|
+
add(summary.email);
|
|
165
|
+
add(summary.accountId);
|
|
166
|
+
add(summary.orgId);
|
|
167
|
+
add(summary.orgName);
|
|
168
|
+
}
|
|
155
169
|
return values;
|
|
156
170
|
}
|
|
157
171
|
|
|
@@ -312,13 +326,15 @@ export function collectUnreportedAccounts(
|
|
|
312
326
|
// multi-subscription): two orgs share every other identifier, so an
|
|
313
327
|
// org-scoped account is covered only by its own org's report, and an
|
|
314
328
|
// org-less legacy account is never covered by an org-attributed sibling
|
|
315
|
-
// report — its own fetch failing must surface as "no usage data".
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
// account
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
329
|
+
// report — its own fetch failing must surface as "no usage data". Its
|
|
330
|
+
// own ORG-LESS report still covers it, though: a mixed pool (fresh
|
|
331
|
+
// org-scoped logins beside pre-org-capture rows) must not duplicate
|
|
332
|
+
// every legacy account. The shared org is a GATE, not a match: two Team
|
|
333
|
+
// members share the org id while drawing on per-user pools, so coverage
|
|
334
|
+
// also requires the account's own base identity inside the same-org
|
|
335
|
+
// subset (an org-only account, with no base identifiers, is covered by
|
|
336
|
+
// any same-org report). The email/account fallback below applies only
|
|
337
|
+
// when both sides are org-less.
|
|
322
338
|
const accountOrg = account.orgId?.toLowerCase();
|
|
323
339
|
const ids = [account.email, account.accountId, account.projectId]
|
|
324
340
|
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
@@ -333,9 +349,15 @@ export function collectUnreportedAccounts(
|
|
|
333
349
|
}
|
|
334
350
|
}
|
|
335
351
|
if (accountOrg || sawReportOrg) {
|
|
336
|
-
|
|
352
|
+
const candidates = accountOrg
|
|
353
|
+
? sameOrgReports
|
|
354
|
+
: providerReports.filter(report => {
|
|
355
|
+
const metaOrg = report.metadata?.orgId;
|
|
356
|
+
return !(typeof metaOrg === "string" && metaOrg);
|
|
357
|
+
});
|
|
358
|
+
if (candidates.length === 0) return true;
|
|
337
359
|
if (ids.length === 0) return false;
|
|
338
|
-
return !
|
|
360
|
+
return !candidates.some(report => {
|
|
339
361
|
const identifiers = reportIdentifiers(report);
|
|
340
362
|
return ids.some(id => identifiers.has(id));
|
|
341
363
|
});
|
|
@@ -509,6 +531,58 @@ export function computeProviderWindowStats(reports: UsageReport[]): ProviderWind
|
|
|
509
531
|
});
|
|
510
532
|
}
|
|
511
533
|
|
|
534
|
+
/** Re-login warnings render once remaining grant life drops below this. */
|
|
535
|
+
const RELOGIN_WARN_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Re-login deadline line for providers whose OAuth grants expire a fixed
|
|
539
|
+
* period after the interactive login (today: Anthropic, ~30 days regardless
|
|
540
|
+
* of refresh rotation). Silent until the deadline is under a week out — a
|
|
541
|
+
* nudge before the broker auto-disables the row, not a permanent countdown.
|
|
542
|
+
*/
|
|
543
|
+
function formatReloginDeadline(
|
|
544
|
+
account: UsageAccountIdentity,
|
|
545
|
+
nowMs: number,
|
|
546
|
+
redaction?: Map<string, string>,
|
|
547
|
+
): string | undefined {
|
|
548
|
+
if (account.provider !== "anthropic" || account.type !== "oauth" || !account.authorizedAt) return undefined;
|
|
549
|
+
const remaining = account.authorizedAt + ANTHROPIC_OAUTH_GRANT_TTL_MS - nowMs;
|
|
550
|
+
if (remaining > RELOGIN_WARN_WINDOW_MS) return undefined;
|
|
551
|
+
const label = accountIdentityLabel(account, redaction);
|
|
552
|
+
if (remaining <= 0) {
|
|
553
|
+
return ` ${chalk.red(`⚠ ${label} — grant is past Anthropic's ~30d lifetime; re-login now`)}`;
|
|
554
|
+
}
|
|
555
|
+
return ` ${chalk.yellow(`⚠ ${label} — re-login within ${formatDuration(remaining)} (Anthropic expires OAuth grants ~30d after login)`)}`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Tombstones worth a row in `omp usage`: OAuth credentials torn down
|
|
560
|
+
* automatically (refresh failure, upstream invalidation). Rows the user
|
|
561
|
+
* replaced or deleted deliberately are lifecycle noise, not lost capacity.
|
|
562
|
+
*/
|
|
563
|
+
function isActionableDisable(summary: DisabledCredentialSummary): boolean {
|
|
564
|
+
if (summary.type !== "oauth") return false;
|
|
565
|
+
return !/^(replaced by|deleted by user)/i.test(summary.cause);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** Human-sized disable cause: the upstream `error_description` when embedded, else the first clause. */
|
|
569
|
+
function shortDisableCause(cause: string): string {
|
|
570
|
+
const description = cause.match(/\\?"error_description\\?"\s*:\s*\\?"([^"\\]+)/)?.[1];
|
|
571
|
+
if (description) return description;
|
|
572
|
+
const stripped = cause.replace(/^oauth refresh failed:\s*/i, "");
|
|
573
|
+
const clause = stripped.split(/[;\n]/, 1)[0] ?? stripped;
|
|
574
|
+
return clause.length > 80 ? `${clause.slice(0, 77)}…` : clause;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Label for a disabled tombstone, masking each identity part under `--redact`. */
|
|
578
|
+
function disabledIdentityLabel(summary: DisabledCredentialSummary, redaction?: Map<string, string>): string {
|
|
579
|
+
const base = summary.email ?? summary.accountId ?? "OAuth account";
|
|
580
|
+
const masked = redaction?.get(base) ?? base;
|
|
581
|
+
const org = summary.orgName ?? summary.orgId;
|
|
582
|
+
if (!org || org === base) return masked;
|
|
583
|
+
return `${masked} · ${redaction?.get(org) ?? org}`;
|
|
584
|
+
}
|
|
585
|
+
|
|
512
586
|
/**
|
|
513
587
|
* Render the full text breakdown: per provider, per account, every limit
|
|
514
588
|
* with a bar, amounts, and reset times; unattributed credentials trail
|
|
@@ -519,6 +593,7 @@ export function formatUsageBreakdown(
|
|
|
519
593
|
accounts: UsageAccountIdentity[],
|
|
520
594
|
nowMs: number,
|
|
521
595
|
redaction?: Map<string, string>,
|
|
596
|
+
disabled: DisabledCredentialSummary[] = [],
|
|
522
597
|
): string {
|
|
523
598
|
const reportsByProvider = new Map<string, UsageReport[]>();
|
|
524
599
|
for (const report of reports) {
|
|
@@ -533,10 +608,17 @@ export function formatUsageBreakdown(
|
|
|
533
608
|
list.push(account);
|
|
534
609
|
unreportedByProvider.set(account.provider, list);
|
|
535
610
|
}
|
|
611
|
+
const disabledByProvider = new Map<string, DisabledCredentialSummary[]>();
|
|
612
|
+
for (const summary of disabled) {
|
|
613
|
+
if (!isActionableDisable(summary)) continue;
|
|
614
|
+
const list = disabledByProvider.get(summary.provider) ?? [];
|
|
615
|
+
list.push(summary);
|
|
616
|
+
disabledByProvider.set(summary.provider, list);
|
|
617
|
+
}
|
|
536
618
|
|
|
537
|
-
const providers = [
|
|
538
|
-
|
|
539
|
-
);
|
|
619
|
+
const providers = [
|
|
620
|
+
...new Set([...reportsByProvider.keys(), ...unreportedByProvider.keys(), ...disabledByProvider.keys()]),
|
|
621
|
+
].sort((a, b) => a.localeCompare(b));
|
|
540
622
|
|
|
541
623
|
const lines: string[] = [];
|
|
542
624
|
const latestFetchedAt = Math.max(0, ...reports.map(report => report.fetchedAt ?? 0));
|
|
@@ -582,6 +664,20 @@ export function formatUsageBreakdown(
|
|
|
582
664
|
lines.push(` ${chalk.dim("○")} ${chalk.dim(`${label} — no usage data`)}`);
|
|
583
665
|
}
|
|
584
666
|
|
|
667
|
+
for (const summary of disabledByProvider.get(provider) ?? []) {
|
|
668
|
+
const label = disabledIdentityLabel(summary, redaction);
|
|
669
|
+
const ago = summary.disabledAtMs !== undefined ? ` ${formatDuration(nowMs - summary.disabledAtMs)} ago` : "";
|
|
670
|
+
lines.push(
|
|
671
|
+
` ${chalk.red(`✗ ${label} — disabled${ago}: ${sanitizeText(shortDisableCause(summary.cause))}`)} ${chalk.dim("(re-login to restore)")}`,
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
for (const account of accounts) {
|
|
676
|
+
if (account.provider !== provider) continue;
|
|
677
|
+
const warning = formatReloginDeadline(account, nowMs, redaction);
|
|
678
|
+
if (warning) lines.push(warning);
|
|
679
|
+
}
|
|
680
|
+
|
|
585
681
|
const stats = computeProviderWindowStats(providerReports);
|
|
586
682
|
if (stats.length > 0) {
|
|
587
683
|
const parts = stats.map(
|
|
@@ -750,6 +846,7 @@ function collectStoredAccounts(authStorage: AuthStorage): UsageAccountIdentity[]
|
|
|
750
846
|
enterpriseUrl: credential.enterpriseUrl,
|
|
751
847
|
orgId: credential.orgId,
|
|
752
848
|
orgName: credential.orgName,
|
|
849
|
+
authorizedAt: credential.authorizedAt,
|
|
753
850
|
});
|
|
754
851
|
} else {
|
|
755
852
|
accounts.push({ provider, type: "api_key" });
|
|
@@ -862,20 +959,41 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
862
959
|
(await authStorage.fetchUsageReports({
|
|
863
960
|
baseUrlResolver: provider => modelRegistry.getProviderBaseUrl(provider),
|
|
864
961
|
})) ?? [];
|
|
962
|
+
// Reports are always fresh (broker-side fetch) but the account list can
|
|
963
|
+
// come from a disk-cached snapshot up to an hour old — revalidate so a
|
|
964
|
+
// just-logged-in (or just-rotated-identity) credential isn't rendered
|
|
965
|
+
// as a stale duplicate. Best-effort: offline broker keeps the cache.
|
|
966
|
+
try {
|
|
967
|
+
await authStorage.revalidateCredentials();
|
|
968
|
+
} catch {
|
|
969
|
+
// Stale identities beat no output.
|
|
970
|
+
}
|
|
865
971
|
const storedAccounts = collectStoredAccounts(authStorage);
|
|
866
972
|
let accounts = selectReportableAccounts(
|
|
867
973
|
storedAccounts,
|
|
868
974
|
provider => authStorage.usageProviderFor(provider) !== undefined,
|
|
869
975
|
cmd.provider,
|
|
870
976
|
);
|
|
977
|
+
// Tombstones ride alongside the live pool so an auto-disabled account
|
|
978
|
+
// (e.g. an expired Anthropic grant) is loudly visible instead of just
|
|
979
|
+
// missing. Best-effort: a broker predating the endpoint yields [].
|
|
980
|
+
let disabled: DisabledCredentialSummary[] = [];
|
|
981
|
+
try {
|
|
982
|
+
disabled = await authStorage.listDisabledCredentials();
|
|
983
|
+
} catch {
|
|
984
|
+
// Usage output must not fail because tombstone listing did.
|
|
985
|
+
}
|
|
871
986
|
let filteredReports = reports;
|
|
872
987
|
if (cmd.provider) {
|
|
873
988
|
const wanted = cmd.provider.toLowerCase();
|
|
874
989
|
filteredReports = reports.filter(report => report.provider.toLowerCase() === wanted);
|
|
875
990
|
accounts = accounts.filter(account => account.provider.toLowerCase() === wanted);
|
|
991
|
+
disabled = disabled.filter(summary => summary.provider.toLowerCase() === wanted);
|
|
876
992
|
}
|
|
877
993
|
|
|
878
|
-
const redaction = cmd.redact
|
|
994
|
+
const redaction = cmd.redact
|
|
995
|
+
? buildRedactionMap(collectIdentityStrings(filteredReports, accounts, disabled))
|
|
996
|
+
: undefined;
|
|
879
997
|
|
|
880
998
|
if (cmd.json) {
|
|
881
999
|
// Drop the heavy provider-specific `raw` payload — same shape as the
|
|
@@ -900,10 +1018,21 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
900
1018
|
const stats = computeProviderWindowStats(filteredReports.filter(peer => peer.provider === report.provider));
|
|
901
1019
|
if (stats.length > 0) capacity[report.provider] = stats;
|
|
902
1020
|
}
|
|
1021
|
+
let disabledForJson = disabled.filter(isActionableDisable);
|
|
1022
|
+
if (redaction) {
|
|
1023
|
+
disabledForJson = disabledForJson.map(summary => ({
|
|
1024
|
+
...summary,
|
|
1025
|
+
email: maskIdentity(redaction, summary.email),
|
|
1026
|
+
accountId: maskIdentity(redaction, summary.accountId),
|
|
1027
|
+
orgId: maskIdentity(redaction, summary.orgId),
|
|
1028
|
+
orgName: maskIdentity(redaction, summary.orgName),
|
|
1029
|
+
}));
|
|
1030
|
+
}
|
|
903
1031
|
const payload = {
|
|
904
1032
|
generatedAt: Date.now(),
|
|
905
1033
|
reports: trimmed,
|
|
906
1034
|
accountsWithoutUsage: unreportedAccounts,
|
|
1035
|
+
disabledCredentials: disabledForJson,
|
|
907
1036
|
capacity,
|
|
908
1037
|
};
|
|
909
1038
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
@@ -923,7 +1052,7 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
|
|
|
923
1052
|
return;
|
|
924
1053
|
}
|
|
925
1054
|
|
|
926
|
-
process.stdout.write(`${formatUsageBreakdown(filteredReports, accounts, Date.now(), redaction)}\n`);
|
|
1055
|
+
process.stdout.write(`${formatUsageBreakdown(filteredReports, accounts, Date.now(), redaction, disabled)}\n`);
|
|
927
1056
|
} finally {
|
|
928
1057
|
authStorage.close();
|
|
929
1058
|
}
|
package/src/cli.ts
CHANGED
|
@@ -24,10 +24,16 @@ import {
|
|
|
24
24
|
setProfile,
|
|
25
25
|
VERSION,
|
|
26
26
|
} from "@oh-my-pi/pi-utils/dirs";
|
|
27
|
-
import {
|
|
27
|
+
import { interceptUnhandledRejections } from "@oh-my-pi/pi-utils/postmortem";
|
|
28
|
+
import { declareWorkerHostEntry, installWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
|
|
28
29
|
import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
|
|
29
30
|
import { extractProfileFlags } from "./cli/profile-bootstrap";
|
|
31
|
+
import { startJsEvalProcess } from "./eval/js/process-entry";
|
|
32
|
+
import type { WorkerInbound as JsWorkerInbound, WorkerOutbound as JsWorkerOutbound } from "./eval/js/worker-protocol";
|
|
30
33
|
import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol";
|
|
34
|
+
import { COMPUTER_WORKER_ARG } from "./tools/computer/protocol";
|
|
35
|
+
import { smokeTestComputerWorker } from "./tools/computer/supervisor";
|
|
36
|
+
import { startComputerWorker } from "./tools/computer/worker-entry";
|
|
31
37
|
|
|
32
38
|
if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
|
|
33
39
|
process.stderr.write(
|
|
@@ -79,8 +85,7 @@ async function runSmokeTest(): Promise<void> {
|
|
|
79
85
|
const { smokeTestTtsWorker } = await import("./tts/tts-client");
|
|
80
86
|
const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client");
|
|
81
87
|
const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
|
|
82
|
-
|
|
83
|
-
// Smoke dependencies stay lazy so normal CLI startup does not load worker clients.
|
|
88
|
+
// Other smoke dependencies stay lazy so normal CLI startup does not load their worker clients.
|
|
84
89
|
const { smokeTestDaemonBroker } = await import("./launch/client");
|
|
85
90
|
await smokeTestSyncWorker();
|
|
86
91
|
|
|
@@ -109,7 +114,6 @@ async function runSmokeTest(): Promise<void> {
|
|
|
109
114
|
const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
|
|
110
115
|
const STATS_SYNC_WORKER_ARG = "__omp_worker_stats_sync";
|
|
111
116
|
const TAB_WORKER_ARG = "__omp_worker_tab";
|
|
112
|
-
const COMPUTER_WORKER_ARG = "__omp_worker_computer";
|
|
113
117
|
const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
|
|
114
118
|
const JS_EVAL_PROCESS_ARG = "__omp_worker_js_eval_process";
|
|
115
119
|
const STT_WORKER_ARG = "__omp_worker_stt";
|
|
@@ -129,7 +133,7 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
129
133
|
// spawning (the smoke ping, the first parse request) would be dropped.
|
|
130
134
|
// Park early events and replay them once the module's handler is live.
|
|
131
135
|
// Worker-thread entries using `parentPort` need the same sync-prefix
|
|
132
|
-
// buffering; the tab/eval cases install that inbox below
|
|
136
|
+
// buffering; the computer/tab/eval cases install that inbox below.
|
|
133
137
|
const scope = globalThis as unknown as { onmessage: ((event: MessageEvent) => void) | null };
|
|
134
138
|
const pending: MessageEvent[] = [];
|
|
135
139
|
const buffer = (event: MessageEvent): void => {
|
|
@@ -144,12 +148,10 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
144
148
|
return true;
|
|
145
149
|
}
|
|
146
150
|
// Bun flushes messages the parent posted before spawn once this entry's
|
|
147
|
-
// top-level evaluation completes
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// (still inside the entry's sync prefix) so the handshake survives; the worker
|
|
152
|
-
// module binds the real handler once loaded.
|
|
151
|
+
// top-level evaluation completes. Install a buffering inbox synchronously
|
|
152
|
+
// before binding the selected worker's real handler so the parent's
|
|
153
|
+
// synchronous `init` survives. The dynamically imported tab/eval modules
|
|
154
|
+
// consume the same inbox after their module evaluation begins.
|
|
153
155
|
if (arg === TAB_WORKER_ARG) {
|
|
154
156
|
if (parentPort) installWorkerInbox(parentPort);
|
|
155
157
|
await import("./tools/browser/tab-worker-entry");
|
|
@@ -157,7 +159,7 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
157
159
|
}
|
|
158
160
|
if (arg === COMPUTER_WORKER_ARG) {
|
|
159
161
|
if (parentPort) installWorkerInbox(parentPort);
|
|
160
|
-
|
|
162
|
+
startComputerWorker();
|
|
161
163
|
return true;
|
|
162
164
|
}
|
|
163
165
|
if (arg === JS_EVAL_WORKER_ARG) {
|
|
@@ -166,11 +168,15 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
166
168
|
return true;
|
|
167
169
|
}
|
|
168
170
|
if (arg === JS_EVAL_PROCESS_ARG) {
|
|
169
|
-
|
|
171
|
+
// The bootstrap-safe interceptor seam is linked statically so this selector
|
|
172
|
+
// cannot load profile-scoped environment state after dispatch has begun.
|
|
170
173
|
// The JS evaluator forwards user-controlled payloads (tool-call args,
|
|
171
174
|
// display outputs); a non-serializable one must fail that cell, not
|
|
172
175
|
// SIGKILL the kernel and erase the eval session's state.
|
|
173
|
-
await runIpcSubprocessWorker
|
|
176
|
+
await runIpcSubprocessWorker<JsWorkerInbound, JsWorkerOutbound>(
|
|
177
|
+
transport => startJsEvalProcess(transport, interceptUnhandledRejections),
|
|
178
|
+
{ rethrowConnectedSendErrors: true },
|
|
179
|
+
);
|
|
174
180
|
return true;
|
|
175
181
|
}
|
|
176
182
|
if (arg === STT_WORKER_ARG) {
|
|
@@ -345,7 +351,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
|
|
345
351
|
// synchronous prefix of `runWorkerEntrypoint`, and Bun flushes the
|
|
346
352
|
// worker's parked initial messages as soon as the entry module's
|
|
347
353
|
// top-level evaluation finishes.
|
|
348
|
-
if (resolvedArgv[0]
|
|
354
|
+
if (isWorkerHostSelector(resolvedArgv[0])) {
|
|
349
355
|
const dispatched = await runWorkerEntrypoint(resolvedArgv[0]);
|
|
350
356
|
if (!dispatched) {
|
|
351
357
|
process.stderr.write(`Error: unknown worker selector: ${resolvedArgv[0]}\n`);
|
package/src/commands/update.ts
CHANGED
|
@@ -15,6 +15,12 @@ export default class Update extends Command {
|
|
|
15
15
|
plugins: Flags.boolean({ char: "l", description: "Update installed plugins", default: false }),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
static examples = [
|
|
19
|
+
"omp update",
|
|
20
|
+
"omp update --check",
|
|
21
|
+
"# If GitHub rate-limits release metadata, set GITHUB_TOKEN or GH_TOKEN\n GITHUB_TOKEN=... omp update",
|
|
22
|
+
];
|
|
23
|
+
|
|
18
24
|
async run(): Promise<void> {
|
|
19
25
|
const { flags } = await this.parse(Update);
|
|
20
26
|
await initTheme();
|
|
@@ -3,24 +3,34 @@ import { mkdtempSync, rmSync } from "node:fs";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { buildModel } from "@oh-my-pi/pi-catalog/build";
|
|
6
7
|
import { ModelRegistry } from "../model-registry";
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
-
* Stub AuthStorage that satisfies the surface used by ModelRegistry's
|
|
10
|
-
* constructor (#loadModels → clearConfigApiKeys, constructor →
|
|
11
|
-
* setFallbackResolver). The awaiter under test never reaches auth-gated code
|
|
12
|
-
* paths, so no real credential store is required.
|
|
13
|
-
*/
|
|
9
|
+
/** Stub auth storage for registry lifecycle and missing-credential coverage. */
|
|
14
10
|
function createStubAuthStorage(): AuthStorage {
|
|
15
11
|
const stub = {
|
|
16
12
|
setFallbackResolver: () => {},
|
|
17
13
|
clearConfigApiKeys: () => {},
|
|
18
14
|
hasAuth: () => false,
|
|
15
|
+
getApiKey: async () => undefined,
|
|
19
16
|
};
|
|
20
17
|
return stub as unknown as AuthStorage;
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
|
|
20
|
+
const testModel = buildModel({
|
|
21
|
+
id: "test-model",
|
|
22
|
+
name: "Test Model",
|
|
23
|
+
api: "openai-completions",
|
|
24
|
+
provider: "test",
|
|
25
|
+
baseUrl: "https://example.test",
|
|
26
|
+
reasoning: false,
|
|
27
|
+
input: ["text"],
|
|
28
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
29
|
+
contextWindow: 1000,
|
|
30
|
+
maxTokens: 100,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("ModelRegistry", () => {
|
|
24
34
|
let tmpDir: string;
|
|
25
35
|
let registry: ModelRegistry;
|
|
26
36
|
|
|
@@ -144,4 +154,29 @@ describe("ModelRegistry.awaitBackgroundRefresh", () => {
|
|
|
144
154
|
secondResolve();
|
|
145
155
|
await registry.awaitBackgroundRefresh();
|
|
146
156
|
});
|
|
157
|
+
test("resolves API keys and provider headers for legacy extensions", async () => {
|
|
158
|
+
const model = testModel;
|
|
159
|
+
vi.spyOn(registry, "getApiKey").mockResolvedValue("test-key");
|
|
160
|
+
vi.spyOn(registry, "getProviderHeaders").mockReturnValue({ "x-test": "value" });
|
|
161
|
+
|
|
162
|
+
expect(await registry.getApiKeyAndHeaders(model)).toEqual({
|
|
163
|
+
ok: true,
|
|
164
|
+
apiKey: "test-key",
|
|
165
|
+
headers: { "x-test": "value" },
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("returns an error when authentication resolves without a credential", async () => {
|
|
170
|
+
expect(await registry.getApiKeyAndHeaders(testModel)).toEqual({
|
|
171
|
+
ok: false,
|
|
172
|
+
error: 'No API key found for "test"',
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("maps legacy extension auth failures into the result contract", async () => {
|
|
177
|
+
const model = testModel;
|
|
178
|
+
vi.spyOn(registry, "getApiKey").mockRejectedValue(new Error("auth failed"));
|
|
179
|
+
|
|
180
|
+
expect(await registry.getApiKeyAndHeaders(model)).toEqual({ ok: false, error: "auth failed" });
|
|
181
|
+
});
|
|
147
182
|
});
|
|
@@ -763,6 +763,16 @@ function getDisabledProviderIdsFromSettings(): Set<string> {
|
|
|
763
763
|
}
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
/** Authentication material returned to legacy extensions for one model request. */
|
|
767
|
+
export type ResolvedRequestAuth =
|
|
768
|
+
| {
|
|
769
|
+
ok: true;
|
|
770
|
+
apiKey?: string;
|
|
771
|
+
headers?: Record<string, string>;
|
|
772
|
+
env?: Record<string, string>;
|
|
773
|
+
}
|
|
774
|
+
| { ok: false; error: string };
|
|
775
|
+
|
|
766
776
|
/**
|
|
767
777
|
* Model registry - loads and manages models, resolves API keys via AuthStorage.
|
|
768
778
|
*/
|
|
@@ -793,6 +803,7 @@ export class ModelRegistry {
|
|
|
793
803
|
// Runtime model managers registered by extensions via fetchDynamicModels.
|
|
794
804
|
// Keyed by provider name; use the same SQLite cache path as builtins.
|
|
795
805
|
#runtimeModelManagers: Map<string, { options: ModelManagerOptions<Api>; sourceId: string }> = new Map();
|
|
806
|
+
#ignoreLocalModelConfig: boolean;
|
|
796
807
|
#fetch: FetchImpl;
|
|
797
808
|
|
|
798
809
|
#resolveCommandBackedApiKey(provider: string): CommandApiKeyResolution {
|
|
@@ -829,8 +840,18 @@ export class ModelRegistry {
|
|
|
829
840
|
constructor(
|
|
830
841
|
readonly authStorage: AuthStorage,
|
|
831
842
|
modelsPath?: string,
|
|
832
|
-
options?: {
|
|
843
|
+
options?: {
|
|
844
|
+
/**
|
|
845
|
+
* Gateway mode: ignore local `models.yml` entirely (provider overrides,
|
|
846
|
+
* config API keys, custom models, custom discovery). A broker-backed
|
|
847
|
+
* gateway serves only bundled + broker-discovered catalog metadata and
|
|
848
|
+
* must never apply client-side credential or routing overrides.
|
|
849
|
+
*/
|
|
850
|
+
ignoreLocalModelConfig?: boolean;
|
|
851
|
+
fetch?: FetchImpl;
|
|
852
|
+
},
|
|
833
853
|
) {
|
|
854
|
+
this.#ignoreLocalModelConfig = options?.ignoreLocalModelConfig ?? false;
|
|
834
855
|
this.#fetch =
|
|
835
856
|
options?.fetch ??
|
|
836
857
|
(isBunTestRuntime()
|
|
@@ -1367,6 +1388,24 @@ export class ModelRegistry {
|
|
|
1367
1388
|
}
|
|
1368
1389
|
|
|
1369
1390
|
#loadCustomModels(): CustomModelsResult {
|
|
1391
|
+
// Gateway mode: serve bundled + broker-discovered catalog metadata only.
|
|
1392
|
+
// Local models.yml provider overrides (baseUrl/apiKey/headers/transport),
|
|
1393
|
+
// custom models, custom discovery, and config API keys are all client-side
|
|
1394
|
+
// routing that MUST NOT reach a broker-backed gateway — applying them would
|
|
1395
|
+
// send broker bearers to a configured endpoint, install config keys that
|
|
1396
|
+
// shadow broker credentials (bypassing account pooling/refresh/accounting),
|
|
1397
|
+
// or route a pi-native gateway back into itself.
|
|
1398
|
+
if (this.#ignoreLocalModelConfig) {
|
|
1399
|
+
return {
|
|
1400
|
+
models: [],
|
|
1401
|
+
overrides: new Map(),
|
|
1402
|
+
modelOverrides: new Map(),
|
|
1403
|
+
keylessProviders: new Set(),
|
|
1404
|
+
discoverableProviders: [],
|
|
1405
|
+
configuredProviders: new Set(),
|
|
1406
|
+
found: false,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1370
1409
|
const { value, error, status } = this.#modelsConfigFile.tryLoad();
|
|
1371
1410
|
|
|
1372
1411
|
if (status === "error") {
|
|
@@ -1958,12 +1997,22 @@ export class ModelRegistry {
|
|
|
1958
1997
|
remoteCompaction: mergeProviderRemoteCompactionConfig(entry.remoteCompaction, override.remoteCompaction),
|
|
1959
1998
|
};
|
|
1960
1999
|
}
|
|
2000
|
+
#applyProviderTransportOverrideToModel(
|
|
2001
|
+
model: Model<Api>,
|
|
2002
|
+
override: Pick<
|
|
2003
|
+
ProviderOverride,
|
|
2004
|
+
"baseUrl" | "headers" | "authHeader" | "apiKey" | "remoteCompaction" | "transport"
|
|
2005
|
+
>,
|
|
2006
|
+
): Model<Api> {
|
|
2007
|
+
return buildModel(this.#applyProviderTransportOverride(toModelSpec(model), override));
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1961
2010
|
#applyRuntimeProviderOverrides(models: Model<Api>[]): Model<Api>[] {
|
|
1962
2011
|
if (this.#runtimeProviderOverrides.size === 0) return models;
|
|
1963
2012
|
return models.map(model => {
|
|
1964
2013
|
const override = this.#runtimeProviderOverrides.get(model.provider);
|
|
1965
2014
|
if (!override) return model;
|
|
1966
|
-
return this.#
|
|
2015
|
+
return this.#applyProviderTransportOverrideToModel(model, override);
|
|
1967
2016
|
});
|
|
1968
2017
|
}
|
|
1969
2018
|
#resolveLiveModelOverride(model: Model<Api>): ModelOverride | undefined {
|
|
@@ -2194,6 +2243,20 @@ export class ModelRegistry {
|
|
|
2194
2243
|
});
|
|
2195
2244
|
}
|
|
2196
2245
|
|
|
2246
|
+
/** Resolve request authentication through the historical Pi extension facade. */
|
|
2247
|
+
async getApiKeyAndHeaders(model: Model<Api>): Promise<ResolvedRequestAuth> {
|
|
2248
|
+
try {
|
|
2249
|
+
const apiKey = await this.getApiKey(model);
|
|
2250
|
+
if (apiKey === undefined) {
|
|
2251
|
+
return { ok: false, error: `No API key found for "${model.provider}"` };
|
|
2252
|
+
}
|
|
2253
|
+
const headers = this.getProviderHeaders(model.provider);
|
|
2254
|
+
return { ok: true, apiKey, headers };
|
|
2255
|
+
} catch (error) {
|
|
2256
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2197
2260
|
/**
|
|
2198
2261
|
* Get API key for a provider (e.g., "openai").
|
|
2199
2262
|
*
|
|
@@ -2404,7 +2467,7 @@ export class ModelRegistry {
|
|
|
2404
2467
|
const withRuntimeTransportOverride = runtimeTransportOverride
|
|
2405
2468
|
? nextModels.map(model => {
|
|
2406
2469
|
if (model.provider !== providerName) return model;
|
|
2407
|
-
return this.#
|
|
2470
|
+
return this.#applyProviderTransportOverrideToModel(model, runtimeTransportOverride);
|
|
2408
2471
|
})
|
|
2409
2472
|
: nextModels;
|
|
2410
2473
|
|
|
@@ -2488,7 +2551,7 @@ export class ModelRegistry {
|
|
|
2488
2551
|
this.#models = this.#applyLlamaCppQwenThinkingToModels(
|
|
2489
2552
|
this.#models.map(m => {
|
|
2490
2553
|
if (m.provider !== providerName) return m;
|
|
2491
|
-
return this.#
|
|
2554
|
+
return this.#applyProviderTransportOverrideToModel(m, transportOverride);
|
|
2492
2555
|
}),
|
|
2493
2556
|
);
|
|
2494
2557
|
}
|