pi-openai-toolkit 0.14.4 → 0.14.6

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.
Files changed (43) hide show
  1. package/README.md +119 -147
  2. package/README.zh.md +119 -151
  3. package/docs/internals.md +42 -21
  4. package/package.json +127 -127
  5. package/src/auto-mode/classifier.ts +165 -165
  6. package/src/auto-mode/policy.ts +146 -146
  7. package/src/auto-mode/prompt.ts +279 -279
  8. package/src/auto-mode/reviewer.ts +325 -325
  9. package/src/auto-mode/transcript.ts +133 -133
  10. package/src/auto-mode/types.ts +137 -137
  11. package/src/codex-astra/effort-planner.ts +175 -175
  12. package/src/codex-astra/extension.ts +122 -122
  13. package/src/config.ts +621 -592
  14. package/src/context-management/codex-provider.ts +260 -260
  15. package/src/context-management/history-notes.ts +357 -325
  16. package/src/context-management/messages.ts +112 -106
  17. package/src/context-management/namespace-tools.ts +426 -426
  18. package/src/context-management/tool-contract.ts +23 -23
  19. package/src/context-management/tools.ts +306 -311
  20. package/src/context-management/types.ts +168 -161
  21. package/src/context-management/window-budget.ts +83 -83
  22. package/src/context-management/window-manager.ts +60 -51
  23. package/src/context-management/window-request.ts +125 -125
  24. package/src/debug.ts +1 -1
  25. package/src/extension-runtime.ts +34 -9
  26. package/src/image-generation/artifacts.ts +237 -237
  27. package/src/image-generation/client.ts +248 -248
  28. package/src/image-generation/eligibility.ts +18 -18
  29. package/src/image-generation/extension.ts +19 -2
  30. package/src/image-generation/protocol.ts +473 -405
  31. package/src/image-generation/references.ts +194 -194
  32. package/src/image-generation/render.ts +252 -252
  33. package/src/image-generation/service.ts +249 -243
  34. package/src/image-generation/types.ts +189 -184
  35. package/src/model-scope.ts +17 -17
  36. package/src/provider-headers.ts +23 -23
  37. package/src/responses-headers.ts +173 -108
  38. package/src/runtime.ts +513 -477
  39. package/src/types.ts +11 -0
  40. package/src/web-search/extension.ts +80 -80
  41. package/src/web-search/payload.ts +96 -96
  42. package/src/web-search/prompt.ts +20 -20
  43. package/src/web-search/types.ts +45 -45
