@linxiraos/pi-ai 1.1.26 → 1.2.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/CHANGELOG.md +9 -8
- package/README.md +15 -0
- package/dist/types/auth/affinity.d.ts +33 -6
- package/dist/types/auth/cascade.d.ts +44 -7
- package/dist/types/auth/oauth.d.ts +9 -6
- package/dist/types/auth/policy.d.ts +1 -0
- package/dist/types/auth/refresh.d.ts +1 -2
- package/dist/types/auth/select.d.ts +6 -4
- package/dist/types/auth/sqlite-credential-store.d.ts +21 -1
- package/dist/types/auth/store.d.ts +8 -2
- package/dist/types/auth/types.d.ts +99 -15
- package/dist/types/auth-broker/client.d.ts +3 -3
- package/dist/types/auth-broker/discover.d.ts +1 -1
- package/dist/types/auth-broker/refresher.d.ts +6 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -4
- package/dist/types/auth-broker/server.d.ts +2 -0
- package/dist/types/auth-broker/snapshot-cache.d.ts +12 -0
- package/dist/types/auth-gateway/dispatch.d.ts +13 -5
- package/dist/types/auth-gateway/http.d.ts +0 -2
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/routes/embeddings.d.ts +2 -2
- package/dist/types/auth-gateway/routes/images.d.ts +3 -3
- package/dist/types/auth-gateway/routes/rerank.d.ts +2 -2
- package/dist/types/auth-gateway/routes/speech.d.ts +2 -2
- package/dist/types/auth-gateway/routes/systemone.d.ts +2 -2
- package/dist/types/auth-gateway/routes/transcriptions.d.ts +2 -2
- package/dist/types/auth-gateway/routes/video.d.ts +4 -4
- package/dist/types/auth-gateway/server.d.ts +22 -5
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/stdio.d.ts +12 -0
- package/dist/types/auth-storage.d.ts +7 -6
- package/dist/types/dialect/terminator-wait.d.ts +19 -0
- package/dist/types/env-api-key.d.ts +21 -0
- package/dist/types/error/auth.d.ts +10 -0
- package/dist/types/error/flags.d.ts +38 -0
- package/dist/types/images/index.d.ts +2 -3
- package/dist/types/images/shared.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/judgment/index.d.ts +2 -1
- package/dist/types/judgment/native.d.ts +80 -0
- package/dist/types/judgment/openai-decisions.d.ts +3 -0
- package/dist/types/judgment/question-file.d.ts +52 -0
- package/dist/types/judgment/systemone.d.ts +7 -0
- package/dist/types/judgment/types.d.ts +2 -2
- package/dist/types/judgment/wire.d.ts +36 -0
- package/dist/types/provider-details.d.ts +1 -1
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-client.d.ts +10 -0
- package/dist/types/providers/anthropic-state.d.ts +2 -0
- package/dist/types/providers/anthropic.d.ts +38 -3
- package/dist/types/providers/aws-credentials.d.ts +4 -8
- package/dist/types/providers/connect-error-detail.d.ts +9 -0
- package/dist/types/providers/connect-frame.d.ts +44 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +0 -2
- package/dist/types/providers/cursor.d.ts +8 -8
- package/dist/types/providers/google-auth.d.ts +4 -1
- package/dist/types/providers/google-shared.d.ts +24 -4
- package/dist/types/providers/inference-headers.d.ts +6 -0
- package/dist/types/providers/mock.d.ts +3 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +2 -0
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-configuration-update.d.ts +20 -11
- package/dist/types/providers/openai-responses-wire.d.ts +1 -55
- package/dist/types/providers/openai-responses.d.ts +40 -3
- package/dist/types/providers/openai-shared.d.ts +30 -5
- package/dist/types/providers/pi-native-client.d.ts +1 -1
- package/dist/types/providers/transform-messages.d.ts +25 -0
- package/dist/types/providers/vision-guard.d.ts +1 -1
- package/dist/types/registry/oauth/cloudflare-workers-ai.d.ts +3 -0
- package/dist/types/registry/oauth/cursor.d.ts +10 -2
- package/dist/types/registry/oauth/index.d.ts +2 -0
- package/dist/types/registry/oauth/snowflake.d.ts +3 -0
- package/dist/types/registry/snowflake.d.ts +4 -0
- package/dist/types/stream.d.ts +0 -21
- package/dist/types/transcription/types.d.ts +2 -1
- package/dist/types/types.d.ts +116 -14
- package/dist/types/usage.d.ts +2 -0
- package/dist/types/utils/anthropic-auth.d.ts +21 -7
- package/dist/types/utils/aws-profile.d.ts +5 -0
- package/dist/types/utils/block-symbols.d.ts +14 -0
- package/dist/types/utils/dsml-leak.d.ts +29 -0
- package/dist/types/utils/glyph-codec.d.ts +1 -1
- package/dist/types/utils/idle-iterator.d.ts +11 -0
- package/dist/types/utils/leaked-thinking-stream.d.ts +1 -1
- package/dist/types/utils/resolve-peer.d.ts +2 -0
- package/dist/types/utils/retry-wait.d.ts +9 -0
- package/dist/types/utils/schema/normalize.d.ts +8 -3
- package/dist/types/utils/stream-markup-healing.d.ts +4 -4
- package/dist/types/utils/tool-call-arguments.d.ts +8 -0
- package/dist/types/utils.d.ts +6 -0
- package/package.json +6 -6
- package/src/auth/affinity.ts +130 -37
- package/src/auth/blocks.ts +94 -15
- package/src/auth/cascade.ts +112 -17
- package/src/auth/health.ts +3 -2
- package/src/auth/oauth.ts +25 -10
- package/src/auth/policy.ts +10 -1
- package/src/auth/pool.ts +0 -5
- package/src/auth/refresh.ts +17 -12
- package/src/auth/resets.ts +1 -1
- package/src/auth/rotation.ts +8 -2
- package/src/auth/select.ts +102 -30
- package/src/auth/sqlite-credential-store.ts +119 -49
- package/src/auth/store.ts +8 -2
- package/src/auth/types.ts +108 -17
- package/src/auth/usage-cache.ts +16 -5
- package/src/auth/usage.ts +2 -2
- package/src/auth-broker/client.ts +7 -4
- package/src/auth-broker/discover.ts +6 -5
- package/src/auth-broker/refresher.ts +8 -1
- package/src/auth-broker/remote-store.ts +200 -62
- package/src/auth-broker/server.ts +316 -130
- package/src/auth-broker/snapshot-cache.ts +142 -2
- package/src/auth-gateway/dispatch.ts +15 -7
- package/src/auth-gateway/http.ts +0 -8
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +2 -2
- package/src/auth-gateway/routes/images.ts +4 -4
- package/src/auth-gateway/routes/rerank.ts +2 -2
- package/src/auth-gateway/routes/speech.ts +2 -2
- package/src/auth-gateway/routes/systemone.ts +22 -14
- package/src/auth-gateway/routes/transcriptions.ts +2 -2
- package/src/auth-gateway/routes/video.ts +7 -7
- package/src/auth-gateway/server.ts +175 -134
- package/src/auth-gateway/session-state.ts +51 -9
- package/src/auth-gateway/stdio.ts +132 -0
- package/src/auth-storage.ts +23 -9
- package/src/dialect/deepseek.ts +67 -8
- package/src/dialect/gemini.ts +8 -2
- package/src/dialect/hermes.ts +8 -2
- package/src/dialect/kimi.ts +8 -2
- package/src/dialect/qwen-xml.ts +8 -2
- package/src/dialect/qwen3.ts +8 -2
- package/src/dialect/terminator-wait.ts +42 -0
- package/src/env-api-key.ts +78 -0
- package/src/error/auth.ts +20 -0
- package/src/error/flags.ts +143 -24
- package/src/error/rate-limit.ts +8 -6
- package/src/images/google-antigravity.ts +0 -1
- package/src/images/index.ts +3 -17
- package/src/images/openai-hosted.ts +13 -4
- package/src/images/openai-images.ts +19 -13
- package/src/images/shared.ts +10 -2
- package/src/index.ts +2 -1
- package/src/judgment/index.ts +2 -1
- package/src/judgment/native.ts +333 -0
- package/src/judgment/openai-decisions-predicate.md +9 -0
- package/src/judgment/openai-decisions.ts +129 -0
- package/src/judgment/question-file.ts +90 -0
- package/src/judgment/systemone.ts +65 -0
- package/src/judgment/text.ts +9 -1
- package/src/judgment/types.ts +2 -2
- package/src/judgment/wire.ts +42 -0
- package/src/provider-details.ts +1 -1
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +19 -8
- package/src/providers/anthropic-client.ts +16 -1
- package/src/providers/anthropic-state.ts +6 -0
- package/src/providers/anthropic.ts +962 -238
- package/src/providers/apple-foundation-models.ts +10 -4
- package/src/providers/aws-credentials.ts +65 -5
- package/src/providers/azure-openai-responses.ts +1 -1
- package/src/providers/connect-error-detail.ts +21 -0
- package/src/providers/connect-frame.ts +107 -0
- package/src/providers/cursor/interaction-query.ts +1 -9
- package/src/providers/cursor.ts +590 -133
- package/src/providers/devin.ts +68 -58
- package/src/providers/factory-droid/gemini.ts +38 -51
- package/src/providers/factory-droid.ts +2 -0
- package/src/providers/gitlab-duo-workflow.ts +5 -2
- package/src/providers/gitlab-duo.ts +1 -0
- package/src/providers/google-auth.ts +10 -3
- package/src/providers/google-gemini-cli.ts +4 -0
- package/src/providers/google-shared.ts +134 -105
- package/src/providers/google-vertex.ts +0 -1
- package/src/providers/google.ts +1 -1
- package/src/providers/inference-headers.ts +19 -0
- package/src/providers/mock.ts +4 -0
- package/src/providers/ollama.ts +1 -1
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-codex/request-transformer.ts +16 -11
- package/src/providers/openai-codex-responses.ts +201 -76
- package/src/providers/openai-completions.ts +12 -2
- package/src/providers/openai-configuration-update.ts +54 -15
- package/src/providers/openai-responses-wire.ts +1 -55
- package/src/providers/openai-responses.ts +437 -33
- package/src/providers/openai-shared.ts +267 -72
- package/src/providers/pi-native-client.ts +2 -2
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +1 -1
- package/src/providers/transcriptions-server.ts +1 -1
- package/src/providers/transform-messages.ts +240 -84
- package/src/providers/vision-guard.ts +1 -1
- package/src/registry/aws.ts +51 -14
- package/src/registry/cloudflare-ai-gateway.ts +26 -3
- package/src/registry/hooks/api-key.ts +1 -0
- package/src/registry/hooks/custom.ts +2 -0
- package/src/registry/hooks/env.ts +2 -5
- package/src/registry/oauth/callback-server.ts +6 -1
- package/src/registry/oauth/cloudflare-ai-gateway.ts +2 -2
- package/src/registry/oauth/cloudflare-workers-ai.ts +32 -0
- package/src/registry/oauth/cursor.ts +80 -17
- package/src/registry/oauth/index.ts +5 -0
- package/src/registry/oauth/snowflake.ts +183 -0
- package/src/registry/registry.ts +2 -0
- package/src/registry/snowflake.ts +149 -0
- package/src/stream.ts +85 -107
- package/src/transcription/types.ts +2 -1
- package/src/types.ts +142 -15
- package/src/usage/claude.ts +19 -12
- package/src/usage/commandcode.ts +1 -1
- package/src/usage/cursor.ts +14 -25
- package/src/usage/github-copilot.ts +1 -1
- package/src/usage/kimi.ts +1 -1
- package/src/usage/minimax-code.ts +8 -1
- package/src/usage.ts +8 -0
- package/src/utils/anthropic-auth.ts +34 -10
- package/src/utils/aws-profile.ts +10 -4
- package/src/utils/block-symbols.ts +17 -0
- package/src/utils/dsml-leak.ts +109 -0
- package/src/utils/empty-completion-retry.ts +2 -3
- package/src/utils/glyph-codec.ts +200 -7
- package/src/utils/idle-iterator.ts +80 -35
- package/src/utils/leaked-thinking-stream.ts +47 -25
- package/src/utils/resolve-peer.ts +7 -0
- package/src/utils/retry-wait.ts +19 -0
- package/src/utils/schema/CONSTRAINTS.md +9 -7
- package/src/utils/schema/fields.ts +20 -0
- package/src/utils/schema/normalize.ts +65 -23
- package/src/utils/schema/wire.ts +3 -3
- package/src/utils/stream-markup-healing.ts +4 -4
- package/src/utils/thinking-loop.ts +36 -13
- package/src/utils/tool-call-arguments.ts +16 -0
- package/src/utils/transport-fetch.ts +1 -1
- package/src/utils.ts +41 -10
- package/dist/types/judgment/typesafe.d.ts +0 -49
- package/src/judgment/typesafe.ts +0 -183
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
$env,
|
|
15
15
|
$flag,
|
|
16
16
|
asRecord,
|
|
17
|
+
cloneJsonTree,
|
|
17
18
|
fetchWithRetry,
|
|
18
19
|
getInstallId,
|
|
19
20
|
logger,
|
|
@@ -22,8 +23,9 @@ import {
|
|
|
22
23
|
USER_AGENT,
|
|
23
24
|
} from "@linxiraos/pi-utils";
|
|
24
25
|
import * as AIError from "../error";
|
|
25
|
-
import { parseToolCallArguments } from "../utils/tool-call-arguments";
|
|
26
|
-
import { getEnvApiKey
|
|
26
|
+
import { parseToolCallArguments, replayableToolCallArguments } from "../utils/tool-call-arguments";
|
|
27
|
+
import { getEnvApiKey } from "../env-api-key";
|
|
28
|
+
import { isOfficialCodexApiUrl } from "../stream";
|
|
27
29
|
import type {
|
|
28
30
|
Api,
|
|
29
31
|
AssistantMessage,
|
|
@@ -45,7 +47,9 @@ import type {
|
|
|
45
47
|
ToolResultMessage,
|
|
46
48
|
Usage,
|
|
47
49
|
} from "../types";
|
|
50
|
+
import { getPremiumServiceTierRequests, parseServiceTier } from "../types";
|
|
48
51
|
import {
|
|
52
|
+
clampOpenAIResponsesImageDetailForReplay,
|
|
49
53
|
createOpenAIResponsesHistoryPayload,
|
|
50
54
|
dropMalformedOpenAIResponsesToolCalls,
|
|
51
55
|
getOpenAIResponsesHistoryItems,
|
|
@@ -77,6 +81,7 @@ import {
|
|
|
77
81
|
type CodexRequestOptions,
|
|
78
82
|
type InputItem,
|
|
79
83
|
type ReasoningConfig,
|
|
84
|
+
lowestCodexWireEffort,
|
|
80
85
|
type RequestBody,
|
|
81
86
|
resolveCodexResponsesLite,
|
|
82
87
|
sanitizeCodexCallId,
|
|
@@ -97,9 +102,10 @@ import { getOpenAICodexWebSocketEnvValue, isOpenAICodexWebSocketPreferred } from
|
|
|
97
102
|
export { setCodexAttestationProvider } from "./openai-codex-attestation";
|
|
98
103
|
export type { CodexAttestationProvider } from "./openai-codex-attestation";
|
|
99
104
|
import {
|
|
100
|
-
getOpenAIEffortControlState,
|
|
101
105
|
type OpenAIEffortControlState,
|
|
102
106
|
planStableOpenAIEffort,
|
|
107
|
+
releaseOpenAIEffortControlSession,
|
|
108
|
+
resolveOpenAIEffortControlState,
|
|
103
109
|
} from "./openai-configuration-update";
|
|
104
110
|
import type {
|
|
105
111
|
ResponseComputerToolCall,
|
|
@@ -144,6 +150,7 @@ import {
|
|
|
144
150
|
normalizeOpenAIPromptCacheKey,
|
|
145
151
|
populateResponsesUsageFromResponse,
|
|
146
152
|
promoteResponsesToolUseStopReason,
|
|
153
|
+
settleReasoningSummary,
|
|
147
154
|
type SequentialCutoffSummaryState,
|
|
148
155
|
} from "./openai-shared";
|
|
149
156
|
import { redactSensitiveInObject, transformMessages } from "./transform-messages";
|
|
@@ -273,7 +280,14 @@ const CODEX_WEBSOCKET_FIRST_EVENT_TIMEOUT_MS = Number($env.PI_CODEX_WEBSOCKET_FI
|
|
|
273
280
|
const CODEX_WEBSOCKET_RETRY_BUDGET = Number($env.PI_CODEX_WEBSOCKET_RETRY_BUDGET || CODEX_MAX_RETRIES);
|
|
274
281
|
const CODEX_WEBSOCKET_RETRY_DELAY_MS = Number($env.PI_CODEX_WEBSOCKET_RETRY_DELAY_MS || CODEX_RETRY_DELAY_MS);
|
|
275
282
|
const CODEX_WEBSOCKET_TRANSPORT_ERROR_PREFIX = "Codex websocket transport error";
|
|
276
|
-
|
|
283
|
+
// The native-lane steering rejection is replayable: the session stops steering
|
|
284
|
+
// first, so the replay cannot trip it again.
|
|
285
|
+
const CODEX_RETRYABLE_EVENT_CODES = new Set([
|
|
286
|
+
"model_error",
|
|
287
|
+
"server_error",
|
|
288
|
+
"internal_error",
|
|
289
|
+
AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE,
|
|
290
|
+
]);
|
|
277
291
|
const CODEX_RETRYABLE_EVENT_MESSAGE =
|
|
278
292
|
/processing your request|retry your request|temporar(?:y|ily)|overloaded|service.?unavailable|internal error|server error/i;
|
|
279
293
|
const CODEX_PROVIDER_SESSION_STATE_KEY = "openai-codex-responses";
|
|
@@ -423,6 +437,7 @@ export interface OpenAICodexWebSocketDebugStats {
|
|
|
423
437
|
* is historical — SSE-only sessions use it too.
|
|
424
438
|
*/
|
|
425
439
|
type CodexWebSocketSessionState = {
|
|
440
|
+
sessionId: string;
|
|
426
441
|
disableWebsocket: boolean;
|
|
427
442
|
lastRequest?: RequestBody;
|
|
428
443
|
/** Last completed response; an in-progress response cannot replace the retry baseline. */
|
|
@@ -437,6 +452,8 @@ type CodexWebSocketSessionState = {
|
|
|
437
452
|
* pending tool output; see {@link planSteeredRequest}.
|
|
438
453
|
*/
|
|
439
454
|
acceptedSteering?: { connection: CodexWebSocketConnection; steers: CodexAcceptedSteer[] };
|
|
455
|
+
/** The server refused steering for this session (native turn lane); later responses are not steered. */
|
|
456
|
+
steeringUnsupported?: boolean;
|
|
440
457
|
lastTransport?: CodexTransport;
|
|
441
458
|
fallbackCount: number;
|
|
442
459
|
lastFallbackAt?: number;
|
|
@@ -473,6 +490,12 @@ interface CodexMetadataSessionState {
|
|
|
473
490
|
interface CodexCompatibilityIdentity {
|
|
474
491
|
installationId: string;
|
|
475
492
|
sessionId: string;
|
|
493
|
+
/**
|
|
494
|
+
* Session the backend derives prompt-cache affinity from, sent in every
|
|
495
|
+
* session header: the parent conversation's for a side request
|
|
496
|
+
* (`parentSessionId`), else `sessionId`. Turn metadata keeps `sessionId`.
|
|
497
|
+
*/
|
|
498
|
+
affinitySessionId?: string;
|
|
476
499
|
threadId: string;
|
|
477
500
|
windowId: string;
|
|
478
501
|
/** Header projection: identity fields only, never the Code Mode snapshot. */
|
|
@@ -611,6 +634,7 @@ function createCodexRequestMetadata(
|
|
|
611
634
|
parentTurnId?: string;
|
|
612
635
|
compaction?: CodexCompactionRequestContext;
|
|
613
636
|
toolNamespacesInfo?: unknown;
|
|
637
|
+
affinitySessionId?: string;
|
|
614
638
|
},
|
|
615
639
|
): CodexRequestMetadata {
|
|
616
640
|
if (options.startNewTurn || !session.turnId) {
|
|
@@ -674,6 +698,7 @@ function createCodexRequestMetadata(
|
|
|
674
698
|
clientMetadata[OPENAI_HEADERS.TURN_METADATA] = turnMetadataJson;
|
|
675
699
|
return {
|
|
676
700
|
...identity,
|
|
701
|
+
affinitySessionId: options.affinitySessionId,
|
|
677
702
|
turnId: session.turnId,
|
|
678
703
|
turnMetadataJson,
|
|
679
704
|
turnMetadataHeaderJson,
|
|
@@ -682,7 +707,7 @@ function createCodexRequestMetadata(
|
|
|
682
707
|
}
|
|
683
708
|
|
|
684
709
|
function applyCodexCompatibilityHeaders(headers: Headers, metadata: CodexCompatibilityIdentity): void {
|
|
685
|
-
headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.sessionId);
|
|
710
|
+
headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.affinitySessionId ?? metadata.sessionId);
|
|
686
711
|
headers.set(OPENAI_HEADERS.THREAD_ID, metadata.threadId);
|
|
687
712
|
headers.set(OPENAI_HEADERS.WINDOW_ID, metadata.windowId);
|
|
688
713
|
if (metadata.turnMetadataHeaderJson) {
|
|
@@ -1045,6 +1070,20 @@ function createCodexProviderSessionState(): CodexProviderSessionState {
|
|
|
1045
1070
|
webSocketPublicToPrivate: new Map(),
|
|
1046
1071
|
metadataSessions: new Map(),
|
|
1047
1072
|
effortControls: new Map(),
|
|
1073
|
+
releaseSession: sessionId => {
|
|
1074
|
+
const normalizedSessionId = normalizeOpenAIPromptCacheKey(sessionId);
|
|
1075
|
+
if (!normalizedSessionId) return;
|
|
1076
|
+
for (const [key, session] of state.webSocketSessions) {
|
|
1077
|
+
if (session.sessionId !== normalizedSessionId) continue;
|
|
1078
|
+
session.connection?.close("session_disposed");
|
|
1079
|
+
state.webSocketSessions.delete(key);
|
|
1080
|
+
for (const [publicKey, privateKey] of state.webSocketPublicToPrivate) {
|
|
1081
|
+
if (privateKey === key) state.webSocketPublicToPrivate.delete(publicKey);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
state.metadataSessions.delete(normalizedSessionId);
|
|
1085
|
+
releaseOpenAIEffortControlSession(state.effortControls, normalizedSessionId);
|
|
1086
|
+
},
|
|
1048
1087
|
close: () => {
|
|
1049
1088
|
for (const session of state.webSocketSessions.values()) {
|
|
1050
1089
|
session.connection?.close("session_disposed");
|
|
@@ -1326,36 +1365,46 @@ function getCodexServiceTierCostMultiplier(
|
|
|
1326
1365
|
model: Pick<Model<"openai-codex-responses">, "serviceTierCost">,
|
|
1327
1366
|
serviceTier: ServiceTier | "default" | undefined,
|
|
1328
1367
|
): number {
|
|
1329
|
-
//
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1368
|
+
// The subscription route draws on included plan usage, so its cost is the
|
|
1369
|
+
// usage-equivalent: each tier's multiplier comes from the catalog table, which
|
|
1370
|
+
// uses OpenAI's included-usage rates (Astra: Fast 2.5x, Ultrafast 8x). A tier
|
|
1371
|
+
// with no entry stays at 1x rather than an invented multiplier.
|
|
1372
|
+
if (serviceTier !== "flex" && serviceTier !== "priority" && serviceTier !== "ultrafast") return 1;
|
|
1332
1373
|
return model.serviceTierCost?.[serviceTier] ?? 1;
|
|
1333
1374
|
}
|
|
1334
1375
|
|
|
1335
1376
|
/**
|
|
1336
|
-
* The tier a Codex
|
|
1337
|
-
*
|
|
1338
|
-
* turn
|
|
1377
|
+
* The tier a Codex turn was served at. The Codex backend echoes
|
|
1378
|
+
* `service_tier: "default"` on turns it serves at the sent tier (a `priority`
|
|
1379
|
+
* turn decodes at Fast speed yet reports `default`), so a `default` or `auto`
|
|
1380
|
+
* echo carries no information and, like an absent one, resolves to the sent
|
|
1381
|
+
* tier, or `default` when none was sent. Any other echo (`flex`, `priority`,
|
|
1382
|
+
* `ultrafast`, `scale`) is authoritative. The tier's identity is preserved even
|
|
1383
|
+
* when it has no pricing entry (`scale`), because the recorded tier also drives
|
|
1384
|
+
* premium-request and speed accounting. The echo cannot reveal a genuine
|
|
1385
|
+
* downgrade, so a downgraded turn is recorded at the sent tier.
|
|
1339
1386
|
*/
|
|
1340
|
-
function resolveCodexCostServiceTier(res: ServiceTier | undefined, req?: unknown): ServiceTier
|
|
1341
|
-
|
|
1342
|
-
return
|
|
1387
|
+
function resolveCodexCostServiceTier(res: ServiceTier | undefined, req?: unknown): ServiceTier {
|
|
1388
|
+
if (res !== undefined && res !== "default" && res !== "auto") return res;
|
|
1389
|
+
return parseServiceTier(req) ?? "default";
|
|
1343
1390
|
}
|
|
1344
1391
|
|
|
1345
1392
|
function applyCodexServiceTierPricing(
|
|
1346
|
-
model: Pick<Model<"openai-codex-responses">, "serviceTierCost">,
|
|
1393
|
+
model: Pick<Model<"openai-codex-responses">, "provider" | "api" | "identity" | "serviceTierCost">,
|
|
1347
1394
|
usage: AssistantMessage["usage"],
|
|
1348
1395
|
resTier: ServiceTier | undefined,
|
|
1349
1396
|
reqTier: unknown,
|
|
1350
|
-
):
|
|
1351
|
-
const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier);
|
|
1397
|
+
): ServiceTier {
|
|
1398
|
+
const resolvedTier = resolveCodexCostServiceTier(resTier, reqTier) ?? "default";
|
|
1399
|
+
usage.premiumRequests ??= getPremiumServiceTierRequests(resolvedTier, model, { served: true });
|
|
1352
1400
|
const multiplier = getCodexServiceTierCostMultiplier(model, resolvedTier);
|
|
1353
|
-
if (multiplier === 1) return;
|
|
1401
|
+
if (multiplier === 1) return resolvedTier;
|
|
1354
1402
|
usage.cost.input *= multiplier;
|
|
1355
1403
|
usage.cost.output *= multiplier;
|
|
1356
1404
|
usage.cost.cacheRead *= multiplier;
|
|
1357
1405
|
usage.cost.cacheWrite *= multiplier;
|
|
1358
1406
|
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
1407
|
+
return resolvedTier;
|
|
1359
1408
|
}
|
|
1360
1409
|
|
|
1361
1410
|
function resetOutputState(output: AssistantMessage): void {
|
|
@@ -1449,14 +1498,14 @@ function createCodexRequestContext(
|
|
|
1449
1498
|
transportProviderSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
1450
1499
|
}
|
|
1451
1500
|
const sharedWebsocketState =
|
|
1452
|
-
sessionKey && providerSessionState
|
|
1501
|
+
sessionKey && transportSessionId && providerSessionState
|
|
1453
1502
|
? isolatedTransportState
|
|
1454
1503
|
? providerSessionState.webSocketSessions.get(sessionKey)
|
|
1455
|
-
: getCodexWebSocketSessionState(sessionKey, providerSessionState)
|
|
1504
|
+
: getCodexWebSocketSessionState(sessionKey, transportSessionId, providerSessionState)
|
|
1456
1505
|
: undefined;
|
|
1457
1506
|
const websocketState =
|
|
1458
|
-
sessionKey && isolatedTransportState
|
|
1459
|
-
? getCodexWebSocketSessionState(sessionKey, isolatedTransportState)
|
|
1507
|
+
sessionKey && transportSessionId && isolatedTransportState
|
|
1508
|
+
? getCodexWebSocketSessionState(sessionKey, transportSessionId, isolatedTransportState)
|
|
1460
1509
|
: sharedWebsocketState;
|
|
1461
1510
|
if (isolatedTransportState && websocketState && sharedWebsocketState) {
|
|
1462
1511
|
websocketState.disableWebsocket = sharedWebsocketState.disableWebsocket;
|
|
@@ -1483,6 +1532,7 @@ function createCodexRequestContext(
|
|
|
1483
1532
|
parentTurnId: options?.parentTurnId,
|
|
1484
1533
|
compaction,
|
|
1485
1534
|
toolNamespacesInfo: options?.toolNamespacesInfo,
|
|
1535
|
+
affinitySessionId: normalizeOpenAIPromptCacheKey(options?.parentSessionId),
|
|
1486
1536
|
});
|
|
1487
1537
|
transformedBody.client_metadata = requestMetadata.clientMetadata;
|
|
1488
1538
|
return {
|
|
@@ -1578,7 +1628,7 @@ export async function buildTransformedCodexRequestBody(
|
|
|
1578
1628
|
|
|
1579
1629
|
/**
|
|
1580
1630
|
* Keep the request-level effort byte-stable across a conversation and carry
|
|
1581
|
-
* later changes as `configuration_update` items (GPT-6
|
|
1631
|
+
* later changes as `configuration_update` items (GPT-6 family). Requires a
|
|
1582
1632
|
* session id and provider session state to remember the baseline; without
|
|
1583
1633
|
* them every request stands alone and sends its own effort.
|
|
1584
1634
|
*/
|
|
@@ -1593,8 +1643,17 @@ function applyCodexStableEffort(
|
|
|
1593
1643
|
const providerState = getCodexProviderSessionState(options?.providerSessionState);
|
|
1594
1644
|
const sessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
|
|
1595
1645
|
if (!providerState || !sessionId) return;
|
|
1596
|
-
const state =
|
|
1597
|
-
|
|
1646
|
+
const state = resolveOpenAIEffortControlState(
|
|
1647
|
+
providerState.effortControls,
|
|
1648
|
+
id => `${model.id}\u0000${id}`,
|
|
1649
|
+
sessionId,
|
|
1650
|
+
normalizeOpenAIPromptCacheKey(options?.parentSessionId),
|
|
1651
|
+
);
|
|
1652
|
+
const lowest = options?.minimizeEffort ? lowestCodexWireEffort(model) : undefined;
|
|
1653
|
+
body.reasoning = {
|
|
1654
|
+
...body.reasoning,
|
|
1655
|
+
effort: planStableOpenAIEffort(state, body.input, effort, lowest === "none" ? undefined : lowest),
|
|
1656
|
+
};
|
|
1598
1657
|
}
|
|
1599
1658
|
|
|
1600
1659
|
async function openInitialCodexEventStream(
|
|
@@ -1858,7 +1917,7 @@ async function openCodexWebSocketTransport(
|
|
|
1858
1917
|
requestSetup.requestSignal,
|
|
1859
1918
|
onSseEvent,
|
|
1860
1919
|
),
|
|
1861
|
-
requestBodyForState:
|
|
1920
|
+
requestBodyForState: cloneJsonTree(requestContext.transformedBody),
|
|
1862
1921
|
transport: "websocket",
|
|
1863
1922
|
};
|
|
1864
1923
|
}
|
|
@@ -1902,15 +1961,21 @@ async function openCodexWebSocketTransport(
|
|
|
1902
1961
|
websocketRequest = replacementWebsocketRequest as typeof websocketRequest;
|
|
1903
1962
|
}
|
|
1904
1963
|
recordCodexTurnRequestDiagnostics(websocketState, websocketRequest, "websocket", canAppendBeforeRequest);
|
|
1905
|
-
const requestBodyForState =
|
|
1906
|
-
// `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options`
|
|
1907
|
-
// recorded state must reflect what was actually
|
|
1908
|
-
// summary decoder
|
|
1964
|
+
const requestBodyForState = cloneJsonTree(requestContext.transformedBody);
|
|
1965
|
+
// `onPayload` may rewrite the outgoing frame (e.g. drop `stream_options` or
|
|
1966
|
+
// change `service_tier`); recorded state must reflect what was actually
|
|
1967
|
+
// sent — the sequential-cutoff summary decoder and the served-tier
|
|
1968
|
+
// fallback key off it.
|
|
1909
1969
|
if (websocketRequest.stream_options === undefined) {
|
|
1910
1970
|
delete requestBodyForState.stream_options;
|
|
1911
1971
|
} else {
|
|
1912
1972
|
requestBodyForState.stream_options = websocketRequest.stream_options;
|
|
1913
1973
|
}
|
|
1974
|
+
if (websocketRequest.service_tier === undefined) {
|
|
1975
|
+
delete requestBodyForState.service_tier;
|
|
1976
|
+
} else {
|
|
1977
|
+
requestBodyForState.service_tier = websocketRequest.service_tier;
|
|
1978
|
+
}
|
|
1914
1979
|
requestContext.rawRequestDump.body = websocketRequest;
|
|
1915
1980
|
CODEX_DEBUG &&
|
|
1916
1981
|
logger.debug("[codex] codex websocket request", {
|
|
@@ -2005,7 +2070,14 @@ async function openCodexSseTransport(
|
|
|
2005
2070
|
wireBody = replacementWireBody as RequestBody;
|
|
2006
2071
|
}
|
|
2007
2072
|
recordCodexTurnRequestDiagnostics(state, wireBody, "sse", canAppendBeforeRequest);
|
|
2008
|
-
|
|
2073
|
+
// SSE turns never chain, so later reads need only the per-turn knobs (summary
|
|
2074
|
+
// delivery, service tier); copying the whole transcript would be wasted work.
|
|
2075
|
+
const requestBodyForState: RequestBody = {
|
|
2076
|
+
model: wireBody.model,
|
|
2077
|
+
stream_options: wireBody.stream_options ? { ...wireBody.stream_options } : undefined,
|
|
2078
|
+
service_tier: wireBody.service_tier,
|
|
2079
|
+
};
|
|
2080
|
+
return { eventStream: await open(wireBody), requestBodyForState, transport: "sse" };
|
|
2009
2081
|
}
|
|
2010
2082
|
|
|
2011
2083
|
function isJsonWhitespaceOnly(value: string): boolean {
|
|
@@ -2454,11 +2526,13 @@ class CodexStreamProcessor {
|
|
|
2454
2526
|
/** Start delivering caller steering into the response `response.created` announced. */
|
|
2455
2527
|
#startSteering(rawEvent: Record<string, unknown>): void {
|
|
2456
2528
|
const source = this.options?.liveSteering;
|
|
2457
|
-
const
|
|
2529
|
+
const state = this.runtime.websocketState;
|
|
2530
|
+
const connection = state?.connection;
|
|
2458
2531
|
const responseId = asRecord(rawEvent.response)?.id;
|
|
2459
2532
|
if (
|
|
2460
2533
|
!source ||
|
|
2461
2534
|
!connection ||
|
|
2535
|
+
state?.steeringUnsupported ||
|
|
2462
2536
|
this.#steerPump ||
|
|
2463
2537
|
typeof responseId !== "string" ||
|
|
2464
2538
|
this.runtime.transport !== "websocket" ||
|
|
@@ -2516,6 +2590,7 @@ class CodexStreamProcessor {
|
|
|
2516
2590
|
if (entry?.block?.type !== "thinking") return;
|
|
2517
2591
|
for (const delta of this.runtime.takeSummaryDeltas(entry)) {
|
|
2518
2592
|
entry.block.thinking += delta;
|
|
2593
|
+
entry.block.summary = true;
|
|
2519
2594
|
this.stream.push({
|
|
2520
2595
|
type: "thinking_delta",
|
|
2521
2596
|
contentIndex: entry.contentIndex,
|
|
@@ -2528,7 +2603,7 @@ class CodexStreamProcessor {
|
|
|
2528
2603
|
const { runtime, output, stream } = this;
|
|
2529
2604
|
const rawItem = rawEvent.item;
|
|
2530
2605
|
if (!rawItem || typeof rawItem !== "object") return;
|
|
2531
|
-
const item =
|
|
2606
|
+
const item = cloneJsonTree(rawItem) as CodexEventItem;
|
|
2532
2607
|
if (item.type === "image_generation_call" && item.result) item.status = "completed";
|
|
2533
2608
|
|
|
2534
2609
|
// Match the finalization to the OPEN ITEM that started this block, not the
|
|
@@ -2558,6 +2633,7 @@ class CodexStreamProcessor {
|
|
|
2558
2633
|
this.#sequentialCutoffSummaries ? this.runtime.cutoffSummaries : undefined,
|
|
2559
2634
|
);
|
|
2560
2635
|
block.thinkingSignature = JSON.stringify(item);
|
|
2636
|
+
settleReasoningSummary(item, block);
|
|
2561
2637
|
stream.push({
|
|
2562
2638
|
type: "thinking_end",
|
|
2563
2639
|
contentIndex,
|
|
@@ -2583,12 +2659,19 @@ class CodexStreamProcessor {
|
|
|
2583
2659
|
}
|
|
2584
2660
|
|
|
2585
2661
|
if (item.type === "function_call") {
|
|
2662
|
+
const partial = block?.type === "toolCall" ? block[kStreamingPartialJson] : undefined;
|
|
2586
2663
|
const toolCall: ToolCall = {
|
|
2587
2664
|
type: "toolCall",
|
|
2588
2665
|
id: encodeResponsesToolCallId(item.call_id, item.id),
|
|
2589
2666
|
name: item.name,
|
|
2590
|
-
|
|
2667
|
+
// An empty terminal item falls back to what `.delta`/`.done` already delivered.
|
|
2668
|
+
arguments: item.arguments
|
|
2669
|
+
? parseToolCallArguments(item.arguments)
|
|
2670
|
+
: block?.type === "toolCall" && !partial
|
|
2671
|
+
? block.arguments
|
|
2672
|
+
: parseToolCallArguments(partial),
|
|
2591
2673
|
};
|
|
2674
|
+
item.arguments = replayableToolCallArguments(item.arguments, toolCall.arguments);
|
|
2592
2675
|
if (block?.type === "toolCall") {
|
|
2593
2676
|
// Persist the authoritative final args on the stored block; the throttled
|
|
2594
2677
|
// delta parser may have left block.arguments stale (often `{}`).
|
|
@@ -2654,8 +2737,7 @@ class CodexStreamProcessor {
|
|
|
2654
2737
|
const response = rawResponse && typeof rawResponse === "object" ? rawResponse : undefined;
|
|
2655
2738
|
const responseId = response && "id" in response && typeof response.id === "string" ? response.id : undefined;
|
|
2656
2739
|
const usage = response && "usage" in response ? parseCodexResponseUsage(response.usage) : undefined;
|
|
2657
|
-
const serviceTier =
|
|
2658
|
-
response && "service_tier" in response ? parseCodexServiceTier(response.service_tier) : undefined;
|
|
2740
|
+
const serviceTier = response && "service_tier" in response ? parseServiceTier(response.service_tier) : undefined;
|
|
2659
2741
|
const status = response && "status" in response ? parseCodexResponseStatus(response.status) : undefined;
|
|
2660
2742
|
const endTurn = response && "end_turn" in response ? response.end_turn : undefined;
|
|
2661
2743
|
|
|
@@ -2688,10 +2770,13 @@ class CodexStreamProcessor {
|
|
|
2688
2770
|
// baseline, which no longer matches the transcript.
|
|
2689
2771
|
resetCodexWebSocketAppendState(state);
|
|
2690
2772
|
} else {
|
|
2691
|
-
|
|
2773
|
+
// requestBodyForState is already a private copy and is not mutated after
|
|
2774
|
+
// the request, so the append baseline takes ownership of it.
|
|
2775
|
+
state.lastRequest = runtime.requestBodyForState;
|
|
2692
2776
|
const nativeOutputItems = runtime.finalizeNativeOutputItems();
|
|
2693
2777
|
const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
2694
|
-
|
|
2778
|
+
cloneJsonTree(nativeOutputItems),
|
|
2779
|
+
{ supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal },
|
|
2695
2780
|
);
|
|
2696
2781
|
if (responseId && replayableResponseItems && replayableResponseItems.length === nativeOutputItems.length) {
|
|
2697
2782
|
state.lastResponseId = responseId;
|
|
@@ -2713,7 +2798,12 @@ class CodexStreamProcessor {
|
|
|
2713
2798
|
finalizePendingResponsesToolCalls(output);
|
|
2714
2799
|
|
|
2715
2800
|
calculateCost(model, output.usage, output.timestamp);
|
|
2716
|
-
|
|
2801
|
+
output.serviceTier = applyCodexServiceTierPricing(
|
|
2802
|
+
model,
|
|
2803
|
+
output.usage,
|
|
2804
|
+
serviceTier,
|
|
2805
|
+
runtime.requestBodyForState.service_tier,
|
|
2806
|
+
);
|
|
2717
2807
|
output.stopReason = mapOpenAIResponsesStopReason(steered ? "completed" : status);
|
|
2718
2808
|
promoteResponsesToolUseStopReason(
|
|
2719
2809
|
output,
|
|
@@ -2722,7 +2812,28 @@ class CodexStreamProcessor {
|
|
|
2722
2812
|
);
|
|
2723
2813
|
}
|
|
2724
2814
|
|
|
2815
|
+
/**
|
|
2816
|
+
* The native turn lane rejected our steering and dropped the response with
|
|
2817
|
+
* its socket: stop steering this session and forget the dead socket so the
|
|
2818
|
+
* provider retry replays on a fresh one.
|
|
2819
|
+
*/
|
|
2820
|
+
#stopSteeringOnNativeLaneRejection(error: unknown): void {
|
|
2821
|
+
const state = this.runtime.websocketState;
|
|
2822
|
+
if (
|
|
2823
|
+
!state ||
|
|
2824
|
+
!(error instanceof CodexProviderStreamError) ||
|
|
2825
|
+
error.code !== AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE
|
|
2826
|
+
) {
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2829
|
+
state.steeringUnsupported = true;
|
|
2830
|
+
state.connection?.close("native-lane-steer-rejected");
|
|
2831
|
+
state.connection = undefined;
|
|
2832
|
+
resetCodexWebSocketAppendState(state);
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2725
2835
|
async #recoverStreamError(error: unknown): Promise<boolean> {
|
|
2836
|
+
this.#stopSteeringOnNativeLaneRejection(error);
|
|
2726
2837
|
if (
|
|
2727
2838
|
error instanceof CodexSteerCommitError &&
|
|
2728
2839
|
this.runtime.websocketState &&
|
|
@@ -3277,7 +3388,14 @@ export async function prewarmOpenAICodexResponses(
|
|
|
3277
3388
|
model: Model<"openai-codex-responses">,
|
|
3278
3389
|
options?: Pick<
|
|
3279
3390
|
OpenAICodexResponsesOptions,
|
|
3280
|
-
|
|
3391
|
+
| "apiKey"
|
|
3392
|
+
| "headers"
|
|
3393
|
+
| "sessionId"
|
|
3394
|
+
| "signal"
|
|
3395
|
+
| "preferWebsockets"
|
|
3396
|
+
| "providerSessionState"
|
|
3397
|
+
| "responsesLite"
|
|
3398
|
+
| "serviceTier"
|
|
3281
3399
|
>,
|
|
3282
3400
|
): Promise<void> {
|
|
3283
3401
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
@@ -3294,17 +3412,18 @@ export async function prewarmOpenAICodexResponses(
|
|
|
3294
3412
|
if (publicSessionKey && sessionKey) {
|
|
3295
3413
|
providerSessionState?.webSocketPublicToPrivate.set(publicSessionKey, sessionKey);
|
|
3296
3414
|
}
|
|
3297
|
-
if (!sessionKey || !providerSessionState) return;
|
|
3298
|
-
const state = getCodexWebSocketSessionState(sessionKey, providerSessionState);
|
|
3415
|
+
if (!sessionKey || !transportSessionId || !providerSessionState) return;
|
|
3416
|
+
const state = getCodexWebSocketSessionState(sessionKey, transportSessionId, providerSessionState);
|
|
3299
3417
|
if (!shouldUseCodexWebSocket(model, state, options?.preferWebsockets)) return;
|
|
3300
|
-
const metadataSession = getOrCreateCodexMetadataSessionState(
|
|
3301
|
-
transportSessionId ?? crypto.randomUUID(),
|
|
3302
|
-
providerSessionState,
|
|
3303
|
-
);
|
|
3418
|
+
const metadataSession = getOrCreateCodexMetadataSessionState(transportSessionId, providerSessionState);
|
|
3304
3419
|
const turnState = getOrCreateCodexTurnState(metadataSession, sessionKey);
|
|
3305
3420
|
const codexClientVersion = CODEX_CLIENT_VERSION;
|
|
3306
3421
|
const requestIdentity = createCodexCompatibilityIdentity(metadataSession);
|
|
3307
3422
|
const attestation = await getCodexAttestationHeader(accountId);
|
|
3423
|
+
const routedRequest: Pick<RequestBody, "model" | "service_tier"> = {
|
|
3424
|
+
model: model.requestModelId ?? model.id,
|
|
3425
|
+
};
|
|
3426
|
+
applyOpenAIServiceTier(routedRequest, options?.serviceTier, model);
|
|
3308
3427
|
const headers = logger.time(
|
|
3309
3428
|
"prewarmCodex:createHeaders",
|
|
3310
3429
|
createCodexHeaders,
|
|
@@ -3319,6 +3438,7 @@ export async function prewarmOpenAICodexResponses(
|
|
|
3319
3438
|
responsesLite,
|
|
3320
3439
|
requestIdentity,
|
|
3321
3440
|
attestation,
|
|
3441
|
+
routedRequest,
|
|
3322
3442
|
);
|
|
3323
3443
|
await logger.time(
|
|
3324
3444
|
"prewarmCodex:establishWs",
|
|
@@ -3351,11 +3471,13 @@ function getCodexWebSocketSessionKey(
|
|
|
3351
3471
|
|
|
3352
3472
|
function getCodexWebSocketSessionState(
|
|
3353
3473
|
sessionKey: string,
|
|
3474
|
+
sessionId: string,
|
|
3354
3475
|
providerSessionState: CodexProviderSessionState,
|
|
3355
3476
|
): CodexWebSocketSessionState {
|
|
3356
3477
|
const existing = providerSessionState.webSocketSessions.get(sessionKey);
|
|
3357
3478
|
if (existing) return existing;
|
|
3358
3479
|
const created: CodexWebSocketSessionState = {
|
|
3480
|
+
sessionId,
|
|
3359
3481
|
disableWebsocket: false,
|
|
3360
3482
|
canAppend: false,
|
|
3361
3483
|
fallbackCount: 0,
|
|
@@ -3510,32 +3632,11 @@ export function getOpenAICodexTransportDetails(
|
|
|
3510
3632
|
};
|
|
3511
3633
|
}
|
|
3512
3634
|
|
|
3513
|
-
const codexDiagnosticsTextEncoder = new TextEncoder();
|
|
3514
|
-
|
|
3515
|
-
function jsonByteLength(value: unknown): number {
|
|
3516
|
-
const json = JSON.stringify(value);
|
|
3517
|
-
return codexDiagnosticsTextEncoder.encode(json === undefined ? "undefined" : json).byteLength;
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
3635
|
function hashJson(value: unknown): string {
|
|
3521
3636
|
const json = JSON.stringify(value);
|
|
3522
3637
|
return String(Bun.hash(json === undefined ? "undefined" : json));
|
|
3523
3638
|
}
|
|
3524
3639
|
|
|
3525
|
-
function parseCodexServiceTier(value: unknown): ServiceTier | undefined {
|
|
3526
|
-
switch (value) {
|
|
3527
|
-
case "auto":
|
|
3528
|
-
case "default":
|
|
3529
|
-
case "flex":
|
|
3530
|
-
case "scale":
|
|
3531
|
-
case "priority":
|
|
3532
|
-
case "ultrafast":
|
|
3533
|
-
return value;
|
|
3534
|
-
default:
|
|
3535
|
-
return undefined;
|
|
3536
|
-
}
|
|
3537
|
-
}
|
|
3538
|
-
|
|
3539
3640
|
function parseCodexResponseStatus(value: unknown): ResponseStatus | undefined {
|
|
3540
3641
|
switch (value) {
|
|
3541
3642
|
case "completed":
|
|
@@ -3662,7 +3763,7 @@ function buildCodexTurnRequestDiagnostics(
|
|
|
3662
3763
|
inputItemCount: inputItems.length,
|
|
3663
3764
|
inputItemTypes,
|
|
3664
3765
|
...(inputItemTypes[0] ? { firstInputItemType: inputItemTypes[0] } : {}),
|
|
3665
|
-
inputJsonBytes:
|
|
3766
|
+
inputJsonBytes: Buffer.byteLength(JSON.stringify(inputItems), "utf8"),
|
|
3666
3767
|
...(promptCacheKey !== undefined ? { promptCacheKey } : {}),
|
|
3667
3768
|
...(toolsHash !== undefined ? { toolsHash } : {}),
|
|
3668
3769
|
optionsHash: createCodexOptionsHash(request),
|
|
@@ -3690,6 +3791,8 @@ function recordCodexTurnRequestDiagnostics(
|
|
|
3690
3791
|
state.stats.lastDeltaInputItems = undefined;
|
|
3691
3792
|
state.stats.lastPreviousResponseId = undefined;
|
|
3692
3793
|
}
|
|
3794
|
+
// Measured at send time: the request object may be reused or mutated by an
|
|
3795
|
+
// `onPayload` hook afterwards, and retaining it would pin the transcript.
|
|
3693
3796
|
state.stats.lastTurn = {
|
|
3694
3797
|
request: buildCodexTurnRequestDiagnostics(request, transport, canAppendBeforeRequest),
|
|
3695
3798
|
};
|
|
@@ -4043,6 +4146,11 @@ class CodexWebSocketConnection {
|
|
|
4043
4146
|
this.#handleSteerEvent(parsed);
|
|
4044
4147
|
return;
|
|
4045
4148
|
}
|
|
4149
|
+
// The native lane answers `response.steer` with a plain `error` that
|
|
4150
|
+
// also ends the response: refuse the submission, then fail the stream.
|
|
4151
|
+
if (parsed.type === "error" && parsed.code === AIError.CODEX_NATIVE_LANE_STEER_REJECTED_CODE) {
|
|
4152
|
+
this.#refuseSteerWaiters(parsed.code, typeof parsed.message === "string" ? parsed.message : undefined);
|
|
4153
|
+
}
|
|
4046
4154
|
this.#push(parsed);
|
|
4047
4155
|
} catch (error) {
|
|
4048
4156
|
notifyCodexWebSocketMalformed(this.#streamObserver, event.data, error);
|
|
@@ -4336,6 +4444,12 @@ class CodexWebSocketConnection {
|
|
|
4336
4444
|
return index < 0 ? undefined : this.#steerWaiters.splice(index, 1)[0];
|
|
4337
4445
|
}
|
|
4338
4446
|
|
|
4447
|
+
#refuseSteerWaiters(code: string, message: string | undefined): void {
|
|
4448
|
+
const waiters = this.#steerWaiters;
|
|
4449
|
+
this.#steerWaiters = [];
|
|
4450
|
+
for (const waiter of waiters) waiter.resolve({ accepted: false, code, message });
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4339
4453
|
#rejectSteerWaiters(reason: string): void {
|
|
4340
4454
|
const waiters = this.#steerWaiters;
|
|
4341
4455
|
if (waiters.length === 0) return;
|
|
@@ -4593,10 +4707,12 @@ async function getOrCreateCodexWebSocketConnection(
|
|
|
4593
4707
|
* compression is disabled or fails, in which case the caller sends the
|
|
4594
4708
|
* plain JSON string without a `content-encoding` header.
|
|
4595
4709
|
*/
|
|
4596
|
-
function compressCodexRequestBody(bodyJson: string, baseUrl: string): Uint8Array | undefined {
|
|
4710
|
+
async function compressCodexRequestBody(bodyJson: string, baseUrl: string): Promise<Uint8Array | undefined> {
|
|
4597
4711
|
if (!isOfficialCodexApiUrl(baseUrl) || !$flag("PI_CODEX_ZSTD", true)) return undefined;
|
|
4598
4712
|
try {
|
|
4599
|
-
|
|
4713
|
+
// Off the main thread: SSE sends the full transcript, so multi-MB bodies
|
|
4714
|
+
// would otherwise block the event loop for the whole compression.
|
|
4715
|
+
return await Bun.zstdCompress(bodyJson, { level: 3 });
|
|
4600
4716
|
} catch (error) {
|
|
4601
4717
|
CODEX_DEBUG &&
|
|
4602
4718
|
logger.debug("[codex] codex request body compression failed", {
|
|
@@ -4663,7 +4779,7 @@ async function openCodexSseEventStream(
|
|
|
4663
4779
|
}
|
|
4664
4780
|
};
|
|
4665
4781
|
const bodyJson = JSON.stringify(body);
|
|
4666
|
-
const compressedBody = compressCodexRequestBody(bodyJson, url);
|
|
4782
|
+
const compressedBody = await compressCodexRequestBody(bodyJson, url);
|
|
4667
4783
|
if (compressedBody !== undefined) {
|
|
4668
4784
|
headers.set("content-encoding", "zstd");
|
|
4669
4785
|
}
|
|
@@ -4794,8 +4910,11 @@ function createCodexHeaders(
|
|
|
4794
4910
|
headers.set(OPENAI_HEADERS.VERSION, codexClientVersion);
|
|
4795
4911
|
headers.set("User-Agent", USER_AGENT);
|
|
4796
4912
|
if (sessionId) {
|
|
4797
|
-
headers
|
|
4798
|
-
|
|
4913
|
+
// The backend derives prompt-cache affinity from the session headers
|
|
4914
|
+
// (codex-rs `responses_session_id`); a side request shares its parent's.
|
|
4915
|
+
const affinitySessionId = requestMetadata?.affinitySessionId ?? sessionId;
|
|
4916
|
+
headers.set(OPENAI_HEADERS.CONVERSATION_ID, affinitySessionId);
|
|
4917
|
+
headers.set(OPENAI_HEADERS.SESSION_ID, affinitySessionId);
|
|
4799
4918
|
headers.set("x-client-request-id", sessionId);
|
|
4800
4919
|
} else {
|
|
4801
4920
|
headers.delete(OPENAI_HEADERS.CONVERSATION_ID);
|
|
@@ -4909,10 +5028,14 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4909
5028
|
if (historyItems) {
|
|
4910
5029
|
const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array<ResponseInput[number]>;
|
|
4911
5030
|
const sanitizedHistoryItems = dropMalformedOpenAIResponsesToolCalls(redactedHistoryItems);
|
|
5031
|
+
const clampedHistoryItems = clampOpenAIResponsesImageDetailForReplay(
|
|
5032
|
+
sanitizedHistoryItems,
|
|
5033
|
+
model.compat.supportsImageDetailOriginal,
|
|
5034
|
+
);
|
|
4912
5035
|
const replayItems =
|
|
4913
5036
|
model.supportsComputerUse === true
|
|
4914
|
-
?
|
|
4915
|
-
: unrollCodexComputerItems(
|
|
5037
|
+
? clampedHistoryItems
|
|
5038
|
+
: unrollCodexComputerItems(clampedHistoryItems, model.compat.supportsImageDetailOriginal);
|
|
4916
5039
|
for (const item of replayItems) {
|
|
4917
5040
|
if (item.type === "custom_tool_call") {
|
|
4918
5041
|
customCallIds.add(item.call_id);
|
|
@@ -4946,7 +5069,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4946
5069
|
const historyItems = providerPayload?.items as Array<Record<string, unknown>> | undefined;
|
|
4947
5070
|
let suppressHiddenEmptyFallback = false;
|
|
4948
5071
|
if (historyItems) {
|
|
4949
|
-
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems
|
|
5072
|
+
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems, {
|
|
5073
|
+
supportsImageDetailOriginal: model.compat.supportsImageDetailOriginal,
|
|
5074
|
+
});
|
|
4950
5075
|
if (sanitizedHistoryItems) {
|
|
4951
5076
|
const rawReplayItems =
|
|
4952
5077
|
model.supportsComputerUse === true
|