chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
@@ -11,17 +11,16 @@ import { registerSessionResourceCleanup } from "../session-resources.ts";
|
||||
import type {
|
||||
Api,
|
||||
AssistantMessage,
|
||||
Context,
|
||||
Model,
|
||||
ProviderEnv,
|
||||
ProviderHeaders,
|
||||
SimpleStreamOptions,
|
||||
StreamFunction,
|
||||
StreamOptions,
|
||||
TranscriptContext,
|
||||
Usage,
|
||||
} from "../types.ts";
|
||||
import { combineAbortSignals } from "../utils/abort-signals.ts";
|
||||
import { splitDeferredTools } from "../utils/deferred-tools.ts";
|
||||
import {
|
||||
appendAssistantMessageDiagnostic,
|
||||
createAssistantMessageDiagnostic,
|
||||
@@ -32,6 +31,14 @@ import { AssistantMessageEventStream } from "../utils/event-stream.ts";
|
||||
import { headersToRecord } from "../utils/headers.ts";
|
||||
import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.ts";
|
||||
import { getPiUserAgent } from "../utils/pi-user-agent.ts";
|
||||
import { getSystemMessageText } from "../utils/text.ts";
|
||||
import {
|
||||
getDeclaredTools,
|
||||
getInitialSystemMessage,
|
||||
normalizeContext,
|
||||
resolveTranscript,
|
||||
resolveTranscriptTools,
|
||||
} from "../utils/transcript.ts";
|
||||
import { uuidv7 } from "../utils/uuid.ts";
|
||||
import { createGrammarToolInputProperties } from "./constrained-sampling.ts";
|
||||
import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.ts";
|
||||
@@ -208,7 +215,7 @@ function loadNodeZlib(): typeof NodeZlib | null {
|
||||
// Returns the zstd-compressed body bytes, or null when compression is
|
||||
// unavailable (browser/Vite builds). Callers fall back to sending the
|
||||
// uncompressed JSON when this returns null.
|
||||
function compressRequestBodyZstd(bodyJson: string): Uint8Array | null {
|
||||
function compressRequestBodyZstd(bodyJson: string): Uint8Array<ArrayBuffer> | null {
|
||||
const zlib = loadNodeZlib();
|
||||
if (!zlib || typeof zlib.zstdCompressSync !== "function") {
|
||||
return null;
|
||||
@@ -217,7 +224,8 @@ function compressRequestBodyZstd(bodyJson: string): Uint8Array | null {
|
||||
const compressed = zlib.zstdCompressSync(bodyJson, {
|
||||
params: { [zlib.constants.ZSTD_c_compressionLevel]: REQUEST_COMPRESSION_ZSTD_LEVEL },
|
||||
});
|
||||
return new Uint8Array(compressed.buffer, compressed.byteOffset, compressed.byteLength);
|
||||
// A copy owns a plain ArrayBuffer, which fetch accepts as a body under every lib.
|
||||
return new Uint8Array(compressed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -229,10 +237,11 @@ function compressRequestBodyZstd(bodyJson: string): Uint8Array | null {
|
||||
|
||||
export const stream: StreamFunction<"openai-codex-responses", OpenAICodexResponsesOptions> = (
|
||||
model: Model<"openai-codex-responses">,
|
||||
context: Context,
|
||||
context: TranscriptContext,
|
||||
options?: OpenAICodexResponsesOptions,
|
||||
): AssistantMessageEventStream => {
|
||||
const stream = new AssistantMessageEventStream();
|
||||
const normalizedContext = resolveTranscript(context, model.compat?.supportsMidConvoSystemMessages);
|
||||
|
||||
(async () => {
|
||||
const output: AssistantMessage = {
|
||||
@@ -261,12 +270,12 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
||||
|
||||
const accountId = extractAccountId(apiKey);
|
||||
const grammarToolInputProperties = createGrammarToolInputProperties(
|
||||
context.tools,
|
||||
getDeclaredTools(normalizedContext.messages),
|
||||
model.compat?.supportsOpenAIGrammarTools ?? false,
|
||||
);
|
||||
const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId;
|
||||
const codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId);
|
||||
let body = buildRequestBody(model, context, options, codexSessionId, grammarToolInputProperties);
|
||||
let body = buildRequestBody(model, normalizedContext, options, codexSessionId, grammarToolInputProperties);
|
||||
const nextBody = await options?.onPayload?.(body, model);
|
||||
if (nextBody !== undefined) {
|
||||
body = nextBody as RequestBody;
|
||||
@@ -349,7 +358,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
||||
output,
|
||||
createAssistantMessageDiagnostic("provider_transport_failure", error, {
|
||||
configuredTransport: transport,
|
||||
fallbackTransport: websocketStarted ? undefined : "sse",
|
||||
...(websocketStarted ? {} : { fallbackTransport: "sse" }),
|
||||
eventsEmitted: websocketStarted,
|
||||
phase: websocketStarted ? "after_message_stream_start" : "before_message_stream_start",
|
||||
requestBytes: new TextEncoder().encode(bodyJson).byteLength,
|
||||
@@ -372,7 +381,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
||||
if (compressedBody) {
|
||||
sseHeaders.set("content-encoding", "zstd");
|
||||
}
|
||||
const sseBody: Uint8Array | string = compressedBody ?? bodyJson;
|
||||
const sseBody: Uint8Array<ArrayBuffer> | string = compressedBody ?? bodyJson;
|
||||
|
||||
// Fetch with retry logic for rate limits and transient errors
|
||||
let response: Response | undefined;
|
||||
@@ -491,7 +500,7 @@ export const stream: StreamFunction<"openai-codex-responses", OpenAICodexRespons
|
||||
|
||||
export const streamSimple: StreamFunction<"openai-codex-responses", SimpleStreamOptions> = (
|
||||
model: Model<"openai-codex-responses">,
|
||||
context: Context,
|
||||
context: TranscriptContext,
|
||||
options?: SimpleStreamOptions,
|
||||
): AssistantMessageEventStream => {
|
||||
const apiKey = options?.apiKey;
|
||||
@@ -518,39 +527,35 @@ export const streamSimple: StreamFunction<"openai-codex-responses", SimpleStream
|
||||
|
||||
function buildRequestBody(
|
||||
model: Model<"openai-codex-responses">,
|
||||
context: Context,
|
||||
context: TranscriptContext,
|
||||
options: OpenAICodexResponsesOptions | undefined,
|
||||
cacheSessionId: string | undefined,
|
||||
grammarToolInputProperties: ReadonlyMap<string, string> = createGrammarToolInputProperties(
|
||||
context.tools,
|
||||
getDeclaredTools(context.messages),
|
||||
model.compat?.supportsOpenAIGrammarTools ?? false,
|
||||
),
|
||||
): RequestBody {
|
||||
const supportsStrictMode = model.compat?.supportsStrictMode ?? true;
|
||||
const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false;
|
||||
const deferredToolsMode = model.compat?.supportsAdditionalTools
|
||||
? "additional-tools"
|
||||
: model.compat?.supportsToolSearch
|
||||
? "tool-search"
|
||||
: undefined;
|
||||
const toolPlacement = splitDeferredTools(context, deferredToolsMode !== undefined);
|
||||
const supportsAdditionalTools = model.compat?.supportsAdditionalTools ?? false;
|
||||
const supportsToolSearch = model.compat?.supportsToolSearch ?? false;
|
||||
const transcriptTools = resolveTranscriptTools(context.messages, supportsAdditionalTools || supportsToolSearch);
|
||||
const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
|
||||
includeSystemPrompt: false,
|
||||
grammarToolInputProperties,
|
||||
deferredTools: toolPlacement.deferred,
|
||||
deferredToolsMode,
|
||||
toolOptions: {
|
||||
strict: null,
|
||||
supportsStrictMode,
|
||||
supportsOpenAIGrammarTools,
|
||||
},
|
||||
supportsMidConvoSystemMessages: model.compat?.supportsMidConvoSystemMessages ?? false,
|
||||
supportsAdditionalTools,
|
||||
supportsToolSearch,
|
||||
toolOptions: { strict: null, supportsStrictMode, supportsOpenAIGrammarTools },
|
||||
});
|
||||
|
||||
const initialSystemMessage = getInitialSystemMessage(context.messages);
|
||||
const instructions = initialSystemMessage ? getSystemMessageText(initialSystemMessage) : "";
|
||||
const body: RequestBody = {
|
||||
model: model.id,
|
||||
store: false,
|
||||
stream: true,
|
||||
instructions: context.systemPrompt || "You are a helpful assistant.",
|
||||
instructions: instructions || "You are a helpful assistant.",
|
||||
input: messages,
|
||||
text: { verbosity: options?.textVerbosity || "low" },
|
||||
include: ["reasoning.encrypted_content"],
|
||||
@@ -567,8 +572,8 @@ function buildRequestBody(
|
||||
body.service_tier = options.serviceTier;
|
||||
}
|
||||
|
||||
if (toolPlacement.immediate.length > 0) {
|
||||
body.tools = convertResponsesTools(toolPlacement.immediate, {
|
||||
if (transcriptTools.requestTools.length > 0) {
|
||||
body.tools = convertResponsesTools(transcriptTools.requestTools, {
|
||||
strict: null,
|
||||
supportsStrictMode,
|
||||
supportsOpenAIGrammarTools,
|
||||
@@ -578,7 +583,9 @@ function buildRequestBody(
|
||||
if (options?.reasoningEffort !== undefined) {
|
||||
const effort =
|
||||
options.reasoningEffort === "none"
|
||||
? (model.thinkingLevelMap?.off ?? "none")
|
||||
? model.thinkingLevelMap?.off === undefined
|
||||
? "none"
|
||||
: model.thinkingLevelMap.off
|
||||
: (model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort);
|
||||
if (effort !== null) {
|
||||
body.reasoning = {
|
||||
@@ -586,6 +593,8 @@ function buildRequestBody(
|
||||
summary: options.reasoningSummary ?? "auto",
|
||||
};
|
||||
}
|
||||
} else if (model.reasoning && model.thinkingLevelMap?.off !== null) {
|
||||
body.reasoning = { effort: model.thinkingLevelMap?.off ?? "none" };
|
||||
}
|
||||
|
||||
return body;
|
||||
@@ -657,12 +666,18 @@ async function processStream(
|
||||
grammarToolInputProperties: ReadonlyMap<string, string>,
|
||||
options?: OpenAICodexResponsesOptions,
|
||||
): Promise<void> {
|
||||
await processResponsesStream(mapCodexEvents(parseSSE(response, options?.signal), output), output, stream, model, {
|
||||
serviceTier: options?.serviceTier,
|
||||
grammarToolInputProperties,
|
||||
resolveServiceTier: resolveCodexServiceTier,
|
||||
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
|
||||
});
|
||||
await processResponsesStream(
|
||||
mapCodexEvents(parseSSE(response, options?.signal), output, model, options?.onProviderStreamEvent),
|
||||
output,
|
||||
stream,
|
||||
model,
|
||||
{
|
||||
serviceTier: options?.serviceTier,
|
||||
grammarToolInputProperties,
|
||||
resolveServiceTier: resolveCodexServiceTier,
|
||||
applyServiceTierPricing: (usage, serviceTier) => applyServiceTierPricing(usage, serviceTier, model),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class CodexApiError extends Error {
|
||||
@@ -689,8 +704,20 @@ class CodexProtocolError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
class ProviderStreamEventCallbackError extends Error {
|
||||
constructor(cause: unknown) {
|
||||
super(formatThrownValue(cause));
|
||||
this.name = "ProviderStreamEventCallbackError";
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
|
||||
function isCodexNonTransportError(error: unknown): boolean {
|
||||
return error instanceof CodexApiError || error instanceof CodexProtocolError;
|
||||
return (
|
||||
error instanceof CodexApiError ||
|
||||
error instanceof CodexProtocolError ||
|
||||
error instanceof ProviderStreamEventCallbackError
|
||||
);
|
||||
}
|
||||
|
||||
function isWebSocketConnectionLimitReachedError(error: unknown): boolean {
|
||||
@@ -717,8 +744,16 @@ function extractCodexEventError(event: Record<string, unknown>): { code?: string
|
||||
async function* mapCodexEvents(
|
||||
events: AsyncIterable<Record<string, unknown>>,
|
||||
output: AssistantMessage,
|
||||
model: Model<"openai-codex-responses">,
|
||||
onProviderStreamEvent: StreamOptions["onProviderStreamEvent"] | undefined,
|
||||
): AsyncGenerator<ResponseStreamEvent> {
|
||||
for await (const event of events) {
|
||||
try {
|
||||
await onProviderStreamEvent?.(event, model);
|
||||
} catch (error) {
|
||||
// Keep callback failures out of Codex's WebSocket retry and SSE fallback path.
|
||||
throw new ProviderStreamEventCallbackError(error);
|
||||
}
|
||||
const type = typeof event.type === "string" ? event.type : undefined;
|
||||
if (!type) continue;
|
||||
|
||||
@@ -1508,7 +1543,12 @@ async function processWebSocketStream(
|
||||
socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
|
||||
await processResponsesStream(
|
||||
startWebSocketOutputOnFirstEvent(
|
||||
mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs), output),
|
||||
mapCodexEvents(
|
||||
parseWebSocket(socket, options?.signal, idleTimeoutMs),
|
||||
output,
|
||||
model,
|
||||
options?.onProviderStreamEvent,
|
||||
),
|
||||
onStart,
|
||||
),
|
||||
output,
|
||||
@@ -1524,10 +1564,15 @@ async function processWebSocketStream(
|
||||
if (options?.signal?.aborted) {
|
||||
keepConnection = false;
|
||||
} else if (useCachedContext && entry && output.responseId) {
|
||||
const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
|
||||
includeSystemPrompt: false,
|
||||
grammarToolInputProperties,
|
||||
}).filter((item) => item.type !== "function_call_output" && item.type !== "custom_tool_call_output");
|
||||
const responseItems = convertResponsesMessages(
|
||||
model,
|
||||
normalizeContext({ messages: [output] }),
|
||||
CODEX_TOOL_CALL_PROVIDERS,
|
||||
{
|
||||
includeSystemPrompt: false,
|
||||
grammarToolInputProperties,
|
||||
},
|
||||
).filter((item) => item.type !== "function_call_output" && item.type !== "custom_tool_call_output");
|
||||
entry.continuation = {
|
||||
lastRequestBody: fullBody,
|
||||
lastResponseId: output.responseId,
|
||||
@@ -1599,7 +1644,11 @@ function buildBaseCodexHeaders(
|
||||
accountId: string,
|
||||
token: string,
|
||||
): Headers {
|
||||
const headers = new Headers(initHeaders);
|
||||
// Defaults first so model and caller headers can override them, matching the other providers.
|
||||
const headers = new Headers({ originator: "pi", "User-Agent": getPiUserAgent() });
|
||||
for (const [key, value] of Object.entries(initHeaders || {})) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
for (const [key, value] of Object.entries(additionalHeaders || {})) {
|
||||
if (value === null) {
|
||||
headers.delete(key);
|
||||
@@ -1609,8 +1658,6 @@ function buildBaseCodexHeaders(
|
||||
}
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
headers.set("chatgpt-account-id", accountId);
|
||||
headers.set("originator", "pi");
|
||||
headers.set("User-Agent", getPiUserAgent());
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user