pi-openai-toolkit 0.7.2 → 0.9.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.
@@ -12,6 +12,7 @@ import type { CompactionConfig } from "./types";
12
12
  export { parseModelSpec } from "./runtime";
13
13
 
14
14
  export type NativeFallbackFailureReason =
15
+ | "disabled"
15
16
  | "no-model-configured"
16
17
  | "invalid-model-spec"
17
18
  | "model-not-found"
@@ -53,23 +54,35 @@ function toErrorMessage(error: unknown): string {
53
54
  }
54
55
 
55
56
  /**
56
- * Run pi's native compaction method with the user-configured compaction model.
57
+ * Run pi's native compaction method with a caller-resolved summary model.
57
58
  *
58
- * Only handles the "configured model differs from the current one" case: when no model
59
- * is configured (or it equals the current model), the caller should return undefined from
60
- * session_before_compact so pi runs the same native path itself, keeping its internal
59
+ * The two compaction chains stay separate: the caller passes `remoteCompactModel` only when a
60
+ * remote v2 request was actually attempted and failed, and `nativeFallback.model` only when the
61
+ * active model cannot use remote v2 at all. This function never inspects `remoteCompactModel`
62
+ * itself, so a model that cannot compact remotely can never inherit the remote producer.
63
+ *
64
+ * Only the "resolved model differs from the current one" case is handled here: when the feature
65
+ * is disabled, nothing is resolved, or it equals the current model, the caller returns undefined
66
+ * from session_before_compact so pi runs the same native path itself, keeping its internal
61
67
  * streamFn/thinkingLevel wiring.
62
68
  */
63
69
  export async function runNativeFallbackCompaction(args: {
64
70
  ctx: ExtensionContext;
65
71
  event: SessionBeforeCompactEvent;
66
72
  config: CompactionConfig;
73
+ /** Model spec supplied by the caller for this specific fallback reason. */
74
+ modelSpec?: string;
67
75
  compactFn?: NativeCompactFn;
68
76
  }): Promise<NativeFallbackResult> {
69
77
  const { ctx, event, config } = args;
70
78
  const compactFn = args.compactFn ?? compact;
79
+ const nativeFallback = config.nativeFallback;
80
+
81
+ if (!nativeFallback.enabled) {
82
+ return { ok: false, reason: "disabled" };
83
+ }
71
84
 
72
- const spec = config.model?.trim();
85
+ const spec = args.modelSpec?.trim() ?? "";
73
86
  if (!spec) {
74
87
  return { ok: false, reason: "no-model-configured" };
75
88
  }
@@ -106,7 +119,7 @@ export async function runNativeFallbackCompaction(args: {
106
119
  mergeProviderHeaders(auth.headers),
107
120
  event.customInstructions,
108
121
  event.signal,
109
- config.thinkingLevel,
122
+ nativeFallback.thinkingLevel,
110
123
  undefined,
111
124
  auth.env,
112
125
  );
@@ -1,5 +1,5 @@
1
1
  import { writeDebugArtifact } from "./debug";
2
- import { mergeProviderHeaders } from "./provider-headers";
2
+ import { buildResponsesRequestHeaders } from "./responses-headers";
3
3
  import type { NativeCompactionRuntime } from "./runtime";
4
4
  import type { NativeCompactionRequestBody, ResponsesInputItem } from "./serializer";
5
5
  import type { ArtifactContext, CompactionConfig } from "./types";
@@ -114,57 +114,13 @@ function normalizeResponseTimestamp(value: unknown): string | undefined {
114
114
  return Number.isNaN(parsed) ? trimmed : new Date(parsed).toISOString();
115
115
  }
116
116
 
117
- function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
118
- const parts = token.split(".");
119
- if (parts.length !== 3) {
120
- return undefined;
121
- }
122
-
123
- try {
124
- const payloadText = Buffer.from(parts[1]!, "base64url").toString("utf8");
125
- const payload = JSON.parse(payloadText);
126
- return isRecord(payload) ? payload : undefined;
127
- } catch {
128
- return undefined;
129
- }
130
- }
131
-
132
- function extractCodexAccountId(token: string): string | undefined {
133
- const payload = decodeJwtPayload(token);
134
- const authClaims = payload?.["https://api.openai.com/auth"];
135
- if (!isRecord(authClaims)) {
136
- return undefined;
137
- }
138
-
139
- const accountId = authClaims.chatgpt_account_id;
140
- return isNonEmptyString(accountId) ? accountId.trim() : undefined;
141
- }
142
-
143
- function buildCodexUserAgent(): string {
144
- const platform = typeof process !== "undefined" ? process.platform : "browser";
145
- const arch = typeof process !== "undefined" ? process.arch : "unknown";
146
- return `pi (${platform}; ${arch})`;
147
- }
148
-
149
117
  function toHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
150
- const headers = new Headers(mergeProviderHeaders(runtime.currentModel.headers, runtime.headers));
151
- headers.set("accept", SSE_CONTENT_TYPE);
152
- headers.set("content-type", JSON_CONTENT_TYPE);
153
- if (!headers.has("authorization")) {
154
- headers.set("authorization", `Bearer ${runtime.apiKey}`);
155
- }
156
-
157
- if (runtime.api === "openai-codex-responses") {
158
- const accountId = extractCodexAccountId(runtime.apiKey);
159
- if (accountId) {
160
- headers.set("chatgpt-account-id", accountId);
161
- }
162
- headers.set("originator", "pi");
163
- headers.set("user-agent", buildCodexUserAgent());
164
- headers.set("openai-beta", "responses=experimental");
165
- }
166
-
167
- return Object.fromEntries(headers.entries());
118
+ return Object.fromEntries(
119
+ buildResponsesRequestHeaders(runtime, {
120
+ accept: SSE_CONTENT_TYPE,
121
+ contentType: JSON_CONTENT_TYPE,
122
+ }).entries(),
123
+ );
168
124
  }
