pi-openai-toolkit 0.14.13 → 0.15.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.
@@ -0,0 +1,100 @@
1
+ import { resolveToolkitConfig } from "../config";
2
+ import type { LoadedToolkitConfig } from "../types";
3
+ import type { ConfigOrigin } from "./policy";
4
+ import { resolveV2Config } from "./v2";
5
+
6
+ export type MigrationPreview = {
7
+ status: "ready" | "needs-review" | "already-v2" | "unavailable";
8
+ candidate?: Record<string, unknown>;
9
+ /** Controlled explanations, never arbitrary raw input values. */
10
+ reasons: string[];
11
+ /** Unknown names are masked; paths never expose unknown values. */
12
+ unmappedPaths: string[];
13
+ /** Candidate leaf -> origin in the unchanged legacy source (or built-in). */
14
+ origins?: Record<string, ConfigOrigin>;
15
+ };
16
+
17
+ function modelRef(key: string): { provider: string; id: string; api: string } {
18
+ const slash = key.indexOf("/");
19
+ return { provider: key.slice(0, slash), id: key.slice(slash + 1), api: "openai-responses" };
20
+ }
21
+
22
+ /** Read-only analysis: never edits a source file or claims a lossy candidate is equivalent. */
23
+ export function previewToolkitMigration(loaded: LoadedToolkitConfig): MigrationPreview {
24
+ if (loaded.document?.format === "v2") return { status: "already-v2", reasons: ["The document already uses schemaVersion 2."], unmappedPaths: [] };
25
+ if (loaded.document?.format === "invalid" || loaded.document?.format === "missing") {
26
+ return { status: "unavailable", reasons: ["A readable legacy document is required; no candidate was produced."], unmappedPaths: [] };
27
+ }
28
+ const baseline = resolveToolkitConfig(loaded);
29
+ const legacy = baseline.config;
30
+ const { compatibility: _compatibility, diagnostics, ...defaults } = structuredClone(baseline.policy);
31
+ const models: Record<string, unknown> = Object.create(null);
32
+ const origins: Record<string, ConfigOrigin> = Object.create(null);
33
+ const unmappedPaths = new Set(baseline.issues.map((issue) => issue.path));
34
+ for (const [leaf, origin] of Object.entries(baseline.origins)) {
35
+ if (leaf.startsWith("compatibility.")) continue;
36
+ origins[leaf.startsWith("diagnostics.") ? leaf : `defaults.${leaf}`] = origin;
37
+ }
38
+ const keys = new Set([
39
+ ...legacy.webSearch.models,
40
+ ...Object.keys(legacy.webSearch.routes ?? {}),
41
+ ...legacy.autoMode.models,
42
+ ...legacy.compaction.gatewayContextModels,
43
+ ]);
44
+ for (const key of keys) {
45
+ const resolved = resolveToolkitConfig(loaded, modelRef(key));
46
+ const override: Record<string, unknown> = {};
47
+ if (resolved.policy.webSearch.route !== defaults.webSearch.route) override.webSearch = resolved.policy.webSearch;
48
+ if (resolved.policy.autoMode.available !== defaults.autoMode.available) override.autoMode = { available: resolved.policy.autoMode.available };
49
+ if (resolved.policy.compatibility.transport !== "standard") override.compatibility = resolved.policy.compatibility;
50
+ if (Object.keys(override).length > 0) {
51
+ models[key] = override;
52
+ for (const [feature, fields] of Object.entries(override)) {
53
+ for (const field of Object.keys(fields as Record<string, unknown>)) {
54
+ const leaf = `${feature}.${field}`;
55
+ origins[`models[${JSON.stringify(key)}].${leaf}`] = resolved.origins[leaf];
56
+ }
57
+ }
58
+ }
59
+ }
60
+ const candidate = { schemaVersion: 2, defaults, models, diagnostics };
61
+ const reasons: string[] = [];
62
+ if (loaded.warnings.length || baseline.issues.length) {
63
+ reasons.push("Legacy warnings, unknown fields or normalized invalid values require review. Their original contents remain in the untouched source file.");
64
+ }
65
+ if (legacy.webSearch.enabled && legacy.webSearch.defaultRoute === undefined &&
66
+ legacy.webSearch.models.some((key) => !Object.prototype.hasOwnProperty.call(legacy.webSearch.routes ?? {}, key))) {
67
+ reasons.push("Legacy hosted allowlists ignore unsupported APIs and malformed payloads; explicit v2 hosted routes fail closed. This candidate is not behavior-equivalent on those paths.");
68
+ }
69
+ if (!legacy.webSearch.enabled && (legacy.webSearch.models.length || Object.keys(legacy.webSearch.routes ?? {}).length || legacy.webSearch.defaultRoute)) {
70
+ reasons.push("Disabled legacy search contains dormant routes or a model list; simplified v2 policy does not retain that enable-later intent.");
71
+ if (legacy.webSearch.models.length) unmappedPaths.add("webSearch.models");
72
+ if (Object.keys(legacy.webSearch.routes ?? {}).length) unmappedPaths.add("webSearch.routes");
73
+ if (legacy.webSearch.defaultRoute) unmappedPaths.add("webSearch.defaultRoute");
74
+ }
75
+ if (legacy.webSearch.models.length && (legacy.webSearch.defaultRoute !== undefined ||
76
+ legacy.webSearch.models.some((key) => Object.hasOwn(legacy.webSearch.routes ?? {}, key)))) {
77
+ unmappedPaths.add("webSearch.models");
78
+ }
79
+ if (!legacy.autoMode.enabled && legacy.autoMode.models.length) {
80
+ reasons.push("Disabled legacy auto mode contains a dormant eligibility list; keep the original file for review.");
81
+ unmappedPaths.add("autoMode.models");
82
+ }
83
+ if (!legacy.compaction.enabled && legacy.compaction.contextManagement === "remote") {
84
+ reasons.push("Disabled legacy context management retains a dormant remote mode; the candidate selects Pi ownership.");
85
+ unmappedPaths.add("compaction.contextManagement");
86
+ }
87
+ // Gateway affinity was previously context-mode gated for compaction, and not used by image.
88
+ if (legacy.compaction.gatewayContextModels.length) {
89
+ reasons.push("Gateway compatibility is now shared across features; review synthetic compaction/image affinity before using this candidate.");
90
+ }
91
+ const candidateChecks = [resolveV2Config(candidate, undefined), ...[...keys].map((key) => resolveV2Config(candidate, key))];
92
+ if (candidateChecks.some((resolved) => resolved.issues.length)) reasons.push("The candidate has v2 validation issues; it cannot be applied as-is.");
93
+ return {
94
+ status: reasons.length ? "needs-review" : "ready",
95
+ candidate,
96
+ origins,
97
+ unmappedPaths: [...unmappedPaths],
98
+ reasons: reasons.length ? reasons : ["Recognized active policies map without a known semantic difference. Preview only; no source file was changed."],
99
+ };
100
+ }
@@ -0,0 +1,28 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { ResolvedToolkitConfig } from "../config";
3
+ import type { ConfigIssue } from "./policy";
4
+ import { redactValue } from "../debug";
5
+
6
+ const lastNotice = new WeakMap<object, string>();
7
+
8
+ export function formatConfigIssues(issues: readonly ConfigIssue[], limit = 12): string {
9
+ const lines = issues.slice(0, limit).map((issue) => `${issue.severity}: ${issue.path.slice(0, 220)} — ${issue.code}`);
10
+ if (issues.length > limit) lines.push(`${issues.length - limit} more issue(s); inspect the configuration file.`);
11
+ return String(redactValue(lines.join("\n")));
12
+ }
13
+
14
+ /** Per-session bounded notice, independent of feature enablement/debug. Never logs raw config. */
15
+ export function notifyConfigIssues(ctx: ExtensionContext, resolved: ResolvedToolkitConfig): void {
16
+ const owner = ctx.sessionManager ?? ctx;
17
+ if (resolved.issues.length === 0) {
18
+ lastNotice.delete(owner);
19
+ return;
20
+ }
21
+ if (!ctx.hasUI || typeof ctx.ui?.notify !== "function") return;
22
+ const message = formatConfigIssues(resolved.issues, 4);
23
+ const fingerprint = `${resolved.source ?? "defaults"}\n${resolved.modelKey ?? ""}\n${message}`;
24
+ if (lastNotice.get(owner) === fingerprint) return;
25
+ lastNotice.set(owner, fingerprint);
26
+ ctx.ui.notify(`Toolkit configuration:\n${message}\nUse /toolkit-config validate for details.`,
27
+ resolved.issues.some((issue) => issue.severity === "error") ? "error" : "warning");
28
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ DEFAULT_AUTO_MODE_CONFIG,
3
+ DEFAULT_COMPACTION_CONFIG,
4
+ DEFAULT_IMAGE_GENERATION_MODEL,
5
+ type AutoModeConfig,
6
+ type LeaveManagedModePolicy,
7
+ type NativeFallbackConfig,
8
+ type RemoteV2ContextSource,
9
+ } from "../types";
10
+
11
+ export type ConfigFeature = "context" | "webSearch" | "imageGeneration" | "autoMode" | "compatibility" | "diagnostics";
12
+ export type SearchRoute = "unmanaged" | "local" | "hosted" | "standalone-alpha";
13
+ export type DiagnosticsConfig = {
14
+ level: "error" | "warn" | "info" | "debug";
15
+ notifyOnLoad: boolean;
16
+ captureRequests: boolean;
17
+ /** Captures compact responses, not ordinary conversation responses. */
18
+ captureResponses: boolean;
19
+ redactSensitiveData: boolean;
20
+ artifactRoot: string;
21
+ };
22
+ export type ContextPolicy = {
23
+ mode: "pi" | "remote-compaction" | "remote-windows";
24
+ remoteCompaction: {
25
+ model: string | null;
26
+ inputSource: RemoteV2ContextSource;
27
+ allowContinuityBreak: boolean;
28
+ apis: string[];
29
+ };
30
+ nativeFallback: Omit<NativeFallbackConfig, "model"> & { model: string | null };
31
+ remoteWindows: {
32
+ leaveManagedMode: LeaveManagedModePolicy;
33
+ reminderThresholdPercent: number;
34
+ };
35
+ };
36
+ export type AutoModePolicy = Omit<AutoModeConfig, "enabled" | "models" | "reviewerModel" | "classifier"> & {
37
+ available: boolean;
38
+ reviewerModel: string | null;
39
+ classifier: Omit<AutoModeConfig["classifier"], "model"> & { model: string | null };
40
+ };
41
+ export type EffectiveToolkitPolicy = {
42
+ context: ContextPolicy;
43
+ webSearch: { route: SearchRoute };
44
+ imageGeneration: { enabled: boolean; defaultModel: string; allowedModels: string[] };
45
+ autoMode: AutoModePolicy;
46
+ compatibility: { transport: "standard" | "codex-gateway" };
47
+ diagnostics: DiagnosticsConfig;
48
+ };
49
+ export type ConfigOrigin = {
50
+ kind: "builtin" | "defaults" | "model" | "legacy";
51
+ path?: string;
52
+ source?: string;
53
+ };
54
+ export type ConfigIssue = {
55
+ severity: "error" | "warning";
56
+ /** Controlled code; do not include raw input values in messages. */
57
+ code: string;
58
+ path: string;
59
+ feature: ConfigFeature | "document";
60
+ /** Issues for a different model do not invalidate the selected model. */
61
+ modelKey?: string;
62
+ };
63
+ export type ResolvedToolkitPolicy = {
64
+ policy: EffectiveToolkitPolicy;
65
+ origins: Record<string, ConfigOrigin>;
66
+ issues: ConfigIssue[];
67
+ invalidFeatures: ConfigFeature[];
68
+ };
69
+ export type ConfigDocumentSnapshot = {
70
+ format: "legacy" | "v2" | "missing" | "invalid";
71
+ configPath: string;
72
+ /** In-memory only; never render arbitrary raw configuration values. */
73
+ raw?: unknown;
74
+ issues: ConfigIssue[];
75
+ };
76
+ export const CONFIG_FEATURES: readonly ConfigFeature[] = [
77
+ "context", "webSearch", "imageGeneration", "autoMode", "compatibility", "diagnostics",
78
+ ];
79
+
80
+ /** Independent per-operation defaults, preserving the existing shipped policy. */
81
+ export function createPolicyDefaults(): EffectiveToolkitPolicy {
82
+ const context = DEFAULT_COMPACTION_CONFIG;
83
+ const auto = structuredClone(DEFAULT_AUTO_MODE_CONFIG);
84
+ const { enabled: _enabled, models: _models, reviewerModel, classifier, ...autoOptions } = auto;
85
+ return {
86
+ context: {
87
+ mode: "remote-compaction",
88
+ remoteCompaction: {
89
+ model: null,
90
+ inputSource: context.remoteV2ContextSource,
91
+ allowContinuityBreak: context.allowCompactionContinuityBreak,
92
+ apis: [...context.responsesApis],
93
+ },
94
+ nativeFallback: { ...context.nativeFallback, model: null },
95
+ remoteWindows: {
96
+ leaveManagedMode: context.leaveManagedMode,
97
+ reminderThresholdPercent: context.contextReminderThresholdPercent,
98
+ },
99
+ },
100
+ webSearch: { route: "unmanaged" },
101
+ imageGeneration: {
102
+ enabled: false,
103
+ defaultModel: DEFAULT_IMAGE_GENERATION_MODEL,
104
+ allowedModels: [DEFAULT_IMAGE_GENERATION_MODEL],
105
+ },
106
+ autoMode: {
107
+ ...autoOptions,
108
+ available: false,
109
+ reviewerModel: reviewerModel ?? null,
110
+ classifier: { ...classifier, model: classifier.model ?? null },
111
+ },
112
+ compatibility: { transport: "standard" },
113
+ diagnostics: {
114
+ level: "info",
115
+ notifyOnLoad: context.notifyOnLoad,
116
+ captureRequests: context.logProviderPayloads,
117
+ captureResponses: context.logCompactResponses,
118
+ redactSensitiveData: context.redactSensitiveData,
119
+ artifactRoot: context.artifactRoot,
120
+ },
121
+ };
122
+ }