pi-openai-toolkit 0.14.3 → 0.14.5
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 +135 -175
- package/README.zh.md +135 -175
- package/docs/internals.md +73 -0
- package/extensions/auto-mode.ts +1 -1
- package/extensions/codex-astra.ts +1 -1
- package/extensions/compaction.ts +1 -1
- package/extensions/image-generation.ts +1 -1
- package/extensions/web-search.ts +1 -1
- package/package.json +127 -126
- package/src/config.ts +621 -582
- package/src/context-management/codex-provider.ts +260 -262
- package/src/context-management/history-notes.ts +325 -319
- package/src/context-management/tools.ts +5 -3
- package/src/context-management/types.ts +161 -160
- package/src/extension-runtime.ts +9 -5
- package/src/image-generation/extension.ts +19 -2
- package/src/image-generation/protocol.ts +473 -405
- package/src/image-generation/service.ts +249 -243
- package/src/image-generation/types.ts +189 -184
- package/src/responses-headers.ts +108 -108
- package/src/runtime.ts +477 -477
- package/src/types.ts +18 -0
- package/src/web-search/prompt.ts +1 -1
|
@@ -1,262 +1,260 @@
|
|
|
1
|
-
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { mergeProviderHeaders } from "../provider-headers";
|
|
4
|
-
import { CODEX_CLIENT_VERSION } from "../responses-headers";
|
|
5
|
-
import { normalizeBaseUrl } from "../runtime";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const CODEX_PROVIDER = "openai-codex";
|
|
14
|
-
const CODEX_API = "openai-codex-responses";
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
): boolean {
|
|
36
|
-
//
|
|
37
|
-
// (
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (pathname.endsWith("/
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (pathname === "
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
headers.set("
|
|
149
|
-
headers.set("
|
|
150
|
-
headers.set("
|
|
151
|
-
headers.set("
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
headers.
|
|
155
|
-
headers.delete("
|
|
156
|
-
headers.
|
|
157
|
-
headers.set("
|
|
158
|
-
headers.set("
|
|
159
|
-
headers.set("
|
|
160
|
-
headers.set("X-Codex-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
headers.set("
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
if (!
|
|
232
|
-
const
|
|
233
|
-
if (!
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
userAgent,
|
|
262
|
-
};
|
|
1
|
+
import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { mergeProviderHeaders } from "../provider-headers";
|
|
4
|
+
import { CODEX_CLIENT_VERSION } from "../responses-headers";
|
|
5
|
+
import { normalizeBaseUrl } from "../runtime";
|
|
6
|
+
import { isExactModelAllowed } from "../model-scope";
|
|
7
|
+
import {
|
|
8
|
+
isNonEmptyString,
|
|
9
|
+
type CodexContextProvider,
|
|
10
|
+
type CodexContextProviderResolution,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
const CODEX_PROVIDER = "openai-codex";
|
|
14
|
+
const CODEX_API = "openai-codex-responses";
|
|
15
|
+
const GATEWAY_API = "openai-responses";
|
|
16
|
+
const CODEX_TOOL_ORIGINATOR = "codex_cli_rs";
|
|
17
|
+
const CODEX_GATEWAY_ORIGINATOR = "codex_cli_rs";
|
|
18
|
+
const CODEX_TOOL_VERSION = "0.0.0";
|
|
19
|
+
const CODEX_SESSION_ID_MAX_LENGTH = 64;
|
|
20
|
+
|
|
21
|
+
export const CODEX_AFFINITY_SCOPE = "codex-session-v1";
|
|
22
|
+
|
|
23
|
+
export const CODEX_CONTEXT_PROVIDER_ERROR =
|
|
24
|
+
"Remote Context management requires a configured Codex-compatible backend";
|
|
25
|
+
|
|
26
|
+
export function isNativeCodexModel(
|
|
27
|
+
model: ExtensionContext["model"] | undefined,
|
|
28
|
+
): boolean {
|
|
29
|
+
return model?.provider === CODEX_PROVIDER && model.api === CODEX_API;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isCodexGatewayModel(
|
|
33
|
+
model: ExtensionContext["model"] | undefined,
|
|
34
|
+
gatewayModels: readonly string[] = [],
|
|
35
|
+
): boolean {
|
|
36
|
+
// Gateway coverage is an operator allowlist of exact "provider/model" keys
|
|
37
|
+
// (compaction.gatewayContextModels). The backend relay must actually pass
|
|
38
|
+
// hosted window markers through; models outside the list stay on remote
|
|
39
|
+
// compaction v2.
|
|
40
|
+
if (!model || model.api !== GATEWAY_API) return false;
|
|
41
|
+
return isExactModelAllowed(model, gatewayModels);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function normalizeCodexBackendBaseUrl(baseUrl: string | undefined | null): string | undefined {
|
|
45
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
46
|
+
if (!normalized) return undefined;
|
|
47
|
+
try {
|
|
48
|
+
const url = new URL(normalized);
|
|
49
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
50
|
+
if (pathname.endsWith("/codex/responses")) {
|
|
51
|
+
url.pathname = pathname.slice(0, -"/responses".length);
|
|
52
|
+
return url.toString().replace(/\/$/, "");
|
|
53
|
+
}
|
|
54
|
+
if (pathname.endsWith("/codex")) return normalized;
|
|
55
|
+
if (pathname.endsWith("/backend-api") || pathname.endsWith("/api")) {
|
|
56
|
+
return `${normalized}/codex`;
|
|
57
|
+
}
|
|
58
|
+
if (pathname === "") return `${normalized}/api/codex`;
|
|
59
|
+
if (pathname === "/") return `${normalized}/api/codex`;
|
|
60
|
+
return undefined;
|
|
61
|
+
} catch {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function headerValue(headers: ProviderHeaders | undefined, name: string): string | undefined {
|
|
67
|
+
const wanted = name.toLowerCase();
|
|
68
|
+
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
69
|
+
if (key.toLowerCase() === wanted && typeof value === "string" && value.trim()) {
|
|
70
|
+
return value.trim();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function bearerToken(value: string | undefined): string | undefined {
|
|
77
|
+
if (!value) return undefined;
|
|
78
|
+
const match = value.trim().match(/^Bearer\s+(.+)$/i);
|
|
79
|
+
return (match?.[1] ?? value).trim() || undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
83
|
+
const parts = token.split(".");
|
|
84
|
+
if (parts.length !== 3) return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const value: unknown = JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf8"));
|
|
87
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
88
|
+
? value as Record<string, unknown>
|
|
89
|
+
: undefined;
|
|
90
|
+
} catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function accountIdFromToken(token: string): string | undefined {
|
|
96
|
+
const payload = decodeJwtPayload(token);
|
|
97
|
+
const direct = payload?.chatgpt_account_id;
|
|
98
|
+
if (isNonEmptyString(direct)) return direct.trim();
|
|
99
|
+
const auth = payload?.["https://api.openai.com/auth"];
|
|
100
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth)) return undefined;
|
|
101
|
+
const nested = (auth as Record<string, unknown>).chatgpt_account_id;
|
|
102
|
+
return isNonEmptyString(nested) ? nested.trim() : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function userAgent(version: string): string {
|
|
106
|
+
const platform = typeof process !== "undefined" ? process.platform : "unknown";
|
|
107
|
+
const arch = typeof process !== "undefined" ? process.arch : "unknown";
|
|
108
|
+
return `codex_cli_rs/${version} (${platform}; ${arch})`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clampSessionId(value: string | undefined): string | undefined {
|
|
112
|
+
if (!value) return undefined;
|
|
113
|
+
const chars = Array.from(value.trim());
|
|
114
|
+
if (chars.length === 0) return undefined;
|
|
115
|
+
return chars.length <= CODEX_SESSION_ID_MAX_LENGTH
|
|
116
|
+
? chars.join("")
|
|
117
|
+
: chars.slice(0, CODEX_SESSION_ID_MAX_LENGTH).join("");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function removeSensitiveGatewayHeaders(headers: ProviderHeaders): Record<string, string> {
|
|
121
|
+
const result: Record<string, string> = {};
|
|
122
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
123
|
+
if (typeof value !== "string") continue;
|
|
124
|
+
const lower = name.toLowerCase();
|
|
125
|
+
if (lower === "authorization" || lower === "cookie" || lower === "chatgpt-account-id" || lower === "x-api-key") {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
result[name] = value;
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export type CodexContextProviderHeaderOptions = {
|
|
134
|
+
sessionId?: string;
|
|
135
|
+
clientRequestId?: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export function codexContextProviderHeaders(
|
|
139
|
+
provider: CodexContextProvider,
|
|
140
|
+
options: CodexContextProviderHeaderOptions = {},
|
|
141
|
+
): Headers {
|
|
142
|
+
const headers = new Headers();
|
|
143
|
+
for (const [name, value] of Object.entries(provider.headers)) {
|
|
144
|
+
if (typeof value === "string") headers.set(name, value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (provider.kind === "native-codex") {
|
|
148
|
+
headers.set("Authorization", `Bearer ${provider.token}`);
|
|
149
|
+
headers.set("ChatGPT-Account-ID", provider.accountId);
|
|
150
|
+
headers.set("originator", CODEX_TOOL_ORIGINATOR);
|
|
151
|
+
headers.set("User-Agent", userAgent(CODEX_TOOL_VERSION));
|
|
152
|
+
headers.set("version", CODEX_TOOL_VERSION);
|
|
153
|
+
} else {
|
|
154
|
+
headers.set("Authorization", `Bearer ${provider.apiKey}`);
|
|
155
|
+
headers.delete("ChatGPT-Account-ID");
|
|
156
|
+
headers.delete("Cookie");
|
|
157
|
+
headers.set("originator", CODEX_GATEWAY_ORIGINATOR);
|
|
158
|
+
headers.set("User-Agent", userAgent(CODEX_CLIENT_VERSION));
|
|
159
|
+
headers.set("version", CODEX_CLIENT_VERSION);
|
|
160
|
+
headers.set("X-Codex-Affinity-Scope", CODEX_AFFINITY_SCOPE);
|
|
161
|
+
headers.set("X-Codex-Model", provider.model);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const sessionId = clampSessionId(options.sessionId);
|
|
165
|
+
if (sessionId) {
|
|
166
|
+
headers.set("Session-Id", sessionId);
|
|
167
|
+
headers.set("X-Client-Request-Id", options.clientRequestId?.trim() || sessionId);
|
|
168
|
+
}
|
|
169
|
+
headers.set("content-type", "application/json");
|
|
170
|
+
return headers;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function resolveCodexContextProvider(
|
|
174
|
+
ctx: ExtensionContext,
|
|
175
|
+
modelOverride: ExtensionContext["model"] = ctx.model,
|
|
176
|
+
gatewayModels: readonly string[] = [],
|
|
177
|
+
): Promise<CodexContextProviderResolution> {
|
|
178
|
+
const model = modelOverride;
|
|
179
|
+
const descriptor = {
|
|
180
|
+
provider: model?.provider,
|
|
181
|
+
api: model?.api,
|
|
182
|
+
model: model?.id,
|
|
183
|
+
baseUrl: normalizeBaseUrl(model?.baseUrl),
|
|
184
|
+
};
|
|
185
|
+
if (!model) return { ok: false, reason: "unsupported-model" };
|
|
186
|
+
|
|
187
|
+
const isNative = isNativeCodexModel(model);
|
|
188
|
+
const isGateway = isCodexGatewayModel(model, gatewayModels);
|
|
189
|
+
if (!isNative && !isGateway) {
|
|
190
|
+
return { ok: false, reason: "unsupported-model", ...descriptor };
|
|
191
|
+
}
|
|
192
|
+
if (!isNative && model.api !== GATEWAY_API) {
|
|
193
|
+
return { ok: false, reason: "unsupported-api", ...descriptor };
|
|
194
|
+
}
|
|
195
|
+
if (isNative && model.api !== CODEX_API) {
|
|
196
|
+
return { ok: false, reason: "unsupported-api", ...descriptor };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let auth: Awaited<ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>>;
|
|
200
|
+
try {
|
|
201
|
+
auth = await ctx.modelRegistry.getApiKeyAndHeaders(model as Model<Api>);
|
|
202
|
+
} catch {
|
|
203
|
+
return { ok: false, reason: "auth-resolution-failed", ...descriptor };
|
|
204
|
+
}
|
|
205
|
+
if (!auth.ok) return { ok: false, reason: "auth-resolution-failed", ...descriptor };
|
|
206
|
+
|
|
207
|
+
const rawBaseUrl = normalizeBaseUrl(auth.baseUrl) ?? normalizeBaseUrl(model.baseUrl);
|
|
208
|
+
if (!rawBaseUrl) return { ok: false, reason: "missing-base-url", ...descriptor };
|
|
209
|
+
|
|
210
|
+
if (isGateway) {
|
|
211
|
+
const apiKey = bearerToken(auth.apiKey) ?? bearerToken(headerValue(auth.headers, "authorization"));
|
|
212
|
+
if (!apiKey) return { ok: false, reason: "missing-api-key", ...descriptor, baseUrl: rawBaseUrl };
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
provider: {
|
|
216
|
+
kind: "codex-gateway",
|
|
217
|
+
route: "codex-gateway",
|
|
218
|
+
provider: model.provider,
|
|
219
|
+
api: GATEWAY_API,
|
|
220
|
+
model: model.id,
|
|
221
|
+
baseUrl: rawBaseUrl,
|
|
222
|
+
apiKey,
|
|
223
|
+
headers: removeSensitiveGatewayHeaders(mergeProviderHeaders(model.headers, auth.headers)),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const token = bearerToken(auth.apiKey) ?? bearerToken(headerValue(auth.headers, "authorization"));
|
|
229
|
+
if (!token) return { ok: false, reason: "missing-token", ...descriptor };
|
|
230
|
+
const accountId = headerValue(auth.headers, "chatgpt-account-id") ?? accountIdFromToken(token);
|
|
231
|
+
if (!accountId) return { ok: false, reason: "missing-account-id", ...descriptor };
|
|
232
|
+
const baseUrl = normalizeCodexBackendBaseUrl(rawBaseUrl);
|
|
233
|
+
if (!baseUrl) return { ok: false, reason: "unsupported-backend", ...descriptor };
|
|
234
|
+
|
|
235
|
+
const headers = mergeProviderHeaders(model.headers, auth.headers);
|
|
236
|
+
return {
|
|
237
|
+
ok: true,
|
|
238
|
+
provider: {
|
|
239
|
+
kind: "native-codex",
|
|
240
|
+
route: "openai-codex",
|
|
241
|
+
provider: CODEX_PROVIDER,
|
|
242
|
+
api: CODEX_API,
|
|
243
|
+
model: model.id,
|
|
244
|
+
baseUrl,
|
|
245
|
+
token,
|
|
246
|
+
accountId,
|
|
247
|
+
headers,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const _codexProviderTest = {
|
|
253
|
+
accountIdFromToken,
|
|
254
|
+
bearerToken,
|
|
255
|
+
clampSessionId,
|
|
256
|
+
decodeJwtPayload,
|
|
257
|
+
headerValue,
|
|
258
|
+
isCodexGatewayModel,
|
|
259
|
+
userAgent,
|
|
260
|
+
};
|