@linxiraos/pi-ai 1.2.1 → 1.3.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 +83 -0
- package/README.md +4 -2
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/auth/cascade.d.ts +5 -3
- package/dist/types/auth/pool.d.ts +3 -3
- package/dist/types/auth/select.d.ts +5 -1
- package/dist/types/auth/types.d.ts +15 -5
- package/dist/types/auth/usage-cache.d.ts +3 -1
- package/dist/types/auth-gateway/clients.d.ts +67 -0
- package/dist/types/auth-gateway/dispatch.d.ts +28 -10
- package/dist/types/auth-gateway/http.d.ts +2 -0
- package/dist/types/auth-gateway/index.d.ts +1 -0
- package/dist/types/auth-gateway/server.d.ts +9 -4
- package/dist/types/auth-gateway/session-state.d.ts +6 -0
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/dialect/coercion.d.ts +6 -0
- package/dist/types/error/classes.d.ts +1 -1
- package/dist/types/error/flags.d.ts +15 -0
- package/dist/types/error/index.d.ts +1 -0
- package/dist/types/error/network-cause.d.ts +40 -0
- package/dist/types/images/shared.d.ts +26 -2
- package/dist/types/index.d.ts +1 -0
- package/dist/types/judgment/native.d.ts +51 -3
- package/dist/types/judgment/systemone.d.ts +9 -2
- package/dist/types/judgment/text.d.ts +11 -5
- package/dist/types/judgment/types.d.ts +5 -1
- package/dist/types/judgment/wire.d.ts +16 -3
- package/dist/types/provider-session-state.d.ts +2 -2
- package/dist/types/providers/anthropic-images.d.ts +7 -0
- package/dist/types/providers/anthropic-signature.d.ts +12 -2
- package/dist/types/providers/anthropic-state.d.ts +7 -0
- package/dist/types/providers/anthropic.d.ts +21 -6
- package/dist/types/providers/connection-prewarm.d.ts +15 -0
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -0
- package/dist/types/providers/cursor.d.ts +7 -0
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/providers/openai-responses.d.ts +9 -0
- package/dist/types/providers/openai-shared.d.ts +30 -0
- package/dist/types/providers/systemone-server.d.ts +7 -3
- package/dist/types/providers/transform-messages.d.ts +26 -5
- package/dist/types/registry/oauth/google-antigravity.d.ts +1 -1
- package/dist/types/stream.d.ts +0 -2
- package/dist/types/types.d.ts +33 -9
- package/dist/types/utils/dsml-leak.d.ts +4 -2
- package/dist/types/utils/narration.d.ts +9 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/dist/types/utils/request-body.d.ts +11 -0
- package/dist/types/utils/request-compression.d.ts +21 -0
- package/dist/types/utils/tool-call-arguments.d.ts +2 -1
- package/dist/types/utils/transport-fetch.d.ts +8 -0
- package/dist/types/utils.d.ts +1 -1
- package/package.json +7 -7
- package/src/auth/cascade.ts +23 -13
- package/src/auth/pool.ts +3 -3
- package/src/auth/refresh.ts +39 -12
- package/src/auth/select.ts +8 -2
- package/src/auth/types.ts +16 -5
- package/src/auth/usage-cache.ts +23 -7
- package/src/auth/usage.ts +2 -4
- package/src/auth-broker/remote-store.ts +31 -6
- package/src/auth-gateway/clients.ts +318 -0
- package/src/auth-gateway/dispatch.ts +66 -35
- package/src/auth-gateway/http.ts +11 -5
- package/src/auth-gateway/index.ts +1 -0
- package/src/auth-gateway/routes/embeddings.ts +4 -10
- package/src/auth-gateway/routes/images.ts +6 -10
- package/src/auth-gateway/routes/rerank.ts +4 -10
- package/src/auth-gateway/routes/speech.ts +10 -10
- package/src/auth-gateway/routes/systemone.ts +22 -12
- package/src/auth-gateway/routes/transcriptions.ts +4 -2
- package/src/auth-gateway/routes/video.ts +12 -15
- package/src/auth-gateway/server.ts +136 -53
- package/src/auth-gateway/session-state.ts +14 -0
- package/src/auth-gateway/types.ts +16 -2
- package/src/dialect/coercion.ts +16 -0
- package/src/dialect/hermes.ts +7 -11
- package/src/dialect/qwen3.ts +3 -11
- package/src/error/classes.ts +3 -2
- package/src/error/flags.ts +24 -3
- package/src/error/index.ts +1 -0
- package/src/error/network-cause.ts +179 -0
- package/src/images/codex-format-directive.md +3 -0
- package/src/images/google-antigravity.ts +2 -1
- package/src/images/google-generative-ai.ts +4 -2
- package/src/images/openai-hosted.ts +25 -2
- package/src/images/openai-images.ts +11 -6
- package/src/images/shared.ts +77 -3
- package/src/index.ts +1 -0
- package/src/judgment/chat.ts +17 -1
- package/src/judgment/native.ts +182 -12
- package/src/judgment/openai-decisions.ts +27 -6
- package/src/judgment/systemone.ts +131 -10
- package/src/judgment/text.ts +20 -7
- package/src/judgment/types.ts +5 -1
- package/src/judgment/wire.ts +17 -3
- package/src/provider-session-state.ts +2 -2
- package/src/providers/amazon-bedrock.ts +4 -1
- package/src/providers/anthropic-client.ts +1 -1
- package/src/providers/anthropic-images.ts +263 -0
- package/src/providers/anthropic-messages-server.ts +84 -1
- package/src/providers/anthropic-signature.ts +35 -8
- package/src/providers/anthropic-state.ts +13 -0
- package/src/providers/anthropic.ts +479 -397
- package/src/providers/connection-prewarm.ts +78 -0
- package/src/providers/cowork-fetch.ts +5 -1
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +19 -0
- package/src/providers/cursor.ts +53 -13
- package/src/providers/devin.ts +10 -4
- package/src/providers/factory-droid/gemini.ts +9 -3
- package/src/providers/google-gemini-cli.ts +11 -1
- package/src/providers/google-shared.ts +2 -6
- package/src/providers/images-server.ts +6 -1
- package/src/providers/ollama.ts +15 -5
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server.ts +29 -0
- package/src/providers/openai-codex/access-programs.ts +2 -1
- package/src/providers/openai-codex-responses.ts +49 -82
- package/src/providers/openai-completions.ts +117 -19
- package/src/providers/openai-responses.ts +25 -2
- package/src/providers/openai-shared.ts +36 -5
- package/src/providers/pi-native-server.ts +1 -0
- package/src/providers/systemone-server.ts +72 -12
- package/src/providers/transform-messages.ts +95 -51
- package/src/registry/oauth/google-antigravity.ts +1 -1
- package/src/stream.ts +13 -73
- package/src/types.ts +33 -9
- package/src/usage/claude.ts +2 -9
- package/src/usage/github-copilot.ts +26 -4
- package/src/usage/openai-codex.ts +13 -7
- package/src/usage/opencode-go.ts +7 -8
- package/src/usage/xai-oauth.ts +2 -9
- package/src/utils/dsml-leak.ts +30 -14
- package/src/utils/leaked-thinking-stream.ts +10 -0
- package/src/utils/narration.ts +18 -0
- package/src/utils/openai-http.ts +27 -18
- package/src/utils/request-body.ts +27 -0
- package/src/utils/request-compression.ts +202 -0
- package/src/utils/request-debug.ts +23 -9
- package/src/utils/schema/dereference.ts +51 -18
- package/src/utils/schema/draft.ts +10 -2
- package/src/utils/schema/json-schema-validator.ts +4 -10
- package/src/utils/schema/normalize.ts +6 -9
- package/src/utils/tool-call-arguments.ts +2 -1
- package/src/utils/transport-fetch.ts +37 -0
- package/src/utils/validation.ts +26 -16
- package/src/utils.ts +38 -23
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { scheduler } from "node:timers/promises";
|
|
2
|
+
import { type } from "@linxiraos/pi-omptype";
|
|
3
|
+
import { getSupportedEfforts } from "@linxiraos/pi-catalog/model-thinking";
|
|
2
4
|
import { calculateCost } from "@linxiraos/pi-catalog/models";
|
|
3
5
|
import {
|
|
4
6
|
applyCodexResidencyHeader,
|
|
@@ -9,7 +11,6 @@ import {
|
|
|
9
11
|
OPENAI_HEADER_VALUES,
|
|
10
12
|
OPENAI_HEADERS,
|
|
11
13
|
} from "@linxiraos/pi-catalog/wire/codex";
|
|
12
|
-
import { type } from "@linxiraos/pi-omptype";
|
|
13
14
|
import {
|
|
14
15
|
$env,
|
|
15
16
|
$flag,
|
|
@@ -25,7 +26,6 @@ import {
|
|
|
25
26
|
import * as AIError from "../error";
|
|
26
27
|
import { parseToolCallArguments, replayableToolCallArguments } from "../utils/tool-call-arguments";
|
|
27
28
|
import { getEnvApiKey } from "../env-api-key";
|
|
28
|
-
import { isOfficialCodexApiUrl } from "../stream";
|
|
29
29
|
import type {
|
|
30
30
|
Api,
|
|
31
31
|
AssistantMessage,
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
iterateWithIdleTimeout,
|
|
72
72
|
} from "../utils/idle-iterator";
|
|
73
73
|
import { getProxyForUrl } from "../utils/proxy";
|
|
74
|
+
import { wrapFetchForRequestCompression } from "../utils/request-compression";
|
|
74
75
|
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
75
76
|
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
76
77
|
import { notifyRawSseEvent } from "../utils/sse-debug";
|
|
@@ -153,10 +154,16 @@ import {
|
|
|
153
154
|
settleReasoningSummary,
|
|
154
155
|
type SequentialCutoffSummaryState,
|
|
155
156
|
} from "./openai-shared";
|
|
156
|
-
import { redactSensitiveInObject, transformMessages } from "./transform-messages";
|
|
157
|
+
import { normalizeToolCallId, redactSensitiveInObject, transformMessages } from "./transform-messages";
|
|
157
158
|
|
|
158
159
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
159
160
|
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
161
|
+
/**
|
|
162
|
+
* Run with the least reasoning the model allows; takes precedence over
|
|
163
|
+
* `reasoning`. Sends the `none` tier where the catalog's disable mode is
|
|
164
|
+
* `none-effort`, else the model's lowest supported effort.
|
|
165
|
+
*/
|
|
166
|
+
disableReasoning?: boolean;
|
|
160
167
|
reasoningSummary?: "auto" | "concise" | "detailed" | null;
|
|
161
168
|
/** Explicit `reasoning.context` replay scope. Omitted by default so Codex applies its native request policy. */
|
|
162
169
|
reasoningContext?: CodexReasoningContext;
|
|
@@ -498,15 +505,12 @@ interface CodexCompatibilityIdentity {
|
|
|
498
505
|
affinitySessionId?: string;
|
|
499
506
|
threadId: string;
|
|
500
507
|
windowId: string;
|
|
501
|
-
|
|
502
|
-
turnMetadataHeaderJson?: string;
|
|
508
|
+
turnMetadataJson?: string;
|
|
503
509
|
}
|
|
504
510
|
|
|
505
511
|
interface CodexRequestMetadata extends CodexCompatibilityIdentity {
|
|
506
512
|
turnId: string;
|
|
507
|
-
/** Canonical body projection, including `tool_namespaces_info` when present. */
|
|
508
513
|
turnMetadataJson: string;
|
|
509
|
-
turnMetadataHeaderJson: string;
|
|
510
514
|
clientMetadata: Record<string, string>;
|
|
511
515
|
}
|
|
512
516
|
|
|
@@ -523,11 +527,6 @@ const CODEX_RESERVED_METADATA_KEYS: Record<string, true> = {
|
|
|
523
527
|
[OPENAI_HEADERS.SUBAGENT]: true,
|
|
524
528
|
request_kind: true,
|
|
525
529
|
compaction: true,
|
|
526
|
-
// codex-rs retired `code_mode_tool_names` in favor of `tool_namespaces_info`
|
|
527
|
-
// for its Responses Lite Code Mode mapping; both stay reserved so callers
|
|
528
|
-
// cannot smuggle them in as extras.
|
|
529
|
-
code_mode_tool_names: true,
|
|
530
|
-
tool_namespaces_info: true,
|
|
531
530
|
turn_started_at_unix_ms: true,
|
|
532
531
|
forked_from_thread_id: true,
|
|
533
532
|
parent_thread_id: true,
|
|
@@ -633,7 +632,6 @@ function createCodexRequestMetadata(
|
|
|
633
632
|
clientMetadata?: Readonly<Record<string, string>>;
|
|
634
633
|
parentTurnId?: string;
|
|
635
634
|
compaction?: CodexCompactionRequestContext;
|
|
636
|
-
toolNamespacesInfo?: unknown;
|
|
637
635
|
affinitySessionId?: string;
|
|
638
636
|
},
|
|
639
637
|
): CodexRequestMetadata {
|
|
@@ -674,17 +672,7 @@ function createCodexRequestMetadata(
|
|
|
674
672
|
turnMetadata.turn_started_at_unix_ms = session.turnStartedAtUnixMs;
|
|
675
673
|
}
|
|
676
674
|
for (const key in extra) turnMetadata[key] = extra[key];
|
|
677
|
-
|
|
678
|
-
// backend, while `tool_namespaces_info` grows with the session's tool count
|
|
679
|
-
// (409 tools ~ 100KB on its own). The body's `client_metadata` is the
|
|
680
|
-
// canonical envelope, so the snapshot rides there only and the header keeps
|
|
681
|
-
// the fixed-size identity projection.
|
|
682
|
-
const turnMetadataHeaderJson = toAsciiJsonString(turnMetadata);
|
|
683
|
-
let turnMetadataJson = turnMetadataHeaderJson;
|
|
684
|
-
if (options.toolNamespacesInfo !== undefined) {
|
|
685
|
-
turnMetadata.tool_namespaces_info = options.toolNamespacesInfo;
|
|
686
|
-
turnMetadataJson = toAsciiJsonString(turnMetadata);
|
|
687
|
-
}
|
|
675
|
+
const turnMetadataJson = toAsciiJsonString(turnMetadata);
|
|
688
676
|
const clientMetadata: Record<string, string> = {
|
|
689
677
|
[OPENAI_HEADERS.INSTALLATION_ID]: identity.installationId,
|
|
690
678
|
session_id: identity.sessionId,
|
|
@@ -701,7 +689,6 @@ function createCodexRequestMetadata(
|
|
|
701
689
|
affinitySessionId: options.affinitySessionId,
|
|
702
690
|
turnId: session.turnId,
|
|
703
691
|
turnMetadataJson,
|
|
704
|
-
turnMetadataHeaderJson,
|
|
705
692
|
clientMetadata,
|
|
706
693
|
};
|
|
707
694
|
}
|
|
@@ -710,8 +697,8 @@ function applyCodexCompatibilityHeaders(headers: Headers, metadata: CodexCompati
|
|
|
710
697
|
headers.set(OPENAI_HEADERS.SCOPED_SESSION_ID, metadata.affinitySessionId ?? metadata.sessionId);
|
|
711
698
|
headers.set(OPENAI_HEADERS.THREAD_ID, metadata.threadId);
|
|
712
699
|
headers.set(OPENAI_HEADERS.WINDOW_ID, metadata.windowId);
|
|
713
|
-
if (metadata.
|
|
714
|
-
headers.set(OPENAI_HEADERS.TURN_METADATA, metadata.
|
|
700
|
+
if (metadata.turnMetadataJson) {
|
|
701
|
+
headers.set(OPENAI_HEADERS.TURN_METADATA, metadata.turnMetadataJson);
|
|
715
702
|
} else {
|
|
716
703
|
headers.delete(OPENAI_HEADERS.TURN_METADATA);
|
|
717
704
|
}
|
|
@@ -1531,7 +1518,6 @@ function createCodexRequestContext(
|
|
|
1531
1518
|
clientMetadata: transformedBody.client_metadata,
|
|
1532
1519
|
parentTurnId: options?.parentTurnId,
|
|
1533
1520
|
compaction,
|
|
1534
|
-
toolNamespacesInfo: options?.toolNamespacesInfo,
|
|
1535
1521
|
affinitySessionId: normalizeOpenAIPromptCacheKey(options?.parentSessionId),
|
|
1536
1522
|
});
|
|
1537
1523
|
transformedBody.client_metadata = requestMetadata.clientMetadata;
|
|
@@ -1568,6 +1554,22 @@ async function buildCodexRequestContext(
|
|
|
1568
1554
|
});
|
|
1569
1555
|
}
|
|
1570
1556
|
|
|
1557
|
+
/**
|
|
1558
|
+
* Codex reasoning controls for the caller's request. `forceReasoningOff` always
|
|
1559
|
+
* sends the `none` tier. `disableReasoning` follows the catalog disable mode:
|
|
1560
|
+
* `none` where the model accepts it, else the lowest supported effort —
|
|
1561
|
+
* omitting effort would let the backend apply its default tier instead.
|
|
1562
|
+
*/
|
|
1563
|
+
function codexReasoningControls(
|
|
1564
|
+
model: Model<"openai-codex-responses">,
|
|
1565
|
+
options: OpenAICodexResponsesOptions | undefined,
|
|
1566
|
+
): Pick<CodexRequestOptions, "reasoningEffort" | "reasoningOff"> {
|
|
1567
|
+
if (options?.forceReasoningOff) return { reasoningOff: true };
|
|
1568
|
+
if (!options?.disableReasoning) return { reasoningEffort: options?.reasoning };
|
|
1569
|
+
if (model.reasoning && model.compat.reasoningDisableMode === "none-effort") return { reasoningOff: true };
|
|
1570
|
+
return { reasoningEffort: getSupportedEfforts(model)[0] };
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1571
1573
|
/** Serialize normal Codex turns and V2 compaction with the same cacheable prefix. */
|
|
1572
1574
|
export async function buildTransformedCodexRequestBody(
|
|
1573
1575
|
model: Model<"openai-codex-responses">,
|
|
@@ -1612,8 +1614,7 @@ export async function buildTransformedCodexRequestBody(
|
|
|
1612
1614
|
params.client_metadata = { ...options.clientMetadata };
|
|
1613
1615
|
}
|
|
1614
1616
|
const codexOptions: CodexRequestOptions = {
|
|
1615
|
-
|
|
1616
|
-
reasoningOff: options?.forceReasoningOff,
|
|
1617
|
+
...codexReasoningControls(model, options),
|
|
1617
1618
|
reasoningSummary: options?.reasoningSummary,
|
|
1618
1619
|
reasoningContext: options?.reasoningContext,
|
|
1619
1620
|
textVerbosity: options?.textVerbosity,
|
|
@@ -2059,7 +2060,7 @@ async function openCodexSseTransport(
|
|
|
2059
2060
|
requestSetup.firstEventTimeoutMs,
|
|
2060
2061
|
options?.codexSseMaxAttempts,
|
|
2061
2062
|
event => options?.onSseEvent?.(event, model),
|
|
2062
|
-
options?.fetch,
|
|
2063
|
+
wrapFetchForRequestCompression(options?.fetch ?? fetch, model),
|
|
2063
2064
|
),
|
|
2064
2065
|
);
|
|
2065
2066
|
};
|
|
@@ -3203,6 +3204,9 @@ class CodexStreamProcessor {
|
|
|
3203
3204
|
this.runtime.sawTerminalEvent = false;
|
|
3204
3205
|
resetOutputState(this.output);
|
|
3205
3206
|
this.firstTokenTime = undefined;
|
|
3207
|
+
if (this.requestSetup.requestAbortController.signal.aborted) {
|
|
3208
|
+
this.requestSetup = createRequestSetup(this.options);
|
|
3209
|
+
}
|
|
3206
3210
|
await scheduler.wait(CODEX_RETRY_DELAY_MS * this.runtime.providerRetryAttempt, {
|
|
3207
3211
|
signal: this.requestSetup.requestSignal,
|
|
3208
3212
|
});
|
|
@@ -4702,26 +4706,6 @@ async function getOrCreateCodexWebSocketConnection(
|
|
|
4702
4706
|
return state.connection;
|
|
4703
4707
|
}
|
|
4704
4708
|
|
|
4705
|
-
/**
|
|
4706
|
-
* Compress an SSE request body with zstd. Returns `undefined` when
|
|
4707
|
-
* compression is disabled or fails, in which case the caller sends the
|
|
4708
|
-
* plain JSON string without a `content-encoding` header.
|
|
4709
|
-
*/
|
|
4710
|
-
async function compressCodexRequestBody(bodyJson: string, baseUrl: string): Promise<Uint8Array | undefined> {
|
|
4711
|
-
if (!isOfficialCodexApiUrl(baseUrl) || !$flag("PI_CODEX_ZSTD", true)) return undefined;
|
|
4712
|
-
try {
|
|
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 });
|
|
4716
|
-
} catch (error) {
|
|
4717
|
-
CODEX_DEBUG &&
|
|
4718
|
-
logger.debug("[codex] codex request body compression failed", {
|
|
4719
|
-
error: error instanceof Error ? error.message : String(error),
|
|
4720
|
-
});
|
|
4721
|
-
return undefined;
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
|
|
4725
4709
|
async function openCodexSseEventStream(
|
|
4726
4710
|
url: string,
|
|
4727
4711
|
requestHeaders: Record<string, string> | undefined,
|
|
@@ -4778,11 +4762,6 @@ async function openCodexSseEventStream(
|
|
|
4778
4762
|
}
|
|
4779
4763
|
}
|
|
4780
4764
|
};
|
|
4781
|
-
const bodyJson = JSON.stringify(body);
|
|
4782
|
-
const compressedBody = await compressCodexRequestBody(bodyJson, url);
|
|
4783
|
-
if (compressedBody !== undefined) {
|
|
4784
|
-
headers.set("content-encoding", "zstd");
|
|
4785
|
-
}
|
|
4786
4765
|
CODEX_DEBUG &&
|
|
4787
4766
|
logger.debug("[codex] codex request", {
|
|
4788
4767
|
url,
|
|
@@ -4792,11 +4771,12 @@ async function openCodexSseEventStream(
|
|
|
4792
4771
|
sentModelsEtagHeader: headers.has(X_MODELS_ETAG_HEADER),
|
|
4793
4772
|
});
|
|
4794
4773
|
|
|
4795
|
-
|
|
4796
|
-
|
|
4774
|
+
let response: Response;
|
|
4775
|
+
try {
|
|
4776
|
+
response = await fetchWithRetry(url, {
|
|
4797
4777
|
method: "POST",
|
|
4798
4778
|
headers,
|
|
4799
|
-
body:
|
|
4779
|
+
body: JSON.stringify(body),
|
|
4800
4780
|
signal,
|
|
4801
4781
|
prepareInit: () => {
|
|
4802
4782
|
// A retried non-2xx attempt leaves its guard armed for the retry-status
|
|
@@ -4812,23 +4792,11 @@ async function openCodexSseEventStream(
|
|
|
4812
4792
|
fetch: fetchAttempt,
|
|
4813
4793
|
timeout: false,
|
|
4814
4794
|
});
|
|
4815
|
-
let response: Response;
|
|
4816
|
-
try {
|
|
4817
|
-
response = await send(compressedBody ?? bodyJson);
|
|
4818
|
-
if (compressedBody !== undefined && (response.status === 400 || response.status === 415)) {
|
|
4819
|
-
const rejectedStatus = response.status;
|
|
4820
|
-
await response.body?.cancel();
|
|
4821
|
-
headers.delete("content-encoding");
|
|
4822
|
-
CODEX_DEBUG &&
|
|
4823
|
-
logger.debug("[codex] retrying request without zstd after encoding rejection", {
|
|
4824
|
-
url,
|
|
4825
|
-
status: rejectedStatus,
|
|
4826
|
-
});
|
|
4827
|
-
response = await send(bodyJson);
|
|
4828
|
-
}
|
|
4829
4795
|
} catch (error) {
|
|
4830
4796
|
clearPreResponseTimeout?.();
|
|
4831
|
-
|
|
4797
|
+
// Transport rejection after the retry budget: name the host and the cause
|
|
4798
|
+
// instead of surfacing the runtime's bare `getaddrinfo …` text.
|
|
4799
|
+
throw AIError.networkFailureError(error, url);
|
|
4832
4800
|
}
|
|
4833
4801
|
CODEX_DEBUG &&
|
|
4834
4802
|
logger.debug("[codex] codex response", {
|
|
@@ -4994,20 +4962,19 @@ export function resolveCodexResponsesUrl(baseUrl: string | undefined): string {
|
|
|
4994
4962
|
function convertMessages(model: Model<"openai-codex-responses">, context: Context): ResponseInput {
|
|
4995
4963
|
const messages: ResponseInput = [];
|
|
4996
4964
|
|
|
4997
|
-
const
|
|
4965
|
+
const normalizeCodexToolCallId = (id: string): string => {
|
|
4998
4966
|
const sep = id.search(/[\n|]/);
|
|
4999
4967
|
const [callId, itemId] = sep > 0 ? [id.slice(0, sep), id.slice(sep + 1)] : [id, undefined];
|
|
5000
4968
|
const normalizedCallId = sanitizeCodexCallId(callId);
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
normalizedItemId = normalizedItemId.replace(/_+$/, "");
|
|
4969
|
+
const rawItemId = itemId ?? Bun.hash(id).toString(36);
|
|
4970
|
+
const normalizedItemId = normalizeToolCallId(rawItemId.startsWith("fc") ? rawItemId : `fc_${rawItemId}`).replace(
|
|
4971
|
+
/_+$/,
|
|
4972
|
+
"",
|
|
4973
|
+
);
|
|
5007
4974
|
return `${normalizedCallId}|${normalizedItemId}`;
|
|
5008
4975
|
};
|
|
5009
4976
|
|
|
5010
|
-
const transformedMessages = transformMessages(context.messages, model,
|
|
4977
|
+
const transformedMessages = transformMessages(context.messages, model, normalizeCodexToolCallId);
|
|
5011
4978
|
// gpt-5.x reject raw Harmony control-token spellings anywhere in replayed
|
|
5012
4979
|
// input, including the model's own tool-call arguments (#6913).
|
|
5013
4980
|
const escapeControlTokens = isHarmonyDialectModel(model);
|
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
ToolChoice,
|
|
35
35
|
ToolResultMessage,
|
|
36
36
|
} from "../types";
|
|
37
|
-
import { normalizeSystemPrompts, resolveCacheRetention } from "../utils";
|
|
37
|
+
import { normalizeSystemPrompts, normalizeToolCallId, resolveCacheRetention } from "../utils";
|
|
38
38
|
import { createAbortSourceTracker } from "../utils/abort";
|
|
39
39
|
import { isDemotedThinking, kStreamingLastParseLen } from "../utils/block-symbols";
|
|
40
40
|
import { hasVisibleAssistantContent, withReplaySafeStreamRetry } from "../utils/empty-completion-retry";
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
} from "../utils/idle-iterator";
|
|
49
49
|
import { OpenAIHttpError, postOpenAIStream } from "../utils/openai-http";
|
|
50
50
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
51
|
+
import { wrapFetchForRequestCompression } from "../utils/request-compression";
|
|
51
52
|
import {
|
|
52
53
|
adaptSchemaForStrict,
|
|
53
54
|
findStrictToolSchemaViolation,
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
type OpenAIRequestSetup,
|
|
112
113
|
type OpenAIStrictToolsState,
|
|
113
114
|
parseAzureDeploymentNameMap,
|
|
115
|
+
readOpenRouterRawMetadata,
|
|
114
116
|
resolveOpenAICompatPolicy,
|
|
115
117
|
resolveOpenAICompletionsOutputClamp,
|
|
116
118
|
resolveOpenAIOutputTokenParam,
|
|
@@ -127,6 +129,8 @@ import {
|
|
|
127
129
|
|
|
128
130
|
export { applyOpenRouterRoutingVariant } from "./openai-shared";
|
|
129
131
|
|
|
132
|
+
const OPENAI_TOOL_CALL_ID_MAX_LENGTH = 40;
|
|
133
|
+
|
|
130
134
|
type OpenAICompletionsReasoningField = NonNullable<ResolvedOpenAICompat["reasoningContentField"]>;
|
|
131
135
|
|
|
132
136
|
type ProviderAttributedChatCompletionChunk = ChatCompletionChunk & {
|
|
@@ -137,11 +141,15 @@ type OpenAICompletionsChoiceUsage = ChatCompletionChunk.Choice & {
|
|
|
137
141
|
usage?: unknown;
|
|
138
142
|
};
|
|
139
143
|
|
|
144
|
+
type OpenAICompletionsRepetitionChoice = ChatCompletionChunk.Choice & {
|
|
145
|
+
stop_reason?: unknown;
|
|
146
|
+
};
|
|
147
|
+
|
|
140
148
|
type OpenAICompletionsDeltaWithReasoningDetails = ChatCompletionChunk.Choice["delta"] & {
|
|
141
149
|
reasoning_details?: unknown;
|
|
142
150
|
};
|
|
143
151
|
|
|
144
|
-
type GeminiMessageThoughtSignatureField = "thinking_signature" | "thought_signature";
|
|
152
|
+
type GeminiMessageThoughtSignatureField = "thinking_signature" | "thought_signature" | "reasoning_signature";
|
|
145
153
|
|
|
146
154
|
type GeminiMessageThoughtSignature = {
|
|
147
155
|
field: GeminiMessageThoughtSignatureField;
|
|
@@ -163,6 +171,7 @@ const GEMINI_THOUGHT_SIGNATURE_NAMESPACES: readonly GeminiThoughtSignatureNamesp
|
|
|
163
171
|
const GEMINI_MESSAGE_THOUGHT_SIGNATURE_FIELDS: readonly GeminiMessageThoughtSignatureField[] = [
|
|
164
172
|
"thinking_signature",
|
|
165
173
|
"thought_signature",
|
|
174
|
+
"reasoning_signature",
|
|
166
175
|
];
|
|
167
176
|
|
|
168
177
|
function getGeminiThoughtSignatureExtraContent(value: unknown): GeminiThoughtSignatureExtraContent | undefined {
|
|
@@ -199,9 +208,10 @@ function parseStoredThoughtSignature(thoughtSignature: string | undefined): unkn
|
|
|
199
208
|
|
|
200
209
|
// A single tool-call turn on an OpenAI-compatible Gemini wire can carry two
|
|
201
210
|
// independent signatures: a per-call one (`extra_content.google|vertex` or an
|
|
202
|
-
// encrypted `reasoning_details` entry) and a message-level
|
|
203
|
-
//
|
|
204
|
-
//
|
|
211
|
+
// encrypted `reasoning_details` entry) and a message-level signature
|
|
212
|
+
// (`thinking_signature`, `thought_signature`, or `reasoning_signature`).
|
|
213
|
+
// Both are stashed together on the originating tool call's `thoughtSignature`
|
|
214
|
+
// so persistence and replay preserve each field.
|
|
205
215
|
// `perCall` holds the raw extra_content object or reasoning detail; `message`
|
|
206
216
|
// holds the message-level signature in its wire shape.
|
|
207
217
|
type StoredGeminiSignature = {
|
|
@@ -927,8 +937,9 @@ const streamOpenAICompletionsOnce = (
|
|
|
927
937
|
headers: headersWithTimeout,
|
|
928
938
|
body: params,
|
|
929
939
|
signal: requestSignal,
|
|
940
|
+
maxAttempts: options?.maxHttpAttempts,
|
|
930
941
|
fetch: wrapFetchForCopilotFallback(
|
|
931
|
-
options?.fetch,
|
|
942
|
+
wrapFetchForRequestCompression(options?.fetch ?? fetch, model),
|
|
932
943
|
model.provider === "github-copilot",
|
|
933
944
|
resolveCopilotRequestIdentity(options?.headers),
|
|
934
945
|
copilotCacheKey,
|
|
@@ -1082,7 +1093,9 @@ const streamOpenAICompletionsOnce = (
|
|
|
1082
1093
|
typeof block.partialArgs === "string" ? parseToolCallArguments(block.partialArgs) : block.partialArgs;
|
|
1083
1094
|
delete block.partialArgs;
|
|
1084
1095
|
if (block.streamIndex !== undefined) {
|
|
1085
|
-
toolCallBlockByIndex.
|
|
1096
|
+
if (toolCallBlockByIndex.get(block.streamIndex) === block) {
|
|
1097
|
+
toolCallBlockByIndex.delete(block.streamIndex);
|
|
1098
|
+
}
|
|
1086
1099
|
delete block.streamIndex;
|
|
1087
1100
|
}
|
|
1088
1101
|
const pendingIndex = pendingToolCallBlocks.indexOf(block);
|
|
@@ -1314,6 +1327,8 @@ const streamOpenAICompletionsOnce = (
|
|
|
1314
1327
|
let streamFinishedAt: number | undefined;
|
|
1315
1328
|
let sawUsagePayload = false;
|
|
1316
1329
|
let awaitTrailingUsageDetails = false;
|
|
1330
|
+
/** Server-declared repetition stop; classified after the finalization sweep. */
|
|
1331
|
+
let repetitionStop: string | undefined;
|
|
1317
1332
|
const applyUsagePayload = (rawUsage: object): void => {
|
|
1318
1333
|
output.usage = parseChunkUsage(
|
|
1319
1334
|
rawUsage,
|
|
@@ -1401,6 +1416,15 @@ const streamOpenAICompletionsOnce = (
|
|
|
1401
1416
|
}
|
|
1402
1417
|
}
|
|
1403
1418
|
|
|
1419
|
+
// A server-declared repetition stop completes the response, so it takes
|
|
1420
|
+
// the same terminal bookkeeping a `finish_reason` chunk would. The
|
|
1421
|
+
// classification itself is deferred to just before the error checks
|
|
1422
|
+
// below: `output.content` is not final until the block sweep has run.
|
|
1423
|
+
// Latched: a trailing frame without the signal must not clear the one
|
|
1424
|
+
// an earlier chunk carried.
|
|
1425
|
+
repetitionStop ??= detectRepetitionStop(choice);
|
|
1426
|
+
if (repetitionStop) streamFinishedAt ??= Date.now();
|
|
1427
|
+
|
|
1404
1428
|
if (choice.finish_reason) {
|
|
1405
1429
|
const finishReasonResult = mapStopReason(choice.finish_reason);
|
|
1406
1430
|
output.stopReason = finishReasonResult.stopReason;
|
|
@@ -1519,9 +1543,17 @@ const streamOpenAICompletionsOnce = (
|
|
|
1519
1543
|
// entries are argument-only. Either way, route by array offset so
|
|
1520
1544
|
// sibling calls stay isolated.
|
|
1521
1545
|
const unkeyedBatchedArrayEntry = toolCalls.length > 1 && streamIndex === undefined && !toolCall.id;
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1546
|
+
// A known id always wins: interleaved continuations resume their own call.
|
|
1547
|
+
// Otherwise reuse the block at this `index` unless the entry opens a new call —
|
|
1548
|
+
// a different id *and* a `function.name`. Some gateways (Gemini) stream every
|
|
1549
|
+
// parallel call at `index: 0` with distinct ids; others mint a fresh id per
|
|
1550
|
+
// argument fragment of one call, but only the opening fragment carries a name.
|
|
1551
|
+
let block = toolCall.id
|
|
1552
|
+
? pendingToolCallBlocks.find(candidate => candidate.id === toolCall.id)
|
|
1553
|
+
: undefined;
|
|
1554
|
+
const indexedBlock = streamIndex !== undefined ? toolCallBlockByIndex.get(streamIndex) : undefined;
|
|
1555
|
+
if (!block && indexedBlock && (!toolCall.id || !indexedBlock.id || !incomingName)) {
|
|
1556
|
+
block = indexedBlock;
|
|
1525
1557
|
}
|
|
1526
1558
|
if (!block && unkeyedBatchedArrayEntry) {
|
|
1527
1559
|
const offsetBlock = unkeyedBatchBlocks[toolCallOffset];
|
|
@@ -1565,7 +1597,8 @@ const streamOpenAICompletionsOnce = (
|
|
|
1565
1597
|
finishCurrentBlock(currentBlock);
|
|
1566
1598
|
}
|
|
1567
1599
|
currentBlock = block;
|
|
1568
|
-
|
|
1600
|
+
// Rebind the index so id-less continuations follow the call that last spoke on it.
|
|
1601
|
+
if (streamIndex !== undefined && toolCallBlockByIndex.get(streamIndex) !== block) {
|
|
1569
1602
|
block.streamIndex = streamIndex;
|
|
1570
1603
|
toolCallBlockByIndex.set(streamIndex, block);
|
|
1571
1604
|
}
|
|
@@ -1747,6 +1780,27 @@ const streamOpenAICompletionsOnce = (
|
|
|
1747
1780
|
if (output.stopReason === "aborted") {
|
|
1748
1781
|
throw new AIError.AbortError();
|
|
1749
1782
|
}
|
|
1783
|
+
|
|
1784
|
+
// Checked before the generic `stopReason === "error"` throw so the flagged
|
|
1785
|
+
// error is the one that reaches `AIError.finalize` — that call rebuilds
|
|
1786
|
+
// `errorId` from the thrown error, so assigning the flag to `output` here
|
|
1787
|
+
// would be overwritten. The partial output is dropped rather than
|
|
1788
|
+
// committed or replayed into the retry; the guard's own loop error does
|
|
1789
|
+
// the same, because degenerate reasoning is replay garbage (#12525).
|
|
1790
|
+
if (repetitionStop) {
|
|
1791
|
+
output.content = [];
|
|
1792
|
+
// The sweep above already closed every block. Clear the handle so the
|
|
1793
|
+
// catch's `finishOpenBlocksOnError` does not re-emit a `thinking_end`
|
|
1794
|
+
// against an index that no longer exists in the emptied content array.
|
|
1795
|
+
currentBlock = undefined;
|
|
1796
|
+
throw AIError.attach(
|
|
1797
|
+
new AIError.ProviderResponseError(`Provider reported a repetition stop: ${repetitionStop}`, {
|
|
1798
|
+
provider: model.provider,
|
|
1799
|
+
kind: "runtime",
|
|
1800
|
+
}),
|
|
1801
|
+
AIError.create(AIError.Flag.ThinkingLoop),
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1750
1804
|
if (output.stopReason === "error") {
|
|
1751
1805
|
throw new AIError.ProviderResponseError(output.errorMessage || "Provider returned an error stop reason", {
|
|
1752
1806
|
provider: model.provider,
|
|
@@ -1779,7 +1833,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
1779
1833
|
output.errorId = result.id;
|
|
1780
1834
|
output.errorMessage = result.message;
|
|
1781
1835
|
// Some providers via OpenRouter include extra details here.
|
|
1782
|
-
const rawMetadata = (
|
|
1836
|
+
const rawMetadata = readOpenRouterRawMetadata(capturedErrorResponse);
|
|
1783
1837
|
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
1784
1838
|
output.duration = performance.now() - startTime;
|
|
1785
1839
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|
|
@@ -1814,7 +1868,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (mo
|
|
|
1814
1868
|
const resolvedModel = resolveOpenAICompletionsCompat(model);
|
|
1815
1869
|
return withReplaySafeStreamRetry(resolvedModel, context, options, streamOpenAICompletionsOnce, {
|
|
1816
1870
|
retryEmptyCompletion: true,
|
|
1817
|
-
retryProviderErrors:
|
|
1871
|
+
retryProviderErrors: options?.maxHttpAttempts === undefined,
|
|
1818
1872
|
maxProviderErrorRetries: 1,
|
|
1819
1873
|
});
|
|
1820
1874
|
};
|
|
@@ -2292,10 +2346,10 @@ export function convertMessages(
|
|
|
2292
2346
|
const maxNormalizedToolCallIdLength = compat.requiresMistralToolIds
|
|
2293
2347
|
? 9
|
|
2294
2348
|
: compat.usesOpenAIToolCallIdLimit
|
|
2295
|
-
?
|
|
2349
|
+
? OPENAI_TOOL_CALL_ID_MAX_LENGTH
|
|
2296
2350
|
: undefined;
|
|
2297
2351
|
const duplicateToolCallIdSuffixPrefix = compat.requiresMistralToolIds ? "dup" : undefined;
|
|
2298
|
-
const
|
|
2352
|
+
const normalizeCompletionsToolCallId = (id: string, source?: AssistantMessage): string => {
|
|
2299
2353
|
if (compat.requiresMistralToolIds) return normalizeMistralToolId(id, true);
|
|
2300
2354
|
|
|
2301
2355
|
const isSameModelSource =
|
|
@@ -2308,16 +2362,17 @@ export function convertMessages(
|
|
|
2308
2362
|
// Chat Completions IDs are provider-issued opaque correlation tokens.
|
|
2309
2363
|
if (!isSameModelSource && id.includes("|")) {
|
|
2310
2364
|
const [callId] = id.split("|");
|
|
2311
|
-
return callId
|
|
2365
|
+
return normalizeToolCallId(callId, OPENAI_TOOL_CALL_ID_MAX_LENGTH);
|
|
2312
2366
|
}
|
|
2313
2367
|
|
|
2314
|
-
if (compat.usesOpenAIToolCallIdLimit)
|
|
2368
|
+
if (compat.usesOpenAIToolCallIdLimit)
|
|
2369
|
+
return id.length > OPENAI_TOOL_CALL_ID_MAX_LENGTH ? id.slice(0, OPENAI_TOOL_CALL_ID_MAX_LENGTH) : id;
|
|
2315
2370
|
return id;
|
|
2316
2371
|
};
|
|
2317
2372
|
const transformedMessages = transformMessages(
|
|
2318
2373
|
context.messages,
|
|
2319
2374
|
model,
|
|
2320
|
-
(id, _target, source) =>
|
|
2375
|
+
(id, _target, source) => normalizeCompletionsToolCallId(id, source),
|
|
2321
2376
|
maxNormalizedToolCallIdLength,
|
|
2322
2377
|
duplicateToolCallIdSuffixPrefix,
|
|
2323
2378
|
compat,
|
|
@@ -2350,7 +2405,7 @@ export function convertMessages(
|
|
|
2350
2405
|
};
|
|
2351
2406
|
|
|
2352
2407
|
const ensureToolCallId = (rawId: string, seed: string, source?: AssistantMessage): string => {
|
|
2353
|
-
const normalized =
|
|
2408
|
+
const normalized = normalizeCompletionsToolCallId(rawId, source);
|
|
2354
2409
|
if (normalized.trim().length > 0) return normalized;
|
|
2355
2410
|
return generateFallbackToolCallId(seed);
|
|
2356
2411
|
};
|
|
@@ -2880,6 +2935,36 @@ function convertTools(
|
|
|
2880
2935
|
const EMPTY_OLLAMA_LENGTH_COMPLETION_MESSAGE =
|
|
2881
2936
|
"Model returned no content: prompt filled the context window; raise Ollama num_ctx or shorten the prompt.";
|
|
2882
2937
|
|
|
2938
|
+
/**
|
|
2939
|
+
* Detect a server-declared repetition stop on a streamed choice.
|
|
2940
|
+
*
|
|
2941
|
+
* vLLM and other OpenAI-compatible servers report a generation that degenerated
|
|
2942
|
+
* into repetition as a terminal signal rather than a content pattern OMP can
|
|
2943
|
+
* see: `finish_reason: "repetition"`, and/or `stop_reason: "repetition_detected"`.
|
|
2944
|
+
* The `stop_reason` form arrives alongside an ordinary `finish_reason: "stop"`,
|
|
2945
|
+
* or with no `finish_reason` at all, so it has to be read independently of it —
|
|
2946
|
+
* otherwise the signal is either masked by the clean finish or never seen.
|
|
2947
|
+
*
|
|
2948
|
+
* Returns the raw values for the diagnostic message, or `undefined` when the
|
|
2949
|
+
* choice carries no repetition signal.
|
|
2950
|
+
*/
|
|
2951
|
+
function detectRepetitionStop(choice: ChatCompletionChunk.Choice): string | undefined {
|
|
2952
|
+
const finishReason: string | null | undefined = choice.finish_reason;
|
|
2953
|
+
const stopReason = (choice as OpenAICompletionsRepetitionChoice).stop_reason;
|
|
2954
|
+
const namesRepetition = (value: string): boolean => {
|
|
2955
|
+
const lower = value.toLowerCase();
|
|
2956
|
+
return lower === "repetition" || lower === "repetition_detected";
|
|
2957
|
+
};
|
|
2958
|
+
const flagged =
|
|
2959
|
+
(typeof finishReason === "string" && namesRepetition(finishReason)) ||
|
|
2960
|
+
(typeof stopReason === "string" && namesRepetition(stopReason));
|
|
2961
|
+
if (!flagged) return undefined;
|
|
2962
|
+
const parts: string[] = [];
|
|
2963
|
+
if (typeof finishReason === "string" && finishReason.length > 0) parts.push(`finish_reason=${finishReason}`);
|
|
2964
|
+
if (typeof stopReason === "string" && stopReason.length > 0) parts.push(`stop_reason=${stopReason}`);
|
|
2965
|
+
return parts.join(" ") || "repetition";
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2883
2968
|
function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | string): {
|
|
2884
2969
|
stopReason: StopReason;
|
|
2885
2970
|
errorMessage?: string;
|
|
@@ -2921,6 +3006,19 @@ function mapStopReason(reason: ChatCompletionChunk.Choice["finish_reason"] | str
|
|
|
2921
3006
|
stopReason: "error",
|
|
2922
3007
|
errorMessage: "Provider returned error finish_reason: insufficient_system_resource",
|
|
2923
3008
|
};
|
|
3009
|
+
case "unknown":
|
|
3010
|
+
case "other":
|
|
3011
|
+
case "unspecified":
|
|
3012
|
+
case "finish_reason_unspecified":
|
|
3013
|
+
// The provider ended the stream without saying why (a dropped
|
|
3014
|
+
// connection behind a gateway, a backend that lost the stop code).
|
|
3015
|
+
// Pinning it as a terminal error strands a turn that a resample
|
|
3016
|
+
// fixes. Word the message to match the transient-transport retry
|
|
3017
|
+
// pattern; the retry budget bounds the replay.
|
|
3018
|
+
return {
|
|
3019
|
+
stopReason: "error",
|
|
3020
|
+
errorMessage: `Provider returned error finish_reason: ${normalized}`,
|
|
3021
|
+
};
|
|
2924
3022
|
default:
|
|
2925
3023
|
return {
|
|
2926
3024
|
stopReason: "error",
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
} from "../utils/idle-iterator";
|
|
40
40
|
import { OpenAIHttpError, postOpenAIStream } from "../utils/openai-http";
|
|
41
41
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
42
|
+
import { wrapFetchForRequestCompression } from "../utils/request-compression";
|
|
42
43
|
import { waitForRetry } from "../utils/retry-wait";
|
|
43
44
|
import {
|
|
44
45
|
adaptSchemaForStrict,
|
|
@@ -103,6 +104,7 @@ import {
|
|
|
103
104
|
isCompiledGrammarTooLargeStrictError,
|
|
104
105
|
isOpenAIResponsesProgressEvent,
|
|
105
106
|
isStrictToolsDisabledForScope,
|
|
107
|
+
readOpenRouterRawMetadata,
|
|
106
108
|
type OpenAIPromptCacheOptions,
|
|
107
109
|
type OpenAIStrictToolsScope,
|
|
108
110
|
type OpenAIStrictToolsState,
|
|
@@ -496,6 +498,27 @@ function getOpenAIResponsesProviderSessionState(
|
|
|
496
498
|
return created;
|
|
497
499
|
}
|
|
498
500
|
|
|
501
|
+
/**
|
|
502
|
+
* Whether the next request for `model` replays native Responses history items
|
|
503
|
+
* (encrypted reasoning, item ids), as {@link buildParams} decides it. A session
|
|
504
|
+
* is cold, rebuilding turns from message content, from the first request on a
|
|
505
|
+
* connection-bound host (`compat.connectionBoundNativeHistory`, GitHub Copilot)
|
|
506
|
+
* and after `close()` on any host, until a response succeeds. Without session
|
|
507
|
+
* state every request replays.
|
|
508
|
+
*/
|
|
509
|
+
export function openAIResponsesReplaysNativeHistory(
|
|
510
|
+
model: Pick<Model, "api" | "provider" | "compat"> | null | undefined,
|
|
511
|
+
providerSessionState: Map<string, ProviderSessionState> | undefined,
|
|
512
|
+
): boolean {
|
|
513
|
+
if (model?.api !== "openai-responses" || !providerSessionState) return true;
|
|
514
|
+
const state = providerSessionState.get(`${OPENAI_RESPONSES_PROVIDER_SESSION_STATE_PREFIX}${model.provider}`) as
|
|
515
|
+
| OpenAIResponsesProviderSessionState
|
|
516
|
+
| undefined;
|
|
517
|
+
if (state) return state.nativeHistoryReplayWarmed;
|
|
518
|
+
const compat = model.compat;
|
|
519
|
+
return !(compat && "connectionBoundNativeHistory" in compat && compat.connectionBoundNativeHistory);
|
|
520
|
+
}
|
|
521
|
+
|
|
499
522
|
/** Host per-provider storage defaults; see {@link configureProviderStoreResponses}. */
|
|
500
523
|
let configuredProviderStoreResponses: Readonly<Record<string, boolean>> = {};
|
|
501
524
|
|
|
@@ -872,7 +895,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
872
895
|
body: requestParams,
|
|
873
896
|
signal: requestSignal,
|
|
874
897
|
fetch: wrapFetchForCopilotFallback(
|
|
875
|
-
options?.fetch,
|
|
898
|
+
wrapFetchForRequestCompression(options?.fetch ?? fetch, model),
|
|
876
899
|
model.provider === "github-copilot",
|
|
877
900
|
resolveCopilotRequestIdentity(options?.headers),
|
|
878
901
|
copilotCacheKey,
|
|
@@ -1355,7 +1378,7 @@ const streamOpenAIResponsesOnce = (
|
|
|
1355
1378
|
output.requestBodyReadTimeoutFullReplay = true;
|
|
1356
1379
|
}
|
|
1357
1380
|
// Some providers via OpenRouter include extra details here.
|
|
1358
|
-
const rawMetadata = (
|
|
1381
|
+
const rawMetadata = readOpenRouterRawMetadata(capturedErrorResponse);
|
|
1359
1382
|
if (rawMetadata) output.errorMessage += `\n${rawMetadata}`;
|
|
1360
1383
|
output.duration = performance.now() - startTime;
|
|
1361
1384
|
if (firstTokenTime) output.ttft = firstTokenTime - startTime;
|