@opengeni/codex 0.2.9 → 0.2.11
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/dist/api-client.d.ts +2 -0
- package/dist/{chunk-YGKQDUY7.js → chunk-HT463QWA.js} +10 -2
- package/dist/chunk-HT463QWA.js.map +1 -0
- package/dist/chunk-MFVGCR3N.js +236 -0
- package/dist/chunk-MFVGCR3N.js.map +1 -0
- package/dist/constants.d.ts +5 -1
- package/dist/constants.js +9 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +350 -10
- package/dist/index.js.map +1 -1
- package/dist/mcp-sanitize.d.ts +4 -3
- package/dist/opaque-artifact.d.ts +4 -0
- package/dist/realtime-v3.d.ts +86 -0
- package/dist/realtime-v3.js +25 -0
- package/dist/realtime-v3.js.map +1 -0
- package/dist/realtime.d.ts +48 -0
- package/dist/request-context.d.ts +6 -0
- package/package.json +5 -1
- package/src/api-client.ts +26 -9
- package/src/constants.ts +8 -1
- package/src/fetch.ts +4 -0
- package/src/index.ts +3 -0
- package/src/mcp-sanitize.ts +5 -4
- package/src/normalize.ts +1 -1
- package/src/opaque-artifact.ts +29 -0
- package/src/realtime-v3.ts +349 -0
- package/src/realtime.ts +356 -0
- package/src/request-context.ts +7 -0
- package/dist/chunk-YGKQDUY7.js.map +0 -1
package/dist/mcp-sanitize.d.ts
CHANGED
|
@@ -25,9 +25,10 @@ export declare function remapToolCallRequestBody(body: string, mapper: ToolNameM
|
|
|
25
25
|
* to the MCP server's original. Only the POST request/response is buffered; the
|
|
26
26
|
* long-lived GET notification SSE stream is passed through untouched.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
28
|
+
* An optional `namespaceSink` Set accumulates the ORIGINAL-dotted
|
|
29
29
|
* connector namespaces seen across every tools/list this turn (captured before the
|
|
30
|
-
* dot is sanitized away). The
|
|
31
|
-
* turn
|
|
30
|
+
* dot is sanitized away). The runtime reads the live by-reference Set only to keep
|
|
31
|
+
* this turn's `tool_search` description accurate; it is never persisted or used
|
|
32
|
+
* for inference selection.
|
|
32
33
|
*/
|
|
33
34
|
export declare function codexAppsSanitizingFetch(base?: FetchLike, namespaceSink?: Set<string>): FetchLike;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Stable, content-hiding identity for one opaque provider artifact. */
|
|
2
|
+
export declare function opaqueProviderArtifactFingerprint(item: unknown): string | null;
|
|
3
|
+
/** Exact opaque artifacts present in one normalized provider input array. */
|
|
4
|
+
export declare function opaqueProviderArtifactFingerprints(input: unknown): string[];
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex GPT-Live Frameless Bidi (V3) wire adapter.
|
|
3
|
+
*
|
|
4
|
+
* These shapes are pinned to openai/codex@fa1d4c40. V3 delegates work with
|
|
5
|
+
* `delegation.created`; it does not expose the ordinary Realtime V2
|
|
6
|
+
* `function_call` protocol.
|
|
7
|
+
*/
|
|
8
|
+
export declare const CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT = 128;
|
|
9
|
+
export declare const CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS = 8192;
|
|
10
|
+
export declare const CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES = 500;
|
|
11
|
+
export declare const CODEX_REALTIME_V3_MAX_EVENT_BYTES = 1048576;
|
|
12
|
+
export declare const CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES = 1024;
|
|
13
|
+
export declare const CODEX_REALTIME_V3_MAX_TEXT_BYTES = 131072;
|
|
14
|
+
export type CodexRealtimeInitialItem = {
|
|
15
|
+
role: "user" | "developer" | "assistant";
|
|
16
|
+
text: string;
|
|
17
|
+
};
|
|
18
|
+
type ProviderEventIdentity = {
|
|
19
|
+
providerEventId: string | null;
|
|
20
|
+
};
|
|
21
|
+
export type CodexRealtimeV3Event = (ProviderEventIdentity & {
|
|
22
|
+
type: "session.started" | "session.updated";
|
|
23
|
+
sessionId: string;
|
|
24
|
+
instructions: string | null;
|
|
25
|
+
}) | (ProviderEventIdentity & {
|
|
26
|
+
type: "input_transcript.added" | "output_transcript.added";
|
|
27
|
+
itemId: string | null;
|
|
28
|
+
text: string;
|
|
29
|
+
}) | (ProviderEventIdentity & {
|
|
30
|
+
type: "output_audio.delta";
|
|
31
|
+
audio: string;
|
|
32
|
+
startMs: number | null;
|
|
33
|
+
endMs: number | null;
|
|
34
|
+
}) | (ProviderEventIdentity & {
|
|
35
|
+
type: "turn.done";
|
|
36
|
+
turnId: string;
|
|
37
|
+
role: "user" | "assistant";
|
|
38
|
+
transcript: string;
|
|
39
|
+
}) | (ProviderEventIdentity & {
|
|
40
|
+
type: "delegation.created";
|
|
41
|
+
delegationItemId: string;
|
|
42
|
+
inputTranscript: string;
|
|
43
|
+
offsetMs: number | null;
|
|
44
|
+
}) | (ProviderEventIdentity & {
|
|
45
|
+
type: "error";
|
|
46
|
+
message: string;
|
|
47
|
+
});
|
|
48
|
+
export type CodexRealtimeV3ParseFailure = {
|
|
49
|
+
ok: false;
|
|
50
|
+
reason: "invalid_json" | "missing_type" | "unsupported_type" | "invalid_shape" | "oversized_event" | "oversized_field";
|
|
51
|
+
eventType: string | null;
|
|
52
|
+
};
|
|
53
|
+
export type CodexRealtimeV3ParseResult = {
|
|
54
|
+
ok: true;
|
|
55
|
+
event: CodexRealtimeV3Event;
|
|
56
|
+
} | CodexRealtimeV3ParseFailure;
|
|
57
|
+
export type CodexRealtimeV3ContextAppendChannel = "speakable" | "commentary";
|
|
58
|
+
export type CodexRealtimeV3DelegationContextAppend = {
|
|
59
|
+
type: "delegation.context.append";
|
|
60
|
+
delegation_item_id: string;
|
|
61
|
+
channel?: CodexRealtimeV3ContextAppendChannel | undefined;
|
|
62
|
+
content: Array<{
|
|
63
|
+
type: "input_text";
|
|
64
|
+
text: string;
|
|
65
|
+
}>;
|
|
66
|
+
};
|
|
67
|
+
export type CodexRealtimeV3SessionContextAppend = {
|
|
68
|
+
type: "session.context.append";
|
|
69
|
+
channel?: CodexRealtimeV3ContextAppendChannel | undefined;
|
|
70
|
+
content: Array<{
|
|
71
|
+
type: "input_text";
|
|
72
|
+
text: string;
|
|
73
|
+
}>;
|
|
74
|
+
};
|
|
75
|
+
export declare function parseCodexRealtimeV3Event(payload: string): CodexRealtimeV3ParseResult;
|
|
76
|
+
export declare function encodeCodexRealtimeV3DelegationContextAppend(input: {
|
|
77
|
+
delegationItemId: string;
|
|
78
|
+
text: string;
|
|
79
|
+
channel?: CodexRealtimeV3ContextAppendChannel | undefined;
|
|
80
|
+
}): CodexRealtimeV3DelegationContextAppend[];
|
|
81
|
+
export declare function encodeCodexRealtimeV3SessionContextAppend(input: {
|
|
82
|
+
text: string;
|
|
83
|
+
channel?: CodexRealtimeV3ContextAppendChannel | undefined;
|
|
84
|
+
}): CodexRealtimeV3SessionContextAppend[];
|
|
85
|
+
export declare function contextAppendChunks(text: string): string[];
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES,
|
|
3
|
+
CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT,
|
|
4
|
+
CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS,
|
|
5
|
+
CODEX_REALTIME_V3_MAX_EVENT_BYTES,
|
|
6
|
+
CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
|
|
7
|
+
CODEX_REALTIME_V3_MAX_TEXT_BYTES,
|
|
8
|
+
contextAppendChunks,
|
|
9
|
+
encodeCodexRealtimeV3DelegationContextAppend,
|
|
10
|
+
encodeCodexRealtimeV3SessionContextAppend,
|
|
11
|
+
parseCodexRealtimeV3Event
|
|
12
|
+
} from "./chunk-MFVGCR3N.js";
|
|
13
|
+
export {
|
|
14
|
+
CODEX_REALTIME_CONTEXT_APPEND_MAX_BYTES,
|
|
15
|
+
CODEX_REALTIME_INITIAL_ITEMS_MAX_COUNT,
|
|
16
|
+
CODEX_REALTIME_INITIAL_ITEMS_MAX_TOKENS,
|
|
17
|
+
CODEX_REALTIME_V3_MAX_EVENT_BYTES,
|
|
18
|
+
CODEX_REALTIME_V3_MAX_IDENTIFIER_BYTES,
|
|
19
|
+
CODEX_REALTIME_V3_MAX_TEXT_BYTES,
|
|
20
|
+
contextAppendChunks,
|
|
21
|
+
encodeCodexRealtimeV3DelegationContextAppend,
|
|
22
|
+
encodeCodexRealtimeV3SessionContextAppend,
|
|
23
|
+
parseCodexRealtimeV3Event
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=realtime-v3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type CodexAuthHeaders } from "./api-client";
|
|
2
|
+
import { CODEX_REALTIME_MODEL, CODEX_REALTIME_VERSION } from "./constants";
|
|
3
|
+
import type { CodexFetch } from "./device-code";
|
|
4
|
+
import { type CodexRealtimeInitialItem } from "./realtime-v3";
|
|
5
|
+
export declare const CODEX_REALTIME_VOICES: readonly ["juniper", "maple", "spruce", "ember", "vale", "breeze", "arbor", "sol", "cove"];
|
|
6
|
+
export type CodexRealtimeVoice = (typeof CODEX_REALTIME_VOICES)[number];
|
|
7
|
+
export type CodexRealtimeCallInput = {
|
|
8
|
+
/** Browser-created WebRTC offer. It must negotiate an audio media section. */
|
|
9
|
+
sdp: string;
|
|
10
|
+
/** This transport intentionally supports only Codex's Frameless/V3 protocol. */
|
|
11
|
+
version: typeof CODEX_REALTIME_VERSION;
|
|
12
|
+
/** Server-owned session/thread binding; sent upstream but never returned. */
|
|
13
|
+
sessionId: string;
|
|
14
|
+
/** Server-projected ordinary-session history for Frameless V3 bootstrap. */
|
|
15
|
+
initialItems?: CodexRealtimeInitialItem[] | undefined;
|
|
16
|
+
instructions?: string | undefined;
|
|
17
|
+
voice?: CodexRealtimeVoice | undefined;
|
|
18
|
+
};
|
|
19
|
+
export type CodexRealtimeCallResult = {
|
|
20
|
+
sdp: string;
|
|
21
|
+
version: typeof CODEX_REALTIME_VERSION;
|
|
22
|
+
model: typeof CODEX_REALTIME_MODEL;
|
|
23
|
+
};
|
|
24
|
+
export type CodexRealtimeErrorCode = "invalid_request" | "incompatible" | "authentication" | "entitlement" | "rate_limited" | "provider" | "invalid_response" | "network" | "timeout" | "cancelled";
|
|
25
|
+
/** Safe provider failure: it contains no response body, credential, or account identity. */
|
|
26
|
+
export declare class CodexRealtimeError extends Error {
|
|
27
|
+
readonly code: CodexRealtimeErrorCode;
|
|
28
|
+
readonly providerStatus: number | null;
|
|
29
|
+
constructor(code: CodexRealtimeErrorCode, message: string, providerStatus?: number | null);
|
|
30
|
+
}
|
|
31
|
+
export type CodexRealtimeCallOptions = {
|
|
32
|
+
signal?: AbortSignal | undefined;
|
|
33
|
+
timeoutMs?: number | undefined;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Create one native subscription-authenticated Codex GPT-Live V3 WebRTC call.
|
|
37
|
+
*
|
|
38
|
+
* There is deliberately no API-key or WebSocket fallback and no transport
|
|
39
|
+
* retry. A caller may refresh the same connected subscription after one 401,
|
|
40
|
+
* but this adapter always performs exactly one provider request.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createCodexRealtimeCall(auth: CodexAuthHeaders, input: CodexRealtimeCallInput, fetchImpl?: CodexFetch, options?: CodexRealtimeCallOptions): Promise<CodexRealtimeCallResult>;
|
|
43
|
+
/** Shared pin→active selection for worker turns and direct realtime calls. */
|
|
44
|
+
export declare function selectCodexCredentialId(args: {
|
|
45
|
+
sessionPinnedCredentialId: string | null;
|
|
46
|
+
activeCredentialId: string | null;
|
|
47
|
+
connectedIds: ReadonlySet<string>;
|
|
48
|
+
}): string | null;
|
|
@@ -49,6 +49,10 @@ export type CodexModelRequestEvent = {
|
|
|
49
49
|
status?: number;
|
|
50
50
|
willRetry?: boolean;
|
|
51
51
|
};
|
|
52
|
+
export type CodexRequestOpaqueArtifacts = {
|
|
53
|
+
requestId: string;
|
|
54
|
+
fingerprints: readonly string[];
|
|
55
|
+
};
|
|
52
56
|
export type CodexRequestContext = {
|
|
53
57
|
clientVersion: string;
|
|
54
58
|
/**
|
|
@@ -81,6 +85,8 @@ export type CodexRequestContext = {
|
|
|
81
85
|
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
82
86
|
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
83
87
|
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
88
|
+
/** Exact opaque artifacts on the normalized wire request, never their ciphertext. */
|
|
89
|
+
onRequestOpaqueArtifacts?: (artifacts: CodexRequestOpaqueArtifacts) => void;
|
|
84
90
|
/** Stable request identity supplied by the owning durable execution. */
|
|
85
91
|
nextRequestId?: () => string;
|
|
86
92
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/codex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"./constants": {
|
|
26
26
|
"types": "./dist/constants.d.ts",
|
|
27
27
|
"import": "./dist/constants.js"
|
|
28
|
+
},
|
|
29
|
+
"./realtime-v3": {
|
|
30
|
+
"types": "./dist/realtime-v3.d.ts",
|
|
31
|
+
"import": "./dist/realtime-v3.js"
|
|
28
32
|
}
|
|
29
33
|
},
|
|
30
34
|
"publishConfig": {
|
package/src/api-client.ts
CHANGED
|
@@ -28,7 +28,8 @@ export type ResetCreditFetchFailureReason =
|
|
|
28
28
|
| "network_error"
|
|
29
29
|
| "timeout";
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/** Server-only headers shared by every ChatGPT/Codex subscription transport. */
|
|
32
|
+
export function codexSubscriptionHeaders(a: CodexAuthHeaders): Record<string, string> {
|
|
32
33
|
return {
|
|
33
34
|
Authorization: `Bearer ${a.accessToken}`,
|
|
34
35
|
...(a.chatgptAccountId ? { "ChatGPT-Account-ID": a.chatgptAccountId } : {}),
|
|
@@ -48,7 +49,7 @@ export async function fetchCodexModels(
|
|
|
48
49
|
const fetched = await runBoundedCodexOperation(async (signal) => {
|
|
49
50
|
const res = await fetchImpl(
|
|
50
51
|
`${CODEX_RESPONSES_BASE}/models?client_version=${encodeURIComponent(a.clientVersion)}`,
|
|
51
|
-
{ method: "GET", headers:
|
|
52
|
+
{ method: "GET", headers: codexSubscriptionHeaders(a), signal },
|
|
52
53
|
);
|
|
53
54
|
if (!res.ok) {
|
|
54
55
|
await res.arrayBuffer().catch(() => undefined);
|
|
@@ -72,7 +73,7 @@ export async function fetchCodexUsage(
|
|
|
72
73
|
const fetched = await runBoundedCodexOperation(async (signal) => {
|
|
73
74
|
const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/usage`, {
|
|
74
75
|
method: "GET",
|
|
75
|
-
headers:
|
|
76
|
+
headers: codexSubscriptionHeaders(a),
|
|
76
77
|
signal,
|
|
77
78
|
});
|
|
78
79
|
// A 404 may carry a usage-limit body; the route layer normalizes it to a limits state (spec §1.8c).
|
|
@@ -102,19 +103,27 @@ export async function fetchCodexRateLimitResetCredits(
|
|
|
102
103
|
const fetched = await runBoundedCodexOperation(async (signal) => {
|
|
103
104
|
const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/rate-limit-reset-credits`, {
|
|
104
105
|
method: "GET",
|
|
105
|
-
headers:
|
|
106
|
+
headers: codexSubscriptionHeaders(a),
|
|
106
107
|
signal,
|
|
107
108
|
});
|
|
108
109
|
if (!res.ok) {
|
|
109
110
|
// Drain the body without retaining/logging it. Provider error bodies may
|
|
110
111
|
// contain account-specific details and are not part of this contract.
|
|
111
112
|
await res.arrayBuffer().catch(() => undefined);
|
|
112
|
-
return {
|
|
113
|
+
return {
|
|
114
|
+
ok: false as const,
|
|
115
|
+
status: res.status,
|
|
116
|
+
reason: "http_error" as const,
|
|
117
|
+
};
|
|
113
118
|
}
|
|
114
119
|
const details = parseCodexRateLimitResetCreditsDetails(await res.json().catch(() => null));
|
|
115
120
|
return details
|
|
116
121
|
? { ok: true as const, status: res.status, details }
|
|
117
|
-
: {
|
|
122
|
+
: {
|
|
123
|
+
ok: false as const,
|
|
124
|
+
status: res.status,
|
|
125
|
+
reason: "invalid_response" as const,
|
|
126
|
+
};
|
|
118
127
|
}, timeoutMs);
|
|
119
128
|
return fetched.ok ? fetched.value : { ok: false, status: 0, reason: fetched.reason };
|
|
120
129
|
}
|
|
@@ -146,7 +155,7 @@ export async function consumeCodexRateLimitResetCredit(
|
|
|
146
155
|
const res = await fetchImpl(`${CODEX_WHAM_BASE}/wham/rate-limit-reset-credits/consume`, {
|
|
147
156
|
method: "POST",
|
|
148
157
|
headers: {
|
|
149
|
-
...
|
|
158
|
+
...codexSubscriptionHeaders(a),
|
|
150
159
|
"content-type": "application/json",
|
|
151
160
|
},
|
|
152
161
|
body: JSON.stringify({
|
|
@@ -157,12 +166,20 @@ export async function consumeCodexRateLimitResetCredit(
|
|
|
157
166
|
});
|
|
158
167
|
if (!res.ok) {
|
|
159
168
|
await res.arrayBuffer().catch(() => undefined);
|
|
160
|
-
return {
|
|
169
|
+
return {
|
|
170
|
+
ok: false as const,
|
|
171
|
+
status: res.status,
|
|
172
|
+
reason: "http_error" as const,
|
|
173
|
+
};
|
|
161
174
|
}
|
|
162
175
|
const result = parseCodexRateLimitResetConsumeResponse(await res.json().catch(() => null));
|
|
163
176
|
return result
|
|
164
177
|
? { ok: true as const, status: res.status, result }
|
|
165
|
-
: {
|
|
178
|
+
: {
|
|
179
|
+
ok: false as const,
|
|
180
|
+
status: res.status,
|
|
181
|
+
reason: "invalid_response" as const,
|
|
182
|
+
};
|
|
166
183
|
}, timeoutMs);
|
|
167
184
|
return fetched.ok ? fetched.value : { ok: false, status: 0, reason: fetched.reason };
|
|
168
185
|
}
|
package/src/constants.ts
CHANGED
|
@@ -52,7 +52,14 @@ export const CODEX_MODEL_AUTO_COMPACT_TOKEN_LIMIT = Math.floor(
|
|
|
52
52
|
// 2026-07-09: 0.142.4 filtered every GPT-5.6 slug out of GET /models, while the
|
|
53
53
|
// official Codex 0.144.0+ releases return all three exact slugs above. Keep
|
|
54
54
|
// this pinned to the latest stable Codex release we have verified end-to-end.
|
|
55
|
-
export const CODEX_CLIENT_VERSION = "0.
|
|
55
|
+
export const CODEX_CLIENT_VERSION = "0.145.0";
|
|
56
|
+
|
|
57
|
+
// Native ChatGPT/Codex subscription WebRTC call creation. Verified against
|
|
58
|
+
// openai/codex 0.145.0 commit bd2de422aa287b97b06ca6425a10935bcf1b3731.
|
|
59
|
+
export const CODEX_REALTIME_MODEL = "gpt-live-1-boulder-alpha";
|
|
60
|
+
export const CODEX_REALTIME_VERSION = "v3";
|
|
61
|
+
export const CODEX_REALTIME_DEFAULT_VOICE = "cove";
|
|
62
|
+
export const CODEX_REALTIME_CALL_TIMEOUT_MS = 15_000;
|
|
56
63
|
|
|
57
64
|
export const CODEX_REFRESH_WINDOW_MS = 5 * 60 * 1000; // proactive refresh when within 5 min of exp (spec §1.1)
|
|
58
65
|
export const CODEX_REFRESH_FALLBACK_MS = 8 * 24 * 60 * 60 * 1000; // 8 days when exp is unparseable
|
package/src/fetch.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { CODEX_ORIGINATOR } from "./constants";
|
|
15
15
|
import { normalizeCodexRequestBody } from "./normalize";
|
|
16
|
+
import { opaqueProviderArtifactFingerprints } from "./opaque-artifact";
|
|
16
17
|
import {
|
|
17
18
|
codexRequestStorage,
|
|
18
19
|
type CodexModelRequestEvent,
|
|
@@ -489,6 +490,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
489
490
|
// summarizer) still gets a single JSON Response back.
|
|
490
491
|
let callerWantsStream = true;
|
|
491
492
|
let model: string | undefined;
|
|
493
|
+
let requestOpaqueArtifacts: string[] = [];
|
|
492
494
|
const nextInit: RequestInit = { ...init, headers };
|
|
493
495
|
if (typeof init?.body === "string") {
|
|
494
496
|
try {
|
|
@@ -497,10 +499,12 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
497
499
|
const normalized = normalizeCodexRequestBody(parsed, ctx.resolveModel);
|
|
498
500
|
model = typeof normalized.model === "string" ? normalized.model : undefined;
|
|
499
501
|
nextInit.body = JSON.stringify(normalized);
|
|
502
|
+
requestOpaqueArtifacts = opaqueProviderArtifactFingerprints(normalized.input);
|
|
500
503
|
} catch {
|
|
501
504
|
/* leave unparseable bodies untouched (already copied from init) */
|
|
502
505
|
}
|
|
503
506
|
}
|
|
507
|
+
ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
|
|
504
508
|
headers.set(
|
|
505
509
|
"Idempotency-Key",
|
|
506
510
|
authenticationAttempt === 0 ? requestId : `${requestId}:auth-${authenticationAttempt}`,
|
package/src/index.ts
CHANGED
package/src/mcp-sanitize.ts
CHANGED
|
@@ -105,7 +105,7 @@ export class ToolNameMapper {
|
|
|
105
105
|
/**
|
|
106
106
|
* Drop bad outputSchemas + sanitize tool names on a JSON-RPC tools/list result, in place.
|
|
107
107
|
*
|
|
108
|
-
*
|
|
108
|
+
* When `namespaceSink` is provided, accumulate each tool's ORIGINAL
|
|
109
109
|
* connector namespace (the segment BEFORE the first dot, e.g. `github` from
|
|
110
110
|
* `github.create_issue`) into it — captured HERE because this pass sees the original
|
|
111
111
|
* dotted name BEFORE mapper.sanitize rewrites the dot away. Only dotted names carry a
|
|
@@ -264,10 +264,11 @@ export function remapToolCallRequestBody(body: string, mapper: ToolNameMapper):
|
|
|
264
264
|
* to the MCP server's original. Only the POST request/response is buffered; the
|
|
265
265
|
* long-lived GET notification SSE stream is passed through untouched.
|
|
266
266
|
*
|
|
267
|
-
*
|
|
267
|
+
* An optional `namespaceSink` Set accumulates the ORIGINAL-dotted
|
|
268
268
|
* connector namespaces seen across every tools/list this turn (captured before the
|
|
269
|
-
* dot is sanitized away). The
|
|
270
|
-
* turn
|
|
269
|
+
* dot is sanitized away). The runtime reads the live by-reference Set only to keep
|
|
270
|
+
* this turn's `tool_search` description accurate; it is never persisted or used
|
|
271
|
+
* for inference selection.
|
|
271
272
|
*/
|
|
272
273
|
export function codexAppsSanitizingFetch(
|
|
273
274
|
base: FetchLike = globalThis.fetch,
|
package/src/normalize.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function normalizeCodexRequestBody(
|
|
|
69
69
|
// strip every item id; PRESERVE call_id. spec §1.6 / verdict §0(b)
|
|
70
70
|
// (This also covers tool_search items: the backend accepts an id-less
|
|
71
71
|
// tool_search_call/output pair correlated by call_id — verified live — and
|
|
72
|
-
// stripping the
|
|
72
|
+
// stripping the provider-stored `tsc_…` id here sanitizes BOTH replay paths.)
|
|
73
73
|
if (Array.isArray(body.input)) {
|
|
74
74
|
for (const item of body.input as unknown[]) {
|
|
75
75
|
if (!item || typeof item !== "object") {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Stable, content-hiding identity for one opaque provider artifact. */
|
|
4
|
+
export function opaqueProviderArtifactFingerprint(item: unknown): string | null {
|
|
5
|
+
if (!item || typeof item !== "object") return null;
|
|
6
|
+
const record = item as Record<string, unknown>;
|
|
7
|
+
if (record.type !== "reasoning" && record.type !== "compaction") return null;
|
|
8
|
+
const providerData =
|
|
9
|
+
record.providerData && typeof record.providerData === "object"
|
|
10
|
+
? (record.providerData as Record<string, unknown>)
|
|
11
|
+
: null;
|
|
12
|
+
const ciphertext =
|
|
13
|
+
(typeof record.encrypted_content === "string" && record.encrypted_content) ||
|
|
14
|
+
(typeof record.encryptedContent === "string" && record.encryptedContent) ||
|
|
15
|
+
(typeof providerData?.encrypted_content === "string" && providerData.encrypted_content) ||
|
|
16
|
+
(typeof providerData?.encryptedContent === "string" && providerData.encryptedContent) ||
|
|
17
|
+
null;
|
|
18
|
+
if (!ciphertext) return null;
|
|
19
|
+
return `${record.type}:${createHash("sha256").update(ciphertext).digest("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Exact opaque artifacts present in one normalized provider input array. */
|
|
23
|
+
export function opaqueProviderArtifactFingerprints(input: unknown): string[] {
|
|
24
|
+
if (!Array.isArray(input)) return [];
|
|
25
|
+
return input.flatMap((item) => {
|
|
26
|
+
const fingerprint = opaqueProviderArtifactFingerprint(item);
|
|
27
|
+
return fingerprint ? [fingerprint] : [];
|
|
28
|
+
});
|
|
29
|
+
}
|