@oh-my-pi/pi-ai 17.2.3 → 17.2.5
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 +26 -0
- package/dist/types/auth-storage.d.ts +8 -0
- package/dist/types/dialect/examples.d.ts +10 -2
- package/dist/types/dialect/inventory.d.ts +6 -9
- package/dist/types/dialect/rendering.d.ts +9 -0
- package/dist/types/dialect/types.d.ts +0 -1
- package/dist/types/error/flags.d.ts +5 -0
- package/dist/types/providers/openai-codex-responses.d.ts +15 -1
- package/dist/types/providers/openai-shared.d.ts +24 -6
- package/dist/types/providers/transform-messages.d.ts +1 -1
- package/dist/types/types.d.ts +6 -4
- package/dist/types/utils/harmony-leak.d.ts +9 -0
- package/dist/types/utils/schema/typescript.d.ts +8 -2
- package/package.json +4 -4
- package/src/auth-broker/discover.ts +2 -1
- package/src/auth-storage.ts +149 -15
- package/src/dialect/examples.ts +24 -12
- package/src/dialect/gemini.ts +17 -31
- package/src/dialect/harmony.ts +1 -2
- package/src/dialect/inventory.ts +20 -63
- package/src/dialect/rendering.ts +54 -0
- package/src/dialect/types.ts +0 -1
- package/src/error/flags.ts +8 -0
- package/src/providers/anthropic.ts +78 -33
- package/src/providers/cursor.ts +6 -3
- package/src/providers/openai-codex-responses.ts +191 -36
- package/src/providers/openai-completions.ts +7 -1
- package/src/providers/openai-responses.ts +5 -6
- package/src/providers/openai-shared.ts +114 -40
- package/src/providers/transform-messages.ts +1 -1
- package/src/stream.ts +2 -2
- package/src/types.ts +9 -8
- package/src/utils/harmony-leak.ts +12 -0
- package/src/utils/schema/typescript.ts +21 -7
- package/src/utils.ts +1 -1
|
@@ -69,6 +69,7 @@ import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, tool
|
|
|
69
69
|
import { notifyRawSseEvent } from "../utils/sse-debug";
|
|
70
70
|
import { compactGrammarDefinition } from "./grammar";
|
|
71
71
|
import {
|
|
72
|
+
type CodexLiteShapedBody,
|
|
72
73
|
type CodexReasoningContext,
|
|
73
74
|
type CodexRequestOptions,
|
|
74
75
|
type InputItem,
|
|
@@ -83,6 +84,7 @@ import type {
|
|
|
83
84
|
ResponseFunctionToolCall,
|
|
84
85
|
ResponseInput,
|
|
85
86
|
ResponseInputContent,
|
|
87
|
+
ResponseOutputItem,
|
|
86
88
|
ResponseOutputMessage,
|
|
87
89
|
ResponseReasoningItem,
|
|
88
90
|
ResponseStatus,
|
|
@@ -95,6 +97,7 @@ import {
|
|
|
95
97
|
appendReasoningSummaryPart,
|
|
96
98
|
appendReasoningSummaryPartDone,
|
|
97
99
|
appendReasoningSummaryTextDelta,
|
|
100
|
+
appendResponsesImageResult,
|
|
98
101
|
appendResponsesToolResultMessages,
|
|
99
102
|
applyOpenAIServiceTier,
|
|
100
103
|
applyReasoningSummaryDone,
|
|
@@ -105,7 +108,7 @@ import {
|
|
|
105
108
|
createSequentialCutoffSummaryState,
|
|
106
109
|
encodeResponsesToolCallId,
|
|
107
110
|
encodeTextSignatureV1,
|
|
108
|
-
|
|
111
|
+
escapeReplayedControlTokens,
|
|
109
112
|
finalizeCustomToolCallInputDone,
|
|
110
113
|
finalizeMessageText,
|
|
111
114
|
finalizePendingResponsesToolCalls,
|
|
@@ -165,6 +168,17 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
|
165
168
|
onModerationMetadata?: (metadata: unknown) => void;
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
/** Raw V2 compaction body accepted by the Codex transport selector. */
|
|
172
|
+
export interface OpenAICodexCompactionBody extends CodexLiteShapedBody {
|
|
173
|
+
model: string;
|
|
174
|
+
[key: string]: unknown;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Transport controls for a provider-native Codex V2 compaction stream. */
|
|
178
|
+
export interface OpenAICodexCompactionStreamOptions extends OpenAICodexResponsesOptions {
|
|
179
|
+
apiKey: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
/** Inputs for synthesizing Codex request identity outside the normal stream path. */
|
|
169
183
|
export interface OpenAICodexCompatibilityMetadataOptions {
|
|
170
184
|
sessionId?: string;
|
|
@@ -359,7 +373,8 @@ type CodexEventItem =
|
|
|
359
373
|
| ResponseOutputMessage
|
|
360
374
|
| ResponseFunctionToolCall
|
|
361
375
|
| ResponseCustomToolCall
|
|
362
|
-
| ResponseComputerToolCall
|
|
376
|
+
| ResponseComputerToolCall
|
|
377
|
+
| ResponseOutputItem.ImageGenerationCall;
|
|
363
378
|
type CodexOutputBlock =
|
|
364
379
|
| ThinkingContent
|
|
365
380
|
| TextContent
|
|
@@ -1360,12 +1375,16 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
1360
1375
|
};
|
|
1361
1376
|
}
|
|
1362
1377
|
|
|
1363
|
-
|
|
1378
|
+
function createCodexRequestContext(
|
|
1364
1379
|
model: Model<"openai-codex-responses">,
|
|
1365
|
-
|
|
1380
|
+
transformedBody: RequestBody,
|
|
1366
1381
|
options: OpenAICodexResponsesOptions | undefined,
|
|
1367
|
-
|
|
1368
|
-
|
|
1382
|
+
contextOptions: {
|
|
1383
|
+
isolateCompactionTransport: boolean;
|
|
1384
|
+
startNewTurn?: boolean;
|
|
1385
|
+
turnStartedAtUnixMs?: number;
|
|
1386
|
+
},
|
|
1387
|
+
): CodexRequestContext {
|
|
1369
1388
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
1370
1389
|
if (!apiKey) {
|
|
1371
1390
|
throw new AIError.MissingApiKeyError(model.provider);
|
|
@@ -1374,15 +1393,13 @@ async function buildCodexRequestContext(
|
|
|
1374
1393
|
const accountId = getCodexAccountId(apiKey);
|
|
1375
1394
|
const baseUrl = model.baseUrl || CODEX_BASE_URL;
|
|
1376
1395
|
const url = resolveCodexResponsesUrl(baseUrl);
|
|
1377
|
-
|
|
1396
|
+
|
|
1378
1397
|
const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
|
|
1379
1398
|
const codexClientVersion = CODEX_CLIENT_VERSION;
|
|
1380
|
-
const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
|
|
1381
|
-
|
|
1382
1399
|
const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
|
|
1383
1400
|
const rawRequestDump: RawHttpRequestDump = {
|
|
1384
1401
|
provider: model.provider,
|
|
1385
|
-
api:
|
|
1402
|
+
api: model.api,
|
|
1386
1403
|
model: model.id,
|
|
1387
1404
|
method: "POST",
|
|
1388
1405
|
url,
|
|
@@ -1390,7 +1407,10 @@ async function buildCodexRequestContext(
|
|
|
1390
1407
|
};
|
|
1391
1408
|
|
|
1392
1409
|
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
1393
|
-
const isolatedTransportState =
|
|
1410
|
+
const isolatedTransportState =
|
|
1411
|
+
contextOptions.isolateCompactionTransport && options?.codexCompaction
|
|
1412
|
+
? createCodexProviderSessionState()
|
|
1413
|
+
: undefined;
|
|
1394
1414
|
const transportProviderSessionState = isolatedTransportState ?? providerSessionState;
|
|
1395
1415
|
const responsesLite = resolveCodexResponsesLite(model, options?.responsesLite);
|
|
1396
1416
|
const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
|
|
@@ -1413,20 +1433,14 @@ async function buildCodexRequestContext(
|
|
|
1413
1433
|
websocketState.turnState = sharedWebsocketState.turnState;
|
|
1414
1434
|
websocketState.modelsEtag = sharedWebsocketState.modelsEtag;
|
|
1415
1435
|
}
|
|
1416
|
-
const withinTurnContinuation = isCodexWithinTurnContinuation(context);
|
|
1417
1436
|
const metadataSessionId = transportSessionId ?? crypto.randomUUID();
|
|
1418
1437
|
const metadataSession = getOrCreateCodexMetadataSessionState(metadataSessionId, providerSessionState);
|
|
1419
1438
|
const compaction = options?.codexCompaction;
|
|
1420
1439
|
const requestKind: OpenAICodexRequestKind = compaction ? "compaction" : "turn";
|
|
1421
|
-
const startNewTurn = resolveCodexStartNewTurn(
|
|
1422
|
-
metadataSession,
|
|
1423
|
-
requestKind,
|
|
1424
|
-
compaction,
|
|
1425
|
-
compaction ? undefined : !withinTurnContinuation,
|
|
1426
|
-
);
|
|
1440
|
+
const startNewTurn = resolveCodexStartNewTurn(metadataSession, requestKind, compaction, contextOptions.startNewTurn);
|
|
1427
1441
|
if (websocketState && startNewTurn) {
|
|
1428
|
-
// Codex scopes turn-state to one turn. Mid-turn compaction
|
|
1429
|
-
//
|
|
1442
|
+
// Codex scopes turn-state to one turn. Mid-turn compaction preserves it;
|
|
1443
|
+
// a pre-turn or standalone compaction starts without it.
|
|
1430
1444
|
websocketState.turnState = undefined;
|
|
1431
1445
|
}
|
|
1432
1446
|
const requestMetadata = createCodexRequestMetadata(metadataSession, requestKind, {
|
|
@@ -1435,7 +1449,7 @@ async function buildCodexRequestContext(
|
|
|
1435
1449
|
? startNewTurn || !metadataSession.turnId
|
|
1436
1450
|
? Date.now()
|
|
1437
1451
|
: undefined
|
|
1438
|
-
:
|
|
1452
|
+
: contextOptions.turnStartedAtUnixMs,
|
|
1439
1453
|
clientMetadata: transformedBody.client_metadata,
|
|
1440
1454
|
parentTurnId: options?.parentTurnId,
|
|
1441
1455
|
compaction,
|
|
@@ -1459,6 +1473,20 @@ async function buildCodexRequestContext(
|
|
|
1459
1473
|
};
|
|
1460
1474
|
}
|
|
1461
1475
|
|
|
1476
|
+
async function buildCodexRequestContext(
|
|
1477
|
+
model: Model<"openai-codex-responses">,
|
|
1478
|
+
context: Context,
|
|
1479
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
1480
|
+
): Promise<CodexRequestContext> {
|
|
1481
|
+
const promptCacheKey = getOpenAIPromptCacheKey(options);
|
|
1482
|
+
const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
|
|
1483
|
+
return createCodexRequestContext(model, transformedBody, options, {
|
|
1484
|
+
isolateCompactionTransport: true,
|
|
1485
|
+
startNewTurn: options?.codexCompaction ? undefined : !isCodexWithinTurnContinuation(context),
|
|
1486
|
+
turnStartedAtUnixMs: options?.codexCompaction ? undefined : getCodexTurnStartedAtUnixMs(context),
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1462
1490
|
/** @internal Exported for tests. */
|
|
1463
1491
|
export async function buildTransformedCodexRequestBody(
|
|
1464
1492
|
model: Model<"openai-codex-responses">,
|
|
@@ -1566,6 +1594,127 @@ async function openInitialCodexEventStream(
|
|
|
1566
1594
|
}
|
|
1567
1595
|
return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
|
|
1568
1596
|
}
|
|
1597
|
+
|
|
1598
|
+
function toCodexRequestBody(body: OpenAICodexCompactionBody): RequestBody {
|
|
1599
|
+
const request: RequestBody = { model: body.model };
|
|
1600
|
+
for (const key in body) {
|
|
1601
|
+
if (key !== "model") request[key] = body[key];
|
|
1602
|
+
}
|
|
1603
|
+
return request;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
/**
|
|
1607
|
+
* Open a provider-native V2 compaction stream through Codex's WebSocket-first
|
|
1608
|
+
* transport, replaying WebSocket transport failures over SSE.
|
|
1609
|
+
*/
|
|
1610
|
+
export async function openCodexCompactionEventStream(
|
|
1611
|
+
model: Model<"openai-codex-responses">,
|
|
1612
|
+
body: OpenAICodexCompactionBody,
|
|
1613
|
+
options: OpenAICodexCompactionStreamOptions,
|
|
1614
|
+
): Promise<AsyncGenerator<Record<string, unknown>>> {
|
|
1615
|
+
const requestSetup = createRequestSetup(options);
|
|
1616
|
+
let requestContext: CodexRequestContext;
|
|
1617
|
+
let initial: {
|
|
1618
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
1619
|
+
requestBodyForState: RequestBody;
|
|
1620
|
+
transport: CodexTransport;
|
|
1621
|
+
};
|
|
1622
|
+
try {
|
|
1623
|
+
requestContext = createCodexRequestContext(model, toCodexRequestBody(body), options, {
|
|
1624
|
+
isolateCompactionTransport: false,
|
|
1625
|
+
});
|
|
1626
|
+
initial = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
|
|
1627
|
+
} catch (error) {
|
|
1628
|
+
requestSetup.requestAbortController.abort();
|
|
1629
|
+
throw error;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
if (requestContext.websocketState) {
|
|
1633
|
+
requestContext.websocketState.lastTransport = initial.transport;
|
|
1634
|
+
// The compaction request may use the existing append baseline, but the
|
|
1635
|
+
// replacement history makes that baseline stale for the next normal turn.
|
|
1636
|
+
resetCodexWebSocketAppendState(requestContext.websocketState);
|
|
1637
|
+
}
|
|
1638
|
+
return streamCodexCompactionEvents(model, options, requestSetup, requestContext, initial);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
async function* streamCodexCompactionEvents(
|
|
1642
|
+
model: Model<"openai-codex-responses">,
|
|
1643
|
+
options: OpenAICodexCompactionStreamOptions,
|
|
1644
|
+
requestSetup: CodexRequestSetup,
|
|
1645
|
+
requestContext: CodexRequestContext,
|
|
1646
|
+
initial: {
|
|
1647
|
+
eventStream: AsyncGenerator<Record<string, unknown>>;
|
|
1648
|
+
requestBodyForState: RequestBody;
|
|
1649
|
+
transport: CodexTransport;
|
|
1650
|
+
},
|
|
1651
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
1652
|
+
let completed = false;
|
|
1653
|
+
const websocketState = requestContext.websocketState;
|
|
1654
|
+
const previousTurnState = websocketState?.turnState;
|
|
1655
|
+
const previousModelsEtag = websocketState?.modelsEtag;
|
|
1656
|
+
try {
|
|
1657
|
+
if (initial.transport === "websocket") {
|
|
1658
|
+
// Do not expose a WebSocket attempt until it finishes: an SSE replay
|
|
1659
|
+
// must replace, not extend, any partial compaction output.
|
|
1660
|
+
const bufferedEvents: Array<Record<string, unknown>> = [];
|
|
1661
|
+
try {
|
|
1662
|
+
for await (const event of initial.eventStream) bufferedEvents.push(event);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
if (options.signal?.aborted || !(error instanceof CodexWebSocketTransportError)) {
|
|
1665
|
+
throw error;
|
|
1666
|
+
}
|
|
1667
|
+
const state = requestContext.websocketState;
|
|
1668
|
+
if (state) recordCodexWebSocketFailure(state, true);
|
|
1669
|
+
const fallback = await openCodexSseTransport(model, requestContext, requestSetup, options, state);
|
|
1670
|
+
if (state) state.lastTransport = fallback.transport;
|
|
1671
|
+
yield* drainCodexCompactionEvents(fallback.eventStream, requestContext.websocketState);
|
|
1672
|
+
completed = true;
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
// Apply metadata only once the WebSocket attempt succeeded: a discarded
|
|
1676
|
+
// attempt must not leak its `x-codex-turn-state` into the session.
|
|
1677
|
+
for (const event of bufferedEvents) {
|
|
1678
|
+
applyCodexCompactionResponseMetadata(requestContext.websocketState, event);
|
|
1679
|
+
yield event;
|
|
1680
|
+
}
|
|
1681
|
+
} else {
|
|
1682
|
+
yield* drainCodexCompactionEvents(initial.eventStream, requestContext.websocketState);
|
|
1683
|
+
}
|
|
1684
|
+
completed = true;
|
|
1685
|
+
} finally {
|
|
1686
|
+
if (!completed) {
|
|
1687
|
+
requestSetup.requestAbortController.abort();
|
|
1688
|
+
if (websocketState) {
|
|
1689
|
+
websocketState.turnState = previousTurnState;
|
|
1690
|
+
websocketState.modelsEtag = previousModelsEtag;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Capture `x-codex-turn-state`/`x-models-etag` refreshes carried by a
|
|
1698
|
+
* `response.metadata` frame so a mid-turn compaction leaves the live session on
|
|
1699
|
+
* the latest turn state, matching the normal Codex stream processor.
|
|
1700
|
+
*/
|
|
1701
|
+
function applyCodexCompactionResponseMetadata(
|
|
1702
|
+
state: CodexWebSocketSessionState | undefined,
|
|
1703
|
+
event: Record<string, unknown>,
|
|
1704
|
+
): void {
|
|
1705
|
+
if (!state || event.type !== "response.metadata") return;
|
|
1706
|
+
updateCodexSessionMetadataFromHeaders(state, toCodexHeaders(event.headers));
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
async function* drainCodexCompactionEvents(
|
|
1710
|
+
events: AsyncGenerator<Record<string, unknown>>,
|
|
1711
|
+
state: CodexWebSocketSessionState | undefined,
|
|
1712
|
+
): AsyncGenerator<Record<string, unknown>> {
|
|
1713
|
+
for await (const event of events) {
|
|
1714
|
+
applyCodexCompactionResponseMetadata(state, event);
|
|
1715
|
+
yield event;
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1569
1718
|
async function openCodexWebSocketTransport(
|
|
1570
1719
|
model: Model<"openai-codex-responses">,
|
|
1571
1720
|
options: OpenAICodexResponsesOptions | undefined,
|
|
@@ -2143,6 +2292,7 @@ class CodexStreamProcessor {
|
|
|
2143
2292
|
const rawItem = rawEvent.item;
|
|
2144
2293
|
if (!rawItem || typeof rawItem !== "object") return;
|
|
2145
2294
|
const item = structuredCloneJSON(rawItem) as CodexEventItem;
|
|
2295
|
+
if (item.type === "image_generation_call" && item.result) item.status = "completed";
|
|
2146
2296
|
runtime.nativeOutputItems.push(item as unknown as Record<string, unknown>);
|
|
2147
2297
|
|
|
2148
2298
|
// Match the finalization to the OPEN ITEM that started this block, not the
|
|
@@ -2155,6 +2305,12 @@ class CodexStreamProcessor {
|
|
|
2155
2305
|
const block = entry?.block ?? null;
|
|
2156
2306
|
const contentIndex = entry?.contentIndex ?? output.content.length - 1;
|
|
2157
2307
|
|
|
2308
|
+
if (item.type === "image_generation_call" && item.result) {
|
|
2309
|
+
appendResponsesImageResult(output, stream, item.result);
|
|
2310
|
+
runtime.closeOpenItem(entry);
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2158
2314
|
if (item.type === "reasoning" && block?.type === "thinking") {
|
|
2159
2315
|
this.#flushSummaryDeltas(entry);
|
|
2160
2316
|
block.thinking = finalizeReasoningThinking(
|
|
@@ -2279,12 +2435,15 @@ class CodexStreamProcessor {
|
|
|
2279
2435
|
resetCodexWebSocketAppendState(state);
|
|
2280
2436
|
} else {
|
|
2281
2437
|
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
2282
|
-
|
|
2438
|
+
const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
2439
|
+
structuredCloneJSON(runtime.nativeOutputItems),
|
|
2440
|
+
);
|
|
2441
|
+
if (responseId && replayableResponseItems) {
|
|
2283
2442
|
state.lastResponseId = responseId;
|
|
2284
|
-
state.lastResponseItems =
|
|
2443
|
+
state.lastResponseItems = replayableResponseItems;
|
|
2285
2444
|
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
|
|
2286
2445
|
} else {
|
|
2287
|
-
// Without a response id the append baseline cannot be trusted.
|
|
2446
|
+
// Without both a response id and replayable output, the append baseline cannot be trusted.
|
|
2288
2447
|
state.canAppend = false;
|
|
2289
2448
|
}
|
|
2290
2449
|
}
|
|
@@ -2710,7 +2869,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2710
2869
|
let requestContext: CodexRequestContext | undefined;
|
|
2711
2870
|
|
|
2712
2871
|
try {
|
|
2713
|
-
requestContext = await buildCodexRequestContext(model, context, options
|
|
2872
|
+
requestContext = await buildCodexRequestContext(model, context, options);
|
|
2714
2873
|
const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
|
|
2715
2874
|
const runtime = new CodexStreamRuntime({
|
|
2716
2875
|
...initialTransport,
|
|
@@ -3005,14 +3164,6 @@ export function getOpenAICodexTransportDetails(
|
|
|
3005
3164
|
};
|
|
3006
3165
|
}
|
|
3007
3166
|
|
|
3008
|
-
function stripInputItemIds(items: Array<Record<string, unknown>>): InputItem[] {
|
|
3009
|
-
return items.map(item => {
|
|
3010
|
-
if (item.id == null) return item as InputItem;
|
|
3011
|
-
const { id: _id, ...rest } = item;
|
|
3012
|
-
return rest as InputItem;
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
3167
|
const codexDiagnosticsTextEncoder = new TextEncoder();
|
|
3017
3168
|
|
|
3018
3169
|
function jsonByteLength(value: unknown): number {
|
|
@@ -4198,6 +4349,9 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4198
4349
|
};
|
|
4199
4350
|
|
|
4200
4351
|
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
|
4352
|
+
// gpt-5.x reject raw Harmony control-token spellings anywhere in replayed
|
|
4353
|
+
// input, including the model's own tool-call arguments (#6913).
|
|
4354
|
+
const escapeControlTokens = isHarmonyDialectModel(model);
|
|
4201
4355
|
let msgIndex = 0;
|
|
4202
4356
|
// Track call_ids that originated as custom tool calls so paired tool-result
|
|
4203
4357
|
// messages can be replayed as `custom_tool_call_output` rather than
|
|
@@ -4227,7 +4381,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4227
4381
|
knownCallIds.add(item.call_id);
|
|
4228
4382
|
}
|
|
4229
4383
|
}
|
|
4230
|
-
messages.push(...(
|
|
4384
|
+
messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
|
|
4231
4385
|
msgIndex += 1;
|
|
4232
4386
|
continue;
|
|
4233
4387
|
}
|
|
@@ -4253,10 +4407,11 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4253
4407
|
if (historyItems) {
|
|
4254
4408
|
const sanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(historyItems);
|
|
4255
4409
|
if (sanitizedHistoryItems) {
|
|
4256
|
-
const
|
|
4410
|
+
const rawReplayItems =
|
|
4257
4411
|
model.supportsComputerUse === true
|
|
4258
4412
|
? sanitizedHistoryItems
|
|
4259
4413
|
: unrollCodexComputerItems(sanitizedHistoryItems, model.compat.supportsImageDetailOriginal);
|
|
4414
|
+
const replayItems = escapeControlTokens ? escapeReplayedControlTokens(rawReplayItems) : rawReplayItems;
|
|
4260
4415
|
for (const item of replayItems) {
|
|
4261
4416
|
if (item.type === "custom_tool_call") {
|
|
4262
4417
|
customCallIds.add(item.call_id);
|
|
@@ -4294,7 +4449,7 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
4294
4449
|
? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
|
|
4295
4450
|
: convertedOutputItems;
|
|
4296
4451
|
if (outputItems.length > 0) {
|
|
4297
|
-
messages.push(...outputItems);
|
|
4452
|
+
messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
|
|
4298
4453
|
}
|
|
4299
4454
|
msgIndex += 1;
|
|
4300
4455
|
continue;
|
|
@@ -757,7 +757,13 @@ const streamOpenAICompletionsOnce = (
|
|
|
757
757
|
disableStrictTools = true;
|
|
758
758
|
openaiStream = await createCompletionsStream("none");
|
|
759
759
|
} else {
|
|
760
|
-
if (
|
|
760
|
+
if (
|
|
761
|
+
!shouldRetryWithoutStrictTools(error, capturedErrorResponse, {
|
|
762
|
+
model,
|
|
763
|
+
strictToolsApplied: appliedStrictTools,
|
|
764
|
+
tools: context.tools,
|
|
765
|
+
})
|
|
766
|
+
) {
|
|
761
767
|
throw error;
|
|
762
768
|
}
|
|
763
769
|
// Remember the rejection for the rest of the session so every
|
|
@@ -610,12 +610,11 @@ const streamOpenAIResponsesOnce = (
|
|
|
610
610
|
strictRetryAvailable &&
|
|
611
611
|
!requestSignal.aborted &&
|
|
612
612
|
(compiledGrammarTooLarge ||
|
|
613
|
-
shouldRetryWithoutStrictTools(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
));
|
|
613
|
+
shouldRetryWithoutStrictTools(error, capturedErrorResponse, {
|
|
614
|
+
model,
|
|
615
|
+
strictToolsApplied: activeStrictToolsApplied,
|
|
616
|
+
tools: context.tools,
|
|
617
|
+
}));
|
|
619
618
|
if (canRetryWithoutStrictTools) {
|
|
620
619
|
strictRetryAvailable = false;
|
|
621
620
|
forceDisableStrictTools = true;
|
|
@@ -78,7 +78,11 @@ import {
|
|
|
78
78
|
kStreamingPartialJson,
|
|
79
79
|
} from "../utils/block-symbols";
|
|
80
80
|
import type { AssistantMessageEventStream } from "../utils/event-stream";
|
|
81
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
escapeHarmonyControlTokens,
|
|
83
|
+
escapeHarmonyControlTokensInJson,
|
|
84
|
+
isHarmonyDialectModel,
|
|
85
|
+
} from "../utils/harmony-leak";
|
|
82
86
|
import type { CapturedHttpErrorResponse } from "../utils/http-inspector";
|
|
83
87
|
import { getOpenRouterHeaders } from "../utils/openrouter-headers";
|
|
84
88
|
import { isForcedToolChoice } from "../utils/tool-choice";
|
|
@@ -329,9 +333,7 @@ export function applyOpenAIServiceTier(
|
|
|
329
333
|
model: Pick<Model, "provider" | "api" | "id">,
|
|
330
334
|
): void {
|
|
331
335
|
if (!shouldSendServiceTier(serviceTier, model)) return;
|
|
332
|
-
|
|
333
|
-
params.service_tier = serviceTier;
|
|
334
|
-
}
|
|
336
|
+
params.service_tier = serviceTier;
|
|
335
337
|
}
|
|
336
338
|
|
|
337
339
|
/**
|
|
@@ -1181,21 +1183,46 @@ export function isCompiledGrammarTooLargeStrictError(
|
|
|
1181
1183
|
);
|
|
1182
1184
|
}
|
|
1183
1185
|
|
|
1186
|
+
interface StrictToolsRetryContext {
|
|
1187
|
+
model: OpenAIModelIdentity;
|
|
1188
|
+
strictToolsApplied: boolean;
|
|
1189
|
+
tools: Tool[] | undefined;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** Decide whether an OpenAI-family request should retry once with non-strict tools. */
|
|
1184
1193
|
export function shouldRetryWithoutStrictTools(
|
|
1185
1194
|
error: unknown,
|
|
1186
1195
|
capturedErrorResponse: CapturedHttpErrorResponse | undefined,
|
|
1187
|
-
|
|
1188
|
-
tools: Tool[] | undefined,
|
|
1196
|
+
context: StrictToolsRetryContext,
|
|
1189
1197
|
): boolean {
|
|
1198
|
+
const { model, strictToolsApplied, tools } = context;
|
|
1190
1199
|
if (!tools || tools.length === 0 || !strictToolsApplied) return false;
|
|
1191
1200
|
const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
|
|
1192
1201
|
if (status !== 400 && status !== 422) return false;
|
|
1202
|
+
const errorMessage = error instanceof Error ? error.message.trim() : "";
|
|
1193
1203
|
const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
|
|
1194
1204
|
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
|
1195
1205
|
.join("\n");
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1206
|
+
if (
|
|
1207
|
+
/wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function|structured[_ -]?outputs?\b[^\n]*(?:not (?:supported|available|enabled)|unsupported)|(?:not support|unsupported)[^\n]*structured[_ -]?outputs?\b/i.test(
|
|
1208
|
+
messageParts,
|
|
1209
|
+
)
|
|
1210
|
+
) {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
if (model.provider !== "openrouter" || !/^(?:400\s+)?Provider returned error$/i.test(errorMessage)) return false;
|
|
1214
|
+
const body = capturedErrorResponse?.bodyJson;
|
|
1215
|
+
if (body && typeof body === "object" && "error" in body) {
|
|
1216
|
+
const errorBody = body.error;
|
|
1217
|
+
if (errorBody && typeof errorBody === "object" && "metadata" in errorBody) {
|
|
1218
|
+
const metadata = errorBody.metadata;
|
|
1219
|
+
if (metadata && typeof metadata === "object" && "raw" in metadata) {
|
|
1220
|
+
const raw = metadata.raw;
|
|
1221
|
+
if (typeof raw === "string" ? raw.trim().length > 0 : raw != null) return false;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return true;
|
|
1199
1226
|
}
|
|
1200
1227
|
|
|
1201
1228
|
function normalizeOpenAIStableId(value: string | undefined, maxLength: number, hashPrefix: string): string | undefined {
|
|
@@ -1611,39 +1638,71 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
|
1611
1638
|
}
|
|
1612
1639
|
|
|
1613
1640
|
/**
|
|
1614
|
-
* Escape reserved Harmony control tokens in the
|
|
1615
|
-
*
|
|
1616
|
-
*
|
|
1617
|
-
*
|
|
1641
|
+
* Escape reserved Harmony control tokens in the free-text fields of replayed
|
|
1642
|
+
* Responses input items: user/developer/system text, tool-result output,
|
|
1643
|
+
* assistant message text, and tool-call payloads.
|
|
1644
|
+
*
|
|
1645
|
+
* Tool-call items are covered deliberately. The original #6913 fix skipped
|
|
1646
|
+
* model-owned items on the theory that they carry no client data — but a model
|
|
1647
|
+
* legitimately writing *about* Harmony samples `<|channel|>` etc. into its own
|
|
1648
|
+
* `function_call.arguments`, and a full-transcript replay (stale or blocked
|
|
1649
|
+
* previous_response_id, provider fallback) feeds those bytes back as input,
|
|
1650
|
+
* which gpt-5.x reject with invalid_prompt / "Request blocked", permanently
|
|
1651
|
+
* poisoning the session. `arguments` is a JSON document, so it uses
|
|
1652
|
+
* {@link escapeHarmonyControlTokensInJson} to stay parseable. Reasoning items
|
|
1653
|
+
* are left untouched: `encrypted_content` is opaque and plaintext summaries
|
|
1654
|
+
* are never rendered back into the prompt.
|
|
1618
1655
|
*
|
|
1619
1656
|
* Native history replay pushes stored `providerPayload` items straight onto the
|
|
1620
1657
|
* wire, bypassing {@link convertResponsesInputContent}; without this a stored
|
|
1621
1658
|
* `input_text` carrying `<|channel|>analysis` still reaches gpt-5.x raw (#6913).
|
|
1622
1659
|
* Callers gate on {@link isHarmonyDialectModel}. Items are copied, not mutated.
|
|
1623
1660
|
*/
|
|
1624
|
-
export function
|
|
1661
|
+
export function escapeReplayedControlTokens(items: ResponseInput): ResponseInput {
|
|
1625
1662
|
return items.map(item => {
|
|
1626
1663
|
if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
1627
1664
|
return typeof item.output === "string" ? { ...item, output: escapeHarmonyControlTokens(item.output) } : item;
|
|
1628
1665
|
}
|
|
1666
|
+
if (item.type === "function_call") {
|
|
1667
|
+
return typeof item.arguments === "string"
|
|
1668
|
+
? { ...item, arguments: escapeHarmonyControlTokensInJson(item.arguments) }
|
|
1669
|
+
: item;
|
|
1670
|
+
}
|
|
1671
|
+
if (item.type === "custom_tool_call") {
|
|
1672
|
+
return typeof item.input === "string" ? { ...item, input: escapeHarmonyControlTokens(item.input) } : item;
|
|
1673
|
+
}
|
|
1629
1674
|
// EasyInputMessage may omit `type` (`{ role, content }`); the responses
|
|
1630
1675
|
// server persists it verbatim, so treat missing type as a message too.
|
|
1631
1676
|
const isTypedMessage = item.type === "message" || item.type === undefined;
|
|
1632
|
-
if (isTypedMessage
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
if (
|
|
1637
|
-
return { ...item, content: escapeHarmonyControlTokens(content) };
|
|
1638
|
-
}
|
|
1639
|
-
if (Array.isArray(content)) {
|
|
1677
|
+
if (!isTypedMessage || !("role" in item) || !("content" in item)) return item;
|
|
1678
|
+
if (item.role === "assistant") {
|
|
1679
|
+
// Assistant output text is model-owned but equally capable of carrying
|
|
1680
|
+
// control tokens as data. `status` discriminates ResponseOutputMessage.
|
|
1681
|
+
if ("status" in item && Array.isArray(item.content)) {
|
|
1640
1682
|
return {
|
|
1641
1683
|
...item,
|
|
1642
|
-
content: content.map(part =>
|
|
1643
|
-
part.type === "
|
|
1684
|
+
content: item.content.map(part =>
|
|
1685
|
+
part.type === "output_text"
|
|
1686
|
+
? { ...part, text: escapeHarmonyControlTokens(part.text) }
|
|
1687
|
+
: part.type === "refusal"
|
|
1688
|
+
? { ...part, refusal: escapeHarmonyControlTokens(part.refusal) }
|
|
1689
|
+
: part,
|
|
1644
1690
|
),
|
|
1645
1691
|
};
|
|
1646
1692
|
}
|
|
1693
|
+
return item;
|
|
1694
|
+
}
|
|
1695
|
+
const content = item.content;
|
|
1696
|
+
if (typeof content === "string") {
|
|
1697
|
+
return { ...item, content: escapeHarmonyControlTokens(content) };
|
|
1698
|
+
}
|
|
1699
|
+
if (Array.isArray(content)) {
|
|
1700
|
+
return {
|
|
1701
|
+
...item,
|
|
1702
|
+
content: content.map(part =>
|
|
1703
|
+
part.type === "input_text" ? { ...part, text: escapeHarmonyControlTokens(part.text) } : part,
|
|
1704
|
+
),
|
|
1705
|
+
};
|
|
1647
1706
|
}
|
|
1648
1707
|
return item;
|
|
1649
1708
|
});
|
|
@@ -1708,7 +1767,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1708
1767
|
customToolWireNameMap,
|
|
1709
1768
|
options.model.supportsComputerUse === true,
|
|
1710
1769
|
);
|
|
1711
|
-
messages.push(...(escapeControlTokens ?
|
|
1770
|
+
messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(replayItems) : replayItems));
|
|
1712
1771
|
knownCallIds = collectKnownCallIds(messages);
|
|
1713
1772
|
for (const id of collectCustomCallIds(messages)) customCallIds.add(id);
|
|
1714
1773
|
for (const id of collectComputerCallIds(messages)) computerCallIds.add(id);
|
|
@@ -1769,10 +1828,16 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1769
1828
|
)
|
|
1770
1829
|
: undefined;
|
|
1771
1830
|
if (nativeReplayEnabled && sanitizedHistoryItems) {
|
|
1831
|
+
// Model-owned replay items can carry reserved control-token
|
|
1832
|
+
// spellings as data (the model writing *about* Harmony); escape the
|
|
1833
|
+
// transport copy just like client turns.
|
|
1834
|
+
const wireItems = escapeControlTokens
|
|
1835
|
+
? escapeReplayedControlTokens(sanitizedHistoryItems)
|
|
1836
|
+
: sanitizedHistoryItems;
|
|
1772
1837
|
if (providerPayload?.dt) {
|
|
1773
|
-
messages.push(...
|
|
1838
|
+
messages.push(...wireItems);
|
|
1774
1839
|
} else {
|
|
1775
|
-
messages.splice(0, messages.length, ...
|
|
1840
|
+
messages.splice(0, messages.length, ...wireItems);
|
|
1776
1841
|
customCallIds.clear();
|
|
1777
1842
|
computerCallIds.clear();
|
|
1778
1843
|
}
|
|
@@ -1801,7 +1866,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
|
|
|
1801
1866
|
? sanitizeOpenAIResponsesAssistantFallbackItemsForReplay(convertedOutputItems)
|
|
1802
1867
|
: convertedOutputItems;
|
|
1803
1868
|
if (outputItems.length === 0) continue;
|
|
1804
|
-
messages.push(...outputItems);
|
|
1869
|
+
messages.push(...(escapeControlTokens ? escapeReplayedControlTokens(outputItems) : outputItems));
|
|
1805
1870
|
} else if (msg.role === "toolResult") {
|
|
1806
1871
|
appendResponsesToolResultMessages(
|
|
1807
1872
|
messages,
|
|
@@ -2394,6 +2459,26 @@ export function computerCallMetadata(item: ResponseComputerToolCall): ComputerTo
|
|
|
2394
2459
|
};
|
|
2395
2460
|
}
|
|
2396
2461
|
|
|
2462
|
+
/** Append a native Responses image result and emit its completion event. */
|
|
2463
|
+
export function appendResponsesImageResult(
|
|
2464
|
+
output: AssistantMessage,
|
|
2465
|
+
stream: AssistantMessageEventStream,
|
|
2466
|
+
result: string,
|
|
2467
|
+
): void {
|
|
2468
|
+
const image: ImageContent = {
|
|
2469
|
+
type: "image",
|
|
2470
|
+
data: result,
|
|
2471
|
+
mimeType: parseImageMetadata(Buffer.from(result, "base64"))?.mimeType ?? "image/png",
|
|
2472
|
+
};
|
|
2473
|
+
output.content.push(image);
|
|
2474
|
+
stream.push({
|
|
2475
|
+
type: "image_end",
|
|
2476
|
+
contentIndex: output.content.length - 1,
|
|
2477
|
+
content: image,
|
|
2478
|
+
partial: output,
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2397
2482
|
export async function processResponsesStream<TApi extends Api>(
|
|
2398
2483
|
openaiStream: AsyncIterable<ResponseStreamEvent>,
|
|
2399
2484
|
output: AssistantMessage,
|
|
@@ -2907,18 +2992,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2907
2992
|
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2908
2993
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2909
2994
|
} else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
|
|
2910
|
-
|
|
2911
|
-
type: "image",
|
|
2912
|
-
data: item.result,
|
|
2913
|
-
mimeType: parseImageMetadata(Buffer.from(item.result, "base64"))?.mimeType ?? "image/png",
|
|
2914
|
-
};
|
|
2915
|
-
output.content.push(image);
|
|
2916
|
-
stream.push({
|
|
2917
|
-
type: "image_end",
|
|
2918
|
-
contentIndex: output.content.length - 1,
|
|
2919
|
-
content: image,
|
|
2920
|
-
partial: output,
|
|
2921
|
-
});
|
|
2995
|
+
appendResponsesImageResult(output, stream, item.result);
|
|
2922
2996
|
}
|
|
2923
2997
|
} else if (terminalEvent) {
|
|
2924
2998
|
const response = terminalEvent.response;
|