@oh-my-pi/pi-ai 15.5.12 → 15.5.15
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 +10 -0
- package/dist/types/provider-models/openai-compat.d.ts +2 -2
- package/dist/types/utils/request-debug.d.ts +29 -0
- package/package.json +2 -2
- package/src/model-manager.ts +5 -3
- package/src/models.json +55 -20
- package/src/provider-models/openai-compat.ts +46 -14
- package/src/providers/amazon-bedrock.ts +1 -0
- package/src/providers/anthropic.ts +1 -33
- package/src/providers/cursor.ts +56 -13
- package/src/providers/openai-codex-responses.ts +40 -2
- package/src/providers/openai-completions-compat.ts +5 -0
- package/src/providers/openai-completions.ts +35 -8
- package/src/stream.ts +36 -24
- package/src/utils/request-debug.ts +336 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.5.15] - 2026-05-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added `PI_REQ_DEBUG=1` request/response recording for provider transports. Each request writes `rr-session-N.json`; each received response writes `rr-session-N.res.log` with response headers followed by raw body bytes.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed OpenCode-Go dynamic model refresh downgrading `qwen3.7-max` from Anthropic Messages to OpenAI-compatible transport, which caused `401 Model qwen3.7-max is not supported for format oa-compat` after `/v1/models` cache refreshes.
|
|
14
|
+
|
|
5
15
|
## [15.5.12] - 2026-05-29
|
|
6
16
|
### Removed
|
|
7
17
|
|
|
@@ -142,8 +142,8 @@ export interface OpenCodeModelManagerConfig {
|
|
|
142
142
|
apiKey?: string;
|
|
143
143
|
baseUrl?: string;
|
|
144
144
|
}
|
|
145
|
-
export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<
|
|
146
|
-
export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<
|
|
145
|
+
export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
|
|
146
|
+
export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
|
|
147
147
|
export interface OllamaModelManagerConfig {
|
|
148
148
|
apiKey?: string;
|
|
149
149
|
baseUrl?: string;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FetchImpl } from "../types";
|
|
2
|
+
export type RequestDebugHeaders = Headers | Record<string, string | string[] | number | undefined | null> | undefined;
|
|
3
|
+
export interface RequestDebugPayload {
|
|
4
|
+
method: string;
|
|
5
|
+
url: string;
|
|
6
|
+
headers?: RequestDebugHeaders;
|
|
7
|
+
body?: unknown;
|
|
8
|
+
bodyText?: string;
|
|
9
|
+
bodyBase64?: string;
|
|
10
|
+
bodyUnavailable?: string;
|
|
11
|
+
protocol?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RequestDebugResponseLog {
|
|
14
|
+
write(chunk: Uint8Array | string): void;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface RequestDebugSession {
|
|
18
|
+
readonly id: number;
|
|
19
|
+
readonly requestPath: string;
|
|
20
|
+
readonly responsePath: string;
|
|
21
|
+
openResponseLog(statusLine: string, headers?: RequestDebugHeaders): Promise<RequestDebugResponseLog>;
|
|
22
|
+
wrapResponse(response: Response): Promise<Response>;
|
|
23
|
+
}
|
|
24
|
+
export declare function isRequestDebugEnabled(): boolean;
|
|
25
|
+
export declare function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl;
|
|
26
|
+
export declare function withRequestDebugFetch<T extends {
|
|
27
|
+
fetch?: FetchImpl;
|
|
28
|
+
} | undefined>(options: T): T;
|
|
29
|
+
export declare function createRequestDebugSession(payload: RequestDebugPayload): Promise<RequestDebugSession>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.5.
|
|
4
|
+
"version": "15.5.15",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@anthropic-ai/sdk": "^0.94.0",
|
|
42
42
|
"@bufbuild/protobuf": "^2.12.0",
|
|
43
|
-
"@oh-my-pi/pi-utils": "15.5.
|
|
43
|
+
"@oh-my-pi/pi-utils": "15.5.15",
|
|
44
44
|
"openai": "^6.36.0",
|
|
45
45
|
"partial-json": "^0.1.7",
|
|
46
46
|
"zod": "4.4.3"
|
package/src/model-manager.ts
CHANGED
|
@@ -289,20 +289,22 @@ function retainModelIds<TApi extends Api>(
|
|
|
289
289
|
* arms calling `resolveProviderModels` with the same `staticModels` array)
|
|
290
290
|
* skip the JSON+hash work after the first call.
|
|
291
291
|
*/
|
|
292
|
+
const MODEL_CACHE_FINGERPRINT_VERSION = "merge-v2";
|
|
292
293
|
const kStaticFingerprint = Symbol("model-manager.staticFingerprint");
|
|
293
294
|
type ModelArrayWithFingerprint = readonly Model<Api>[] & { [kStaticFingerprint]?: string };
|
|
294
295
|
function fingerprintStatic<TApi extends Api>(
|
|
295
296
|
models: readonly Model<TApi>[],
|
|
296
297
|
dynamicModelsAuthoritative = false,
|
|
297
298
|
): string {
|
|
298
|
-
if (models.length === 0) return
|
|
299
|
-
if (dynamicModelsAuthoritative)
|
|
299
|
+
if (models.length === 0) return `${MODEL_CACHE_FINGERPRINT_VERSION}:empty`;
|
|
300
|
+
if (dynamicModelsAuthoritative)
|
|
301
|
+
return `${MODEL_CACHE_FINGERPRINT_VERSION}:authoritative:${fingerprintStatic(models)}`;
|
|
300
302
|
const tagged = models as ModelArrayWithFingerprint;
|
|
301
303
|
const cached = tagged[kStaticFingerprint];
|
|
302
304
|
if (cached !== undefined) return cached;
|
|
303
305
|
// `Bun.hash` returns a `bigint`; base36 keeps the string short for the
|
|
304
306
|
// SQLite column without sacrificing distinguishability.
|
|
305
|
-
const fingerprint = Bun.hash(JSON.stringify(models)).toString(36)
|
|
307
|
+
const fingerprint = `${MODEL_CACHE_FINGERPRINT_VERSION}:${Bun.hash(JSON.stringify(models)).toString(36)}`;
|
|
306
308
|
tagged[kStaticFingerprint] = fingerprint;
|
|
307
309
|
return fingerprint;
|
|
308
310
|
}
|
package/src/models.json
CHANGED
|
@@ -78417,9 +78417,9 @@
|
|
|
78417
78417
|
"mimo-v2-flash": {
|
|
78418
78418
|
"id": "mimo-v2-flash",
|
|
78419
78419
|
"name": "MiMo-V2-Flash",
|
|
78420
|
-
"api": "
|
|
78420
|
+
"api": "openai-completions",
|
|
78421
78421
|
"provider": "xiaomi",
|
|
78422
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
78422
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
78423
78423
|
"reasoning": true,
|
|
78424
78424
|
"input": [
|
|
78425
78425
|
"text"
|
|
@@ -78432,18 +78432,25 @@
|
|
|
78432
78432
|
},
|
|
78433
78433
|
"contextWindow": 262144,
|
|
78434
78434
|
"maxTokens": 65536,
|
|
78435
|
+
"compat": {
|
|
78436
|
+
"supportsStore": false,
|
|
78437
|
+
"thinkingFormat": "zai",
|
|
78438
|
+
"reasoningContentField": "reasoning_content",
|
|
78439
|
+
"requiresReasoningContentForToolCalls": true,
|
|
78440
|
+
"allowsSyntheticReasoningContentForToolCalls": false
|
|
78441
|
+
},
|
|
78435
78442
|
"thinking": {
|
|
78436
|
-
"mode": "
|
|
78443
|
+
"mode": "effort",
|
|
78437
78444
|
"minLevel": "minimal",
|
|
78438
|
-
"maxLevel": "
|
|
78445
|
+
"maxLevel": "high"
|
|
78439
78446
|
}
|
|
78440
78447
|
},
|
|
78441
78448
|
"mimo-v2-omni": {
|
|
78442
78449
|
"id": "mimo-v2-omni",
|
|
78443
78450
|
"name": "MiMo-V2-Omni",
|
|
78444
|
-
"api": "
|
|
78451
|
+
"api": "openai-completions",
|
|
78445
78452
|
"provider": "xiaomi",
|
|
78446
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
78453
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
78447
78454
|
"reasoning": true,
|
|
78448
78455
|
"input": [
|
|
78449
78456
|
"text",
|
|
@@ -78457,18 +78464,25 @@
|
|
|
78457
78464
|
},
|
|
78458
78465
|
"contextWindow": 262144,
|
|
78459
78466
|
"maxTokens": 131072,
|
|
78467
|
+
"compat": {
|
|
78468
|
+
"supportsStore": false,
|
|
78469
|
+
"thinkingFormat": "zai",
|
|
78470
|
+
"reasoningContentField": "reasoning_content",
|
|
78471
|
+
"requiresReasoningContentForToolCalls": true,
|
|
78472
|
+
"allowsSyntheticReasoningContentForToolCalls": false
|
|
78473
|
+
},
|
|
78460
78474
|
"thinking": {
|
|
78461
|
-
"mode": "
|
|
78475
|
+
"mode": "effort",
|
|
78462
78476
|
"minLevel": "minimal",
|
|
78463
|
-
"maxLevel": "
|
|
78477
|
+
"maxLevel": "high"
|
|
78464
78478
|
}
|
|
78465
78479
|
},
|
|
78466
78480
|
"mimo-v2-pro": {
|
|
78467
78481
|
"id": "mimo-v2-pro",
|
|
78468
78482
|
"name": "MiMo-V2-Pro",
|
|
78469
|
-
"api": "
|
|
78483
|
+
"api": "openai-completions",
|
|
78470
78484
|
"provider": "xiaomi",
|
|
78471
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
78485
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
78472
78486
|
"reasoning": true,
|
|
78473
78487
|
"input": [
|
|
78474
78488
|
"text"
|
|
@@ -78481,18 +78495,25 @@
|
|
|
78481
78495
|
},
|
|
78482
78496
|
"contextWindow": 1048576,
|
|
78483
78497
|
"maxTokens": 131072,
|
|
78498
|
+
"compat": {
|
|
78499
|
+
"supportsStore": false,
|
|
78500
|
+
"thinkingFormat": "zai",
|
|
78501
|
+
"reasoningContentField": "reasoning_content",
|
|
78502
|
+
"requiresReasoningContentForToolCalls": true,
|
|
78503
|
+
"allowsSyntheticReasoningContentForToolCalls": false
|
|
78504
|
+
},
|
|
78484
78505
|
"thinking": {
|
|
78485
|
-
"mode": "
|
|
78506
|
+
"mode": "effort",
|
|
78486
78507
|
"minLevel": "minimal",
|
|
78487
|
-
"maxLevel": "
|
|
78508
|
+
"maxLevel": "high"
|
|
78488
78509
|
}
|
|
78489
78510
|
},
|
|
78490
78511
|
"mimo-v2.5": {
|
|
78491
78512
|
"id": "mimo-v2.5",
|
|
78492
78513
|
"name": "MiMo-V2.5",
|
|
78493
|
-
"api": "
|
|
78514
|
+
"api": "openai-completions",
|
|
78494
78515
|
"provider": "xiaomi",
|
|
78495
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
78516
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
78496
78517
|
"reasoning": true,
|
|
78497
78518
|
"input": [
|
|
78498
78519
|
"text",
|
|
@@ -78506,18 +78527,25 @@
|
|
|
78506
78527
|
},
|
|
78507
78528
|
"contextWindow": 1048576,
|
|
78508
78529
|
"maxTokens": 131072,
|
|
78530
|
+
"compat": {
|
|
78531
|
+
"supportsStore": false,
|
|
78532
|
+
"thinkingFormat": "zai",
|
|
78533
|
+
"reasoningContentField": "reasoning_content",
|
|
78534
|
+
"requiresReasoningContentForToolCalls": true,
|
|
78535
|
+
"allowsSyntheticReasoningContentForToolCalls": false
|
|
78536
|
+
},
|
|
78509
78537
|
"thinking": {
|
|
78510
|
-
"mode": "
|
|
78538
|
+
"mode": "effort",
|
|
78511
78539
|
"minLevel": "minimal",
|
|
78512
|
-
"maxLevel": "
|
|
78540
|
+
"maxLevel": "high"
|
|
78513
78541
|
}
|
|
78514
78542
|
},
|
|
78515
78543
|
"mimo-v2.5-pro": {
|
|
78516
78544
|
"id": "mimo-v2.5-pro",
|
|
78517
78545
|
"name": "MiMo-V2.5-Pro",
|
|
78518
|
-
"api": "
|
|
78546
|
+
"api": "openai-completions",
|
|
78519
78547
|
"provider": "xiaomi",
|
|
78520
|
-
"baseUrl": "https://api.xiaomimimo.com/
|
|
78548
|
+
"baseUrl": "https://api.xiaomimimo.com/v1",
|
|
78521
78549
|
"reasoning": true,
|
|
78522
78550
|
"input": [
|
|
78523
78551
|
"text"
|
|
@@ -78530,10 +78558,17 @@
|
|
|
78530
78558
|
},
|
|
78531
78559
|
"contextWindow": 1048576,
|
|
78532
78560
|
"maxTokens": 131072,
|
|
78561
|
+
"compat": {
|
|
78562
|
+
"supportsStore": false,
|
|
78563
|
+
"thinkingFormat": "zai",
|
|
78564
|
+
"reasoningContentField": "reasoning_content",
|
|
78565
|
+
"requiresReasoningContentForToolCalls": true,
|
|
78566
|
+
"allowsSyntheticReasoningContentForToolCalls": false
|
|
78567
|
+
},
|
|
78533
78568
|
"thinking": {
|
|
78534
|
-
"mode": "
|
|
78569
|
+
"mode": "effort",
|
|
78535
78570
|
"minLevel": "minimal",
|
|
78536
|
-
"maxLevel": "
|
|
78571
|
+
"maxLevel": "high"
|
|
78537
78572
|
}
|
|
78538
78573
|
}
|
|
78539
78574
|
},
|
|
@@ -1218,37 +1218,62 @@ export interface OpenCodeModelManagerConfig {
|
|
|
1218
1218
|
baseUrl?: string;
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
+
function normalizeOpenCodeBasePath(baseUrl: string | undefined, fallbackBasePath: string): string {
|
|
1222
|
+
const value = normalizeAnthropicBaseUrl(baseUrl, fallbackBasePath);
|
|
1223
|
+
return value.endsWith("/v1") ? value.slice(0, -3) : value;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function openCodeBaseUrlForApi(api: Api, basePath: string): string {
|
|
1227
|
+
return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1221
1230
|
function openCodeModelManagerOptions(
|
|
1222
1231
|
providerId: "opencode-go" | "opencode-zen",
|
|
1223
|
-
|
|
1232
|
+
defaultBasePath: string,
|
|
1224
1233
|
config?: OpenCodeModelManagerConfig,
|
|
1225
|
-
): ModelManagerOptions<
|
|
1234
|
+
): ModelManagerOptions<Api> {
|
|
1226
1235
|
const apiKey = config?.apiKey;
|
|
1227
|
-
const
|
|
1236
|
+
const basePath = normalizeOpenCodeBasePath(config?.baseUrl, defaultBasePath);
|
|
1237
|
+
const discoveryBaseUrl = openCodeBaseUrlForApi("openai-completions", basePath);
|
|
1238
|
+
const references = createBundledReferenceMap<Api>(providerId);
|
|
1228
1239
|
return {
|
|
1229
1240
|
providerId,
|
|
1230
1241
|
...(apiKey && {
|
|
1231
1242
|
fetchDynamicModels: () =>
|
|
1232
|
-
fetchOpenAICompatibleModels({
|
|
1243
|
+
fetchOpenAICompatibleModels<Api>({
|
|
1233
1244
|
api: "openai-completions",
|
|
1234
1245
|
provider: providerId,
|
|
1235
|
-
baseUrl,
|
|
1246
|
+
baseUrl: discoveryBaseUrl,
|
|
1236
1247
|
apiKey,
|
|
1248
|
+
mapModel: (entry, defaults) => {
|
|
1249
|
+
const reference = references.get(defaults.id);
|
|
1250
|
+
const name = toModelName(entry.name, reference?.name ?? defaults.name);
|
|
1251
|
+
if (!reference) {
|
|
1252
|
+
return {
|
|
1253
|
+
...defaults,
|
|
1254
|
+
name,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
...reference,
|
|
1259
|
+
id: defaults.id,
|
|
1260
|
+
name,
|
|
1261
|
+
baseUrl: openCodeBaseUrlForApi(reference.api, basePath),
|
|
1262
|
+
contextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),
|
|
1263
|
+
maxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),
|
|
1264
|
+
};
|
|
1265
|
+
},
|
|
1237
1266
|
}),
|
|
1238
1267
|
}),
|
|
1239
1268
|
};
|
|
1240
1269
|
}
|
|
1241
1270
|
|
|
1242
|
-
export function opencodeZenModelManagerOptions(
|
|
1243
|
-
|
|
1244
|
-
): ModelManagerOptions<"openai-completions"> {
|
|
1245
|
-
return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen/v1", config);
|
|
1271
|
+
export function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
|
|
1272
|
+
return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen", config);
|
|
1246
1273
|
}
|
|
1247
1274
|
|
|
1248
|
-
export function opencodeGoModelManagerOptions(
|
|
1249
|
-
|
|
1250
|
-
): ModelManagerOptions<"openai-completions"> {
|
|
1251
|
-
return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go/v1", config);
|
|
1275
|
+
export function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
|
|
1276
|
+
return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go", config);
|
|
1252
1277
|
}
|
|
1253
1278
|
|
|
1254
1279
|
// ---------------------------------------------------------------------------
|
|
@@ -2643,9 +2668,16 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
|
|
|
2643
2668
|
// --- zAI ---
|
|
2644
2669
|
anthropicMessagesDescriptor("zai-coding-plan", "zai", "https://api.z.ai/api/anthropic"),
|
|
2645
2670
|
// --- Xiaomi ---
|
|
2646
|
-
|
|
2671
|
+
openAiCompletionsDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/v1", {
|
|
2647
2672
|
defaultContextWindow: 262144,
|
|
2648
2673
|
defaultMaxTokens: 8192,
|
|
2674
|
+
compat: {
|
|
2675
|
+
supportsStore: false,
|
|
2676
|
+
thinkingFormat: "zai",
|
|
2677
|
+
reasoningContentField: "reasoning_content",
|
|
2678
|
+
requiresReasoningContentForToolCalls: true,
|
|
2679
|
+
allowsSyntheticReasoningContentForToolCalls: false,
|
|
2680
|
+
},
|
|
2649
2681
|
}),
|
|
2650
2682
|
// --- MiniMax Coding Plan ---
|
|
2651
2683
|
openAiCompletionsDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/v1", {
|
|
@@ -654,17 +654,6 @@ function convertContentBlocks(
|
|
|
654
654
|
return blocks;
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
-
/**
|
|
658
|
-
* Marker phrase that Claude has been observed to hallucinate inside reasoning summaries
|
|
659
|
-
* (e.g. "I don't see any current rewritten thinking or next thinking to process. Could
|
|
660
|
-
* you provide..."). When this substring appears in a streamed thinking block we collapse
|
|
661
|
-
* the entire block to {@link BROKEN_THINKING_REPLACEMENT} and drop the signature so
|
|
662
|
-
* downstream UI/transcripts don't surface the meta-prompt and replay can't re-anchor on
|
|
663
|
-
* the garbled chain.
|
|
664
|
-
*/
|
|
665
|
-
const BROKEN_THINKING_MARKER = "rewritten thinking";
|
|
666
|
-
const BROKEN_THINKING_REPLACEMENT = "Thinking...";
|
|
667
|
-
|
|
668
657
|
export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
|
669
658
|
export type AnthropicThinkingDisplay = "summarized" | "omitted";
|
|
670
659
|
|
|
@@ -1221,12 +1210,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1221
1210
|
const requestTimeoutMs =
|
|
1222
1211
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
1223
1212
|
const blocks = output.content as Block[];
|
|
1224
|
-
// Recent Claude releases occasionally hallucinate meta-prompts asking the operator
|
|
1225
|
-
// to supply "rewritten thinking" / "next thinking" as reasoning content. The summary
|
|
1226
|
-
// is useless and confuses the UI, so we collapse any thinking block whose stream
|
|
1227
|
-
// contains the marker phrase down to a plain "Thinking..." placeholder and drop the
|
|
1228
|
-
// (now invalid) signature so subsequent turns don't replay the garbled chain.
|
|
1229
|
-
const suppressedThinkingBlocks = new WeakSet<Block>();
|
|
1230
1213
|
stream.push({ type: "start", partial: output });
|
|
1231
1214
|
// Retry loop for transient errors from the stream.
|
|
1232
1215
|
// Provider-level transport/rate-limit failures: only before any streamed content starts.
|
|
@@ -1381,14 +1364,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1381
1364
|
const index = blocks.findIndex(b => b.index === event.index);
|
|
1382
1365
|
const block = blocks[index];
|
|
1383
1366
|
if (block && block.type === "thinking") {
|
|
1384
|
-
if (suppressedThinkingBlocks.has(block)) continue;
|
|
1385
1367
|
block.thinking += event.delta.thinking;
|
|
1386
|
-
if (block.thinking.includes(BROKEN_THINKING_MARKER)) {
|
|
1387
|
-
suppressedThinkingBlocks.add(block);
|
|
1388
|
-
block.thinking = BROKEN_THINKING_REPLACEMENT;
|
|
1389
|
-
block.thinkingSignature = "";
|
|
1390
|
-
continue;
|
|
1391
|
-
}
|
|
1392
1368
|
stream.push({
|
|
1393
1369
|
type: "thinking_delta",
|
|
1394
1370
|
contentIndex: index,
|
|
@@ -1412,7 +1388,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1412
1388
|
} else if (event.delta.type === "signature_delta") {
|
|
1413
1389
|
const index = blocks.findIndex(b => b.index === event.index);
|
|
1414
1390
|
const block = blocks[index];
|
|
1415
|
-
if (block && block.type === "thinking"
|
|
1391
|
+
if (block && block.type === "thinking") {
|
|
1416
1392
|
block.thinkingSignature = block.thinkingSignature || "";
|
|
1417
1393
|
block.thinkingSignature += event.delta.signature;
|
|
1418
1394
|
}
|
|
@@ -1430,14 +1406,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
|
|
|
1430
1406
|
partial: output,
|
|
1431
1407
|
});
|
|
1432
1408
|
} else if (block.type === "thinking") {
|
|
1433
|
-
if (
|
|
1434
|
-
!suppressedThinkingBlocks.has(block) &&
|
|
1435
|
-
block.thinking.includes(BROKEN_THINKING_MARKER)
|
|
1436
|
-
) {
|
|
1437
|
-
suppressedThinkingBlocks.add(block);
|
|
1438
|
-
block.thinking = BROKEN_THINKING_REPLACEMENT;
|
|
1439
|
-
block.thinkingSignature = "";
|
|
1440
|
-
}
|
|
1441
1409
|
stream.push({
|
|
1442
1410
|
type: "thinking_end",
|
|
1443
1411
|
contentIndex: index,
|
package/src/providers/cursor.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type {
|
|
|
28
28
|
import { normalizeSystemPrompts } from "../utils";
|
|
29
29
|
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
30
30
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
31
|
+
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
31
32
|
import { formatErrorMessageWithRetryAfter } from "../utils/retry-after";
|
|
32
33
|
import { toolWireSchema } from "../utils/schema/wire";
|
|
33
34
|
import type { McpToolDefinition } from "./cursor/gen/agent_pb";
|
|
@@ -331,6 +332,7 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
331
332
|
let h2Client: http2.ClientHttp2Session | null = null;
|
|
332
333
|
let h2Request: http2.ClientHttp2Stream | null = null;
|
|
333
334
|
let heartbeatTimer: NodeJS.Timeout | null = null;
|
|
335
|
+
let debugResponseLogPromise: Promise<RequestDebugResponseLog | undefined> | undefined;
|
|
334
336
|
|
|
335
337
|
try {
|
|
336
338
|
const apiKey = options?.apiKey;
|
|
@@ -351,11 +353,10 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
351
353
|
const requestContextTools = buildMcpToolDefinitions(context.tools);
|
|
352
354
|
|
|
353
355
|
const baseUrl = model.baseUrl || CURSOR_API_URL;
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
h2Request = h2Client.request({
|
|
356
|
+
const requestPath = "/agent.v1.AgentService/Run";
|
|
357
|
+
const requestHeaders = {
|
|
357
358
|
":method": "POST",
|
|
358
|
-
":path":
|
|
359
|
+
":path": requestPath,
|
|
359
360
|
"content-type": "application/connect+proto",
|
|
360
361
|
"connect-protocol-version": "1",
|
|
361
362
|
te: "trailers",
|
|
@@ -364,7 +365,20 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
364
365
|
"x-cursor-client-version": CURSOR_CLIENT_VERSION,
|
|
365
366
|
"x-cursor-client-type": "cli",
|
|
366
367
|
"x-request-id": crypto.randomUUID(),
|
|
367
|
-
}
|
|
368
|
+
};
|
|
369
|
+
const debugSession = isRequestDebugEnabled()
|
|
370
|
+
? await createRequestDebugSession({
|
|
371
|
+
protocol: "http2",
|
|
372
|
+
method: "POST",
|
|
373
|
+
url: new URL(requestPath, baseUrl).toString(),
|
|
374
|
+
headers: requestHeaders,
|
|
375
|
+
bodyBase64: Buffer.from(requestBytes).toString("base64"),
|
|
376
|
+
})
|
|
377
|
+
: undefined;
|
|
378
|
+
|
|
379
|
+
h2Client = http2.connect(baseUrl);
|
|
380
|
+
|
|
381
|
+
h2Request = h2Client.request(requestHeaders);
|
|
368
382
|
|
|
369
383
|
stream.push({ type: "start", partial: output });
|
|
370
384
|
|
|
@@ -408,7 +422,19 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
408
422
|
|
|
409
423
|
let resolveH2: (() => void) | undefined;
|
|
410
424
|
|
|
425
|
+
h2Request.on("response", headers => {
|
|
426
|
+
debugResponseLogPromise = debugSession?.openResponseLog(
|
|
427
|
+
`HTTP/2 ${headers[":status"] ?? ""}`.trim(),
|
|
428
|
+
headers,
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
|
|
411
432
|
h2Request.on("data", (chunk: Buffer) => {
|
|
433
|
+
if (debugResponseLogPromise) {
|
|
434
|
+
void debugResponseLogPromise.then(log => {
|
|
435
|
+
log?.write(chunk);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
412
438
|
pendingBuffer = Buffer.concat([pendingBuffer, chunk]);
|
|
413
439
|
|
|
414
440
|
while (pendingBuffer.length >= 5) {
|
|
@@ -480,29 +506,44 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
480
506
|
await new Promise<void>((resolve, reject) => {
|
|
481
507
|
resolveH2 = resolve;
|
|
482
508
|
|
|
509
|
+
const closeDebugLog = async (): Promise<void> => {
|
|
510
|
+
const log = await debugResponseLogPromise;
|
|
511
|
+
await log?.close();
|
|
512
|
+
};
|
|
513
|
+
|
|
483
514
|
h2Request!.on("trailers", trailers => {
|
|
484
515
|
const status = trailers["grpc-status"];
|
|
485
516
|
const msg = trailers["grpc-message"];
|
|
486
517
|
if (status && status !== "0") {
|
|
487
|
-
|
|
518
|
+
void closeDebugLog().finally(() => {
|
|
519
|
+
reject(new Error(`gRPC error ${status}: ${decodeURIComponent(String(msg || ""))}`));
|
|
520
|
+
});
|
|
488
521
|
}
|
|
489
522
|
});
|
|
490
523
|
|
|
491
524
|
h2Request!.on("end", () => {
|
|
492
525
|
resolveH2 = undefined;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
526
|
+
void closeDebugLog()
|
|
527
|
+
.then(() => {
|
|
528
|
+
if (endStreamError) {
|
|
529
|
+
reject(endStreamError);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
resolve();
|
|
533
|
+
})
|
|
534
|
+
.catch(reject);
|
|
498
535
|
});
|
|
499
536
|
|
|
500
|
-
h2Request!.on("error",
|
|
537
|
+
h2Request!.on("error", error => {
|
|
538
|
+
void closeDebugLog().finally(() => reject(error));
|
|
539
|
+
});
|
|
501
540
|
|
|
502
541
|
if (options?.signal) {
|
|
503
542
|
options.signal.addEventListener("abort", () => {
|
|
504
543
|
h2Request?.close();
|
|
505
|
-
|
|
544
|
+
void closeDebugLog().finally(() => {
|
|
545
|
+
reject(new Error("Request was aborted"));
|
|
546
|
+
});
|
|
506
547
|
});
|
|
507
548
|
}
|
|
508
549
|
});
|
|
@@ -557,6 +598,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
557
598
|
stream.push({ type: "error", reason: output.stopReason, error: output });
|
|
558
599
|
stream.end();
|
|
559
600
|
} finally {
|
|
601
|
+
const log = await debugResponseLogPromise;
|
|
602
|
+
await log?.close();
|
|
560
603
|
if (heartbeatTimer) {
|
|
561
604
|
clearInterval(heartbeatTimer);
|
|
562
605
|
heartbeatTimer = null;
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
iterateWithIdleTimeout,
|
|
55
55
|
} from "../utils/idle-iterator";
|
|
56
56
|
import { parseStreamingJson } from "../utils/json-parse";
|
|
57
|
+
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
57
58
|
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
58
59
|
import { notifyRawSseEvent } from "../utils/sse-debug";
|
|
59
60
|
import { compactGrammarDefinition } from "./grammar";
|
|
@@ -2046,6 +2047,8 @@ class CodexWebSocketConnection {
|
|
|
2046
2047
|
#streamObserver?: (event: RawSseEvent) => void;
|
|
2047
2048
|
#heartbeatInterval: NodeJS.Timeout | undefined;
|
|
2048
2049
|
#removePongListener?: () => void;
|
|
2050
|
+
#handshakeHeaders?: Headers;
|
|
2051
|
+
#debugResponseLog?: RequestDebugResponseLog;
|
|
2049
2052
|
/**
|
|
2050
2053
|
* Wall-clock of the most recent inbound activity on this socket — any
|
|
2051
2054
|
* decoded message, any pong, or the moment the handshake completed. Used
|
|
@@ -2189,6 +2192,7 @@ class CodexWebSocketConnection {
|
|
|
2189
2192
|
// the liveness clock — what matters for reuse health is that the upstream
|
|
2190
2193
|
// is still talking to us, not that every frame is well-formed.
|
|
2191
2194
|
this.#lastInboundAt = Date.now();
|
|
2195
|
+
this.#writeDebugWebSocketFrame(event.data);
|
|
2192
2196
|
try {
|
|
2193
2197
|
const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString("utf-8");
|
|
2194
2198
|
if (!text) return;
|
|
@@ -2256,6 +2260,19 @@ class CodexWebSocketConnection {
|
|
|
2256
2260
|
}
|
|
2257
2261
|
|
|
2258
2262
|
try {
|
|
2263
|
+
const debugSession = isRequestDebugEnabled()
|
|
2264
|
+
? await createRequestDebugSession({
|
|
2265
|
+
protocol: "websocket",
|
|
2266
|
+
method: "POST",
|
|
2267
|
+
url: this.#url,
|
|
2268
|
+
headers: this.#headers,
|
|
2269
|
+
body: request,
|
|
2270
|
+
})
|
|
2271
|
+
: undefined;
|
|
2272
|
+
this.#debugResponseLog = debugSession
|
|
2273
|
+
? await debugSession.openResponseLog("WebSocket 101 Switching Protocols", this.#handshakeHeaders)
|
|
2274
|
+
: undefined;
|
|
2275
|
+
|
|
2259
2276
|
const requestPayload = JSON.stringify(request);
|
|
2260
2277
|
notifyCodexWebSocketOutbound(onSseEvent, request, requestPayload);
|
|
2261
2278
|
try {
|
|
@@ -2336,14 +2353,35 @@ class CodexWebSocketConnection {
|
|
|
2336
2353
|
if (signal) {
|
|
2337
2354
|
signal.removeEventListener("abort", onAbort);
|
|
2338
2355
|
}
|
|
2356
|
+
const debugResponseLog = this.#debugResponseLog;
|
|
2357
|
+
this.#debugResponseLog = undefined;
|
|
2358
|
+
await debugResponseLog?.close();
|
|
2339
2359
|
}
|
|
2340
2360
|
}
|
|
2341
2361
|
|
|
2342
2362
|
#captureHandshakeHeaders(socket: Bun.WebSocket, openEvent?: Event): void {
|
|
2343
|
-
if (!this.#onHandshakeHeaders) return;
|
|
2344
2363
|
const headers = extractCodexWebSocketHandshakeHeaders(socket, openEvent);
|
|
2345
2364
|
if (!headers) return;
|
|
2346
|
-
this.#
|
|
2365
|
+
this.#handshakeHeaders = headers;
|
|
2366
|
+
this.#onHandshakeHeaders?.(headers);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
#writeDebugWebSocketFrame(data: unknown): void {
|
|
2370
|
+
const log = this.#debugResponseLog;
|
|
2371
|
+
if (!log) return;
|
|
2372
|
+
if (typeof data === "string") {
|
|
2373
|
+
log.write(data);
|
|
2374
|
+
return;
|
|
2375
|
+
}
|
|
2376
|
+
if (data instanceof Uint8Array) {
|
|
2377
|
+
log.write(data);
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
if (data instanceof ArrayBuffer) {
|
|
2381
|
+
log.write(new Uint8Array(data));
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
2384
|
+
log.write(String(data));
|
|
2347
2385
|
}
|
|
2348
2386
|
|
|
2349
2387
|
#startHeartbeat(socket: Bun.WebSocket): void {
|
|
@@ -208,6 +208,11 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
|
|
|
208
208
|
requiresAssistantAfterToolResult: false,
|
|
209
209
|
requiresThinkingAsText: isMistral,
|
|
210
210
|
requiresMistralToolIds: isMistral,
|
|
211
|
+
// Only Kimi's native hosts (Moonshot / Kimi-code, matched by `isMoonshotKimi`)
|
|
212
|
+
// speak the z.ai binary `thinking: { type }` field. Kimi reached through
|
|
213
|
+
// OpenAI-compatible proxies — Fireworks' Fire Pass router, OpenCode's gateway,
|
|
214
|
+
// etc. — drives reasoning via OpenAI-style `reasoning_effort`
|
|
215
|
+
// (low|medium|high|xhigh|max|none), so those stay on the "openai" path.
|
|
211
216
|
thinkingFormat:
|
|
212
217
|
isZai || isZhipu || isMoonshotKimi
|
|
213
218
|
? "zai"
|
|
@@ -1341,29 +1341,56 @@ export function parseChunkUsage(
|
|
|
1341
1341
|
const completionTokenDetails = getOptionalObjectProperty(rawUsage, "completion_tokens_details");
|
|
1342
1342
|
const cachedTokens =
|
|
1343
1343
|
getOptionalNumberProperty(rawUsage, "cached_tokens") ??
|
|
1344
|
+
getOptionalNumberProperty(rawUsage, "prompt_cache_hit_tokens") ??
|
|
1344
1345
|
(promptTokenDetails ? getOptionalNumberProperty(promptTokenDetails, "cached_tokens") : undefined) ??
|
|
1345
1346
|
0;
|
|
1346
1347
|
// OpenRouter exposes cache writes via `prompt_tokens_details.cache_write_tokens`
|
|
1347
|
-
// and INCLUDES them in `prompt_tokens
|
|
1348
|
-
//
|
|
1348
|
+
// and INCLUDES them in `prompt_tokens` — they are billed on top of the input, so
|
|
1349
|
+
// we subtract them to get the real billed input.
|
|
1350
|
+
// DeepSeek exposes cache hit/miss via `prompt_cache_hit_tokens` /
|
|
1351
|
+
// `prompt_cache_miss_tokens` at the top level where `prompt_tokens` equals their
|
|
1352
|
+
// sum. The miss portion IS the billed input — we must NOT subtract it.
|
|
1349
1353
|
// Ref: https://openrouter.ai/docs/guides/best-practices/prompt-caching
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1354
|
+
// Ref: https://api-docs.deepseek.com/api/create-chat-completion
|
|
1355
|
+
//
|
|
1356
|
+
// Resolve cacheWrite from both possible sources separately.
|
|
1357
|
+
// They have different billing semantics: OpenRouter's cache_write is billed
|
|
1358
|
+
// on top of prompt_tokens, while DeepSeek's miss IS the billed input.
|
|
1359
|
+
const cacheWriteOpenRouter = promptTokenDetails
|
|
1360
|
+
? getOptionalNumberProperty(promptTokenDetails, "cache_write_tokens")
|
|
1361
|
+
: undefined;
|
|
1362
|
+
const cacheWriteDeepSeek = getOptionalNumberProperty(rawUsage, "prompt_cache_miss_tokens");
|
|
1363
|
+
// Prefer OpenRouter's value for the input subtraction; fall back to DeepSeek.
|
|
1364
|
+
const cacheWriteTokens = cacheWriteOpenRouter ?? cacheWriteDeepSeek ?? 0;
|
|
1365
|
+
|
|
1353
1366
|
const reasoningTokens =
|
|
1354
1367
|
(completionTokenDetails ? getOptionalNumberProperty(completionTokenDetails, "reasoning_tokens") : undefined) ?? 0;
|
|
1355
1368
|
const promptTokens = getOptionalNumberProperty(rawUsage, "prompt_tokens") ?? 0;
|
|
1356
|
-
|
|
1369
|
+
|
|
1370
|
+
const isDeepSeekNative =
|
|
1371
|
+
getOptionalNumberProperty(rawUsage, "prompt_cache_hit_tokens") !== undefined && cacheWriteDeepSeek !== undefined;
|
|
1372
|
+
// Only use the DeepSeek input path when cacheWrite came from DeepSeek's
|
|
1373
|
+
// miss field, not from prompt_tokens_details. Avoids false positives when
|
|
1374
|
+
// DeepSeek models route through OpenRouter (which may pass through native
|
|
1375
|
+
// fields alongside its own cache_write_tokens).
|
|
1376
|
+
const isDeepSeekUsage = isDeepSeekNative && cacheWriteOpenRouter === undefined && cacheWriteDeepSeek > 0;
|
|
1377
|
+
const input = isDeepSeekUsage
|
|
1378
|
+
? Math.max(0, promptTokens - cachedTokens)
|
|
1379
|
+
: Math.max(0, promptTokens - cachedTokens - cacheWriteTokens);
|
|
1357
1380
|
// Per OpenAI's CompletionUsage spec, `reasoning_tokens` is a subset of
|
|
1358
1381
|
// `completion_tokens` (which is the total billed output). Adding them would
|
|
1359
1382
|
// double-count.
|
|
1360
1383
|
const outputTokens = getOptionalNumberProperty(rawUsage, "completion_tokens") ?? 0;
|
|
1384
|
+
// DeepSeek only exposes cache hit/miss (no cache-write data).
|
|
1385
|
+
// Emitting miss tokens as cacheWrite would make downstream consumers
|
|
1386
|
+
// double-count them (input already equals miss for DeepSeek).
|
|
1387
|
+
const emittedCacheWrite = isDeepSeekUsage ? 0 : cacheWriteTokens;
|
|
1361
1388
|
const usage: AssistantMessage["usage"] = {
|
|
1362
1389
|
input,
|
|
1363
1390
|
output: outputTokens,
|
|
1364
1391
|
cacheRead: cachedTokens,
|
|
1365
|
-
cacheWrite:
|
|
1366
|
-
totalTokens: input + outputTokens + cachedTokens +
|
|
1392
|
+
cacheWrite: emittedCacheWrite,
|
|
1393
|
+
totalTokens: input + outputTokens + cachedTokens + emittedCacheWrite,
|
|
1367
1394
|
...(reasoningTokens > 0 ? { reasoningTokens } : {}),
|
|
1368
1395
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
1369
1396
|
...(premiumRequests !== undefined ? { premiumRequests } : {}),
|
package/src/stream.ts
CHANGED
|
@@ -61,6 +61,7 @@ import type {
|
|
|
61
61
|
} from "./types";
|
|
62
62
|
import { AssistantMessageEventStream } from "./utils/event-stream";
|
|
63
63
|
import { isFoundryEnabled } from "./utils/foundry";
|
|
64
|
+
import { withRequestDebugFetch } from "./utils/request-debug";
|
|
64
65
|
|
|
65
66
|
let cachedVertexAdcCredentialsExists: boolean | null = null;
|
|
66
67
|
|
|
@@ -295,42 +296,50 @@ export function stream<TApi extends Api>(
|
|
|
295
296
|
context: Context,
|
|
296
297
|
options?: OptionsForApi<TApi>,
|
|
297
298
|
): AssistantMessageEventStream {
|
|
299
|
+
const requestOptions = withRequestDebugFetch(options as StreamOptions | undefined) as
|
|
300
|
+
| OptionsForApi<TApi>
|
|
301
|
+
| undefined;
|
|
302
|
+
|
|
298
303
|
// Check custom API registry first (extension-provided APIs like "vertex-claude-api")
|
|
299
304
|
const customApiProvider = getCustomApi(model.api);
|
|
300
305
|
if (customApiProvider) {
|
|
301
|
-
return customApiProvider.stream(model, context,
|
|
306
|
+
return customApiProvider.stream(model, context, requestOptions as StreamOptions);
|
|
302
307
|
}
|
|
303
308
|
|
|
304
309
|
if (isGitLabDuoModel(model)) {
|
|
305
|
-
const apiKey = (
|
|
310
|
+
const apiKey = (requestOptions as StreamOptions | undefined)?.apiKey || getEnvApiKey(model.provider);
|
|
306
311
|
if (!apiKey) {
|
|
307
312
|
throw new Error(`No API key for provider: ${model.provider}`);
|
|
308
313
|
}
|
|
309
314
|
return streamGitLabDuo(model, context, {
|
|
310
|
-
...(
|
|
315
|
+
...(requestOptions as SimpleStreamOptions | undefined),
|
|
311
316
|
apiKey,
|
|
312
317
|
});
|
|
313
318
|
}
|
|
314
319
|
|
|
315
320
|
// Vertex AI uses Application Default Credentials, not API keys
|
|
316
321
|
if (model.api === "google-vertex") {
|
|
317
|
-
return streamGoogleVertex(model as Model<"google-vertex">, context,
|
|
322
|
+
return streamGoogleVertex(model as Model<"google-vertex">, context, requestOptions as GoogleVertexOptions);
|
|
318
323
|
} else if (model.api === "bedrock-converse-stream") {
|
|
319
324
|
// Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
|
|
320
|
-
return streamBedrock(
|
|
325
|
+
return streamBedrock(
|
|
326
|
+
model as Model<"bedrock-converse-stream">,
|
|
327
|
+
context,
|
|
328
|
+
(requestOptions || {}) as BedrockOptions,
|
|
329
|
+
);
|
|
321
330
|
}
|
|
322
331
|
|
|
323
|
-
const apiKey =
|
|
332
|
+
const apiKey = requestOptions?.apiKey || getEnvApiKey(model.provider);
|
|
324
333
|
if (!apiKey) {
|
|
325
334
|
throw new Error(`No API key for provider: ${model.provider}`);
|
|
326
335
|
}
|
|
327
336
|
const providerOptions = isGoogleVertexAuthenticatedModel(model)
|
|
328
337
|
? {
|
|
329
|
-
...
|
|
338
|
+
...requestOptions,
|
|
330
339
|
apiKey: "vertex-adc",
|
|
331
|
-
fetch: createVertexAuthenticatedFetch(
|
|
340
|
+
fetch: createVertexAuthenticatedFetch(requestOptions as StreamOptions | undefined),
|
|
332
341
|
}
|
|
333
|
-
: { ...
|
|
342
|
+
: { ...requestOptions, apiKey };
|
|
334
343
|
|
|
335
344
|
const api: Api = model.api;
|
|
336
345
|
switch (api) {
|
|
@@ -430,10 +439,13 @@ export function streamSimple<TApi extends Api>(
|
|
|
430
439
|
context: Context,
|
|
431
440
|
options?: SimpleStreamOptions,
|
|
432
441
|
): AssistantMessageEventStream {
|
|
433
|
-
const
|
|
442
|
+
const requestOptions = withRequestDebugFetch(options);
|
|
443
|
+
const retryApiKey = requestOptions?.onAuthError
|
|
444
|
+
? (requestOptions.apiKey ?? getEnvApiKey(model.provider))
|
|
445
|
+
: undefined;
|
|
434
446
|
if (retryApiKey) {
|
|
435
447
|
const outer = new AssistantMessageEventStream();
|
|
436
|
-
const onAuthError =
|
|
448
|
+
const onAuthError = requestOptions!.onAuthError!;
|
|
437
449
|
const runAttempt = async (apiKey: string, captureAuthFailure: boolean): Promise<AuthRetryFailure | undefined> => {
|
|
438
450
|
const bufferedEvents: AssistantMessageEvent[] = [];
|
|
439
451
|
let emittedReplayUnsafeEvent = false;
|
|
@@ -443,7 +455,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
443
455
|
};
|
|
444
456
|
|
|
445
457
|
try {
|
|
446
|
-
const inner = streamSimple(model, context, { ...
|
|
458
|
+
const inner = streamSimple(model, context, { ...requestOptions, apiKey, onAuthError: undefined });
|
|
447
459
|
for await (const event of inner) {
|
|
448
460
|
if (!emittedReplayUnsafeEvent && event.type === "start") {
|
|
449
461
|
bufferedEvents.push(event);
|
|
@@ -519,26 +531,26 @@ export function streamSimple<TApi extends Api>(
|
|
|
519
531
|
// extension-registered APIs can't accidentally override a configured
|
|
520
532
|
// pi-native transport.
|
|
521
533
|
if (model.transport === "pi-native") {
|
|
522
|
-
return streamPiNative(model, context,
|
|
534
|
+
return streamPiNative(model, context, requestOptions);
|
|
523
535
|
}
|
|
524
536
|
|
|
525
537
|
// Check custom API registry (extension-provided APIs)
|
|
526
538
|
const customApiProvider = getCustomApi(model.api);
|
|
527
539
|
if (customApiProvider) {
|
|
528
|
-
return customApiProvider.streamSimple(model, context,
|
|
540
|
+
return customApiProvider.streamSimple(model, context, requestOptions);
|
|
529
541
|
}
|
|
530
542
|
|
|
531
543
|
// Vertex AI uses Application Default Credentials, not API keys
|
|
532
544
|
if (model.api === "google-vertex") {
|
|
533
|
-
const providerOptions = mapOptionsForApi(model,
|
|
545
|
+
const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
|
|
534
546
|
return stream(model, context, providerOptions);
|
|
535
547
|
} else if (model.api === "bedrock-converse-stream") {
|
|
536
548
|
// Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
|
|
537
|
-
const providerOptions = mapOptionsForApi(model,
|
|
549
|
+
const providerOptions = mapOptionsForApi(model, requestOptions, undefined);
|
|
538
550
|
return stream(model, context, providerOptions);
|
|
539
551
|
}
|
|
540
552
|
|
|
541
|
-
const apiKey =
|
|
553
|
+
const apiKey = requestOptions?.apiKey || getEnvApiKey(model.provider);
|
|
542
554
|
if (!apiKey) {
|
|
543
555
|
throw new Error(`No API key for provider: ${model.provider}`);
|
|
544
556
|
}
|
|
@@ -546,7 +558,7 @@ export function streamSimple<TApi extends Api>(
|
|
|
546
558
|
// GitLab Duo - wraps Anthropic/OpenAI behind GitLab AI Gateway direct access tokens
|
|
547
559
|
if (isGitLabDuoModel(model)) {
|
|
548
560
|
return streamGitLabDuo(model, context, {
|
|
549
|
-
...
|
|
561
|
+
...requestOptions,
|
|
550
562
|
apiKey,
|
|
551
563
|
});
|
|
552
564
|
}
|
|
@@ -555,9 +567,9 @@ export function streamSimple<TApi extends Api>(
|
|
|
555
567
|
if (isKimiModel(model)) {
|
|
556
568
|
// Pass raw SimpleStreamOptions - streamKimi handles mapping internally
|
|
557
569
|
return streamKimi(model as Model<"openai-completions">, context, {
|
|
558
|
-
...
|
|
570
|
+
...requestOptions,
|
|
559
571
|
apiKey,
|
|
560
|
-
format:
|
|
572
|
+
format: requestOptions?.kimiApiFormat ?? "anthropic",
|
|
561
573
|
});
|
|
562
574
|
}
|
|
563
575
|
|
|
@@ -565,13 +577,12 @@ export function streamSimple<TApi extends Api>(
|
|
|
565
577
|
if (isSyntheticModel(model)) {
|
|
566
578
|
// Pass raw SimpleStreamOptions - streamSynthetic handles mapping internally
|
|
567
579
|
return streamSynthetic(model as Model<"openai-completions">, context, {
|
|
568
|
-
...
|
|
580
|
+
...requestOptions,
|
|
569
581
|
apiKey,
|
|
570
|
-
format:
|
|
582
|
+
format: requestOptions?.syntheticApiFormat ?? "openai", // Default to OpenAI format
|
|
571
583
|
});
|
|
572
584
|
}
|
|
573
|
-
|
|
574
|
-
const providerOptions = mapOptionsForApi(model, options, apiKey);
|
|
585
|
+
const providerOptions = mapOptionsForApi(model, requestOptions, apiKey);
|
|
575
586
|
return stream(model, context, providerOptions);
|
|
576
587
|
}
|
|
577
588
|
|
|
@@ -716,6 +727,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
716
727
|
onResponse: options?.onResponse,
|
|
717
728
|
onSseEvent: options?.onSseEvent,
|
|
718
729
|
execHandlers: options?.execHandlers,
|
|
730
|
+
fetch: options?.fetch,
|
|
719
731
|
};
|
|
720
732
|
|
|
721
733
|
switch (model.api) {
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import type { FetchImpl } from "../types";
|
|
4
|
+
|
|
5
|
+
const REQUEST_DEBUG_ENV = "PI_REQ_DEBUG";
|
|
6
|
+
const DEBUG_FETCH_MARKER = Symbol("omp.requestDebugFetch");
|
|
7
|
+
const textEncoder = new TextEncoder();
|
|
8
|
+
const utf8Decoder = new TextDecoder("utf-8", { fatal: true });
|
|
9
|
+
|
|
10
|
+
let nextSessionId = 1;
|
|
11
|
+
|
|
12
|
+
type DebugFetch = FetchImpl & { [DEBUG_FETCH_MARKER]?: true };
|
|
13
|
+
type RequestBodyInit = NonNullable<RequestInit["body"]>;
|
|
14
|
+
|
|
15
|
+
type RequestDebugBody = { body: unknown } | { bodyText: string } | { bodyBase64: string } | { bodyUnavailable: string };
|
|
16
|
+
|
|
17
|
+
export type RequestDebugHeaders = Headers | Record<string, string | string[] | number | undefined | null> | undefined;
|
|
18
|
+
|
|
19
|
+
export interface RequestDebugPayload {
|
|
20
|
+
method: string;
|
|
21
|
+
url: string;
|
|
22
|
+
headers?: RequestDebugHeaders;
|
|
23
|
+
body?: unknown;
|
|
24
|
+
bodyText?: string;
|
|
25
|
+
bodyBase64?: string;
|
|
26
|
+
bodyUnavailable?: string;
|
|
27
|
+
protocol?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RequestDebugResponseLog {
|
|
31
|
+
write(chunk: Uint8Array | string): void;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RequestDebugSession {
|
|
36
|
+
readonly id: number;
|
|
37
|
+
readonly requestPath: string;
|
|
38
|
+
readonly responsePath: string;
|
|
39
|
+
openResponseLog(statusLine: string, headers?: RequestDebugHeaders): Promise<RequestDebugResponseLog>;
|
|
40
|
+
wrapResponse(response: Response): Promise<Response>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isRequestDebugEnabled(): boolean {
|
|
44
|
+
return Bun.env[REQUEST_DEBUG_ENV] === "1";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl {
|
|
48
|
+
if (!isRequestDebugEnabled()) return fetchImpl;
|
|
49
|
+
const maybeWrapped = fetchImpl as DebugFetch;
|
|
50
|
+
if (maybeWrapped[DEBUG_FETCH_MARKER]) return fetchImpl;
|
|
51
|
+
|
|
52
|
+
const wrapped = Object.assign(
|
|
53
|
+
async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
|
54
|
+
const session = await createFetchRequestDebugSession(input, init);
|
|
55
|
+
const response = await fetchImpl(input, init);
|
|
56
|
+
return session.wrapResponse(response);
|
|
57
|
+
},
|
|
58
|
+
fetchImpl.preconnect ? { preconnect: fetchImpl.preconnect } : {},
|
|
59
|
+
{ [DEBUG_FETCH_MARKER]: true as const },
|
|
60
|
+
);
|
|
61
|
+
return wrapped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function withRequestDebugFetch<T extends { fetch?: FetchImpl } | undefined>(options: T): T {
|
|
65
|
+
if (!isRequestDebugEnabled()) return options;
|
|
66
|
+
const fetchImpl = options?.fetch ?? (globalThis.fetch as FetchImpl);
|
|
67
|
+
const wrapped = wrapFetchForRequestDebug(fetchImpl);
|
|
68
|
+
return { ...(options ?? {}), fetch: wrapped } as T;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createRequestDebugSession(payload: RequestDebugPayload): Promise<RequestDebugSession> {
|
|
72
|
+
const { id, requestPath, responsePath, handle } = await reserveRequestDebugFile();
|
|
73
|
+
const requestDump: Record<string, unknown> = {
|
|
74
|
+
id,
|
|
75
|
+
protocol: payload.protocol ?? "http",
|
|
76
|
+
method: payload.method,
|
|
77
|
+
url: payload.url,
|
|
78
|
+
};
|
|
79
|
+
const headers = headersToRecord(payload.headers);
|
|
80
|
+
if (headers) requestDump.headers = headers;
|
|
81
|
+
if (payload.body !== undefined) requestDump.body = payload.body;
|
|
82
|
+
if (payload.bodyText !== undefined) requestDump.bodyText = payload.bodyText;
|
|
83
|
+
if (payload.bodyBase64 !== undefined) requestDump.bodyBase64 = payload.bodyBase64;
|
|
84
|
+
if (payload.bodyUnavailable !== undefined) requestDump.bodyUnavailable = payload.bodyUnavailable;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await handle.writeFile(`${JSON.stringify(requestDump, null, 2)}\n`, "utf8");
|
|
88
|
+
} finally {
|
|
89
|
+
await handle.close();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return new FileRequestDebugSession(id, requestPath, responsePath);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function createFetchRequestDebugSession(
|
|
96
|
+
input: string | URL | Request,
|
|
97
|
+
init: RequestInit | undefined,
|
|
98
|
+
): Promise<RequestDebugSession> {
|
|
99
|
+
const headers = resolveRequestHeaders(input, init);
|
|
100
|
+
const body = await snapshotRequestBody(input, init, headers.get("content-type"));
|
|
101
|
+
return createRequestDebugSession({
|
|
102
|
+
method: resolveRequestMethod(input, init),
|
|
103
|
+
url: resolveRequestUrl(input),
|
|
104
|
+
headers,
|
|
105
|
+
...body,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class FileRequestDebugSession implements RequestDebugSession {
|
|
110
|
+
readonly id: number;
|
|
111
|
+
readonly requestPath: string;
|
|
112
|
+
readonly responsePath: string;
|
|
113
|
+
|
|
114
|
+
constructor(id: number, requestPath: string, responsePath: string) {
|
|
115
|
+
this.id = id;
|
|
116
|
+
this.requestPath = requestPath;
|
|
117
|
+
this.responsePath = responsePath;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async openResponseLog(statusLine: string, headers?: RequestDebugHeaders): Promise<RequestDebugResponseLog> {
|
|
121
|
+
const handle = await fs.open(this.responsePath, "wx");
|
|
122
|
+
const headerBlock = formatResponseHeaderBlock(statusLine, headers);
|
|
123
|
+
await handle.write(textEncoder.encode(headerBlock));
|
|
124
|
+
return new FileRequestDebugResponseLog(handle);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async wrapResponse(response: Response): Promise<Response> {
|
|
128
|
+
const log = await this.openResponseLog(`HTTP ${response.status} ${response.statusText}`.trim(), response.headers);
|
|
129
|
+
if (!response.body) {
|
|
130
|
+
await log.close();
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const reader = response.body.getReader();
|
|
135
|
+
const teed = new ReadableStream<Uint8Array>({
|
|
136
|
+
async pull(controller) {
|
|
137
|
+
try {
|
|
138
|
+
const { done, value } = await reader.read();
|
|
139
|
+
if (done) {
|
|
140
|
+
await log.close();
|
|
141
|
+
controller.close();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
log.write(value);
|
|
145
|
+
controller.enqueue(value);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
await log.close().catch(() => undefined);
|
|
148
|
+
controller.error(error);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async cancel(reason) {
|
|
152
|
+
try {
|
|
153
|
+
await reader.cancel(reason);
|
|
154
|
+
} finally {
|
|
155
|
+
await log.close();
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const wrapped = new Response(teed, {
|
|
161
|
+
status: response.status,
|
|
162
|
+
statusText: response.statusText,
|
|
163
|
+
headers: response.headers,
|
|
164
|
+
});
|
|
165
|
+
copyResponseMetadata(wrapped, response);
|
|
166
|
+
return wrapped;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
class FileRequestDebugResponseLog implements RequestDebugResponseLog {
|
|
171
|
+
#handle: fs.FileHandle | undefined;
|
|
172
|
+
#pending: Promise<void> = Promise.resolve();
|
|
173
|
+
|
|
174
|
+
constructor(handle: fs.FileHandle) {
|
|
175
|
+
this.#handle = handle;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
write(chunk: Uint8Array | string): void {
|
|
179
|
+
const handle = this.#handle;
|
|
180
|
+
if (!handle) return;
|
|
181
|
+
const bytes = typeof chunk === "string" ? textEncoder.encode(chunk) : chunk.slice();
|
|
182
|
+
this.#pending = this.#pending.then(async () => {
|
|
183
|
+
await handle.write(bytes);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async close(): Promise<void> {
|
|
188
|
+
const handle = this.#handle;
|
|
189
|
+
if (!handle) return;
|
|
190
|
+
this.#handle = undefined;
|
|
191
|
+
try {
|
|
192
|
+
await this.#pending;
|
|
193
|
+
} finally {
|
|
194
|
+
await handle.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function copyResponseMetadata(target: Response, source: Response): void {
|
|
200
|
+
const sourceUrl = source.url;
|
|
201
|
+
if (!sourceUrl) return;
|
|
202
|
+
try {
|
|
203
|
+
Object.defineProperty(target, "url", { value: sourceUrl, configurable: true });
|
|
204
|
+
} catch {
|
|
205
|
+
// Some runtimes may expose Response.url as non-configurable. The body
|
|
206
|
+
// capture remains correct; callers that need url already tolerate the
|
|
207
|
+
// platform default on other response wrappers in this package.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function reserveRequestDebugFile(): Promise<{
|
|
212
|
+
id: number;
|
|
213
|
+
requestPath: string;
|
|
214
|
+
responsePath: string;
|
|
215
|
+
handle: fs.FileHandle;
|
|
216
|
+
}> {
|
|
217
|
+
for (;;) {
|
|
218
|
+
const id = nextSessionId++;
|
|
219
|
+
const requestPath = `rr-session-${id}.json`;
|
|
220
|
+
try {
|
|
221
|
+
const handle = await fs.open(requestPath, "wx");
|
|
222
|
+
return { id, requestPath, responsePath: `rr-session-${id}.res.log`, handle };
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (isFileExistsError(error)) continue;
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function resolveRequestMethod(input: string | URL | Request, init: RequestInit | undefined): string {
|
|
231
|
+
return (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function resolveRequestUrl(input: string | URL | Request): string {
|
|
235
|
+
return input instanceof Request ? input.url : input.toString();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resolveRequestHeaders(input: string | URL | Request, init: RequestInit | undefined): Headers {
|
|
239
|
+
if (init?.headers) return new Headers(init.headers);
|
|
240
|
+
return input instanceof Request ? new Headers(input.headers) : new Headers();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function snapshotRequestBody(
|
|
244
|
+
input: string | URL | Request,
|
|
245
|
+
init: RequestInit | undefined,
|
|
246
|
+
contentType: string | null,
|
|
247
|
+
): Promise<RequestDebugBody | undefined> {
|
|
248
|
+
if (init?.body !== undefined && init.body !== null) return snapshotBodyInit(init.body, contentType);
|
|
249
|
+
if (input instanceof Request && input.body) {
|
|
250
|
+
return snapshotBytes(new Uint8Array(await input.clone().arrayBuffer()), contentType);
|
|
251
|
+
}
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function snapshotBodyInit(body: RequestBodyInit, contentType: string | null): Promise<RequestDebugBody> {
|
|
256
|
+
if (typeof body === "string") return snapshotText(body, contentType);
|
|
257
|
+
if (body instanceof URLSearchParams) return { bodyText: body.toString() };
|
|
258
|
+
if (body instanceof FormData) return { bodyUnavailable: "FormData" };
|
|
259
|
+
if (body instanceof Blob) return snapshotBytes(new Uint8Array(await body.arrayBuffer()), body.type || contentType);
|
|
260
|
+
if (body instanceof ArrayBuffer) return snapshotBytes(new Uint8Array(body), contentType);
|
|
261
|
+
if (ArrayBuffer.isView(body)) {
|
|
262
|
+
return snapshotBytes(new Uint8Array(body.buffer, body.byteOffset, body.byteLength), contentType);
|
|
263
|
+
}
|
|
264
|
+
if (body instanceof ReadableStream) return { bodyUnavailable: "ReadableStream" };
|
|
265
|
+
return { bodyText: String(body) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function snapshotBytes(bytes: Uint8Array, contentType: string | null): RequestDebugBody {
|
|
269
|
+
try {
|
|
270
|
+
return snapshotText(utf8Decoder.decode(bytes), contentType);
|
|
271
|
+
} catch {
|
|
272
|
+
return { bodyBase64: Buffer.from(bytes).toString("base64") };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function snapshotText(text: string, contentType: string | null): RequestDebugBody {
|
|
277
|
+
if (isJsonContentType(contentType) || looksLikeJson(text)) {
|
|
278
|
+
try {
|
|
279
|
+
return { body: JSON.parse(text) };
|
|
280
|
+
} catch {
|
|
281
|
+
// Fall through to bodyText: malformed JSON is still useful as raw text.
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { bodyText: text };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function isJsonContentType(contentType: string | null): boolean {
|
|
288
|
+
if (!contentType) return false;
|
|
289
|
+
const lower = contentType.toLowerCase();
|
|
290
|
+
return lower.includes("application/json") || lower.includes("+json");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function looksLikeJson(text: string): boolean {
|
|
294
|
+
const trimmed = text.trimStart();
|
|
295
|
+
return trimmed.startsWith("{") || trimmed.startsWith("[");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function formatResponseHeaderBlock(statusLine: string, headers?: RequestDebugHeaders): string {
|
|
299
|
+
const lines = [statusLine];
|
|
300
|
+
const record = headersToRecord(headers);
|
|
301
|
+
if (record) {
|
|
302
|
+
for (const name in record) {
|
|
303
|
+
const value = record[name];
|
|
304
|
+
if (Array.isArray(value)) {
|
|
305
|
+
for (const item of value) lines.push(`${name}: ${item}`);
|
|
306
|
+
} else {
|
|
307
|
+
lines.push(`${name}: ${value}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return `${lines.join("\r\n")}\r\n\r\n`;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function headersToRecord(headers: RequestDebugHeaders): Record<string, string | string[]> | undefined {
|
|
315
|
+
if (!headers) return undefined;
|
|
316
|
+
const record: Record<string, string | string[]> = {};
|
|
317
|
+
let hasHeaders = false;
|
|
318
|
+
if (headers instanceof Headers) {
|
|
319
|
+
headers.forEach((value, key) => {
|
|
320
|
+
hasHeaders = true;
|
|
321
|
+
record[key] = value;
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
for (const key in headers) {
|
|
325
|
+
const value = headers[key];
|
|
326
|
+
if (value === undefined || value === null) continue;
|
|
327
|
+
hasHeaders = true;
|
|
328
|
+
record[key] = Array.isArray(value) ? value.map(String) : String(value);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return hasHeaders ? record : undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isFileExistsError(error: unknown): boolean {
|
|
335
|
+
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "EEXIST";
|
|
336
|
+
}
|