pi-openai-toolkit 0.23.2 → 0.24.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 +4 -15
- package/README.zh.md +4 -15
- package/config.schema.json +5 -19
- package/docs/configuration.md +19 -21
- package/docs/internals.md +34 -120
- package/package.json +3 -16
- package/src/config/legacy.ts +12 -62
- package/src/config/migration.ts +19 -7
- package/src/config/notifications.ts +10 -4
- package/src/config/policy.ts +9 -10
- package/src/config/v2.ts +12 -51
- package/src/config.ts +20 -10
- package/src/extension-runtime.ts +163 -987
- package/src/native-fallback.ts +11 -23
- package/src/responses-headers.ts +1 -1
- package/src/retired-remote-windows.ts +35 -0
- package/src/runtime.ts +12 -2
- package/src/types.ts +4 -28
- package/src/context-management/codex-provider.ts +0 -270
- package/src/context-management/history-notes.ts +0 -357
- package/src/context-management/manual-compact.ts +0 -247
- package/src/context-management/messages.ts +0 -238
- package/src/context-management/namespace-tools.ts +0 -426
- package/src/context-management/responses-tools.ts +0 -238
- package/src/context-management/scoped-compaction.ts +0 -89
- package/src/context-management/tool-contract.ts +0 -62
- package/src/context-management/tools.ts +0 -371
- package/src/context-management/types.ts +0 -168
- package/src/context-management/window-budget.ts +0 -83
- package/src/context-management/window-bulk.ts +0 -155
- package/src/context-management/window-manager.ts +0 -912
- package/src/context-management/window-request.ts +0 -125
package/src/native-fallback.ts
CHANGED
|
@@ -20,9 +20,7 @@ export type NativeFallbackFailureReason =
|
|
|
20
20
|
| "auth-failed"
|
|
21
21
|
| "aborted"
|
|
22
22
|
| "empty-summary"
|
|
23
|
-
| "compact-failed"
|
|
24
|
-
| "public-runtime-unavailable"
|
|
25
|
-
| "missing-model";
|
|
23
|
+
| "compact-failed";
|
|
26
24
|
|
|
27
25
|
export type NativeFallbackResult =
|
|
28
26
|
| {
|
|
@@ -63,10 +61,8 @@ function toErrorMessage(error: unknown): string {
|
|
|
63
61
|
* active model cannot use remote v2 at all. This function never inspects `remoteCompactModel`
|
|
64
62
|
* itself, so a model that cannot compact remotely can never inherit the remote producer.
|
|
65
63
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* stream, current thinking and auth, rather than reopening unscoped host history.
|
|
69
|
-
* Host-private custom stream/retry callbacks are not exposed by ExtensionContext.
|
|
64
|
+
* Unset or same-current-model defaults stay with Pi. This helper does not
|
|
65
|
+
* select a session model or take ownership of Pi's scheduler.
|
|
70
66
|
*/
|
|
71
67
|
export async function runNativeFallbackCompaction(args: {
|
|
72
68
|
ctx: ExtensionContext;
|
|
@@ -75,10 +71,7 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
75
71
|
/** Model spec supplied by the caller for this specific fallback reason. */
|
|
76
72
|
modelSpec?: string;
|
|
77
73
|
compactFn?: NativeCompactFn;
|
|
78
|
-
/**
|
|
79
|
-
scoped?: boolean;
|
|
80
|
-
/** Run current-model native policy, independently of Toolkit's fallback config. */
|
|
81
|
-
useCurrentModel?: boolean;
|
|
74
|
+
/** Reject results if the originating session/branch/selection changed. */
|
|
82
75
|
isCurrent?: () => boolean;
|
|
83
76
|
}): Promise<NativeFallbackResult> {
|
|
84
77
|
const { ctx, event, config } = args;
|
|
@@ -87,21 +80,16 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
87
80
|
const isCurrent = () => !event.signal.aborted && (args.isCurrent?.() ?? true);
|
|
88
81
|
if (!isCurrent()) return { ok: false, reason: "aborted" };
|
|
89
82
|
|
|
90
|
-
if (!
|
|
91
|
-
const spec = args.
|
|
92
|
-
if (!spec) return { ok: false, reason:
|
|
83
|
+
if (!nativeFallback.enabled) return { ok: false, reason: "disabled" };
|
|
84
|
+
const spec = args.modelSpec?.trim() ?? "";
|
|
85
|
+
if (!spec) return { ok: false, reason: "no-model-configured" };
|
|
93
86
|
const parsed = parseModelSpec(spec);
|
|
94
87
|
if (!parsed) return { ok: false, reason: "invalid-model-spec", modelSpec: spec };
|
|
95
|
-
const model =
|
|
88
|
+
const model = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
96
89
|
if (!model) return { ok: false, reason: "model-not-found", modelSpec: spec };
|
|
97
|
-
if (
|
|
90
|
+
if (ctx.model && ctx.model.provider === model.provider && ctx.model.id === model.id) {
|
|
98
91
|
return { ok: false, reason: "same-as-current-model", modelSpec: spec };
|
|
99
92
|
}
|
|
100
|
-
// The public registry preserves provider-native stream/auth wiring. Do not
|
|
101
|
-
// silently use a compat-only transport for scoped current-model compression.
|
|
102
|
-
if (args.scoped && typeof ctx.modelRegistry.streamSimple !== "function" && !args.compactFn) {
|
|
103
|
-
return { ok: false, reason: "public-runtime-unavailable", modelSpec: spec };
|
|
104
|
-
}
|
|
105
93
|
|
|
106
94
|
// Pi owns prompt construction, tool-result truncation and split-turn requests.
|
|
107
95
|
// A chars/4 estimate is not target-model token accounting and must not veto
|
|
@@ -126,8 +114,8 @@ export async function runNativeFallbackCompaction(args: {
|
|
|
126
114
|
mergeProviderHeaders(auth.headers),
|
|
127
115
|
event.customInstructions,
|
|
128
116
|
event.signal,
|
|
129
|
-
|
|
130
|
-
|
|
117
|
+
nativeFallback.thinkingLevel,
|
|
118
|
+
undefined,
|
|
131
119
|
auth.env,
|
|
132
120
|
);
|
|
133
121
|
|
package/src/responses-headers.ts
CHANGED
|
@@ -91,7 +91,7 @@ export function buildCodexCliUserAgent(version: string): string {
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* Codex client identity used by synthetic gateway requests in their User-Agent.
|
|
94
|
-
* Native live and
|
|
94
|
+
* Native live and shared synthetic requests do not generate a `version`
|
|
95
95
|
* header; explicitly configured provider values remain caller-owned.
|
|
96
96
|
*/
|
|
97
97
|
export const CODEX_CLIENT_VERSION = "0.153.0";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Reserved persisted identifiers, retained only to refuse retired sessions. */
|
|
2
|
+
export const RETIRED_REMOTE_WINDOW_TYPE = "codex-context-window" as const;
|
|
3
|
+
export const RETIRED_MANUAL_COMPACT_TYPE = "pi-openai-toolkit:manual-compact" as const;
|
|
4
|
+
export const RETIRED_REMOTE_WINDOW_SUMMARY =
|
|
5
|
+
"[Pi Codex context-window boundary; no conversation summary was generated.]";
|
|
6
|
+
|
|
7
|
+
export type RetiredRemoteWindowsReason =
|
|
8
|
+
| "window-message"
|
|
9
|
+
| "window-compaction"
|
|
10
|
+
| "window-summary"
|
|
11
|
+
| "manual-compact-record";
|
|
12
|
+
|
|
13
|
+
export const RETIRED_REMOTE_WINDOWS_MESSAGE =
|
|
14
|
+
"Remote Windows has been removed. This branch contains retired window or manual-compaction records and cannot continue. Start a new session and explicitly choose the model you want to use. Existing history is unchanged; no model or thinking level was restored.";
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Inspect only the supplied active branch. Reserved identifiers suffice even
|
|
22
|
+
* when their data is damaged, terminal or superseded by a newer checkpoint.
|
|
23
|
+
* This deliberately interprets no ownership, recovery or migration state.
|
|
24
|
+
*/
|
|
25
|
+
export function detectRetiredRemoteWindows(entries: readonly unknown[]): RetiredRemoteWindowsReason | undefined {
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!isRecord(entry)) continue;
|
|
28
|
+
if (entry.type === "custom_message" && entry.customType === RETIRED_REMOTE_WINDOW_TYPE) return "window-message";
|
|
29
|
+
if (entry.type === "custom" && entry.customType === RETIRED_MANUAL_COMPACT_TYPE) return "manual-compact-record";
|
|
30
|
+
if (entry.type !== "compaction") continue;
|
|
31
|
+
if (isRecord(entry.details) && entry.details.strategy === RETIRED_REMOTE_WINDOW_TYPE) return "window-compaction";
|
|
32
|
+
if (entry.summary === RETIRED_REMOTE_WINDOW_SUMMARY) return "window-summary";
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -784,7 +784,8 @@ export async function resolveRemoteCompactionExecution(
|
|
|
784
784
|
options: NativeCompactionSupportOptions = {},
|
|
785
785
|
remoteModelSpec?: string,
|
|
786
786
|
): Promise<RemoteCompactionExecutionResolution> {
|
|
787
|
-
const
|
|
787
|
+
const selectedModel = ctx.model;
|
|
788
|
+
const consumerResolution = await resolveNativeCompactionEnvironmentForModel(ctx, selectedModel, options);
|
|
788
789
|
if (!consumerResolution.ok) {
|
|
789
790
|
return consumerResolution;
|
|
790
791
|
}
|
|
@@ -809,6 +810,16 @@ export async function resolveRemoteCompactionExecution(
|
|
|
809
810
|
};
|
|
810
811
|
}
|
|
811
812
|
|
|
813
|
+
const consumer = consumerResolution.runtime;
|
|
814
|
+
const matches = (model: { provider: string; id: string } | undefined) =>
|
|
815
|
+
model?.provider === parsed.provider && model.id === parsed.modelId;
|
|
816
|
+
// Reuse this operation's physical/auth snapshot for either the current
|
|
817
|
+
// logical selection or its resolved physical identity. Do not resolve a
|
|
818
|
+
// virtual selection again or look up/authenticate an identical producer.
|
|
819
|
+
if (matches(selectedModel) || matches(consumer.currentModel)) {
|
|
820
|
+
return { ok: true, execution: { consumer, compactor: consumer } };
|
|
821
|
+
}
|
|
822
|
+
|
|
812
823
|
const compactorModel = ctx.modelRegistry.find(parsed.provider, parsed.modelId);
|
|
813
824
|
if (!compactorModel) {
|
|
814
825
|
return {
|
|
@@ -832,7 +843,6 @@ export async function resolveRemoteCompactionExecution(
|
|
|
832
843
|
};
|
|
833
844
|
}
|
|
834
845
|
|
|
835
|
-
const consumer = consumerResolution.runtime;
|
|
836
846
|
const compactor = compactorResolution.runtime;
|
|
837
847
|
if (compactor.baseUrl !== consumer.baseUrl) {
|
|
838
848
|
return {
|
package/src/types.ts
CHANGED
|
@@ -19,8 +19,8 @@ export const RESPONSES_COMPACT_CAPABLE_APIS = [
|
|
|
19
19
|
] as const;
|
|
20
20
|
/**
|
|
21
21
|
* Bare model ID used by the independent Astra effort compatibility layer.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
22
|
+
* Gateway transport is an independent exact-model opt-in; this constant
|
|
23
|
+
* does not verify backend support or grant that opt-in.
|
|
24
24
|
*/
|
|
25
25
|
export const ASTRA_MODEL_ID = "gpt-6-astra";
|
|
26
26
|
export const LEGACY_NATIVE_COMPACTION_STRATEGY = "openai-native-compact-v1";
|
|
@@ -60,8 +60,6 @@ export type DebugArtifactKind =
|
|
|
60
60
|
| "compaction-event"
|
|
61
61
|
| "lifecycle";
|
|
62
62
|
|
|
63
|
-
export type ContextManagementMode = "off" | "remote";
|
|
64
|
-
|
|
65
63
|
/** How Remote V2 obtains the history it sends to the compaction endpoint. */
|
|
66
64
|
export type RemoteV2ContextSource = "pi-context-hook" | "legacy";
|
|
67
65
|
|
|
@@ -79,12 +77,6 @@ export type NativeFallbackConfig = {
|
|
|
79
77
|
thinkingLevel: ThinkingLevel;
|
|
80
78
|
};
|
|
81
79
|
|
|
82
|
-
/**
|
|
83
|
-
* Legacy compatibility setting. Remote Context history is no longer bulk-compacted when
|
|
84
|
-
* selecting a non-window model; local projection and a no-summary boundary own that handoff.
|
|
85
|
-
*/
|
|
86
|
-
export type LeaveManagedModePolicy = "warn" | "compact";
|
|
87
|
-
|
|
88
80
|
export type DebugConfig = {
|
|
89
81
|
debug: boolean;
|
|
90
82
|
notifyOnLoad: boolean;
|
|
@@ -96,13 +88,6 @@ export type DebugConfig = {
|
|
|
96
88
|
|
|
97
89
|
export type CompactionConfig = {
|
|
98
90
|
enabled: boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Legacy compatibility setting. Non-window consumers never receive retired Remote Context
|
|
101
|
-
* history, regardless of this value; retained config files continue to parse it.
|
|
102
|
-
*/
|
|
103
|
-
leaveManagedMode: LeaveManagedModePolicy;
|
|
104
|
-
/** Optional Codex Remote Context management. Disabled by default for compatibility. */
|
|
105
|
-
contextManagement: ContextManagementMode;
|
|
106
91
|
/**
|
|
107
92
|
* Allow a Responses session whose latest compaction was not created by this extension
|
|
108
93
|
* to restart native compaction from Pi's current serialized session context.
|
|
@@ -125,16 +110,10 @@ export type CompactionConfig = {
|
|
|
125
110
|
*/
|
|
126
111
|
responsesApis: string[];
|
|
127
112
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* this list; gateway coverage is opt-in per model.
|
|
113
|
+
* Legacy alias for exact "provider/model" Codex gateway transport opt-ins.
|
|
114
|
+
* Shared by compaction and image generation; not a window capability.
|
|
131
115
|
*/
|
|
132
116
|
gatewayContextModels: string[];
|
|
133
|
-
/**
|
|
134
|
-
* Percentage of the context window that, when remaining tokens drop below it,
|
|
135
|
-
* triggers a checkpoint/new_context reminder (0-100; 0 disables reminders).
|
|
136
|
-
*/
|
|
137
|
-
contextReminderThresholdPercent: number;
|
|
138
117
|
notifyOnLoad: boolean;
|
|
139
118
|
debug: boolean;
|
|
140
119
|
logProviderPayloads: boolean;
|
|
@@ -526,7 +505,6 @@ export const DEFAULT_NATIVE_FALLBACK_CONFIG: NativeFallbackConfig = {
|
|
|
526
505
|
|
|
527
506
|
export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|
528
507
|
enabled: true,
|
|
529
|
-
contextManagement: "off",
|
|
530
508
|
allowCompactionContinuityBreak: false,
|
|
531
509
|
remoteCompactModel: undefined,
|
|
532
510
|
remoteV2ContextSource: "legacy",
|
|
@@ -534,8 +512,6 @@ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|
|
534
512
|
nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
|
|
535
513
|
responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
|
|
536
514
|
gatewayContextModels: [],
|
|
537
|
-
leaveManagedMode: "warn",
|
|
538
|
-
contextReminderThresholdPercent: 5,
|
|
539
515
|
notifyOnLoad: false,
|
|
540
516
|
debug: false,
|
|
541
517
|
logProviderPayloads: false,
|
|
@@ -1,270 +0,0 @@
|
|
|
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 {
|
|
5
|
-
CODEX_AFFINITY_SCOPE,
|
|
6
|
-
CODEX_CLIENT_VERSION,
|
|
7
|
-
CODEX_GATEWAY_ORIGINATOR,
|
|
8
|
-
buildCodexCliUserAgent,
|
|
9
|
-
filterCodexGatewayHeaders,
|
|
10
|
-
} from "../responses-headers";
|
|
11
|
-
export { CODEX_AFFINITY_SCOPE } from "../responses-headers";
|
|
12
|
-
import { normalizeBaseUrl, resolveEffectiveModel, VIRTUAL_MODEL_API } from "../runtime";
|
|
13
|
-
import { isExactModelAllowed } from "../model-scope";
|
|
14
|
-
import {
|
|
15
|
-
isNonEmptyString,
|
|
16
|
-
type CodexContextProvider,
|
|
17
|
-
type CodexContextProviderResolution,
|
|
18
|
-
} from "./types";
|
|
19
|
-
|
|
20
|
-
const CODEX_PROVIDER = "openai-codex";
|
|
21
|
-
const CODEX_API = "openai-codex-responses";
|
|
22
|
-
const GATEWAY_API = "openai-responses";
|
|
23
|
-
const CODEX_TOOL_ORIGINATOR = CODEX_GATEWAY_ORIGINATOR;
|
|
24
|
-
const CODEX_TOOL_VERSION = "0.0.0";
|
|
25
|
-
const CODEX_SESSION_ID_MAX_LENGTH = 64;
|
|
26
|
-
|
|
27
|
-
export const CODEX_CONTEXT_PROVIDER_ERROR =
|
|
28
|
-
"Remote Context management requires a configured Codex-compatible backend";
|
|
29
|
-
|
|
30
|
-
export function isNativeCodexModel(
|
|
31
|
-
model: ExtensionContext["model"] | undefined,
|
|
32
|
-
): boolean {
|
|
33
|
-
return model?.provider === CODEX_PROVIDER && model.api === CODEX_API;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function isCodexGatewayModel(
|
|
37
|
-
model: ExtensionContext["model"] | undefined,
|
|
38
|
-
gatewayModels: readonly string[] = [],
|
|
39
|
-
): boolean {
|
|
40
|
-
// Gateway coverage is an operator allowlist of exact "provider/model" keys
|
|
41
|
-
// (compaction.gatewayContextModels). The backend relay must actually pass
|
|
42
|
-
// hosted window markers through; models outside the list stay on remote
|
|
43
|
-
// compaction v2.
|
|
44
|
-
if (!model || model.api !== GATEWAY_API) return false;
|
|
45
|
-
return isExactModelAllowed(model, gatewayModels);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function normalizeCodexBackendBaseUrl(baseUrl: string | undefined | null): string | undefined {
|
|
49
|
-
const normalized = normalizeBaseUrl(baseUrl);
|
|
50
|
-
if (!normalized) return undefined;
|
|
51
|
-
try {
|
|
52
|
-
const url = new URL(normalized);
|
|
53
|
-
const pathname = url.pathname.replace(/\/+$/, "");
|
|
54
|
-
if (pathname.endsWith("/codex/responses")) {
|
|
55
|
-
url.pathname = pathname.slice(0, -"/responses".length);
|
|
56
|
-
return url.toString().replace(/\/$/, "");
|
|
57
|
-
}
|
|
58
|
-
if (pathname.endsWith("/codex")) return normalized;
|
|
59
|
-
if (pathname.endsWith("/backend-api") || pathname.endsWith("/api")) {
|
|
60
|
-
return `${normalized}/codex`;
|
|
61
|
-
}
|
|
62
|
-
if (pathname === "") return `${normalized}/api/codex`;
|
|
63
|
-
if (pathname === "/") return `${normalized}/api/codex`;
|
|
64
|
-
return undefined;
|
|
65
|
-
} catch {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function headerValue(headers: ProviderHeaders | undefined, name: string): string | undefined {
|
|
71
|
-
const wanted = name.toLowerCase();
|
|
72
|
-
for (const [key, value] of Object.entries(headers ?? {})) {
|
|
73
|
-
if (key.toLowerCase() === wanted && typeof value === "string" && value.trim()) {
|
|
74
|
-
return value.trim();
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function bearerToken(value: string | undefined): string | undefined {
|
|
81
|
-
if (!value) return undefined;
|
|
82
|
-
const match = value.trim().match(/^Bearer\s+(.+)$/i);
|
|
83
|
-
return (match?.[1] ?? value).trim() || undefined;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
|
|
87
|
-
const parts = token.split(".");
|
|
88
|
-
if (parts.length !== 3) return undefined;
|
|
89
|
-
try {
|
|
90
|
-
const value: unknown = JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf8"));
|
|
91
|
-
return value && typeof value === "object" && !Array.isArray(value)
|
|
92
|
-
? value as Record<string, unknown>
|
|
93
|
-
: undefined;
|
|
94
|
-
} catch {
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function accountIdFromToken(token: string): string | undefined {
|
|
100
|
-
const payload = decodeJwtPayload(token);
|
|
101
|
-
const direct = payload?.chatgpt_account_id;
|
|
102
|
-
if (isNonEmptyString(direct)) return direct.trim();
|
|
103
|
-
const auth = payload?.["https://api.openai.com/auth"];
|
|
104
|
-
if (!auth || typeof auth !== "object" || Array.isArray(auth)) return undefined;
|
|
105
|
-
const nested = (auth as Record<string, unknown>).chatgpt_account_id;
|
|
106
|
-
return isNonEmptyString(nested) ? nested.trim() : undefined;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function userAgent(version: string): string {
|
|
110
|
-
return buildCodexCliUserAgent(version);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function clampSessionId(value: string | undefined): string | undefined {
|
|
114
|
-
if (!value) return undefined;
|
|
115
|
-
const chars = Array.from(value.trim());
|
|
116
|
-
if (chars.length === 0) return undefined;
|
|
117
|
-
return chars.length <= CODEX_SESSION_ID_MAX_LENGTH
|
|
118
|
-
? chars.join("")
|
|
119
|
-
: chars.slice(0, CODEX_SESSION_ID_MAX_LENGTH).join("");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function resolveContextSessionId(ctx: ExtensionContext): string | undefined {
|
|
123
|
-
try {
|
|
124
|
-
return clampSessionId(ctx.sessionManager.getSessionId());
|
|
125
|
-
} catch {
|
|
126
|
-
return undefined;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export type CodexContextProviderHeaderOptions = {
|
|
131
|
-
sessionId?: string;
|
|
132
|
-
clientRequestId?: string;
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
export function codexContextProviderHeaders(
|
|
136
|
-
provider: CodexContextProvider,
|
|
137
|
-
options: CodexContextProviderHeaderOptions = {},
|
|
138
|
-
): Headers {
|
|
139
|
-
const headers = new Headers();
|
|
140
|
-
for (const [name, value] of Object.entries(provider.headers)) {
|
|
141
|
-
if (typeof value === "string") headers.set(name, value);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (provider.kind === "native-codex") {
|
|
145
|
-
headers.set("Authorization", `Bearer ${provider.token}`);
|
|
146
|
-
headers.set("ChatGPT-Account-ID", provider.accountId);
|
|
147
|
-
headers.set("originator", CODEX_TOOL_ORIGINATOR);
|
|
148
|
-
headers.set("User-Agent", userAgent(CODEX_TOOL_VERSION));
|
|
149
|
-
} else {
|
|
150
|
-
headers.set("Authorization", `Bearer ${provider.apiKey}`);
|
|
151
|
-
headers.delete("ChatGPT-Account-ID");
|
|
152
|
-
headers.delete("Cookie");
|
|
153
|
-
headers.set("originator", CODEX_GATEWAY_ORIGINATOR);
|
|
154
|
-
headers.set("User-Agent", userAgent(CODEX_CLIENT_VERSION));
|
|
155
|
-
// Let the gateway own its version policy; preserve an explicitly configured
|
|
156
|
-
// version already copied from provider.headers above.
|
|
157
|
-
headers.set("X-Codex-Affinity-Scope", CODEX_AFFINITY_SCOPE);
|
|
158
|
-
headers.set("X-Codex-Model", provider.model);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const sessionId = clampSessionId(options.sessionId);
|
|
162
|
-
if (sessionId) {
|
|
163
|
-
headers.set("Session-Id", sessionId);
|
|
164
|
-
headers.set("X-Client-Request-Id", clampSessionId(options.clientRequestId) ?? sessionId);
|
|
165
|
-
}
|
|
166
|
-
headers.set("content-type", "application/json");
|
|
167
|
-
return headers;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export async function resolveCodexContextProvider(
|
|
171
|
-
ctx: ExtensionContext,
|
|
172
|
-
modelOverride: ExtensionContext["model"] = ctx.model,
|
|
173
|
-
gatewayModels: readonly string[] = [],
|
|
174
|
-
): Promise<CodexContextProviderResolution> {
|
|
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;
|
|
185
|
-
const descriptor = {
|
|
186
|
-
provider: model.provider,
|
|
187
|
-
api: model.api,
|
|
188
|
-
model: model.id,
|
|
189
|
-
baseUrl: normalizeBaseUrl(model.baseUrl),
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const isNative = isNativeCodexModel(model);
|
|
193
|
-
const isLogicalVirtualGateway = selectedModel?.api === VIRTUAL_MODEL_API && isExactModelAllowed(selectedModel, gatewayModels);
|
|
194
|
-
const isGateway = isCodexGatewayModel(model, gatewayModels) ||
|
|
195
|
-
(isLogicalVirtualGateway && model.api === GATEWAY_API);
|
|
196
|
-
if (!isNative && !isGateway) {
|
|
197
|
-
return { ok: false, reason: "unsupported-model", ...descriptor };
|
|
198
|
-
}
|
|
199
|
-
if (!isNative && model.api !== GATEWAY_API) {
|
|
200
|
-
return { ok: false, reason: "unsupported-api", ...descriptor };
|
|
201
|
-
}
|
|
202
|
-
if (isNative && model.api !== CODEX_API) {
|
|
203
|
-
return { ok: false, reason: "unsupported-api", ...descriptor };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
let auth: Awaited<ReturnType<typeof ctx.modelRegistry.getApiKeyAndHeaders>>;
|
|
207
|
-
try {
|
|
208
|
-
auth = await ctx.modelRegistry.getApiKeyAndHeaders(model as Model<Api>);
|
|
209
|
-
} catch {
|
|
210
|
-
return { ok: false, reason: "auth-resolution-failed", ...descriptor };
|
|
211
|
-
}
|
|
212
|
-
if (!auth.ok) return { ok: false, reason: "auth-resolution-failed", ...descriptor };
|
|
213
|
-
|
|
214
|
-
const rawBaseUrl = normalizeBaseUrl(auth.baseUrl) ?? normalizeBaseUrl(model.baseUrl);
|
|
215
|
-
if (!rawBaseUrl) return { ok: false, reason: "missing-base-url", ...descriptor };
|
|
216
|
-
|
|
217
|
-
if (isGateway) {
|
|
218
|
-
if (!resolveContextSessionId(ctx)) {
|
|
219
|
-
return { ok: false, reason: "missing-session-id", ...descriptor, baseUrl: rawBaseUrl };
|
|
220
|
-
}
|
|
221
|
-
const apiKey = bearerToken(auth.apiKey) ?? bearerToken(headerValue(auth.headers, "authorization"));
|
|
222
|
-
if (!apiKey) return { ok: false, reason: "missing-api-key", ...descriptor, baseUrl: rawBaseUrl };
|
|
223
|
-
return {
|
|
224
|
-
ok: true,
|
|
225
|
-
provider: {
|
|
226
|
-
kind: "codex-gateway",
|
|
227
|
-
route: "codex-gateway",
|
|
228
|
-
provider: model.provider,
|
|
229
|
-
api: GATEWAY_API,
|
|
230
|
-
model: model.id,
|
|
231
|
-
baseUrl: rawBaseUrl,
|
|
232
|
-
apiKey,
|
|
233
|
-
headers: filterCodexGatewayHeaders(mergeProviderHeaders(model.headers, auth.headers)),
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const token = bearerToken(auth.apiKey) ?? bearerToken(headerValue(auth.headers, "authorization"));
|
|
239
|
-
if (!token) return { ok: false, reason: "missing-token", ...descriptor };
|
|
240
|
-
const accountId = headerValue(auth.headers, "chatgpt-account-id") ?? accountIdFromToken(token);
|
|
241
|
-
if (!accountId) return { ok: false, reason: "missing-account-id", ...descriptor };
|
|
242
|
-
const baseUrl = normalizeCodexBackendBaseUrl(rawBaseUrl);
|
|
243
|
-
if (!baseUrl) return { ok: false, reason: "unsupported-backend", ...descriptor };
|
|
244
|
-
|
|
245
|
-
const headers = mergeProviderHeaders(model.headers, auth.headers);
|
|
246
|
-
return {
|
|
247
|
-
ok: true,
|
|
248
|
-
provider: {
|
|
249
|
-
kind: "native-codex",
|
|
250
|
-
route: "openai-codex",
|
|
251
|
-
provider: CODEX_PROVIDER,
|
|
252
|
-
api: CODEX_API,
|
|
253
|
-
model: model.id,
|
|
254
|
-
baseUrl,
|
|
255
|
-
token,
|
|
256
|
-
accountId,
|
|
257
|
-
headers,
|
|
258
|
-
},
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export const _codexProviderTest = {
|
|
263
|
-
accountIdFromToken,
|
|
264
|
-
bearerToken,
|
|
265
|
-
clampSessionId,
|
|
266
|
-
decodeJwtPayload,
|
|
267
|
-
headerValue,
|
|
268
|
-
isCodexGatewayModel,
|
|
269
|
-
userAgent,
|
|
270
|
-
};
|