pi-openai-toolkit 0.18.0 → 0.20.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.
- package/README.md +156 -242
- package/README.zh.md +156 -242
- package/config.schema.json +15 -1
- package/docs/configuration.md +135 -7
- package/package.json +9 -6
- package/src/config/policy.ts +4 -2
- package/src/config/v2.ts +36 -3
- package/src/config.ts +1 -0
- package/src/context-management/codex-provider.ts +18 -8
- package/src/extension-runtime.ts +47 -26
- package/src/image-generation/artifacts.ts +28 -7
- package/src/image-generation/client.ts +5 -3
- package/src/image-generation/eligibility.ts +10 -5
- package/src/image-generation/extension.ts +6 -4
- package/src/image-generation/protocol.ts +175 -38
- package/src/image-generation/render.ts +13 -5
- package/src/image-generation/service.ts +35 -7
- package/src/image-generation/types.ts +23 -3
- package/src/payload-rewrite.ts +16 -1
- package/src/responses-headers.ts +6 -1
- package/src/responses-websocket/extension.ts +32 -0
- package/src/responses-websocket/transport.ts +577 -0
- package/src/runtime.ts +295 -28
- package/src/serializer.ts +214 -0
- package/src/supported-environment.ts +5 -0
- package/src/types.ts +6 -2
- package/src/web-search/extension.ts +4 -3
- package/src/web-search/types.ts +25 -3
- package/tsconfig.check.json +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-openai-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"./src/web-search/extension.ts",
|
|
39
39
|
"./src/image-generation/extension.ts",
|
|
40
40
|
"./src/auto-mode/extension.ts",
|
|
41
|
-
"./src/codex-astra/extension.ts"
|
|
41
|
+
"./src/codex-astra/extension.ts",
|
|
42
|
+
"./src/responses-websocket/extension.ts"
|
|
42
43
|
]
|
|
43
44
|
},
|
|
44
45
|
"files": [
|
|
@@ -89,6 +90,8 @@
|
|
|
89
90
|
"src/auto-mode/types.ts",
|
|
90
91
|
"src/codex-astra/effort-planner.ts",
|
|
91
92
|
"src/codex-astra/extension.ts",
|
|
93
|
+
"src/responses-websocket/extension.ts",
|
|
94
|
+
"src/responses-websocket/transport.ts",
|
|
92
95
|
"src/model-scope.ts",
|
|
93
96
|
"src/native-fallback.ts",
|
|
94
97
|
"src/payload-rewrite.ts",
|
|
@@ -126,10 +129,10 @@
|
|
|
126
129
|
"@earendil-works/pi-tui": ">=0.87.0"
|
|
127
130
|
},
|
|
128
131
|
"devDependencies": {
|
|
129
|
-
"@earendil-works/pi-agent-core": "0.
|
|
130
|
-
"@earendil-works/pi-ai": "0.
|
|
131
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
132
|
-
"@earendil-works/pi-tui": "0.
|
|
132
|
+
"@earendil-works/pi-agent-core": "0.99.1",
|
|
133
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
134
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
135
|
+
"@earendil-works/pi-tui": "0.99.1",
|
|
133
136
|
"@types/node": "24.12.4",
|
|
134
137
|
"bun": "1.2.23",
|
|
135
138
|
"typescript": "5.9.3"
|
package/src/config/policy.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type RemoteV2ContextSource,
|
|
9
9
|
} from "../types";
|
|
10
10
|
|
|
11
|
-
export type ConfigFeature = "context" | "webSearch" | "imageGeneration" | "autoMode" | "compatibility" | "diagnostics";
|
|
11
|
+
export type ConfigFeature = "context" | "webSearch" | "imageGeneration" | "autoMode" | "compatibility" | "responsesWebSocket" | "diagnostics";
|
|
12
12
|
export type SearchRoute = "unmanaged" | "local" | "hosted" | "standalone-alpha";
|
|
13
13
|
export type DiagnosticsConfig = {
|
|
14
14
|
level: "error" | "warn" | "info" | "debug";
|
|
@@ -42,6 +42,7 @@ export type EffectiveToolkitPolicy = {
|
|
|
42
42
|
context: ContextPolicy;
|
|
43
43
|
webSearch: { route: SearchRoute };
|
|
44
44
|
imageGeneration: { enabled: boolean; defaultModel: string; allowedModels: string[] };
|
|
45
|
+
responsesWebSocket: { enabled: boolean };
|
|
45
46
|
autoMode: AutoModePolicy;
|
|
46
47
|
compatibility: { transport: "standard" | "codex-gateway" };
|
|
47
48
|
diagnostics: DiagnosticsConfig;
|
|
@@ -74,7 +75,7 @@ export type ConfigDocumentSnapshot = {
|
|
|
74
75
|
issues: ConfigIssue[];
|
|
75
76
|
};
|
|
76
77
|
export const CONFIG_FEATURES: readonly ConfigFeature[] = [
|
|
77
|
-
"context", "webSearch", "imageGeneration", "autoMode", "compatibility", "diagnostics",
|
|
78
|
+
"context", "webSearch", "imageGeneration", "autoMode", "compatibility", "responsesWebSocket", "diagnostics",
|
|
78
79
|
];
|
|
79
80
|
|
|
80
81
|
/** Independent per-operation defaults, preserving the existing shipped policy. */
|
|
@@ -103,6 +104,7 @@ export function createPolicyDefaults(): EffectiveToolkitPolicy {
|
|
|
103
104
|
defaultModel: DEFAULT_IMAGE_GENERATION_MODEL,
|
|
104
105
|
allowedModels: [DEFAULT_IMAGE_GENERATION_MODEL],
|
|
105
106
|
},
|
|
107
|
+
responsesWebSocket: { enabled: false },
|
|
106
108
|
autoMode: {
|
|
107
109
|
...autoOptions,
|
|
108
110
|
available: false,
|
package/src/config/v2.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* {
|
|
14
14
|
* "schemaVersion": 2,
|
|
15
15
|
* "$schema": "…metadata only…",
|
|
16
|
-
* "defaults": { "context": …, "webSearch": …, "imageGeneration": …, "autoMode": … },
|
|
16
|
+
* "defaults": { "context": …, "webSearch": …, "imageGeneration": …, "responsesWebSocket": …, "autoMode": … },
|
|
17
17
|
* "models": { "provider/model-id": { "context": …, "webSearch": …, "autoMode": …, "compatibility": … } },
|
|
18
18
|
* "diagnostics": { … }
|
|
19
19
|
* }
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
* raw values and unknown subtrees are never copied into diagnostics.
|
|
30
30
|
* - `defaults.compatibility` is not a recognized scope: the standard transport is internal and
|
|
31
31
|
* the Codex gateway transport stays an exact-model opt-in.
|
|
32
|
-
* - Image output settings (`defaults.imageGeneration`)
|
|
32
|
+
* - Image output settings (`defaults.imageGeneration`), Responses WebSocket transport
|
|
33
|
+
* (`defaults.responsesWebSocket`), and `diagnostics` are global only.
|
|
33
34
|
* - Exact model keys and model references use the legacy `provider/model-id` grammar: no
|
|
34
35
|
* whitespace, globs or bracket patterns. Keys are trimmed like the legacy decoder, and two
|
|
35
36
|
* raw keys that normalize to the same key are rejected instead of resolved last-wins.
|
|
@@ -120,7 +121,7 @@ const DIAGNOSTIC_LEVELS = ["error", "warn", "info", "debug"] as const;
|
|
|
120
121
|
const LEGACY_ROOT_KEYS = ["compaction", "webSearch", "imageGeneration", "autoMode"] as const;
|
|
121
122
|
|
|
122
123
|
const ROOT_KEYS = new Set(["$schema", "schemaVersion", "defaults", "models", "diagnostics"]);
|
|
123
|
-
const DEFAULTS_KEYS = new Set(["context", "webSearch", "imageGeneration", "autoMode"]);
|
|
124
|
+
const DEFAULTS_KEYS = new Set(["context", "webSearch", "imageGeneration", "responsesWebSocket", "autoMode"]);
|
|
124
125
|
const MODEL_OVERRIDE_KEYS = new Set(["context", "webSearch", "autoMode", "compatibility"]);
|
|
125
126
|
const CONTEXT_KEYS = new Set(["mode", "remoteCompaction", "nativeFallback", "remoteWindows"]);
|
|
126
127
|
const REMOTE_COMPACTION_KEYS = new Set(["model", "inputSource", "allowContinuityBreak", "apis"]);
|
|
@@ -128,6 +129,7 @@ const NATIVE_FALLBACK_KEYS = new Set(["enabled", "model", "thinkingLevel"]);
|
|
|
128
129
|
const REMOTE_WINDOWS_KEYS = new Set(["leaveManagedMode", "reminderThresholdPercent"]);
|
|
129
130
|
const WEB_SEARCH_KEYS = new Set(["route"]);
|
|
130
131
|
const IMAGE_GENERATION_KEYS = new Set(["enabled", "defaultModel", "allowedModels"]);
|
|
132
|
+
const RESPONSES_WEBSOCKET_KEYS = new Set(["enabled"]);
|
|
131
133
|
const AUTO_MODE_KEYS = new Set([
|
|
132
134
|
"available",
|
|
133
135
|
"reviewerModel",
|
|
@@ -170,6 +172,7 @@ const FEATURE_NAMES: ReadonlyMap<string, ConfigFeature> = new Map<string, Config
|
|
|
170
172
|
["compaction", "context"],
|
|
171
173
|
["webSearch", "webSearch"],
|
|
172
174
|
["imageGeneration", "imageGeneration"],
|
|
175
|
+
["responsesWebSocket", "responsesWebSocket"],
|
|
173
176
|
["autoMode", "autoMode"],
|
|
174
177
|
["compatibility", "compatibility"],
|
|
175
178
|
["diagnostics", "diagnostics"],
|
|
@@ -194,6 +197,7 @@ const POLICY_LEAVES: readonly { leaf: string; feature: ConfigFeature }[] = [
|
|
|
194
197
|
{ leaf: "imageGeneration.enabled", feature: "imageGeneration" },
|
|
195
198
|
{ leaf: "imageGeneration.defaultModel", feature: "imageGeneration" },
|
|
196
199
|
{ leaf: "imageGeneration.allowedModels", feature: "imageGeneration" },
|
|
200
|
+
{ leaf: "responsesWebSocket.enabled", feature: "responsesWebSocket" },
|
|
197
201
|
{ leaf: "autoMode.available", feature: "autoMode" },
|
|
198
202
|
{ leaf: "autoMode.reviewerModel", feature: "autoMode" },
|
|
199
203
|
{ leaf: "autoMode.gate", feature: "autoMode" },
|
|
@@ -841,6 +845,25 @@ function applyImageGenerationSettings(
|
|
|
841
845
|
);
|
|
842
846
|
}
|
|
843
847
|
|
|
848
|
+
function applyResponsesWebSocketSettings(
|
|
849
|
+
raw: Record<string, unknown>,
|
|
850
|
+
scope: Scope,
|
|
851
|
+
state: ResolutionState,
|
|
852
|
+
): void {
|
|
853
|
+
reportUnknownKeys(raw, RESPONSES_WEBSOCKET_KEYS, scope, state, "responsesWebSocket");
|
|
854
|
+
|
|
855
|
+
applyField(
|
|
856
|
+
raw,
|
|
857
|
+
{ key: "enabled", leaf: "responsesWebSocket.enabled", feature: "responsesWebSocket" },
|
|
858
|
+
scope,
|
|
859
|
+
state,
|
|
860
|
+
readBoolean,
|
|
861
|
+
(value) => {
|
|
862
|
+
state.policy.responsesWebSocket.enabled = value;
|
|
863
|
+
},
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
844
867
|
function applyAutoModeSettings(
|
|
845
868
|
raw: Record<string, unknown>,
|
|
846
869
|
scope: Scope,
|
|
@@ -1149,6 +1172,15 @@ function applyImageGenerationScope(
|
|
|
1149
1172
|
if (group) applyImageGenerationSettings(group.raw, group.scope, state);
|
|
1150
1173
|
}
|
|
1151
1174
|
|
|
1175
|
+
function applyResponsesWebSocketScope(
|
|
1176
|
+
container: Record<string, unknown>,
|
|
1177
|
+
scope: Scope,
|
|
1178
|
+
state: ResolutionState,
|
|
1179
|
+
): void {
|
|
1180
|
+
const group = openGroup(container, "responsesWebSocket", scope, state, "responsesWebSocket");
|
|
1181
|
+
if (group) applyResponsesWebSocketSettings(group.raw, group.scope, state);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1152
1184
|
function applyAutoModeScope(
|
|
1153
1185
|
container: Record<string, unknown>,
|
|
1154
1186
|
scope: Scope,
|
|
@@ -1190,6 +1222,7 @@ function applyDefaults(doc: Record<string, unknown>, state: ResolutionState): vo
|
|
|
1190
1222
|
applyContextScope(raw, scope, state);
|
|
1191
1223
|
applyWebSearchScope(raw, scope, state);
|
|
1192
1224
|
applyImageGenerationScope(raw, scope, state);
|
|
1225
|
+
applyResponsesWebSocketScope(raw, scope, state);
|
|
1193
1226
|
applyAutoModeScope(raw, scope, state);
|
|
1194
1227
|
}
|
|
1195
1228
|
|
package/src/config.ts
CHANGED
|
@@ -208,6 +208,7 @@ function fromLegacy(loaded: LoadedToolkitConfig, model: WebSearchModel | undefin
|
|
|
208
208
|
defaultModel: config.imageGeneration.models[0],
|
|
209
209
|
allowedModels: [...config.imageGeneration.models],
|
|
210
210
|
};
|
|
211
|
+
policy.responsesWebSocket = { enabled: false };
|
|
211
212
|
const { enabled, models, reviewerModel, classifier, ...auto } = config.autoMode;
|
|
212
213
|
policy.autoMode = {
|
|
213
214
|
...structuredClone(auto),
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
filterCodexGatewayHeaders,
|
|
10
10
|
} from "../responses-headers";
|
|
11
11
|
export { CODEX_AFFINITY_SCOPE } from "../responses-headers";
|
|
12
|
-
import { normalizeBaseUrl } from "../runtime";
|
|
12
|
+
import { normalizeBaseUrl, resolveEffectiveModel, VIRTUAL_MODEL_API } from "../runtime";
|
|
13
13
|
import { isExactModelAllowed } from "../model-scope";
|
|
14
14
|
import {
|
|
15
15
|
isNonEmptyString,
|
|
@@ -172,17 +172,27 @@ export async function resolveCodexContextProvider(
|
|
|
172
172
|
modelOverride: ExtensionContext["model"] = ctx.model,
|
|
173
173
|
gatewayModels: readonly string[] = [],
|
|
174
174
|
): Promise<CodexContextProviderResolution> {
|
|
175
|
-
const
|
|
175
|
+
const selectedModel = modelOverride;
|
|
176
|
+
const selectedDescriptor = {
|
|
177
|
+
provider: selectedModel?.provider,
|
|
178
|
+
api: selectedModel?.api,
|
|
179
|
+
model: selectedModel?.id,
|
|
180
|
+
baseUrl: normalizeBaseUrl(selectedModel?.baseUrl),
|
|
181
|
+
};
|
|
182
|
+
const effective = await resolveEffectiveModel(ctx, selectedModel);
|
|
183
|
+
if (!effective.ok) return { ok: false, reason: "unsupported-model", ...selectedDescriptor };
|
|
184
|
+
const model = effective.model;
|
|
176
185
|
const descriptor = {
|
|
177
|
-
provider: model
|
|
178
|
-
api: model
|
|
179
|
-
model: model
|
|
180
|
-
baseUrl: normalizeBaseUrl(model
|
|
186
|
+
provider: model.provider,
|
|
187
|
+
api: model.api,
|
|
188
|
+
model: model.id,
|
|
189
|
+
baseUrl: normalizeBaseUrl(model.baseUrl),
|
|
181
190
|
};
|
|
182
|
-
if (!model) return { ok: false, reason: "unsupported-model" };
|
|
183
191
|
|
|
184
192
|
const isNative = isNativeCodexModel(model);
|
|
185
|
-
const
|
|
193
|
+
const isLogicalVirtualGateway = selectedModel?.api === VIRTUAL_MODEL_API && isExactModelAllowed(selectedModel, gatewayModels);
|
|
194
|
+
const isGateway = isCodexGatewayModel(model, gatewayModels) ||
|
|
195
|
+
(isLogicalVirtualGateway && model.api === GATEWAY_API);
|
|
186
196
|
if (!isNative && !isGateway) {
|
|
187
197
|
return { ok: false, reason: "unsupported-model", ...descriptor };
|
|
188
198
|
}
|
package/src/extension-runtime.ts
CHANGED
|
@@ -53,20 +53,22 @@ import {
|
|
|
53
53
|
rewriteResponsesPayloadWithNativeReplay,
|
|
54
54
|
hasInvalidatedCompactionContext,
|
|
55
55
|
removeNativeCompactionRetainedMessages,
|
|
56
|
-
|
|
56
|
+
serializeLiveTailToResponsesInputWithLimits,
|
|
57
57
|
} from "./payload-rewrite";
|
|
58
58
|
import { getCompactionRequestExtras, rememberRequestContext } from "./request-context-cache";
|
|
59
59
|
import { resolveWebSearchRoute } from "./web-search/types";
|
|
60
60
|
import { executeRemoteV2Compaction } from "./remote-v2-client";
|
|
61
61
|
import {
|
|
62
|
+
rememberPhysicalModel,
|
|
63
|
+
resolveEffectiveModel,
|
|
62
64
|
resolveNativeCompactionEnvironment,
|
|
63
65
|
resolveRemoteCompactionExecution,
|
|
64
66
|
parseModelSpec,
|
|
65
67
|
type RemoteCompactionExecution,
|
|
66
68
|
} from "./runtime";
|
|
67
69
|
import {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
serializeMessagesToCompactRequestWithLimits,
|
|
71
|
+
serializeMessagesToResponsesInputWithLimits,
|
|
70
72
|
type NativeCompactionRequestBody,
|
|
71
73
|
type ResponsesInputItem,
|
|
72
74
|
} from "./serializer";
|
|
@@ -183,6 +185,14 @@ function notifyWarning(ctx: ExtensionContext, message: string): void {
|
|
|
183
185
|
}
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
async function resolveContextCapabilityModel(
|
|
189
|
+
ctx: ExtensionContext,
|
|
190
|
+
model: ExtensionContext["model"] = ctx.model,
|
|
191
|
+
): Promise<ExtensionContext["model"]> {
|
|
192
|
+
const effective = await resolveEffectiveModel(ctx, model);
|
|
193
|
+
return effective.ok ? effective.model : undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
186
196
|
async function isRemoteContextActive(
|
|
187
197
|
ctx: ExtensionContext,
|
|
188
198
|
config: CompactionConfig,
|
|
@@ -362,14 +372,14 @@ async function runResponsesNativeCompact(
|
|
|
362
372
|
}
|
|
363
373
|
const input: ResponsesInputItem[] = [
|
|
364
374
|
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
365
|
-
...
|
|
375
|
+
...(await serializeMessagesToResponsesInputWithLimits(
|
|
366
376
|
compactor.currentModel,
|
|
367
377
|
[...projection.messages.slice(summaryIndex + 1)],
|
|
368
378
|
{ firstSystemMessageIsUpdate: true },
|
|
369
|
-
),
|
|
379
|
+
)),
|
|
370
380
|
];
|
|
371
381
|
request = {
|
|
372
|
-
model: compactor.model,
|
|
382
|
+
model: compactor.requestModel ?? compactor.model,
|
|
373
383
|
input,
|
|
374
384
|
instructions,
|
|
375
385
|
};
|
|
@@ -377,10 +387,10 @@ async function runResponsesNativeCompact(
|
|
|
377
387
|
const liveTailEntries = branchEntries.slice(latestNativeCompaction.index + 1);
|
|
378
388
|
const input: ResponsesInputItem[] = [
|
|
379
389
|
...(cloneOpaqueWindow(details.compactedWindow) as ResponsesInputItem[]),
|
|
380
|
-
...
|
|
390
|
+
...(await serializeLiveTailToResponsesInputWithLimits({ model: compactor.currentModel, entries: liveTailEntries })),
|
|
381
391
|
];
|
|
382
392
|
request = {
|
|
383
|
-
model: compactor.model,
|
|
393
|
+
model: compactor.requestModel ?? compactor.model,
|
|
384
394
|
input,
|
|
385
395
|
instructions,
|
|
386
396
|
};
|
|
@@ -397,17 +407,19 @@ async function runResponsesNativeCompact(
|
|
|
397
407
|
if (!projection.ok) {
|
|
398
408
|
return { outcome: "unprojected-input", reason: projection.reason };
|
|
399
409
|
}
|
|
400
|
-
|
|
410
|
+
const serialized = await serializeMessagesToCompactRequestWithLimits({
|
|
401
411
|
model: compactor.currentModel,
|
|
402
412
|
messages: [...projection.messages],
|
|
403
413
|
instructions,
|
|
404
414
|
});
|
|
415
|
+
request = { ...serialized, model: compactor.requestModel ?? serialized.model };
|
|
405
416
|
} else {
|
|
406
|
-
|
|
417
|
+
const serialized = await serializeMessagesToCompactRequestWithLimits({
|
|
407
418
|
model: compactor.currentModel,
|
|
408
419
|
messages: getLegacySessionContextMessages(event, ctx),
|
|
409
420
|
instructions,
|
|
410
421
|
});
|
|
422
|
+
request = { ...serialized, model: compactor.requestModel ?? serialized.model };
|
|
411
423
|
}
|
|
412
424
|
} else {
|
|
413
425
|
writeDebugArtifact(
|
|
@@ -441,7 +453,7 @@ async function runResponsesNativeCompact(
|
|
|
441
453
|
const extras = getCompactionRequestExtras({
|
|
442
454
|
provider: consumer.provider,
|
|
443
455
|
api: consumer.api,
|
|
444
|
-
model: consumer.model,
|
|
456
|
+
model: consumer.requestModel ?? consumer.model,
|
|
445
457
|
baseUrl: consumer.baseUrl,
|
|
446
458
|
sessionId: getSessionId(ctx),
|
|
447
459
|
}, compactor.currentModel);
|
|
@@ -591,8 +603,9 @@ async function handleSessionBeforeCompact(
|
|
|
591
603
|
|
|
592
604
|
// Remote Context management owns this eligible Codex session. It persists a
|
|
593
605
|
// no-summary boundary and deliberately never calls remote_compaction_v2.
|
|
594
|
-
|
|
595
|
-
|
|
606
|
+
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
607
|
+
if (isCodexContextModel(contextModel, config)) {
|
|
608
|
+
if (await remoteContextActive(ctx, config, contextModel)) {
|
|
596
609
|
try {
|
|
597
610
|
dependencies.contextWindows.synchronize(ctx);
|
|
598
611
|
if (event.reason === "manual" && dependencies.manualCompact && !dependencies.manualCompact.isMaintenance) {
|
|
@@ -864,6 +877,7 @@ async function handleContextInternal(
|
|
|
864
877
|
}
|
|
865
878
|
|
|
866
879
|
if (config.contextManagement === "remote") {
|
|
880
|
+
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
867
881
|
try {
|
|
868
882
|
contextWindows.synchronize(ctx);
|
|
869
883
|
} catch {
|
|
@@ -873,7 +887,7 @@ async function handleContextInternal(
|
|
|
873
887
|
return undefined;
|
|
874
888
|
}
|
|
875
889
|
|
|
876
|
-
const remoteActive = await remoteContextActive(ctx, config);
|
|
890
|
+
const remoteActive = await remoteContextActive(ctx, config, contextModel);
|
|
877
891
|
if (remoteActive) {
|
|
878
892
|
try {
|
|
879
893
|
// Measure the provider-visible current window, not Pi's durable transcript.
|
|
@@ -902,8 +916,8 @@ async function handleContextInternal(
|
|
|
902
916
|
// older compaction-replay path. Gateway traffic must also fail closed when
|
|
903
917
|
// its transport capability is unavailable; otherwise Pi could send an
|
|
904
918
|
// ordinary unscoped request and lose CPA OAuth/session affinity.
|
|
905
|
-
if (isCodexContextModel(
|
|
906
|
-
if (isCodexGatewayModel(
|
|
919
|
+
if (isCodexContextModel(contextModel, config)) {
|
|
920
|
+
if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
|
|
907
921
|
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
908
922
|
reportPiContextHookFailure(ctx, "codex-context-unavailable");
|
|
909
923
|
ctx.abort();
|
|
@@ -982,6 +996,9 @@ async function handleBeforeProviderRequest(
|
|
|
982
996
|
return undefined;
|
|
983
997
|
}
|
|
984
998
|
|
|
999
|
+
const contextModel = config.contextManagement === "remote"
|
|
1000
|
+
? await resolveContextCapabilityModel(ctx)
|
|
1001
|
+
: undefined;
|
|
985
1002
|
if (config.contextManagement === "remote") {
|
|
986
1003
|
try {
|
|
987
1004
|
contextWindows.synchronize(ctx);
|
|
@@ -991,7 +1008,7 @@ async function handleBeforeProviderRequest(
|
|
|
991
1008
|
return undefined;
|
|
992
1009
|
}
|
|
993
1010
|
}
|
|
994
|
-
if (config.contextManagement === "remote" && await remoteContextActive(ctx, config)) {
|
|
1011
|
+
if (config.contextManagement === "remote" && await remoteContextActive(ctx, config, contextModel)) {
|
|
995
1012
|
try {
|
|
996
1013
|
return contextWindows.rewritePayload(event.payload, ctx);
|
|
997
1014
|
} catch {
|
|
@@ -1000,12 +1017,12 @@ async function handleBeforeProviderRequest(
|
|
|
1000
1017
|
return undefined;
|
|
1001
1018
|
}
|
|
1002
1019
|
}
|
|
1003
|
-
if (isCodexContextModel(
|
|
1020
|
+
if (isCodexContextModel(contextModel, config)) {
|
|
1004
1021
|
// Keep Codex Remote mutually exclusive with the legacy replay pipeline
|
|
1005
1022
|
// when authentication or tool ownership is unavailable. Gateway traffic
|
|
1006
1023
|
// cannot continue as an ordinary Responses request because that would
|
|
1007
1024
|
// silently lose the required CPA session/account selection.
|
|
1008
|
-
if (isCodexGatewayModel(
|
|
1025
|
+
if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
|
|
1009
1026
|
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
1010
1027
|
ctx.abort();
|
|
1011
1028
|
}
|
|
@@ -1057,7 +1074,7 @@ async function handleBeforeProviderRequest(
|
|
|
1057
1074
|
{
|
|
1058
1075
|
provider: runtime.provider,
|
|
1059
1076
|
api: runtime.api,
|
|
1060
|
-
model: runtime.model,
|
|
1077
|
+
model: runtime.requestModel ?? runtime.model,
|
|
1061
1078
|
baseUrl: runtime.baseUrl,
|
|
1062
1079
|
sessionId: getSessionId(ctx),
|
|
1063
1080
|
},
|
|
@@ -1255,11 +1272,12 @@ export default function registerCompactionExtension(
|
|
|
1255
1272
|
const config = resolved.config.compaction;
|
|
1256
1273
|
if (resolved.invalidFeatures.length > 0) return;
|
|
1257
1274
|
if (!config.enabled) return;
|
|
1275
|
+
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
1258
1276
|
|
|
1259
1277
|
let activationReason: string | undefined;
|
|
1260
1278
|
// Only models the built-in Remote Context coverage targets may activate or
|
|
1261
1279
|
// warn; everything else silently runs Pi's normal compaction path.
|
|
1262
|
-
if (config.contextManagement === "remote" && isCodexContextModel(
|
|
1280
|
+
if (config.contextManagement === "remote" && isCodexContextModel(contextModel, config) && !active) {
|
|
1263
1281
|
if (syncOutcome.registrationState === "conflict") {
|
|
1264
1282
|
activationReason = "tool-name-conflict";
|
|
1265
1283
|
notifyRemoteContextFailure(ctx, activationReason);
|
|
@@ -1361,10 +1379,11 @@ export default function registerCompactionExtension(
|
|
|
1361
1379
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1362
1380
|
requireContextPolicy(resolved, ctx);
|
|
1363
1381
|
const config = resolved.config.compaction;
|
|
1364
|
-
|
|
1365
|
-
|
|
1382
|
+
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
1383
|
+
if (!isCodexContextModel(contextModel, config)) return;
|
|
1384
|
+
const active = await remoteContextActive(ctx, config, contextModel);
|
|
1366
1385
|
if (!active) {
|
|
1367
|
-
if (isCodexGatewayModel(
|
|
1386
|
+
if (isCodexGatewayModel(contextModel, config.gatewayContextModels)) {
|
|
1368
1387
|
notifyRemoteContextFailure(ctx, "codex-context-unavailable");
|
|
1369
1388
|
ctx.abort();
|
|
1370
1389
|
}
|
|
@@ -1402,11 +1421,13 @@ export default function registerCompactionExtension(
|
|
|
1402
1421
|
contextWindows.rewriteHeaders(event.headers, ctx);
|
|
1403
1422
|
});
|
|
1404
1423
|
pi.on("message_end", async (event, ctx) => {
|
|
1424
|
+
rememberPhysicalModel(ctx, event.message);
|
|
1405
1425
|
const resolved = contextOperation(dependencies.loadConfig, ctx);
|
|
1406
1426
|
requireContextPolicy(resolved, ctx);
|
|
1407
1427
|
const config = resolved.config.compaction;
|
|
1408
|
-
|
|
1409
|
-
if (!(
|
|
1428
|
+
const contextModel = await resolveContextCapabilityModel(ctx);
|
|
1429
|
+
if (!isCodexContextModel(contextModel, config) || !tools.isRegistered) return undefined;
|
|
1430
|
+
if (!(await remoteContextActive(ctx, config, contextModel))) return undefined;
|
|
1410
1431
|
const message = routeContextNamespaceToolMessage(event.message);
|
|
1411
1432
|
return message === event.message ? undefined : { message };
|
|
1412
1433
|
});
|
|
@@ -20,13 +20,17 @@ import {
|
|
|
20
20
|
} from "node:path";
|
|
21
21
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import {
|
|
23
|
+
IMAGE_GENERATION_MIME_TYPE,
|
|
23
24
|
MAX_IMAGE_PATH_CHARS,
|
|
24
25
|
ImageGenerationError,
|
|
26
|
+
imageFileExtension,
|
|
25
27
|
sanitizeImageDiagnostic,
|
|
28
|
+
type ImageGenerationMimeType,
|
|
26
29
|
} from "./types";
|
|
27
30
|
|
|
28
31
|
export type ExplicitOutputPlan = {
|
|
29
32
|
path: string;
|
|
33
|
+
extension?: string;
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
function isPathInsideOrEqual(root: string, candidate: string): boolean {
|
|
@@ -105,10 +109,15 @@ export async function prepareExplicitOutputPath(args: {
|
|
|
105
109
|
}): Promise<ExplicitOutputPlan | undefined> {
|
|
106
110
|
if (args.rawPath === undefined) return undefined;
|
|
107
111
|
const trimmed = args.rawPath.trim();
|
|
108
|
-
|
|
112
|
+
const extension = extname(trimmed).toLowerCase();
|
|
113
|
+
if (
|
|
114
|
+
!trimmed ||
|
|
115
|
+
trimmed.length > MAX_IMAGE_PATH_CHARS ||
|
|
116
|
+
![".png", ".jpg", ".jpeg", ".webp"].includes(extension)
|
|
117
|
+
) {
|
|
109
118
|
throw new ImageGenerationError(
|
|
110
119
|
"output-path-invalid",
|
|
111
|
-
"outputPath must be a non-empty .png file path.",
|
|
120
|
+
"outputPath must be a non-empty .png, .jpg, .jpeg, or .webp file path.",
|
|
112
121
|
);
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -127,7 +136,7 @@ export async function prepareExplicitOutputPath(args: {
|
|
|
127
136
|
realDirectory(args.agentDir),
|
|
128
137
|
realDirectory(args.ctx.cwd),
|
|
129
138
|
]);
|
|
130
|
-
if (isPathInsideOrEqual(agentRoot, target)) return { path: target };
|
|
139
|
+
if (isPathInsideOrEqual(agentRoot, target)) return { path: target, extension };
|
|
131
140
|
|
|
132
141
|
if (isPathInsideOrEqual(projectRoot, target)) {
|
|
133
142
|
if (!args.ctx.isProjectTrusted()) {
|
|
@@ -136,7 +145,7 @@ export async function prepareExplicitOutputPath(args: {
|
|
|
136
145
|
"Writing generated images inside an untrusted project is not allowed.",
|
|
137
146
|
);
|
|
138
147
|
}
|
|
139
|
-
return { path: target };
|
|
148
|
+
return { path: target, extension };
|
|
140
149
|
}
|
|
141
150
|
|
|
142
151
|
if (!args.ctx.hasUI) {
|
|
@@ -148,7 +157,7 @@ export async function prepareExplicitOutputPath(args: {
|
|
|
148
157
|
const approved = await args.ctx.ui.confirm(
|
|
149
158
|
"Write generated image outside safe roots?",
|
|
150
159
|
[
|
|
151
|
-
"The generated
|
|
160
|
+
"The generated image will be copied to this external path without overwriting an existing file:",
|
|
152
161
|
"",
|
|
153
162
|
target,
|
|
154
163
|
"",
|
|
@@ -160,7 +169,7 @@ export async function prepareExplicitOutputPath(args: {
|
|
|
160
169
|
if (!approved) {
|
|
161
170
|
throw new ImageGenerationError("output-path-declined", "External output path was declined.");
|
|
162
171
|
}
|
|
163
|
-
return { path: target };
|
|
172
|
+
return { path: target, extension };
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
async function publishBytesNoOverwrite(bytes: Uint8Array, target: string): Promise<void> {
|
|
@@ -190,15 +199,18 @@ export async function saveCanonicalImage(args: {
|
|
|
190
199
|
agentDir: string;
|
|
191
200
|
sessionId: string;
|
|
192
201
|
imageCallId: string;
|
|
202
|
+
mimeType?: ImageGenerationMimeType;
|
|
193
203
|
}): Promise<string> {
|
|
194
204
|
const root = getGeneratedImagesRoot(args.agentDir);
|
|
205
|
+
const mimeType = args.mimeType ?? IMAGE_GENERATION_MIME_TYPE;
|
|
206
|
+
const extension = imageFileExtension(mimeType);
|
|
195
207
|
const sessionPart = sanitizePathPart(args.sessionId, "session");
|
|
196
208
|
const imagePart = sanitizePathPart(args.imageCallId, "image_generation");
|
|
197
209
|
const directory = join(root, sessionPart);
|
|
198
210
|
|
|
199
211
|
for (let index = 1; index <= 1000; index += 1) {
|
|
200
212
|
const suffix = index === 1 ? "" : `-${index}`;
|
|
201
|
-
const target = join(directory, `${imagePart}${suffix}
|
|
213
|
+
const target = join(directory, `${imagePart}${suffix}.${extension}`);
|
|
202
214
|
try {
|
|
203
215
|
await publishBytesNoOverwrite(args.bytes, target);
|
|
204
216
|
return target;
|
|
@@ -222,7 +234,16 @@ export async function saveCanonicalImage(args: {
|
|
|
222
234
|
export async function copyImageToExplicitPath(args: {
|
|
223
235
|
bytes: Uint8Array;
|
|
224
236
|
plan: ExplicitOutputPlan;
|
|
237
|
+
mimeType?: ImageGenerationMimeType;
|
|
225
238
|
}): Promise<void> {
|
|
239
|
+
const mimeType = args.mimeType ?? IMAGE_GENERATION_MIME_TYPE;
|
|
240
|
+
const extension = args.plan.extension ?? extname(args.plan.path).toLowerCase();
|
|
241
|
+
const expectedExtension = `.${imageFileExtension(mimeType)}`;
|
|
242
|
+
const extensionMatches = extension === expectedExtension ||
|
|
243
|
+
(mimeType === "image/jpeg" && extension === ".jpeg");
|
|
244
|
+
if (!extensionMatches) {
|
|
245
|
+
throw new Error(`Output path extension ${extension || "(none)"} does not match generated ${mimeType} data.`);
|
|
246
|
+
}
|
|
226
247
|
const currentTarget = await resolveWriteTarget(args.plan.path);
|
|
227
248
|
if (currentTarget !== args.plan.path) {
|
|
228
249
|
throw new Error("Output path changed after approval.");
|
|
@@ -60,13 +60,15 @@ function dataUrl(reference: PreparedReferenceImage): string {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
function jsonRequestBody(runtime: ResponsesRuntime, body: ImageGenerationRequestBody): Record<string, unknown> {
|
|
63
|
-
const common = {
|
|
63
|
+
const common: Record<string, unknown> = {
|
|
64
64
|
model: body.model,
|
|
65
65
|
prompt: body.prompt,
|
|
66
66
|
n: body.n,
|
|
67
67
|
size: body.size,
|
|
68
|
-
quality: body.quality,
|
|
69
68
|
};
|
|
69
|
+
// `auto` is the Images API default, but some compatible gateways (including
|
|
70
|
+
// Grok image routes) reject the literal value instead of treating it as default.
|
|
71
|
+
if (body.quality !== "auto") common.quality = body.quality;
|
|
70
72
|
if (body.action === "edit") {
|
|
71
73
|
return {
|
|
72
74
|
...common,
|
|
@@ -84,7 +86,7 @@ function multipartRequestBody(body: ImageGenerationRequestBody): FormData {
|
|
|
84
86
|
form.append("prompt", body.prompt);
|
|
85
87
|
form.append("n", String(body.n));
|
|
86
88
|
form.append("size", body.size);
|
|
87
|
-
form.append("quality", body.quality);
|
|
89
|
+
if (body.quality !== "auto") form.append("quality", body.quality);
|
|
88
90
|
form.append("output_format", "png");
|
|
89
91
|
form.append("response_format", "b64_json");
|
|
90
92
|
for (const [index, reference] of body.references.entries()) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { VIRTUAL_MODEL_API } from "../runtime";
|
|
1
2
|
import type { ImageGenerationConfig } from "../types";
|
|
2
3
|
import { IMAGE_GENERATION_CAPABLE_APIS, type ImageGenerationModel } from "./types";
|
|
3
4
|
|
|
@@ -9,10 +10,14 @@ import { IMAGE_GENERATION_CAPABLE_APIS, type ImageGenerationModel } from "./type
|
|
|
9
10
|
export function isImageGenerationEnabledForModel(
|
|
10
11
|
model: ImageGenerationModel | undefined,
|
|
11
12
|
config: ImageGenerationConfig,
|
|
13
|
+
effectiveModel: ImageGenerationModel | undefined = model,
|
|
12
14
|
): boolean {
|
|
13
|
-
return
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
if (!config.enabled) return false;
|
|
16
|
+
// A virtual model is only a candidate until execution resolves its physical
|
|
17
|
+
// model. The service performs that resolution and fails closed if it is not
|
|
18
|
+
// available; this keeps the tool discoverable instead of silently hiding a
|
|
19
|
+
// valid virtual route at startup.
|
|
20
|
+
if (model?.api === VIRTUAL_MODEL_API && effectiveModel === model) return true;
|
|
21
|
+
return typeof effectiveModel?.api === "string" &&
|
|
22
|
+
(IMAGE_GENERATION_CAPABLE_APIS as readonly string[]).includes(effectiveModel.api);
|
|
18
23
|
}
|
|
@@ -2,6 +2,7 @@ import { StringEnum, Type, type Static } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { loadToolkitConfig, resolveToolkitConfig } from "../config";
|
|
4
4
|
import { notifyConfigIssues } from "../config/notifications";
|
|
5
|
+
import { registerPhysicalModelObserver } from "../runtime";
|
|
5
6
|
import { executeImageGeneration } from "./service";
|
|
6
7
|
import { isImageGenerationEnabledForModel } from "./eligibility";
|
|
7
8
|
import { renderImageGenerationResult } from "./render";
|
|
@@ -54,13 +55,13 @@ const GenerateImageParameters = Type.Object(
|
|
|
54
55
|
],
|
|
55
56
|
{
|
|
56
57
|
description:
|
|
57
|
-
"Use null when the user did not explicitly request a destination; otherwise provide
|
|
58
|
+
"Use null when the user did not explicitly request a destination; otherwise provide an explicit .png, .jpg, .jpeg, or .webp file path matching the provider output format.",
|
|
58
59
|
},
|
|
59
60
|
),
|
|
60
61
|
),
|
|
61
62
|
size: Type.Optional(
|
|
62
63
|
StringEnum(IMAGE_GENERATION_SIZES, {
|
|
63
|
-
description: "Requested
|
|
64
|
+
description: "Requested raster-image dimensions, or auto.",
|
|
64
65
|
}),
|
|
65
66
|
),
|
|
66
67
|
quality: Type.Optional(
|
|
@@ -123,13 +124,14 @@ export function registerImageGenerationExtension(
|
|
|
123
124
|
): void {
|
|
124
125
|
if (registeredApis.has(pi)) return;
|
|
125
126
|
registeredApis.add(pi);
|
|
127
|
+
registerPhysicalModelObserver(pi);
|
|
126
128
|
|
|
127
129
|
pi.registerTool<typeof GenerateImageParameters, ImageGenerationToolDetails, Record<string, never>>({
|
|
128
130
|
name: IMAGE_GENERATION_TOOL_NAME,
|
|
129
131
|
label: "OpenAI Generate Image",
|
|
130
132
|
description:
|
|
131
|
-
"Generate
|
|
132
|
-
promptSnippet: "Generate or edit
|
|
133
|
+
"Generate raster images, or edit from one to five user-approved local reference images, through an independent Images API request using a configured image model. PNG, JPEG, and WebP responses are validated and saved in their actual format. This is a paid provider operation; batchSize runs independent requests concurrently.",
|
|
134
|
+
promptSnippet: "Generate or edit raster images through an independent Images API request and a configured imageGeneration.allowedModels entry.",
|
|
133
135
|
promptGuidelines: [
|
|
134
136
|
"Use openai_generate_image when the user explicitly asks to create, draw, render, or edit a raster image and the active model speaks a Responses API.",
|
|
135
137
|
"Do not call openai_generate_image speculatively: it consumes the user's provider or gateway image quota.",
|