@fractaal/pi-ai 0.84.8 → 0.85.1

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.
@@ -15,8 +15,10 @@ import { formatProviderError, normalizeProviderError } from "../utils/error-body
15
15
  import { AssistantMessageEventStream } from "../utils/event-stream.js";
16
16
  import { headersToRecord } from "../utils/headers.js";
17
17
  import { resolveHttpProxyUrlForTarget } from "../utils/node-http-proxy.js";
18
+ import { isRetryableAssistantError, retryAssistantCall } from "../utils/retry.js";
18
19
  import { uuidv7 } from "../utils/uuid.js";
19
20
  import { createGrammarToolInputProperties } from "./constrained-sampling.js";
21
+ import { retainCodexCompactionInput, trimCodexCompactionToolOutputs } from "./openai-codex-compaction-history.js";
20
22
  import { clampOpenAIPromptCacheKey } from "./openai-prompt-cache.js";
21
23
  import { convertResponsesMessages, convertResponsesTools, processResponsesStream } from "./openai-responses-shared.js";
22
24
  import { buildBaseOptions } from "./simple-options.js";
@@ -44,6 +46,7 @@ const CODEX_RESPONSE_STATUSES = new Set([
44
46
  "queued",
45
47
  "in_progress",
46
48
  ]);
49
+ export { estimateOpenAINativeCompactionTokens } from "./openai-codex-compaction-history.js";
47
50
  export function isOpenAICodexProvider(provider) {
48
51
  if (!provider || provider.id !== "openai-codex")
49
52
  return false;
@@ -207,11 +210,15 @@ export const stream = (model, context, options) => {
207
210
  timestamp: Date.now(),
208
211
  };
209
212
  try {
210
- const apiKey = options?.apiKey;
211
- if (!apiKey) {
213
+ const transportAuth = options?.authMode === "transport";
214
+ if (transportAuth && (!options?.fetch || options.fetch === globalThis.fetch)) {
215
+ throw new Error("Codex transport auth requires a custom fetch that owns authentication");
216
+ }
217
+ const apiKey = transportAuth ? undefined : options?.apiKey;
218
+ if (!transportAuth && !apiKey) {
212
219
  throw new Error(`No API key for provider: ${model.provider}`);
213
220
  }
214
- const accountId = extractAccountId(apiKey);
221
+ const accountId = apiKey ? extractAccountId(apiKey) : undefined;
215
222
  const grammarToolInputProperties = createGrammarToolInputProperties(context.tools, model.compat?.supportsOpenAIGrammarTools ?? false);
216
223
  const cacheSessionId = options?.cacheRetention === "none" ? undefined : options?.sessionId;
217
224
  const codexSessionId = clampOpenAIPromptCacheKey(cacheSessionId);
@@ -226,7 +233,7 @@ export const stream = (model, context, options) => {
226
233
  const bodyJson = JSON.stringify(body);
227
234
  const httpTimeoutMs = normalizeTimeoutMs(options?.timeoutMs);
228
235
  const websocketConnectTimeoutMs = normalizeTimeoutMs(options?.websocketConnectTimeoutMs);
229
- const transport = options?.transport || "auto";
236
+ const transport = transportAuth ? "sse" : options?.transport || "auto";
230
237
  let startEmitted = false;
231
238
  const websocketDisabledForSession = transport !== "sse" && isWebSocketSseFallbackActive(cacheSessionId);
232
239
  if (websocketDisabledForSession) {
@@ -413,13 +420,14 @@ export const stream = (model, context, options) => {
413
420
  };
414
421
  function buildSimpleCodexOptions(model, context, options) {
415
422
  const apiKey = options?.apiKey;
416
- if (!apiKey) {
423
+ if (!apiKey && options?.authMode !== "transport") {
417
424
  throw new Error(`No API key for provider: ${model.provider}`);
418
425
  }
419
426
  const base = buildBaseOptions(model, context, options, apiKey);
420
427
  const clampedReasoning = options?.reasoning ? clampThinkingLevel(model, options.reasoning) : undefined;
421
428
  return {
422
429
  ...base,
430
+ authMode: options?.authMode,
423
431
  reasoningEffort: clampedReasoning === "off" ? undefined : clampedReasoning,
424
432
  nativeCompactionCheckpoint: options?.nativeCompactionCheckpoint,
425
433
  };
@@ -427,12 +435,25 @@ function buildSimpleCodexOptions(model, context, options) {
427
435
  export const streamSimple = (model, context, options) => stream(model, context, buildSimpleCodexOptions(model, context, options));
428
436
  export async function compactOpenAICodexResponses(model, context, options) {
429
437
  const items = [];
430
- const result = await stream(model, context, {
431
- ...buildSimpleCodexOptions(model, context, options),
432
- transport: "sse",
433
- nativeCompaction: true,
434
- onNativeCompactionItem: (item) => items.push(item),
435
- }).result();
438
+ const base = buildSimpleCodexOptions(model, context, options);
439
+ let transport = base.transport;
440
+ const produce = () => {
441
+ items.length = 0;
442
+ return stream(model, context, {
443
+ ...base,
444
+ transport,
445
+ // Own one bounded stream retry budget; do not multiply HTTP retries.
446
+ maxRetries: 0,
447
+ nativeCompaction: true,
448
+ onNativeCompactionItem: (item) => items.push(item),
449
+ }).result();
450
+ };
451
+ const policy = { enabled: true, maxRetries: 2, baseDelayMs: BASE_DELAY_MS };
452
+ let result = await retryAssistantCall(produce, policy, options?.signal);
453
+ if (transport !== "sse" && base.authMode !== "transport" && isRetryableAssistantError(result)) {
454
+ transport = "sse";
455
+ result = await retryAssistantCall(produce, policy, options?.signal);
456
+ }
436
457
  if (result.stopReason !== "stop") {
437
458
  throw new OpenAICodexNativeCompactionError(result.errorMessage || `OpenAI native compaction did not complete (${result.stopReason})`, getOpenAICodexResponseFailure(result));
438
459
  }
@@ -443,13 +464,12 @@ export async function compactOpenAICodexResponses(model, context, options) {
443
464
  if (!isOpenAINativeCompactionItem(item)) {
444
465
  throw new Error("OpenAI native compaction returned a malformed checkpoint item");
445
466
  }
446
- closeOpenAICodexWebSocketSessions(options?.sessionId);
447
467
  return {
448
- item: {
449
- type: "compaction",
450
- encrypted_content: item.encrypted_content,
451
- id: typeof item.id === "string" ? item.id : undefined,
452
- },
468
+ item: { ...item },
469
+ retainedInput: retainCodexCompactionInput(buildRequestBody(model, {
470
+ ...context,
471
+ messages: (options?.nativeCompactionRetainedMessages ?? context.messages).filter((message) => message.role === "user"),
472
+ }, base, undefined).input ?? []),
453
473
  tokensBefore: result.usage.input + result.usage.cacheRead,
454
474
  usage: result.usage,
455
475
  };
@@ -461,7 +481,12 @@ function buildRequestBody(model, context, options, cacheSessionId, grammarToolIn
461
481
  const supportsStrictMode = model.compat?.supportsStrictMode ?? true;
462
482
  const supportsOpenAIGrammarTools = model.compat?.supportsOpenAIGrammarTools ?? false;
463
483
  const toolPlacement = splitDeferredTools(context, model.compat?.supportsToolSearch ?? false);
464
- const messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {
484
+ const checkpoint = options?.nativeCompactionCheckpoint;
485
+ const checkpointInput = checkpoint ? [...(checkpoint.retainedInput ?? []), checkpoint.item] : [];
486
+ const requestContext = options?.nativeCompaction
487
+ ? trimCodexCompactionToolOutputs(context, checkpointInput, model.contextWindow)
488
+ : context;
489
+ const messages = convertResponsesMessages(model, requestContext, CODEX_TOOL_CALL_PROVIDERS, {
465
490
  includeSystemPrompt: false,
466
491
  grammarToolInputProperties,
467
492
  deferredTools: toolPlacement.deferred,
@@ -471,15 +496,16 @@ function buildRequestBody(model, context, options, cacheSessionId, grammarToolIn
471
496
  supportsOpenAIGrammarTools,
472
497
  },
473
498
  });
474
- const checkpoint = options?.nativeCompactionCheckpoint;
475
499
  if (checkpoint) {
476
- if (checkpoint.provider !== model.provider || checkpoint.modelId !== model.id) {
477
- throw new Error(`OpenAI native compaction checkpoint requires ${checkpoint.provider}/${checkpoint.modelId}; current model is ${model.provider}/${model.id}`);
500
+ if (checkpoint.provider !== model.provider || model.api !== "openai-codex-responses") {
501
+ throw new Error(`OpenAI native compaction checkpoint requires the openai-codex Responses route; current model is ${model.provider}/${model.id}`);
478
502
  }
479
503
  if (!isOpenAINativeCompactionItem(checkpoint.item)) {
480
504
  throw new Error("Stored OpenAI native compaction checkpoint is malformed");
481
505
  }
482
- messages.unshift({ ...checkpoint.item });
506
+ messages.unshift(...(checkpoint.retainedInput ?? []), {
507
+ ...checkpoint.item,
508
+ });
483
509
  }
484
510
  if (options?.nativeCompaction) {
485
511
  messages.push({ type: "compaction_trigger" });
@@ -1266,7 +1292,7 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
1266
1292
  }
1267
1293
  try {
1268
1294
  socket.send(JSON.stringify({ type: "response.create", ...requestBody }));
1269
- await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs)), onStart), output, stream, model, {
1295
+ await processResponsesStream(startWebSocketOutputOnFirstEvent(mapCodexEvents(parseWebSocket(socket, options?.signal, idleTimeoutMs), options?.onNativeCompactionItem), onStart), output, stream, model, {
1270
1296
  serviceTier: options?.serviceTier,
1271
1297
  grammarToolInputProperties,
1272
1298
  resolveServiceTier: resolveCodexServiceTier,
@@ -1275,6 +1301,10 @@ async function processWebSocketStream(url, body, headers, output, stream, model,
1275
1301
  if (options?.signal?.aborted) {
1276
1302
  keepConnection = false;
1277
1303
  }
1304
+ else if (options?.nativeCompaction && entry) {
1305
+ // The rewritten history must be sent in full, but the connection stays reusable.
1306
+ entry.continuation = undefined;
1307
+ }
1278
1308
  else if (useCachedContext && entry && output.responseId) {
1279
1309
  const responseItems = convertResponsesMessages(model, { messages: [output] }, CODEX_TOOL_CALL_PROVIDERS, {
1280
1310
  includeSystemPrompt: false,
@@ -1354,8 +1384,15 @@ function buildBaseCodexHeaders(initHeaders, additionalHeaders, accountId, token)
1354
1384
  headers.set(key, value);
1355
1385
  }
1356
1386
  }
1357
- headers.set("Authorization", `Bearer ${token}`);
1358
- headers.set("chatgpt-account-id", accountId);
1387
+ if (token && accountId) {
1388
+ headers.set("Authorization", `Bearer ${token}`);
1389
+ headers.set("chatgpt-account-id", accountId);
1390
+ }
1391
+ else {
1392
+ // The custom transport supplies its own authentication, never a copied token.
1393
+ headers.delete("Authorization");
1394
+ headers.delete("chatgpt-account-id");
1395
+ }
1359
1396
  headers.set("originator", "pi");
1360
1397
  const userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : "pi (browser)";
1361
1398
  headers.set("User-Agent", userAgent);