package/src/runtime.ts CHANGED
@@ -1,477 +1,513 @@
1
- import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
2
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { RESPONSES_COMPACT_CAPABLE_APIS } from "./types";
4
-
5
- const OPENAI_RESPONSES_PATH = "responses";
6
- const CODEX_RESPONSES_PATH = "codex/responses";
7
- const OPENAI_COMPACT_PATH = "responses/compact";
8
- const CODEX_COMPACT_PATH = "codex/responses/compact";
9
-
10
- export type ResponsesApi = (typeof RESPONSES_COMPACT_CAPABLE_APIS)[number];
11
-
12
- type ResponsesCompactApi = ResponsesApi;
13
- type RuntimeModel = Model<Api>;
14
-
15
- export type ParsedModelSpec = {
16
- provider: string;
17
- modelId: string;
18
- };
19
-
20
- export type NativeCompactionFailureReason =
21
- | "disabled"
22
- | "missing-model"
23
- | "invalid-model-spec"
24
- | "model-not-found"
25
- | "unsupported-api"
26
- | "missing-base-url"
27
- | "missing-api-key"
28
- | "auth-resolution-failed"
29
- | "unsupported-payload"
30
- | "payload-model-mismatch"
31
- | "base-url-mismatch";
32
-
33
- export type NativeCompactionSupportOptions = {
34
- enabled?: boolean;
35
- /** Which Responses APIs should use the compact endpoint; defaults to all capable APIs. */
36
- responsesApis?: readonly string[];
37
- };
38
-
39
- export type ResponsesSupportOptions = NativeCompactionSupportOptions;
40
-
41
- export type ResponsesCompatibleRequestPayload = {
42
- model: string;
43
- input: unknown[];
44
- instructions?: unknown;
45
- [key: string]: unknown;
46
- };
47
-
48
- export type ResponsesRuntime = {
49
- provider: string;
50
- api: ResponsesApi;
51
- model: string;
52
- baseUrl: string;
53
- apiKey: string;
54
- headers?: ProviderHeaders;
55
- responsesPath: string;
56
- responsesUrl: string;
57
- /**
58
- * Conversation identity for Codex prompt-cache affinity headers. Synthetic
59
- * compaction/review requests must present the same id the live transcript
60
- * requests used or the backend treats them as a new conversation.
61
- */
62
- sessionId?: string;
63
- currentModel: RuntimeModel;
64
- };
65
-
66
- export type NativeCompactionRuntime = ResponsesRuntime & {
67
- compactPath: string;
68
- compactUrl: string;
69
- payload?: ResponsesCompatibleRequestPayload;
70
- };
71
-
72
- export type NativeCompactionEnvironmentFailure = {
73
- ok: false;
74
- reason: NativeCompactionFailureReason;
75
- provider?: string;
76
- api?: string;
77
- model?: string;
78
- baseUrl?: string;
79
- modelSpec?: string;
80
- errorMessage?: string;
81
- };
82
-
83
- export type NativeCompactionEnvironmentSuccess = {
84
- ok: true;
85
- runtime: NativeCompactionRuntime;
86
- };
87
-
88
- export type NativeCompactionEnvironmentResolution =
89
- | NativeCompactionEnvironmentFailure
90
- | NativeCompactionEnvironmentSuccess;
91
-
92
- export type ResponsesEnvironmentSuccess = {
93
- ok: true;
94
- runtime: ResponsesRuntime;
95
- };
96
-
97
- export type ResponsesEnvironmentResolution =
98
- | NativeCompactionEnvironmentFailure
99
- | ResponsesEnvironmentSuccess;
100
-
101
- export type RemoteCompactionExecution = {
102
- /** Active session model. Its identity owns replay matching and is never mutated. */
103
- consumer: NativeCompactionRuntime;
104
- /** Model used only for the synthetic remote_compaction_v2 request. */
105
- compactor: NativeCompactionRuntime;
106
- };
107
-
108
- export type RemoteCompactionExecutionSuccess = {
109
- ok: true;
110
- execution: RemoteCompactionExecution;
111
- };
112
-
113
- export type RemoteCompactionExecutionResolution =
114
- | NativeCompactionEnvironmentFailure
115
- | RemoteCompactionExecutionSuccess;
116
-
117
- type ResolvedRequestAuth =
118
- | {
119
- ok: true;
120
- apiKey?: string;
121
- headers?: ProviderHeaders;
122
- baseUrl?: string;
123
- }
124
- | { ok: false; error: string };
125
-
126
- function normalizeConfiguredApis(values: readonly string[] | undefined): Set<string> {
127
- if (values === undefined) {
128
- return new Set(RESPONSES_COMPACT_CAPABLE_APIS);
129
- }
130
- return new Set(values.map((value) => value.trim()).filter((value) => value.length > 0));
131
- }
132
-
133
- /** Parse "provider/model-id" (model ids may themselves contain slashes). */
134
- export function parseModelSpec(spec: string): ParsedModelSpec | undefined {
135
- const trimmed = spec.trim();
136
- const separatorIndex = trimmed.indexOf("/");
137
- if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
138
- return undefined;
139
- }
140
-
141
- const provider = trimmed.slice(0, separatorIndex).trim();
142
- const modelId = trimmed.slice(separatorIndex + 1).trim();
143
- if (!provider || !modelId) {
144
- return undefined;
145
- }
146
-
147
- return { provider, modelId };
148
- }
149
-
150
- export function normalizeBaseUrl(baseUrl: string | undefined | null): string | undefined {
151
- const normalized = baseUrl?.trim().replace(/\/+$/, "");
152
- return normalized ? normalized : undefined;
153
- }
154
-
155
- function buildOpenAIResponsesUrl(baseUrl: string): string {
156
- const normalized = normalizeBaseUrl(baseUrl) ?? baseUrl;
157
- return normalized.endsWith("/responses") ? normalized : `${normalized}/${OPENAI_RESPONSES_PATH}`;
158
- }
159
-
160
- function buildCodexResponsesUrl(baseUrl: string): string {
161
- const normalized = normalizeBaseUrl(baseUrl) ?? baseUrl;
162
- if (normalized.endsWith("/codex/responses")) {
163
- return normalized;
164
- }
165
- if (normalized.endsWith("/codex")) {
166
- return `${normalized}/responses`;
167
- }
168
- return `${normalized}/${CODEX_RESPONSES_PATH}`;
169
- }
170
-
171
- function buildOpenAICompactUrl(baseUrl: string): string {
172
- return `${buildOpenAIResponsesUrl(baseUrl)}/compact`;
173
- }
174
-
175
- function buildCodexCompactUrl(baseUrl: string): string {
176
- return `${buildCodexResponsesUrl(baseUrl)}/compact`;
177
- }
178
-
179
- export function buildResponsesUrl(baseUrl: string, api: ResponsesCompactApi): string {
180
- return api === "openai-codex-responses" ? buildCodexResponsesUrl(baseUrl) : buildOpenAIResponsesUrl(baseUrl);
181
- }
182
-
183
- export function buildResponsesPath(api: ResponsesCompactApi): string {
184
- return api === "openai-codex-responses" ? CODEX_RESPONSES_PATH : OPENAI_RESPONSES_PATH;
185
- }
186
-
187
- export function buildCompactUrl(baseUrl: string, api: ResponsesCompactApi): string {
188
- return api === "openai-codex-responses" ? buildCodexCompactUrl(baseUrl) : buildOpenAICompactUrl(baseUrl);
189
- }
190
-
191
- export function buildCompactPath(api: ResponsesCompactApi): string {
192
- return api === "openai-codex-responses" ? CODEX_COMPACT_PATH : OPENAI_COMPACT_PATH;
193
- }
194
-
195
- async function resolveRequestAuth(ctx: ExtensionContext, model: RuntimeModel): Promise<ResolvedRequestAuth> {
196
- const modelRegistry = ctx.modelRegistry as {
197
- getApiKeyAndHeaders?: (currentModel: RuntimeModel) => Promise<ResolvedRequestAuth>;
198
- };
199
-
200
- if (typeof modelRegistry.getApiKeyAndHeaders !== "function") {
201
- return { ok: true };
202
- }
203
-
204
- return modelRegistry.getApiKeyAndHeaders(model);
205
- }
206
-
207
- export function isSupportedApi(api: string): api is ResponsesCompactApi {
208
- return (RESPONSES_COMPACT_CAPABLE_APIS as readonly string[]).includes(api);
209
- }
210
-
211
- export function isResponsesCompatiblePayload(payload: unknown): payload is ResponsesCompatibleRequestPayload {
212
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
213
- return false;
214
- }
215
-
216
- const candidate = payload as Record<string, unknown>;
217
- return typeof candidate.model === "string" && Array.isArray(candidate.input);
218
- }
219
-
220
- export function getRuntimeModelDescriptor(model: RuntimeModel | undefined): {
221
- provider?: string;
222
- api?: string;
223
- model?: string;
224
- baseUrl?: string;
225
- } {
226
- if (!model) {
227
- return {};
228
- }
229
-
230
- return {
231
- provider: model.provider,
232
- api: model.api,
233
- model: model.id,
234
- baseUrl: normalizeBaseUrl(model.baseUrl),
235
- };
236
- }
237
-
238
- /** Best-effort conversation id; header emission then simply skips the affinity headers. */
239
- export function resolveRuntimeSessionId(ctx: ExtensionContext): string | undefined {
240
- try {
241
- return ctx.sessionManager.getSessionId();
242
- } catch {
243
- return undefined;
244
- }
245
- }
246
-
247
- async function resolveNativeCompactionEnvironmentForModel(
248
- ctx: ExtensionContext,
249
- currentModel: RuntimeModel | undefined,
250
- options: NativeCompactionSupportOptions,
251
- payload?: unknown,
252
- ): Promise<NativeCompactionEnvironmentResolution> {
253
- if (options.enabled === false) {
254
- return {
255
- ok: false,
256
- reason: "disabled",
257
- };
258
- }
259
-
260
- const descriptor = getRuntimeModelDescriptor(currentModel);
261
- if (!currentModel || !descriptor.provider || !descriptor.api || !descriptor.model) {
262
- return {
263
- ok: false,
264
- reason: "missing-model",
265
- ...descriptor,
266
- };
267
- }
268
-
269
- // The compact endpoint is selected purely by API family: any provider speaking
270
- // openai-responses/openai-codex-responses gets a native compact attempt and fails
271
- // open (to the fallback model or pi's default) when the endpoint is missing.
272
- const configuredApis = normalizeConfiguredApis(options.responsesApis);
273
- if (!configuredApis.has(descriptor.api) || !isSupportedApi(descriptor.api)) {
274
- return {
275
- ok: false,
276
- reason: "unsupported-api",
277
- ...descriptor,
278
- };
279
- }
280
-
281
- let requestPayload: ResponsesCompatibleRequestPayload | undefined;
282
- if (payload !== undefined) {
283
- if (!isResponsesCompatiblePayload(payload)) {
284
- return {
285
- ok: false,
286
- reason: "unsupported-payload",
287
- ...descriptor,
288
- };
289
- }
290
-
291
- if (payload.model !== descriptor.model) {
292
- return {
293
- ok: false,
294
- reason: "payload-model-mismatch",
295
- ...descriptor,
296
- };
297
- }
298
-
299
- requestPayload = payload;
300
- }
301
-
302
- let auth: ResolvedRequestAuth;
303
- try {
304
- auth = await resolveRequestAuth(ctx, currentModel);
305
- } catch (error) {
306
- return {
307
- ok: false,
308
- reason: "auth-resolution-failed",
309
- errorMessage: error instanceof Error ? error.message : String(error),
310
- ...descriptor,
311
- };
312
- }
313
-
314
- if (!auth.ok) {
315
- return {
316
- ok: false,
317
- reason: "auth-resolution-failed",
318
- errorMessage: auth.error,
319
- ...descriptor,
320
- };
321
- }
322
-
323
- const baseUrl = normalizeBaseUrl(auth.baseUrl) ?? descriptor.baseUrl;
324
- if (!baseUrl) {
325
- return {
326
- ok: false,
327
- reason: "missing-base-url",
328
- ...descriptor,
329
- };
330
- }
331
-
332
- if (!auth.apiKey) {
333
- return {
334
- ok: false,
335
- reason: "missing-api-key",
336
- ...descriptor,
337
- baseUrl,
338
- };
339
- }
340
-
341
- return {
342
- ok: true,
343
- runtime: {
344
- provider: descriptor.provider,
345
- api: descriptor.api,
346
- model: descriptor.model,
347
- baseUrl,
348
- apiKey: auth.apiKey,
349
- headers: auth.headers,
350
- responsesPath: buildResponsesPath(descriptor.api),
351
- responsesUrl: buildResponsesUrl(baseUrl, descriptor.api),
352
- compactPath: buildCompactPath(descriptor.api),
353
- compactUrl: buildCompactUrl(baseUrl, descriptor.api),
354
- sessionId: resolveRuntimeSessionId(ctx),
355
- payload: requestPayload,
356
- currentModel,
357
- },
358
- };
359
- }
360
-
361
- export async function resolveResponsesEnvironment(
362
- ctx: ExtensionContext,
363
- options: ResponsesSupportOptions = {},
364
- ): Promise<ResponsesEnvironmentResolution> {
365
- const resolution = await resolveNativeCompactionEnvironmentForModel(ctx, ctx.model, options);
366
- if (!resolution.ok) return resolution;
367
-
368
- const runtime = resolution.runtime;
369
- return {
370
- ok: true,
371
- runtime: {
372
- provider: runtime.provider,
373
- api: runtime.api,
374
- model: runtime.model,
375
- baseUrl: runtime.baseUrl,
376
- apiKey: runtime.apiKey,
377
- headers: runtime.headers,
378
- responsesPath: runtime.responsesPath,
379
- responsesUrl: runtime.responsesUrl,
380
- sessionId: runtime.sessionId,
381
- currentModel: runtime.currentModel,
382
- },
383
- };
384
- }
385
-
386
- export async function resolveNativeCompactionEnvironment(
387
- ctx: ExtensionContext,
388
- options: NativeCompactionSupportOptions = {},
389
- payload?: unknown,
390
- ): Promise<NativeCompactionEnvironmentResolution> {
391
- return resolveNativeCompactionEnvironmentForModel(ctx, ctx.model, options, payload);
392
- }
393
-
394
- export async function resolveRemoteCompactionExecution(
395
- ctx: ExtensionContext,
396
- options: NativeCompactionSupportOptions = {},
397
- remoteModelSpec?: string,
398
- ): Promise<RemoteCompactionExecutionResolution> {
399
- const consumerResolution = await resolveNativeCompactionEnvironment(ctx, options);
400
- if (!consumerResolution.ok) {
401
- return consumerResolution;
402
- }
403
-
404
- const spec = remoteModelSpec?.trim();
405
- if (!spec) {
406
- return {
407
- ok: true,
408
- execution: {
409
- consumer: consumerResolution.runtime,
410
- compactor: consumerResolution.runtime,
411
- },
412
- };
413
- }
414
-
415
- const parsed = parseModelSpec(spec);
416
- if (!parsed) {
417
- return {
418
- ok: false,
419
- reason: "invalid-model-spec",
420
- modelSpec: spec,
421
- };
422
- }
423
-
424
- const compactorModel = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
425
- if (!compactorModel) {
426
- return {
427
- ok: false,
428
- reason: "model-not-found",
429
- provider: parsed.provider,
430
- model: parsed.modelId,
431
- modelSpec: spec,
432
- };
433
- }
434
-
435
- const compactorResolution = await resolveNativeCompactionEnvironmentForModel(
436
- ctx,
437
- compactorModel,
438
- options,
439
- );
440
- if (!compactorResolution.ok) {
441
- return {
442
- ...compactorResolution,
443
- modelSpec: spec,
444
- };
445
- }
446
-
447
- const consumer = consumerResolution.runtime;
448
- const compactor = compactorResolution.runtime;
449
- if (compactor.baseUrl !== consumer.baseUrl) {
450
- return {
451
- ok: false,
452
- reason: "base-url-mismatch",
453
- provider: compactor.provider,
454
- api: compactor.api,
455
- model: compactor.model,
456
- baseUrl: compactor.baseUrl,
457
- modelSpec: spec,
458
- };
459
- }
460
-
461
- return {
462
- ok: true,
463
- execution: {
464
- consumer,
465
- compactor,
466
- },
467
- };
468
- }
469
-
470
- export async function getNativeCompactionRuntime(
471
- ctx: ExtensionContext,
472
- options: NativeCompactionSupportOptions = {},
473
- payload?: unknown,
474
- ): Promise<NativeCompactionRuntime | undefined> {
475
- const resolution = await resolveNativeCompactionEnvironment(ctx, options, payload);
476
- return resolution.ok ? resolution.runtime : undefined;
477
- }
1
+ import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { isExactModelAllowed } from "./model-scope";
4
+ import {
5
+ CODEX_AFFINITY_SCOPE,
6
+ type CodexAffinity,
7
+ } from "./responses-headers";
8
+ import { RESPONSES_COMPACT_CAPABLE_APIS } from "./types";
9
+
10
+ const OPENAI_RESPONSES_PATH = "responses";
11
+ const CODEX_RESPONSES_PATH = "codex/responses";
12
+ const OPENAI_COMPACT_PATH = "responses/compact";
13
+ const CODEX_COMPACT_PATH = "codex/responses/compact";
14
+
15
+ export type ResponsesApi = (typeof RESPONSES_COMPACT_CAPABLE_APIS)[number];
16
+
17
+ type ResponsesCompactApi = ResponsesApi;
18
+ type RuntimeModel = Model<Api>;
19
+
20
+ export type ParsedModelSpec = {
21
+ provider: string;
22
+ modelId: string;
23
+ };
24
+
25
+ export type NativeCompactionFailureReason =
26
+ | "disabled"
27
+ | "missing-model"
28
+ | "invalid-model-spec"
29
+ | "model-not-found"
30
+ | "unsupported-api"
31
+ | "missing-base-url"
32
+ | "missing-session-id"
33
+ | "missing-api-key"
34
+ | "auth-resolution-failed"
35
+ | "unsupported-payload"
36
+ | "payload-model-mismatch"
37
+ | "base-url-mismatch";
38
+
39
+ export type NativeCompactionSupportOptions = {
40
+ enabled?: boolean;
41
+ /** Which Responses APIs should use the compact endpoint; defaults to all capable APIs. */
42
+ responsesApis?: readonly string[];
43
+ /** Exact provider/model keys whose Responses traffic carries gateway Codex affinity metadata. */
44
+ codexGatewayModels?: readonly string[];
45
+ };
46
+
47
+ export type ResponsesSupportOptions = NativeCompactionSupportOptions;
48
+
49
+ export type ResponsesCompatibleRequestPayload = {
50
+ model: string;
51
+ input: unknown[];
52
+ instructions?: unknown;
53
+ [key: string]: unknown;
54
+ };
55
+
56
+ export type ResponsesRuntime = {
57
+ provider: string;
58
+ api: ResponsesApi;
59
+ model: string;
60
+ baseUrl: string;
61
+ apiKey: string;
62
+ headers?: ProviderHeaders;
63
+ responsesPath: string;
64
+ responsesUrl: string;
65
+ /**
66
+ * Conversation identity for Codex prompt-cache affinity headers. Synthetic
67
+ * compaction/review requests must present the same id the live transcript
68
+ * requests used or the backend treats them as a new conversation.
69
+ */
70
+ sessionId?: string;
71
+ /** Opt-in gateway Codex routing metadata for synthetic Responses/compact requests. */
72
+ codexAffinity?: CodexAffinity;
73
+ currentModel: RuntimeModel;
74
+ };
75
+
76
+ export type NativeCompactionRuntime = ResponsesRuntime & {
77
+ compactPath: string;
78
+ compactUrl: string;
79
+ payload?: ResponsesCompatibleRequestPayload;
80
+ };
81
+
82
+ export type NativeCompactionEnvironmentFailure = {
83
+ ok: false;
84
+ reason: NativeCompactionFailureReason;
85
+ provider?: string;
86
+ api?: string;
87
+ model?: string;
88
+ baseUrl?: string;
89
+ modelSpec?: string;
90
+ errorMessage?: string;
91
+ };
92
+
93
+ export type NativeCompactionEnvironmentSuccess = {
94
+ ok: true;
95
+ runtime: NativeCompactionRuntime;
96
+ };
97
+
98
+ export type NativeCompactionEnvironmentResolution =
99
+ | NativeCompactionEnvironmentFailure
100
+ | NativeCompactionEnvironmentSuccess;
101
+
102
+ export type ResponsesEnvironmentSuccess = {
103
+ ok: true;
104
+ runtime: ResponsesRuntime;
105
+ };
106
+
107
+ export type ResponsesEnvironmentResolution =
108
+ | NativeCompactionEnvironmentFailure
109
+ | ResponsesEnvironmentSuccess;
110
+
111
+ export type RemoteCompactionExecution = {
112
+ /** Active session model. Its identity owns replay matching and is never mutated. */
113
+ consumer: NativeCompactionRuntime;
114
+ /** Model used only for the synthetic remote_compaction_v2 request. */
115
+ compactor: NativeCompactionRuntime;
116
+ };
117
+
118
+ export type RemoteCompactionExecutionSuccess = {
119
+ ok: true;
120
+ execution: RemoteCompactionExecution;
121
+ };
122
+
123
+ export type RemoteCompactionExecutionResolution =
124
+ | NativeCompactionEnvironmentFailure
125
+ | RemoteCompactionExecutionSuccess;
126
+
127
+ type ResolvedRequestAuth =
128
+ | {
129
+ ok: true;
130
+ apiKey?: string;
131
+ headers?: ProviderHeaders;
132
+ baseUrl?: string;
133
+ }
134
+ | { ok: false; error: string };
135
+
136
+ function normalizeConfiguredApis(values: readonly string[] | undefined): Set<string> {
137
+ if (values === undefined) {
138
+ return new Set(RESPONSES_COMPACT_CAPABLE_APIS);
139
+ }
140
+ return new Set(values.map((value) => value.trim()).filter((value) => value.length > 0));
141
+ }
142
+
143
+ function resolveCodexGatewayAffinity(
144
+ model: RuntimeModel,
145
+ gatewayModels: readonly string[] | undefined,
146
+ ): CodexAffinity | undefined {
147
+ if (model.api !== "openai-responses" || !isExactModelAllowed(model, gatewayModels ?? [])) {
148
+ return undefined;
149
+ }
150
+ return {
151
+ model: model.id,
152
+ scope: CODEX_AFFINITY_SCOPE,
153
+ };
154
+ }
155
+
156
+ /** Parse "provider/model-id" (model ids may themselves contain slashes). */
157
+ export function parseModelSpec(spec: string): ParsedModelSpec | undefined {
158
+ const trimmed = spec.trim();
159
+ const separatorIndex = trimmed.indexOf("/");
160
+ if (separatorIndex <= 0 || separatorIndex >= trimmed.length - 1) {
161
+ return undefined;
162
+ }
163
+
164
+ const provider = trimmed.slice(0, separatorIndex).trim();
165
+ const modelId = trimmed.slice(separatorIndex + 1).trim();
166
+ if (!provider || !modelId) {
167
+ return undefined;
168
+ }
169
+
170
+ return { provider, modelId };
171
+ }
172
+
173
+ export function normalizeBaseUrl(baseUrl: string | undefined | null): string | undefined {
174
+ const normalized = baseUrl?.trim().replace(/\/+$/, "");
175
+ return normalized ? normalized : undefined;
176
+ }
177
+
178
+ function buildOpenAIResponsesUrl(baseUrl: string): string {
179
+ const normalized = normalizeBaseUrl(baseUrl) ?? baseUrl;
180
+ return normalized.endsWith("/responses") ? normalized : `${normalized}/${OPENAI_RESPONSES_PATH}`;
181
+ }
182
+
183
+ function buildCodexResponsesUrl(baseUrl: string): string {
184
+ const normalized = normalizeBaseUrl(baseUrl) ?? baseUrl;
185
+ if (normalized.endsWith("/codex/responses")) {
186
+ return normalized;
187
+ }
188
+ if (normalized.endsWith("/codex")) {
189
+ return `${normalized}/responses`;
190
+ }
191
+ return `${normalized}/${CODEX_RESPONSES_PATH}`;
192
+ }
193
+
194
+ function buildOpenAICompactUrl(baseUrl: string): string {
195
+ return `${buildOpenAIResponsesUrl(baseUrl)}/compact`;
196
+ }
197
+
198
+ function buildCodexCompactUrl(baseUrl: string): string {
199
+ return `${buildCodexResponsesUrl(baseUrl)}/compact`;
200
+ }
201
+
202
+ export function buildResponsesUrl(baseUrl: string, api: ResponsesCompactApi): string {
203
+ return api === "openai-codex-responses" ? buildCodexResponsesUrl(baseUrl) : buildOpenAIResponsesUrl(baseUrl);
204
+ }
205
+
206
+ export function buildResponsesPath(api: ResponsesCompactApi): string {
207
+ return api === "openai-codex-responses" ? CODEX_RESPONSES_PATH : OPENAI_RESPONSES_PATH;
208
+ }
209
+
210
+ export function buildCompactUrl(baseUrl: string, api: ResponsesCompactApi): string {
211
+ return api === "openai-codex-responses" ? buildCodexCompactUrl(baseUrl) : buildOpenAICompactUrl(baseUrl);
212
+ }
213
+
214
+ export function buildCompactPath(api: ResponsesCompactApi): string {
215
+ return api === "openai-codex-responses" ? CODEX_COMPACT_PATH : OPENAI_COMPACT_PATH;
216
+ }
217
+
218
+ async function resolveRequestAuth(ctx: ExtensionContext, model: RuntimeModel): Promise<ResolvedRequestAuth> {
219
+ const modelRegistry = ctx.modelRegistry as {
220
+ getApiKeyAndHeaders?: (currentModel: RuntimeModel) => Promise<ResolvedRequestAuth>;
221
+ };
222
+
223
+ if (typeof modelRegistry.getApiKeyAndHeaders !== "function") {
224
+ return { ok: true };
225
+ }
226
+
227
+ return modelRegistry.getApiKeyAndHeaders(model);
228
+ }
229
+
230
+ export function isSupportedApi(api: string): api is ResponsesCompactApi {
231
+ return (RESPONSES_COMPACT_CAPABLE_APIS as readonly string[]).includes(api);
232
+ }
233
+
234
+ export function isResponsesCompatiblePayload(payload: unknown): payload is ResponsesCompatibleRequestPayload {
235
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
236
+ return false;
237
+ }
238
+
239
+ const candidate = payload as Record<string, unknown>;
240
+ return typeof candidate.model === "string" && Array.isArray(candidate.input);
241
+ }
242
+
243
+ export function getRuntimeModelDescriptor(model: RuntimeModel | undefined): {
244
+ provider?: string;
245
+ api?: string;
246
+ model?: string;
247
+ baseUrl?: string;
248
+ } {
249
+ if (!model) {
250
+ return {};
251
+ }
252
+
253
+ return {
254
+ provider: model.provider,
255
+ api: model.api,
256
+ model: model.id,
257
+ baseUrl: normalizeBaseUrl(model.baseUrl),
258
+ };
259
+ }
260
+
261
+ /** Best-effort conversation id; header emission then simply skips the affinity headers. */
262
+ export function resolveRuntimeSessionId(ctx: ExtensionContext): string | undefined {
263
+ try {
264
+ return ctx.sessionManager.getSessionId();
265
+ } catch {
266
+ return undefined;
267
+ }
268
+ }
269
+
270
+ async function resolveNativeCompactionEnvironmentForModel(
271
+ ctx: ExtensionContext,
272
+ currentModel: RuntimeModel | undefined,
273
+ options: NativeCompactionSupportOptions,
274
+ payload?: unknown,
275
+ ): Promise<NativeCompactionEnvironmentResolution> {
276
+ if (options.enabled === false) {
277
+ return {
278
+ ok: false,
279
+ reason: "disabled",
280
+ };
281
+ }
282
+
283
+ const descriptor = getRuntimeModelDescriptor(currentModel);
284
+ if (!currentModel || !descriptor.provider || !descriptor.api || !descriptor.model) {
285
+ return {
286
+ ok: false,
287
+ reason: "missing-model",
288
+ ...descriptor,
289
+ };
290
+ }
291
+
292
+ // The compact endpoint is selected purely by API family: any provider speaking
293
+ // openai-responses/openai-codex-responses gets a native compact attempt and fails
294
+ // open (to the fallback model or pi's default) when the endpoint is missing.
295
+ const configuredApis = normalizeConfiguredApis(options.responsesApis);
296
+ if (!configuredApis.has(descriptor.api) || !isSupportedApi(descriptor.api)) {
297
+ return {
298
+ ok: false,
299
+ reason: "unsupported-api",
300
+ ...descriptor,
301
+ };
302
+ }
303
+
304
+ let requestPayload: ResponsesCompatibleRequestPayload | undefined;
305
+ if (payload !== undefined) {
306
+ if (!isResponsesCompatiblePayload(payload)) {
307
+ return {
308
+ ok: false,
309
+ reason: "unsupported-payload",
310
+ ...descriptor,
311
+ };
312
+ }
313
+
314
+ if (payload.model !== descriptor.model) {
315
+ return {
316
+ ok: false,
317
+ reason: "payload-model-mismatch",
318
+ ...descriptor,
319
+ };
320
+ }
321
+
322
+ requestPayload = payload;
323
+ }
324
+
325
+ let auth: ResolvedRequestAuth;
326
+ try {
327
+ auth = await resolveRequestAuth(ctx, currentModel);
328
+ } catch (error) {
329
+ return {
330
+ ok: false,
331
+ reason: "auth-resolution-failed",
332
+ errorMessage: error instanceof Error ? error.message : String(error),
333
+ ...descriptor,
334
+ };
335
+ }
336
+
337
+ if (!auth.ok) {
338
+ return {
339
+ ok: false,
340
+ reason: "auth-resolution-failed",
341
+ errorMessage: auth.error,
342
+ ...descriptor,
343
+ };
344
+ }
345
+
346
+ const baseUrl = normalizeBaseUrl(auth.baseUrl) ?? descriptor.baseUrl;
347
+ if (!baseUrl) {
348
+ return {
349
+ ok: false,
350
+ reason: "missing-base-url",
351
+ ...descriptor,
352
+ };
353
+ }
354
+
355
+ const codexAffinity = resolveCodexGatewayAffinity(currentModel, options.codexGatewayModels);
356
+ const sessionId = resolveRuntimeSessionId(ctx);
357
+ if (codexAffinity && !sessionId) {
358
+ return {
359
+ ok: false,
360
+ reason: "missing-session-id",
361
+ ...descriptor,
362
+ baseUrl,
363
+ };
364
+ }
365
+
366
+ if (!auth.apiKey) {
367
+ return {
368
+ ok: false,
369
+ reason: "missing-api-key",
370
+ ...descriptor,
371
+ baseUrl,
372
+ };
373
+ }
374
+
375
+ return {
376
+ ok: true,
377
+ runtime: {
378
+ provider: descriptor.provider,
379
+ api: descriptor.api,
380
+ model: descriptor.model,
381
+ baseUrl,
382
+ apiKey: auth.apiKey,
383
+ headers: auth.headers,
384
+ responsesPath: buildResponsesPath(descriptor.api),
385
+ responsesUrl: buildResponsesUrl(baseUrl, descriptor.api),
386
+ compactPath: buildCompactPath(descriptor.api),
387
+ compactUrl: buildCompactUrl(baseUrl, descriptor.api),
388
+ sessionId,
389
+ codexAffinity,
390
+ payload: requestPayload,
391
+ currentModel,
392
+ },
393
+ };
394
+ }
395
+
396
+ export async function resolveResponsesEnvironment(
397
+ ctx: ExtensionContext,
398
+ options: ResponsesSupportOptions = {},
399
+ ): Promise<ResponsesEnvironmentResolution> {
400
+ const resolution = await resolveNativeCompactionEnvironmentForModel(ctx, ctx.model, options);
401
+ if (!resolution.ok) return resolution;
402
+
403
+ const runtime = resolution.runtime;
404
+ return {
405
+ ok: true,
406
+ runtime: {
407
+ provider: runtime.provider,
408
+ api: runtime.api,
409
+ model: runtime.model,
410
+ baseUrl: runtime.baseUrl,
411
+ apiKey: runtime.apiKey,
412
+ headers: runtime.headers,
413
+ responsesPath: runtime.responsesPath,
414
+ responsesUrl: runtime.responsesUrl,
415
+ sessionId: runtime.sessionId,
416
+ codexAffinity: runtime.codexAffinity,
417
+ currentModel: runtime.currentModel,
418
+ },
419
+ };
420
+ }
421
+
422
+ export async function resolveNativeCompactionEnvironment(
423
+ ctx: ExtensionContext,
424
+ options: NativeCompactionSupportOptions = {},
425
+ payload?: unknown,
426
+ ): Promise<NativeCompactionEnvironmentResolution> {
427
+ return resolveNativeCompactionEnvironmentForModel(ctx, ctx.model, options, payload);
428
+ }
429
+
430
+ export async function resolveRemoteCompactionExecution(
431
+ ctx: ExtensionContext,
432
+ options: NativeCompactionSupportOptions = {},
433
+ remoteModelSpec?: string,
434
+ ): Promise<RemoteCompactionExecutionResolution> {
435
+ const consumerResolution = await resolveNativeCompactionEnvironment(ctx, options);
436
+ if (!consumerResolution.ok) {
437
+ return consumerResolution;
438
+ }
439
+
440
+ const spec = remoteModelSpec?.trim();
441
+ if (!spec) {
442
+ return {
443
+ ok: true,
444
+ execution: {
445
+ consumer: consumerResolution.runtime,
446
+ compactor: consumerResolution.runtime,
447
+ },
448
+ };
449
+ }
450
+
451
+ const parsed = parseModelSpec(spec);
452
+ if (!parsed) {
453
+ return {
454
+ ok: false,
455
+ reason: "invalid-model-spec",
456
+ modelSpec: spec,
457
+ };
458
+ }
459
+
460
+ const compactorModel = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
461
+ if (!compactorModel) {
462
+ return {
463
+ ok: false,
464
+ reason: "model-not-found",
465
+ provider: parsed.provider,
466
+ model: parsed.modelId,
467
+ modelSpec: spec,
468
+ };
469
+ }
470
+
471
+ const compactorResolution = await resolveNativeCompactionEnvironmentForModel(
472
+ ctx,
473
+ compactorModel,
474
+ options,
475
+ );
476
+ if (!compactorResolution.ok) {
477
+ return {
478
+ ...compactorResolution,
479
+ modelSpec: spec,
480
+ };
481
+ }
482
+
483
+ const consumer = consumerResolution.runtime;
484
+ const compactor = compactorResolution.runtime;
485
+ if (compactor.baseUrl !== consumer.baseUrl) {
486
+ return {
487
+ ok: false,
488
+ reason: "base-url-mismatch",
489
+ provider: compactor.provider,
490
+ api: compactor.api,
491
+ model: compactor.model,
492
+ baseUrl: compactor.baseUrl,
493
+ modelSpec: spec,
494
+ };
495
+ }
496
+
497
+ return {
498
+ ok: true,
499
+ execution: {
500
+ consumer,
501
+ compactor,
502
+ },
503
+ };
504
+ }
505
+
506
+ export async function getNativeCompactionRuntime(
507
+ ctx: ExtensionContext,
508
+ options: NativeCompactionSupportOptions = {},
509
+ payload?: unknown,
510
+ ): Promise<NativeCompactionRuntime | undefined> {
511
+ const resolution = await resolveNativeCompactionEnvironment(ctx, options, payload);
512
+ return resolution.ok ? resolution.runtime : undefined;
513
+ }