169
125
 
170
126
  export function buildRemoteV2CompactionRequest(
@@ -0,0 +1,67 @@
1
+ import type { ProviderHeaders } from "@earendil-works/pi-ai";
2
+ import { mergeProviderHeaders } from "./provider-headers";
3
+
4
+ export type ResponsesHeaderRuntime = {
5
+ api: string;
6
+ apiKey: string;
7
+ headers?: ProviderHeaders;
8
+ currentModel: {
9
+ headers?: ProviderHeaders;
10
+ };
11
+ };
12
+
13
+ function isRecord(value: unknown): value is Record<string, unknown> {
14
+ return !!value && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+
17
+ function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
18
+ const parts = token.split(".");
19
+ if (parts.length !== 3) return undefined;
20
+ try {
21
+ const payload = JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf8")) as unknown;
22
+ return isRecord(payload) ? payload : undefined;
23
+ } catch {
24
+ return undefined;
25
+ }
26
+ }
27
+
28
+ function extractCodexAccountId(token: string): string | undefined {
29
+ const authClaims = decodeJwtPayload(token)?.["https://api.openai.com/auth"];
30
+ if (!isRecord(authClaims)) return undefined;
31
+ const accountId = authClaims.chatgpt_account_id;
32
+ return typeof accountId === "string" && accountId.trim() ? accountId.trim() : undefined;
33
+ }
34
+
35
+ function buildCodexUserAgent(): string {
36
+ const platform = typeof process !== "undefined" ? process.platform : "browser";
37
+ const arch = typeof process !== "undefined" ? process.arch : "unknown";
38
+ return `pi (${platform}; ${arch})`;
39
+ }
40
+
41
+ export function buildResponsesRequestHeaders(
42
+ runtime: ResponsesHeaderRuntime,
43
+ options: { accept: string; contentType?: string },
44
+ ): Headers {
45
+ const headers = new Headers(
46
+ mergeProviderHeaders(runtime.currentModel.headers, runtime.headers),
47
+ );
48
+ headers.set("accept", options.accept);
49
+ headers.set("content-type", options.contentType ?? "application/json");
50
+ if (!headers.has("authorization")) {
51
+ headers.set("authorization", `Bearer ${runtime.apiKey}`);
52
+ }
53
+
54
+ if (runtime.api === "openai-codex-responses") {
55
+ const accountId = extractCodexAccountId(runtime.apiKey);
56
+ if (accountId) headers.set("chatgpt-account-id", accountId);
57
+ headers.set("originator", "pi");
58
+ headers.set("user-agent", buildCodexUserAgent());
59
+ headers.set("openai-beta", "responses=experimental");
60
+ }
61
+
62
+ return headers;
63
+ }
64
+
65
+ export const _responsesHeadersTest = {
66
+ extractCodexAccountId,
67
+ };
package/src/runtime.ts CHANGED
@@ -7,8 +7,9 @@ const CODEX_RESPONSES_PATH = "codex/responses";
7
7
  const OPENAI_COMPACT_PATH = "responses/compact";
8
8
  const CODEX_COMPACT_PATH = "codex/responses/compact";
9
9
 
10
- type ResponsesCompactApi = (typeof RESPONSES_COMPACT_CAPABLE_APIS)[number];
10
+ export type ResponsesApi = (typeof RESPONSES_COMPACT_CAPABLE_APIS)[number];
11
11
 
12
+ type ResponsesCompactApi = ResponsesApi;
12
13
  type RuntimeModel = Model<Api>;
13
14
 
14
15
  export type ParsedModelSpec = {
@@ -35,6 +36,8 @@ export type NativeCompactionSupportOptions = {
35
36
  responsesApis?: readonly string[];
36
37
  };
37
38
 
39
+ export type ResponsesSupportOptions = NativeCompactionSupportOptions;
40
+
38
41
  export type ResponsesCompatibleRequestPayload = {
39
42
  model: string;
40
43
  input: unknown[];
@@ -42,19 +45,22 @@ export type ResponsesCompatibleRequestPayload = {
42
45
  [key: string]: unknown;
43
46
  };
44
47
 
45
- export type NativeCompactionRuntime = {
48
+ export type ResponsesRuntime = {
46
49
  provider: string;
47
- api: ResponsesCompactApi;
50
+ api: ResponsesApi;
48
51
  model: string;
49
52
  baseUrl: string;
50
53
  apiKey: string;
51
54
  headers?: ProviderHeaders;
52
55
  responsesPath: string;
53
56
  responsesUrl: string;
57
+ currentModel: RuntimeModel;
58
+ };
59
+
60
+ export type NativeCompactionRuntime = ResponsesRuntime & {
54
61
  compactPath: string;
55
62
  compactUrl: string;
56
63
  payload?: ResponsesCompatibleRequestPayload;
57
- currentModel: RuntimeModel;
58
64
  };
59
65
 
60
66
  export type NativeCompactionEnvironmentFailure = {
@@ -77,6 +83,15 @@ export type NativeCompactionEnvironmentResolution =
77
83
  | NativeCompactionEnvironmentFailure
78
84
  | NativeCompactionEnvironmentSuccess;
79
85
 
86
+ export type ResponsesEnvironmentSuccess = {
87
+ ok: true;
88
+ runtime: ResponsesRuntime;
89
+ };
90
+
91
+ export type ResponsesEnvironmentResolution =
92
+ | NativeCompactionEnvironmentFailure
93
+ | ResponsesEnvironmentSuccess;
94
+
80
95
  export type RemoteCompactionExecution = {
81
96
  /** Active session model. Its identity owns replay matching and is never mutated. */
82
97
  consumer: NativeCompactionRuntime;
@@ -327,6 +342,30 @@ async function resolveNativeCompactionEnvironmentForModel(
327
342
  };
328
343
  }
329
344
 
345
+ export async function resolveResponsesEnvironment(
346
+ ctx: ExtensionContext,
347
+ options: ResponsesSupportOptions = {},
348
+ ): Promise<ResponsesEnvironmentResolution> {
349
+ const resolution = await resolveNativeCompactionEnvironmentForModel(ctx, ctx.model, options);
350
+ if (!resolution.ok) return resolution;
351
+
352
+ const runtime = resolution.runtime;
353
+ return {
354
+ ok: true,
355
+ runtime: {
356
+ provider: runtime.provider,
357
+ api: runtime.api,
358
+ model: runtime.model,
359
+ baseUrl: runtime.baseUrl,
360
+ apiKey: runtime.apiKey,
361
+ headers: runtime.headers,
362
+ responsesPath: runtime.responsesPath,
363
+ responsesUrl: runtime.responsesUrl,
364
+ currentModel: runtime.currentModel,
365
+ },
366
+ };
367
+ }
368
+
330
369
  export async function resolveNativeCompactionEnvironment(
331
370
  ctx: ExtensionContext,
332
371
  options: NativeCompactionSupportOptions = {},
package/src/types.ts CHANGED
@@ -4,6 +4,8 @@ import type { CompactionEntry, CompactionResult, ExtensionContext, SessionEntry
4
4
  export const TOOLKIT_ID = "pi-openai-toolkit";
5
5
  export const COMPACTION_EXTENSION_ID = `${TOOLKIT_ID}:compaction`;
6
6
  export const WEB_SEARCH_EXTENSION_ID = `${TOOLKIT_ID}:web-search`;
7
+ export const IMAGE_GENERATION_EXTENSION_ID = `${TOOLKIT_ID}:image-generation`;
8
+ export const AUTO_MODE_EXTENSION_ID = `${TOOLKIT_ID}:auto-mode`;
7
9
  export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
8
10
  export const REDACTED_VALUE = "[REDACTED]";
9
11
  /**
@@ -38,6 +40,20 @@ export type DebugArtifactKind =
38
40
 
39
41
  export type CompactionContinuationMode = "inline" | "followUp" | "off";
40
42
 
43
+ /** Native-method fallback compaction: which model runs pi's compact() and how deeply. */
44
+ export type NativeFallbackConfig = {
45
+ enabled: boolean;
46
+ /**
47
+ * "provider/model-id" used for native-method fallback compaction when the active model
48
+ * cannot use remote v2 at all. A failed remote request is instead summarized by
49
+ * `remoteCompactModel`; the two sources never substitute for each other. Unset means the
50
+ * active model via pi's default path.
51
+ */
52
+ model?: string;
53
+ /** Thinking level passed to pi's native compact() when the fallback model runs. */
54
+ thinkingLevel: ThinkingLevel;
55
+ };
56
+
41
57
  export type AutoCompactionConfig = {
42
58
  enabled: boolean;
43
59
  continuation: CompactionContinuationMode;
@@ -57,13 +73,8 @@ export type CompactionConfig = {
57
73
  * remains the checkpoint consumer and continues handling normal requests.
58
74
  */
59
75
  remoteCompactModel?: string;
60
- /**
61
- * "provider/model-id" used for native-method fallback compaction (non-Responses APIs,
62
- * or when the compact endpoint fails). Unset = current model via pi's default path.
63
- */
64
- model?: string;
65
- /** Thinking level passed to pi's native compact() when the fallback model runs. */
66
- thinkingLevel: ThinkingLevel;
76
+ /** Native-method fallback compaction policy (non-Responses APIs, or when the compact endpoint fails). */
77
+ nativeFallback: NativeFallbackConfig;
67
78
  /** Threshold-triggered compaction and same-tool-loop continuation policy. */
68
79
  autoCompaction: AutoCompactionConfig;
69
80
  /** Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction. */
@@ -82,9 +93,34 @@ export type WebSearchConfig = {
82
93
  models: string[];
83
94
  };
84
95
 
96
+ export type ImageGenerationConfig = {
97
+ enabled: boolean;
98
+ };
99
+
100
+ /** `side-effect` reviews bash/write/edit plus extras; `all` reviews every tool call. */
101
+ export type AutoModeGate = "side-effect" | "all";
102
+
103
+ export type AutoModeConfig = {
104
+ enabled: boolean;
105
+ /** Exact provider/model keys whose sessions may engage auto mode. */
106
+ models: string[];
107
+ /** "provider/model-id" that approves gated tool calls while auto mode is engaged. */
108
+ reviewerModel?: string;
109
+ gate: AutoModeGate;
110
+ /** Extra tool names reviewed on top of the side-effect default set. */
111
+ extraTools: string[];
112
+ /** Reviewer deadline in ms; expiry degrades to human confirmation or fail-closed. */
113
+ timeoutMs: number;
114
+ };
115
+
116
+ export const REVIEWER_TIMEOUT_MIN_MS = 1_000;
117
+ export const REVIEWER_TIMEOUT_MAX_MS = 120_000;
118
+
85
119
  export type ToolkitConfig = {
86
120
  compaction: CompactionConfig;
87
121
  webSearch: WebSearchConfig;
122
+ imageGeneration: ImageGenerationConfig;
123
+ autoMode: AutoModeConfig;
88
124
  };
89
125
 
90
126
  export type LoadedToolkitConfig = {
@@ -417,12 +453,17 @@ export const DEFAULT_AUTO_COMPACTION_CONFIG: AutoCompactionConfig = {
417
453
  reserveTokens: undefined,
418
454
  };
419
455
 
456
+ export const DEFAULT_NATIVE_FALLBACK_CONFIG: NativeFallbackConfig = {
457
+ enabled: true,
458
+ model: undefined,
459
+ thinkingLevel: "off",
460
+ };
461
+
420
462
  export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
421
463
  enabled: true,
422
464
  allowCompactionContinuityBreak: false,
423
465
  remoteCompactModel: undefined,
424
- model: undefined,
425
- thinkingLevel: "off",
466
+ nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
426
467
  autoCompaction: { ...DEFAULT_AUTO_COMPACTION_CONFIG },
427
468
  responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
428
469
  notifyOnLoad: false,
@@ -438,7 +479,22 @@ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchConfig = {
438
479
  models: [],
439
480
  };
440
481
 
482
+ export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
483
+ enabled: false,
484
+ };
485
+
486
+ export const DEFAULT_AUTO_MODE_CONFIG: AutoModeConfig = {
487
+ enabled: true,
488
+ models: [],
489
+ reviewerModel: undefined,
490
+ gate: "side-effect",
491
+ extraTools: [],
492
+ timeoutMs: 30_000,
493
+ };
494
+
441
495
  export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
442
496
  compaction: DEFAULT_COMPACTION_CONFIG,
443
497
  webSearch: DEFAULT_WEB_SEARCH_CONFIG,
498
+ imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
499
+ autoMode: DEFAULT_AUTO_MODE_CONFIG,
444
500
  };
@@ -1,3 +1,4 @@
1
+ import { getExactModelKey, isExactModelAllowed } from "../model-scope";
1
2
  import type { WebSearchConfig } from "../types";
2
3
 
3
4
  export const WEB_SEARCH_CAPABLE_APIS = ["openai-responses", "openai-codex-responses"] as const;
@@ -28,20 +29,17 @@ export type WebSearchPayloadTransform = {
28
29
  };
29
30
 
30
31
  export function getWebSearchModelKey(model: WebSearchModel | undefined): string | undefined {
31
- if (!model?.provider || !model.id) return undefined;
32
- return `${model.provider}/${model.id}`;
32
+ return getExactModelKey(model);
33
33
  }
34
34
 
35
35
  export function isWebSearchEnabledForModel(
36
36
  model: WebSearchModel | undefined,
37
37
  config: WebSearchConfig,
38
38
  ): boolean {
39
- const modelKey = getWebSearchModelKey(model);
40
39
  return (
41
40
  config.enabled &&
42
41
  typeof model?.api === "string" &&
43
42
  (WEB_SEARCH_CAPABLE_APIS as readonly string[]).includes(model.api) &&
44
- modelKey !== undefined &&
45
- config.models.includes(modelKey)
43
+ isExactModelAllowed(model, config.models)
46
44
  );
47
45
  }
@@ -14,6 +14,8 @@
14
14
  "include": [
15
15
  "extensions/compaction.ts",
16
16
  "extensions/web-search.ts",
17
+ "extensions/image-generation.ts",
18
+ "extensions/auto-mode.ts",
17
19
  "src/compact-client.ts",
18
20
  "src/config.ts",
19
21
  "src/debug.ts",
@@ -21,9 +23,24 @@
21
23
  "src/deferred-tool-carryover.ts",
22
24
  "src/inline-compaction.ts",
23
25
  "src/extension-runtime.ts",
26
+ "src/image-generation/artifacts.ts",
27
+ "src/image-generation/client.ts",
28
+ "src/image-generation/eligibility.ts",
29
+ "src/image-generation/extension.ts",
30
+ "src/image-generation/protocol.ts",
31
+ "src/image-generation/references.ts",
32
+ "src/image-generation/render.ts",
33
+ "src/image-generation/service.ts",
34
+ "src/image-generation/types.ts",
35
+ "src/auto-mode/extension.ts",
36
+ "src/auto-mode/policy.ts",
37
+ "src/auto-mode/reviewer.ts",
38
+ "src/auto-mode/types.ts",
39
+ "src/model-scope.ts",
24
40
  "src/native-fallback.ts",
25
41
  "src/payload-rewrite.ts",
26
42
  "src/provider-headers.ts",
43
+ "src/responses-headers.ts",
27
44
  "src/remote-v2-client.ts",
28
45
  "src/request-context-cache.ts",
29
46
  "src/runtime.ts",