@oh-my-pi/pi-ai 17.0.0 → 17.0.2
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 +28 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +13 -0
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/error/auth-classify.d.ts +2 -0
- package/dist/types/error/rate-limit.d.ts +9 -5
- package/dist/types/providers/cursor.d.ts +2 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +9 -3
- package/dist/types/providers/openai-shared.d.ts +12 -6
- package/dist/types/providers/transform-messages.d.ts +5 -9
- package/dist/types/types.d.ts +6 -1
- package/dist/types/utils/empty-completion-retry.d.ts +3 -3
- package/dist/types/utils/schema/normalize.d.ts +3 -1
- package/package.json +4 -4
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/auth-retry.ts +2 -2
- package/src/auth-storage.ts +120 -79
- package/src/dialect/owned-stream.ts +11 -0
- package/src/dialect/thinking.ts +186 -16
- package/src/error/auth-classify.ts +13 -0
- package/src/error/flags.ts +2 -2
- package/src/error/rate-limit.ts +28 -9
- package/src/providers/__tests__/kimi-code-thinking.test.ts +62 -7
- package/src/providers/anthropic.ts +82 -25
- package/src/providers/cursor.ts +53 -30
- package/src/providers/gitlab-duo-workflow.ts +6 -2
- package/src/providers/kimi.ts +1 -4
- package/src/providers/openai-codex/request-transformer.ts +12 -3
- package/src/providers/openai-codex-responses.ts +22 -4
- package/src/providers/openai-completions.ts +28 -22
- package/src/providers/openai-responses.ts +5 -1
- package/src/providers/openai-shared.ts +66 -14
- package/src/providers/transform-messages.ts +171 -1
- package/src/stream.ts +2 -0
- package/src/types.ts +9 -1
- package/src/utils/empty-completion-retry.ts +6 -3
- package/src/utils/leaked-thinking-stream.ts +19 -1
- package/src/utils/proxy.ts +1 -1
- package/src/utils/schema/CONSTRAINTS.md +1 -0
- package/src/utils/schema/fields.ts +3 -0
- package/src/utils/schema/normalize.ts +94 -24
- package/src/utils.ts +4 -1
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
getOpenAIStreamIdleTimeoutMs,
|
|
61
61
|
iterateWithIdleTimeout,
|
|
62
62
|
} from "../utils/idle-iterator";
|
|
63
|
+
import { getProxyForProvider, shouldBypassProxy } from "../utils/proxy";
|
|
63
64
|
import { createRequestDebugSession, isRequestDebugEnabled, type RequestDebugResponseLog } from "../utils/request-debug";
|
|
64
65
|
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, toolWireSchema } from "../utils/schema";
|
|
65
66
|
import { notifyRawSseEvent } from "../utils/sse-debug";
|
|
@@ -111,7 +112,7 @@ import {
|
|
|
111
112
|
promoteResponsesToolUseStopReason,
|
|
112
113
|
type SequentialCutoffSummaryState,
|
|
113
114
|
} from "./openai-shared";
|
|
114
|
-
import { transformMessages } from "./transform-messages";
|
|
115
|
+
import { redactSensitiveInObject, transformMessages } from "./transform-messages";
|
|
115
116
|
|
|
116
117
|
export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
117
118
|
reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
@@ -1481,6 +1482,7 @@ async function openCodexWebSocketTransport(
|
|
|
1481
1482
|
websocketState,
|
|
1482
1483
|
toWebSocketUrl(requestContext.url),
|
|
1483
1484
|
websocketHeaders,
|
|
1485
|
+
model.provider,
|
|
1484
1486
|
requestSetup.requestSignal,
|
|
1485
1487
|
);
|
|
1486
1488
|
const eventStream = websocketConnection.streamRequest(
|
|
@@ -2614,6 +2616,7 @@ export async function prewarmOpenAICodexResponses(
|
|
|
2614
2616
|
state,
|
|
2615
2617
|
toWebSocketUrl(url),
|
|
2616
2618
|
headers,
|
|
2619
|
+
model.provider,
|
|
2617
2620
|
options?.signal,
|
|
2618
2621
|
);
|
|
2619
2622
|
state.prewarmed = true;
|
|
@@ -3076,11 +3079,13 @@ interface CodexWebSocketRequestTimeouts {
|
|
|
3076
3079
|
|
|
3077
3080
|
interface CodexWebSocketConnectionOptions {
|
|
3078
3081
|
onHandshakeHeaders?: (headers: Headers) => void;
|
|
3082
|
+
proxy?: string;
|
|
3079
3083
|
}
|
|
3080
3084
|
|
|
3081
3085
|
class CodexWebSocketConnection {
|
|
3082
3086
|
#url: string;
|
|
3083
3087
|
#headers: Record<string, string>;
|
|
3088
|
+
#proxy?: string;
|
|
3084
3089
|
#onHandshakeHeaders?: (headers: Headers) => void;
|
|
3085
3090
|
#socket: Bun.WebSocket | null = null;
|
|
3086
3091
|
#queue: Array<Record<string, unknown> | Error | null> = [];
|
|
@@ -3111,6 +3116,7 @@ class CodexWebSocketConnection {
|
|
|
3111
3116
|
constructor(url: string, headers: Record<string, string>, options: CodexWebSocketConnectionOptions) {
|
|
3112
3117
|
this.#url = url;
|
|
3113
3118
|
this.#headers = headers;
|
|
3119
|
+
this.#proxy = options.proxy;
|
|
3114
3120
|
this.#onHandshakeHeaders = options.onHandshakeHeaders;
|
|
3115
3121
|
}
|
|
3116
3122
|
|
|
@@ -3172,7 +3178,7 @@ class CodexWebSocketConnection {
|
|
|
3172
3178
|
this.#connectPromise = promise;
|
|
3173
3179
|
const socket = new (WebSocket as unknown as new (url: string, opts: Bun.WebSocketOptions) => Bun.WebSocket)(
|
|
3174
3180
|
this.#url,
|
|
3175
|
-
{ headers: this.#headers },
|
|
3181
|
+
{ headers: this.#headers, proxy: this.#proxy },
|
|
3176
3182
|
);
|
|
3177
3183
|
socket.binaryType = "nodebuffer";
|
|
3178
3184
|
this.#socket = socket;
|
|
@@ -3663,8 +3669,18 @@ async function getOrCreateCodexWebSocketConnection(
|
|
|
3663
3669
|
state: CodexWebSocketSessionState,
|
|
3664
3670
|
url: string,
|
|
3665
3671
|
headers: Headers,
|
|
3672
|
+
provider: string,
|
|
3666
3673
|
signal?: AbortSignal,
|
|
3667
3674
|
): Promise<CodexWebSocketConnection> {
|
|
3675
|
+
const targetUrl = new URL(url);
|
|
3676
|
+
const proxy = shouldBypassProxy(targetUrl)
|
|
3677
|
+
? undefined
|
|
3678
|
+
: (getProxyForProvider(provider) ??
|
|
3679
|
+
(targetUrl.protocol === "wss:"
|
|
3680
|
+
? Bun.env.HTTPS_PROXY || Bun.env.https_proxy
|
|
3681
|
+
: Bun.env.HTTP_PROXY || Bun.env.http_proxy) ??
|
|
3682
|
+
Bun.env.ALL_PROXY ??
|
|
3683
|
+
Bun.env.all_proxy);
|
|
3668
3684
|
const headerRecord = headersToRecord(headers);
|
|
3669
3685
|
// Join an in-flight handshake instead of tearing it down: closing a
|
|
3670
3686
|
// CONNECTING socket rejects the concurrent caller (prewarm racing the first
|
|
@@ -3707,6 +3723,7 @@ async function getOrCreateCodexWebSocketConnection(
|
|
|
3707
3723
|
onHandshakeHeaders: handshakeHeaders => {
|
|
3708
3724
|
updateCodexSessionMetadataFromHeaders(state, handshakeHeaders);
|
|
3709
3725
|
},
|
|
3726
|
+
proxy,
|
|
3710
3727
|
});
|
|
3711
3728
|
await state.connection.connect(signal);
|
|
3712
3729
|
return state.connection;
|
|
@@ -3937,13 +3954,14 @@ function convertMessages(model: Model<"openai-codex-responses">, context: Contex
|
|
|
3937
3954
|
| Array<ResponseInput[number]>
|
|
3938
3955
|
| undefined;
|
|
3939
3956
|
if (historyItems) {
|
|
3940
|
-
|
|
3957
|
+
const redactedHistoryItems = redactSensitiveInObject(historyItems).result as Array<ResponseInput[number]>;
|
|
3958
|
+
for (const item of redactedHistoryItems) {
|
|
3941
3959
|
const maybe = item as { type?: string; call_id?: string };
|
|
3942
3960
|
if (maybe.type === "custom_tool_call" && typeof maybe.call_id === "string") {
|
|
3943
3961
|
customCallIds.add(maybe.call_id);
|
|
3944
3962
|
}
|
|
3945
3963
|
}
|
|
3946
|
-
messages.push(...
|
|
3964
|
+
messages.push(...redactedHistoryItems);
|
|
3947
3965
|
msgIndex += 1;
|
|
3948
3966
|
continue;
|
|
3949
3967
|
}
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
applyOpenAIExtraBody,
|
|
77
77
|
applyOpenAIGatewayRouting,
|
|
78
78
|
applyOpenAIServiceTier,
|
|
79
|
+
applyOpenRouterReportedCost,
|
|
79
80
|
applyWireModelIdTransform,
|
|
80
81
|
calculateOpenAIUsageAccounting,
|
|
81
82
|
clearOpenAIStrictToolsState,
|
|
@@ -93,9 +94,9 @@ import {
|
|
|
93
94
|
type OpenAIStrictToolsState,
|
|
94
95
|
parseAzureDeploymentNameMap,
|
|
95
96
|
resolveOpenAICompatPolicy,
|
|
97
|
+
resolveOpenAICompletionsOutputClamp,
|
|
96
98
|
resolveOpenAIOutputTokenParam,
|
|
97
99
|
resolveOpenAIRequestSetup,
|
|
98
|
-
resolveZaiReasoningOutputClamp,
|
|
99
100
|
shouldRetryWithoutStrictTools,
|
|
100
101
|
} from "./openai-shared";
|
|
101
102
|
import { transformMessages } from "./transform-messages";
|
|
@@ -1460,31 +1461,35 @@ function buildParams(
|
|
|
1460
1461
|
params.store = false;
|
|
1461
1462
|
}
|
|
1462
1463
|
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1464
|
+
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
|
1465
|
+
// sampling params with a 400 on every serving host (#5606).
|
|
1466
|
+
if (initialCompat.supportsSamplingParams) {
|
|
1467
|
+
if (options?.temperature !== undefined) {
|
|
1468
|
+
params.temperature = options.temperature;
|
|
1469
|
+
}
|
|
1470
|
+
if (options?.topP !== undefined) {
|
|
1471
|
+
params.top_p = options.topP;
|
|
1472
|
+
}
|
|
1473
|
+
if (options?.topK !== undefined) {
|
|
1474
|
+
params.top_k = options.topK;
|
|
1475
|
+
}
|
|
1476
|
+
if (options?.minP !== undefined) {
|
|
1477
|
+
params.min_p = options.minP;
|
|
1478
|
+
}
|
|
1479
|
+
if (options?.presencePenalty !== undefined) {
|
|
1480
|
+
params.presence_penalty = options.presencePenalty;
|
|
1481
|
+
}
|
|
1482
|
+
if (options?.repetitionPenalty !== undefined) {
|
|
1483
|
+
params.repetition_penalty = options.repetitionPenalty;
|
|
1484
|
+
}
|
|
1485
|
+
if (options?.frequencyPenalty !== undefined) {
|
|
1486
|
+
params.frequency_penalty = options.frequencyPenalty;
|
|
1487
|
+
}
|
|
1480
1488
|
}
|
|
1481
1489
|
if (options?.stopSequences?.length) {
|
|
1482
1490
|
const seqs = options.stopSequences;
|
|
1483
1491
|
params.stop = seqs.length === 1 ? seqs[0] : seqs.slice(0, 4);
|
|
1484
1492
|
}
|
|
1485
|
-
if (options?.frequencyPenalty !== undefined) {
|
|
1486
|
-
params.frequency_penalty = options.frequencyPenalty;
|
|
1487
|
-
}
|
|
1488
1493
|
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
|
1489
1494
|
|
|
1490
1495
|
if (context.tools?.length) {
|
|
@@ -1566,7 +1571,7 @@ function buildParams(
|
|
|
1566
1571
|
omitMaxOutputTokens: model.omitMaxOutputTokens ?? false,
|
|
1567
1572
|
isOpenRouterHost: compat.isOpenRouterHost,
|
|
1568
1573
|
alwaysSendMaxTokens: compat.alwaysSendMaxTokens,
|
|
1569
|
-
providerOutputClamp:
|
|
1574
|
+
providerOutputClamp: resolveOpenAICompletionsOutputClamp(model, compat),
|
|
1570
1575
|
});
|
|
1571
1576
|
if (outputToken) {
|
|
1572
1577
|
if (outputToken.field === "max_tokens") {
|
|
@@ -1629,6 +1634,7 @@ export function parseChunkUsage(
|
|
|
1629
1634
|
...(premiumRequests !== undefined ? { premiumRequests } : {}),
|
|
1630
1635
|
};
|
|
1631
1636
|
calculateCost(model, usage);
|
|
1637
|
+
applyOpenRouterReportedCost(model, usage, rawUsage);
|
|
1632
1638
|
return usage;
|
|
1633
1639
|
}
|
|
1634
1640
|
|
|
@@ -594,7 +594,11 @@ const streamOpenAIResponsesOnce = (
|
|
|
594
594
|
error instanceof Error &&
|
|
595
595
|
/previous[ _]?response/i.test(error.message) &&
|
|
596
596
|
/zero[ _-]?data[ _-]?retention/i.test(error.message);
|
|
597
|
-
|
|
597
|
+
const isPromptBlocked =
|
|
598
|
+
error instanceof Error &&
|
|
599
|
+
((error as { code?: string }).code === "invalid_prompt" ||
|
|
600
|
+
/invalid_prompt|Request blocked/i.test(error.message));
|
|
601
|
+
if (!zdrRejection && !isPromptBlocked && !isOpenAIResponsesStalePreviousResponseError(error)) {
|
|
598
602
|
throw error;
|
|
599
603
|
}
|
|
600
604
|
// Server rejected the chain baseline: reset, count the failure (or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Effort } from "@oh-my-pi/pi-catalog/effort";
|
|
2
2
|
import { toFirepassWireModelId, toFireworksWireModelId } from "@oh-my-pi/pi-catalog/fireworks-model-id";
|
|
3
|
-
import { isGlm52ReasoningEffortModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
3
|
+
import { isGlm52ReasoningEffortModelId, isKimiK3ModelId } from "@oh-my-pi/pi-catalog/identity";
|
|
4
4
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
5
5
|
import { calculateCost } from "@oh-my-pi/pi-catalog/models";
|
|
6
6
|
import type {
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
classifyJsonPrefix,
|
|
26
26
|
extractHttpStatusFromError,
|
|
27
27
|
logger,
|
|
28
|
+
parseImageMetadata,
|
|
28
29
|
parseStreamingJson,
|
|
29
30
|
parseStreamingJsonThrottled,
|
|
30
31
|
stringifyJson,
|
|
@@ -338,6 +339,29 @@ export function applyOpenAIResponsesServiceTierCost(
|
|
|
338
339
|
usage.cost.total = usage.cost.input + usage.cost.output + usage.cost.cacheRead + usage.cost.cacheWrite;
|
|
339
340
|
}
|
|
340
341
|
|
|
342
|
+
/** Reconcile token-price estimates with OpenRouter's authoritative account charge. */
|
|
343
|
+
export function applyOpenRouterReportedCost(model: Pick<Model, "provider">, usage: Usage, rawUsage: unknown): void {
|
|
344
|
+
if (model.provider !== "openrouter" || typeof rawUsage !== "object" || rawUsage === null) return;
|
|
345
|
+
const reportedCost = Reflect.get(rawUsage, "cost");
|
|
346
|
+
if (typeof reportedCost !== "number" || !Number.isFinite(reportedCost) || reportedCost < 0) return;
|
|
347
|
+
|
|
348
|
+
const estimatedCost = usage.cost.total;
|
|
349
|
+
if (Number.isFinite(estimatedCost) && estimatedCost > 0) {
|
|
350
|
+
const scale = reportedCost / estimatedCost;
|
|
351
|
+
usage.cost.input *= scale;
|
|
352
|
+
usage.cost.output *= scale;
|
|
353
|
+
usage.cost.cacheRead *= scale;
|
|
354
|
+
usage.cost.cacheWrite *= scale;
|
|
355
|
+
} else {
|
|
356
|
+
// Keep legacy component-only aggregators additive when catalog pricing is unavailable.
|
|
357
|
+
usage.cost.input = reportedCost;
|
|
358
|
+
usage.cost.output = 0;
|
|
359
|
+
usage.cost.cacheRead = 0;
|
|
360
|
+
usage.cost.cacheWrite = 0;
|
|
361
|
+
}
|
|
362
|
+
usage.cost.total = reportedCost;
|
|
363
|
+
}
|
|
364
|
+
|
|
341
365
|
export interface OpenAIUsageAccountingInput {
|
|
342
366
|
promptTokens: number;
|
|
343
367
|
outputTokens: number;
|
|
@@ -1001,16 +1025,24 @@ function isZaiReasoningEffortDialect(model: Model<"openai-completions">, compat:
|
|
|
1001
1025
|
}
|
|
1002
1026
|
|
|
1003
1027
|
/**
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1028
|
+
* Provider-specific Chat Completions output clamp.
|
|
1029
|
+
*
|
|
1030
|
+
* Most OpenAI-compatible endpoints retain the conservative 64k ceiling from
|
|
1031
|
+
* {@link resolveOpenAIOutputTokenParam}. Z.AI/GLM-5.2 reasoning and native
|
|
1032
|
+
* Moonshot K3 explicitly accept their full advertised model caps, so those
|
|
1033
|
+
* routes clamp to `model.maxTokens` instead.
|
|
1008
1034
|
*/
|
|
1009
|
-
export function
|
|
1035
|
+
export function resolveOpenAICompletionsOutputClamp(
|
|
1010
1036
|
model: Model<"openai-completions">,
|
|
1011
1037
|
compat: ResolvedOpenAICompat,
|
|
1012
1038
|
): number | undefined {
|
|
1013
|
-
|
|
1039
|
+
if (isZaiReasoningEffortDialect(model, compat)) {
|
|
1040
|
+
return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
|
|
1041
|
+
}
|
|
1042
|
+
if (model.provider === "moonshot" && isKimiK3ModelId(model.id)) {
|
|
1043
|
+
return model.maxTokens ?? OPENAI_MAX_OUTPUT_TOKENS;
|
|
1044
|
+
}
|
|
1045
|
+
return undefined;
|
|
1014
1046
|
}
|
|
1015
1047
|
|
|
1016
1048
|
/**
|
|
@@ -2532,6 +2564,19 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2532
2564
|
}
|
|
2533
2565
|
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2534
2566
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2567
|
+
} else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
|
|
2568
|
+
const image: ImageContent = {
|
|
2569
|
+
type: "image",
|
|
2570
|
+
data: item.result,
|
|
2571
|
+
mimeType: parseImageMetadata(Buffer.from(item.result, "base64"))?.mimeType ?? "image/png",
|
|
2572
|
+
};
|
|
2573
|
+
output.content.push(image);
|
|
2574
|
+
stream.push({
|
|
2575
|
+
type: "image_end",
|
|
2576
|
+
contentIndex: output.content.length - 1,
|
|
2577
|
+
content: image,
|
|
2578
|
+
partial: output,
|
|
2579
|
+
});
|
|
2535
2580
|
}
|
|
2536
2581
|
} else if (terminalEvent) {
|
|
2537
2582
|
const response = terminalEvent.response;
|
|
@@ -2541,6 +2586,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2541
2586
|
}
|
|
2542
2587
|
populateResponsesUsageFromResponse(output, response?.usage);
|
|
2543
2588
|
calculateCost(model, output.usage);
|
|
2589
|
+
applyOpenRouterReportedCost(model, output.usage, response?.usage);
|
|
2544
2590
|
applyOpenAIResponsesServiceTierCost(
|
|
2545
2591
|
model,
|
|
2546
2592
|
output.usage,
|
|
@@ -2717,7 +2763,9 @@ type CommonSamplingOptions = Pick<
|
|
|
2717
2763
|
export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
|
|
2718
2764
|
params: P,
|
|
2719
2765
|
options: CommonSamplingOptions | undefined,
|
|
2720
|
-
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"
|
|
2766
|
+
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
|
|
2767
|
+
compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
|
|
2768
|
+
},
|
|
2721
2769
|
): void {
|
|
2722
2770
|
if (options?.maxTokens && !model.omitMaxOutputTokens) {
|
|
2723
2771
|
params.max_output_tokens = Math.min(
|
|
@@ -2726,12 +2774,16 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
2726
2774
|
OPENAI_MAX_OUTPUT_TOKENS,
|
|
2727
2775
|
);
|
|
2728
2776
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
if (
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2777
|
+
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
|
2778
|
+
// sampling params with a 400 on every serving host (#5606).
|
|
2779
|
+
if (model.compat.supportsSamplingParams) {
|
|
2780
|
+
if (options?.temperature !== undefined) params.temperature = options.temperature;
|
|
2781
|
+
if (options?.topP !== undefined) params.top_p = options.topP;
|
|
2782
|
+
if (options?.topK !== undefined) params.top_k = options.topK;
|
|
2783
|
+
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
2784
|
+
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
2785
|
+
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
2786
|
+
}
|
|
2735
2787
|
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
|
2736
2788
|
}
|
|
2737
2789
|
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { renderDemotedThinking } from "../dialect/demotion";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Api,
|
|
4
|
+
AssistantMessage,
|
|
5
|
+
DeveloperMessage,
|
|
6
|
+
Message,
|
|
7
|
+
Model,
|
|
8
|
+
ToolCall,
|
|
9
|
+
ToolResultMessage,
|
|
10
|
+
UserMessage,
|
|
11
|
+
} from "../types";
|
|
3
12
|
import { isDemotedThinking, kDemotedThinking } from "../utils/block-symbols";
|
|
4
13
|
|
|
5
14
|
const enum ToolCallStatus {
|
|
@@ -286,6 +295,156 @@ function normalizeAnthropicTargetToolCallId<TApi extends Api>(
|
|
|
286
295
|
* - Preserves tool call structure (unlike converting to text summaries)
|
|
287
296
|
* - Injects synthetic "aborted" tool results
|
|
288
297
|
*/
|
|
298
|
+
const SENSITIVE_TOKEN_RE =
|
|
299
|
+
/(?<![a-zA-Z0-9_*-])(gh[opusr]_[a-zA-Z0-9_*]{36,}|github_pat_[a-zA-Z0-9_*]{36,}|glpat-[a-zA-Z0-9_*-]{20,}|sk-proj-[a-zA-Z0-9_*-]{36,}|sk-ant-[a-zA-Z0-9_*-]{36,}|sk-[a-zA-Z0-9_*-]{48,})(?![a-zA-Z0-9_*-])/gi;
|
|
300
|
+
|
|
301
|
+
function hasPlausibleCredentialEntropy(token: string): boolean {
|
|
302
|
+
const lower = token.toLowerCase();
|
|
303
|
+
const prefixLength = lower.startsWith("github_pat_")
|
|
304
|
+
? "github_pat_".length
|
|
305
|
+
: lower.startsWith("glpat-")
|
|
306
|
+
? "glpat-".length
|
|
307
|
+
: lower.startsWith("sk-proj-")
|
|
308
|
+
? "sk-proj-".length
|
|
309
|
+
: lower.startsWith("sk-ant-")
|
|
310
|
+
? "sk-ant-".length
|
|
311
|
+
: lower.startsWith("gh")
|
|
312
|
+
? 4
|
|
313
|
+
: 3;
|
|
314
|
+
const secret = token.slice(prefixLength);
|
|
315
|
+
if (/^\*+$/.test(secret)) return true;
|
|
316
|
+
return [/[a-z]/, /[A-Z]/, /\d/, /[_-]/].filter(pattern => pattern.test(secret)).length >= 2;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function redactSensitiveCredentials(text: string): string {
|
|
320
|
+
return text.replace(SENSITIVE_TOKEN_RE, match => {
|
|
321
|
+
if (!hasPlausibleCredentialEntropy(match)) return match;
|
|
322
|
+
const lower = match.toLowerCase();
|
|
323
|
+
if (lower.startsWith("gh")) {
|
|
324
|
+
return "[github_token_redacted]";
|
|
325
|
+
}
|
|
326
|
+
if (lower.startsWith("gl")) {
|
|
327
|
+
return "[gitlab_token_redacted]";
|
|
328
|
+
}
|
|
329
|
+
if (lower.startsWith("sk-ant-")) {
|
|
330
|
+
return "[anthropic_token_redacted]";
|
|
331
|
+
}
|
|
332
|
+
if (lower.startsWith("sk")) {
|
|
333
|
+
return "[openai_token_redacted]";
|
|
334
|
+
}
|
|
335
|
+
return "[token_redacted]";
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function redactSensitiveInObject(val: unknown): { result: unknown; changed: boolean } {
|
|
340
|
+
if (typeof val === "string") {
|
|
341
|
+
const redacted = redactSensitiveCredentials(val);
|
|
342
|
+
return { result: redacted, changed: redacted !== val };
|
|
343
|
+
}
|
|
344
|
+
if (Array.isArray(val)) {
|
|
345
|
+
let changed = false;
|
|
346
|
+
const result = val.map(item => {
|
|
347
|
+
const res = redactSensitiveInObject(item);
|
|
348
|
+
if (res.changed) changed = true;
|
|
349
|
+
return res.result;
|
|
350
|
+
});
|
|
351
|
+
return { result, changed };
|
|
352
|
+
}
|
|
353
|
+
if (val !== null && typeof val === "object") {
|
|
354
|
+
let changed = false;
|
|
355
|
+
const res: Record<string, unknown> = {};
|
|
356
|
+
for (const [k, v] of Object.entries(val)) {
|
|
357
|
+
const sub = redactSensitiveInObject(v);
|
|
358
|
+
if (sub.changed) changed = true;
|
|
359
|
+
res[k] = sub.result;
|
|
360
|
+
}
|
|
361
|
+
return { result: res, changed };
|
|
362
|
+
}
|
|
363
|
+
return { result: val, changed: false };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function redactSensitiveCredentialsInMessages(messages: Message[]): Message[] {
|
|
367
|
+
return messages.map((msg): Message => {
|
|
368
|
+
if (msg.role === "user" || msg.role === "developer") {
|
|
369
|
+
const userMsg = msg as UserMessage | DeveloperMessage;
|
|
370
|
+
if (typeof userMsg.content === "string") {
|
|
371
|
+
const redacted = redactSensitiveCredentials(userMsg.content);
|
|
372
|
+
if (redacted === userMsg.content) return msg;
|
|
373
|
+
return { ...userMsg, content: redacted } as Message;
|
|
374
|
+
}
|
|
375
|
+
const contentArray = userMsg.content;
|
|
376
|
+
let changed = false;
|
|
377
|
+
const content = contentArray.map((block): UserMessage["content"][number] => {
|
|
378
|
+
if (block.type === "text") {
|
|
379
|
+
const redacted = redactSensitiveCredentials(block.text);
|
|
380
|
+
if (redacted !== block.text) {
|
|
381
|
+
changed = true;
|
|
382
|
+
return { ...block, text: redacted };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return block;
|
|
386
|
+
});
|
|
387
|
+
return (changed ? { ...userMsg, content } : userMsg) as Message;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (msg.role === "toolResult") {
|
|
391
|
+
const toolResultMsg = msg as ToolResultMessage;
|
|
392
|
+
let changed = false;
|
|
393
|
+
const content = toolResultMsg.content.map((block): ToolResultMessage["content"][number] => {
|
|
394
|
+
if (block.type === "text") {
|
|
395
|
+
const redacted = redactSensitiveCredentials(block.text);
|
|
396
|
+
if (redacted !== block.text) {
|
|
397
|
+
changed = true;
|
|
398
|
+
return { ...block, text: redacted };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return block;
|
|
402
|
+
});
|
|
403
|
+
return (changed ? { ...toolResultMsg, content } : toolResultMsg) as Message;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (msg.role === "assistant") {
|
|
407
|
+
const assistantMsg = msg as AssistantMessage;
|
|
408
|
+
let changed = false;
|
|
409
|
+
const content = assistantMsg.content.map((block): AssistantMessage["content"][number] => {
|
|
410
|
+
if (block.type === "text") {
|
|
411
|
+
const redacted = redactSensitiveCredentials(block.text);
|
|
412
|
+
if (redacted !== block.text) {
|
|
413
|
+
changed = true;
|
|
414
|
+
return { ...block, text: redacted };
|
|
415
|
+
}
|
|
416
|
+
} else if (block.type === "thinking") {
|
|
417
|
+
const redacted = redactSensitiveCredentials(block.thinking);
|
|
418
|
+
if (redacted !== block.thinking) {
|
|
419
|
+
changed = true;
|
|
420
|
+
return { ...block, thinking: redacted, thinkingSignature: undefined };
|
|
421
|
+
}
|
|
422
|
+
} else if (block.type === "toolCall") {
|
|
423
|
+
if (block.arguments) {
|
|
424
|
+
const { result: redactedArgs, changed: argsChanged } = redactSensitiveInObject(block.arguments);
|
|
425
|
+
if (argsChanged) {
|
|
426
|
+
changed = true;
|
|
427
|
+
const castArgs =
|
|
428
|
+
redactedArgs && typeof redactedArgs === "object" && !Array.isArray(redactedArgs)
|
|
429
|
+
? (redactedArgs as Record<string, unknown>)
|
|
430
|
+
: undefined;
|
|
431
|
+
return {
|
|
432
|
+
...block,
|
|
433
|
+
arguments: castArgs,
|
|
434
|
+
thoughtSignature: undefined,
|
|
435
|
+
} as AssistantMessage["content"][number];
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return block;
|
|
440
|
+
});
|
|
441
|
+
return (changed ? { ...assistantMsg, content } : assistantMsg) as Message;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return msg;
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
289
448
|
export function transformMessages<TApi extends Api>(
|
|
290
449
|
messages: Message[],
|
|
291
450
|
model: Model<TApi>,
|
|
@@ -294,6 +453,10 @@ export function transformMessages<TApi extends Api>(
|
|
|
294
453
|
duplicateToolCallIdSuffixPrefix = "_dup",
|
|
295
454
|
targetCompat: Model<TApi>["compat"] = model.compat,
|
|
296
455
|
): Message[] {
|
|
456
|
+
// Redact sensitive credential-like patterns from all outbound messages
|
|
457
|
+
// to prevent security block errors from LLM providers (e.g. invalid_prompt).
|
|
458
|
+
messages = redactSensitiveCredentialsInMessages(messages);
|
|
459
|
+
|
|
297
460
|
// Drop assistant `toolCall` blocks with empty/whitespace `id` or `name`
|
|
298
461
|
// (and their matched `toolResult` messages) before anything else looks at
|
|
299
462
|
// the history. Replays of these would 400 every provider — see
|
|
@@ -554,6 +717,13 @@ export function transformMessages<TApi extends Api>(
|
|
|
554
717
|
return [];
|
|
555
718
|
}
|
|
556
719
|
|
|
720
|
+
if (block.type === "image") {
|
|
721
|
+
// Assistant images are display artifacts. No provider accepts them
|
|
722
|
+
// in an assistant replay turn; the native Responses result remains
|
|
723
|
+
// in providerPayload for OpenAI replay.
|
|
724
|
+
return [];
|
|
725
|
+
}
|
|
726
|
+
|
|
557
727
|
if (block.type === "text") {
|
|
558
728
|
if (isSameModel) return block;
|
|
559
729
|
return {
|
package/src/stream.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { getCustomApi } from "./api-registry";
|
|
|
20
20
|
import { createAuthRetryKeyState, isApiKeyResolver, resolveNextAuthRetryKey } from "./auth-retry";
|
|
21
21
|
import * as AIError from "./error";
|
|
22
22
|
import { ProviderHttpError } from "./error";
|
|
23
|
+
import { isInvalidatedOAuthTokenError } from "./error/auth-classify";
|
|
23
24
|
import { isUsageLimitOutcome } from "./error/rate-limit";
|
|
24
25
|
import type { BedrockOptions } from "./providers/amazon-bedrock";
|
|
25
26
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
@@ -979,6 +980,7 @@ function isRetryableUpstreamError(error: unknown, status: number | undefined, me
|
|
|
979
980
|
// `parseRateLimitReason` and stay in the provider's own backoff layer
|
|
980
981
|
// instead of burning siblings.
|
|
981
982
|
if (AIError.isUsageLimit(error)) return true;
|
|
983
|
+
if (isInvalidatedOAuthTokenError(error)) return true;
|
|
982
984
|
if (status === 401) return true;
|
|
983
985
|
return isUsageLimitOutcome(status, message);
|
|
984
986
|
}
|
package/src/types.ts
CHANGED
|
@@ -706,7 +706,14 @@ export interface ContextSnapshot {
|
|
|
706
706
|
|
|
707
707
|
export interface AssistantMessage {
|
|
708
708
|
role: "assistant";
|
|
709
|
-
content: (
|
|
709
|
+
content: (
|
|
710
|
+
| TextContent
|
|
711
|
+
| ThinkingContent
|
|
712
|
+
| RedactedThinkingContent
|
|
713
|
+
| AnthropicFallbackContent
|
|
714
|
+
| ImageContent
|
|
715
|
+
| ToolCall
|
|
716
|
+
)[];
|
|
710
717
|
api: Api;
|
|
711
718
|
provider: Provider;
|
|
712
719
|
model: string;
|
|
@@ -893,6 +900,7 @@ export type AssistantMessageEvent =
|
|
|
893
900
|
| { type: "thinking_start"; contentIndex: number; partial: AssistantMessage }
|
|
894
901
|
| { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
895
902
|
| { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage }
|
|
903
|
+
| { type: "image_end"; contentIndex: number; content: ImageContent; partial: AssistantMessage }
|
|
896
904
|
| { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage }
|
|
897
905
|
| { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage }
|
|
898
906
|
| { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage }
|
|
@@ -27,12 +27,13 @@ export const EMPTY_COMPLETION_BASE_DELAY_MS = 500;
|
|
|
27
27
|
const NON_WHITESPACE_RE = /\S/;
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Whether a completed assistant message carries content worth delivering:
|
|
31
|
-
* call or any non-whitespace text. An empty/whitespace-only message
|
|
32
|
-
* that only ever produced thinking — is the "empty response" failure.
|
|
30
|
+
* Whether a completed assistant message carries content worth delivering: an
|
|
31
|
+
* image, tool call, or any non-whitespace text. An empty/whitespace-only message
|
|
32
|
+
* — or one that only ever produced thinking — is the "empty response" failure.
|
|
33
33
|
*/
|
|
34
34
|
export function hasVisibleAssistantContent(message: AssistantMessage): boolean {
|
|
35
35
|
for (const block of message.content) {
|
|
36
|
+
if (block.type === "image") return true;
|
|
36
37
|
if (block.type === "toolCall") return true;
|
|
37
38
|
if (block.type === "text" && NON_WHITESPACE_RE.test(block.text)) return true;
|
|
38
39
|
}
|
|
@@ -49,6 +50,8 @@ function isMeaningfulCompletionEvent(event: AssistantMessageEvent): boolean {
|
|
|
49
50
|
case "text_end":
|
|
50
51
|
case "thinking_end":
|
|
51
52
|
return event.content.length > 0;
|
|
53
|
+
case "image_end":
|
|
54
|
+
return true;
|
|
52
55
|
case "toolcall_start":
|
|
53
56
|
case "toolcall_end":
|
|
54
57
|
return true;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* events are forwarded verbatim.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import type { AssistantMessage, TextContent, ThinkingContent, ToolCall } from "../types";
|
|
28
|
+
import type { AssistantMessage, ImageContent, TextContent, ThinkingContent, ToolCall } from "../types";
|
|
29
29
|
import {
|
|
30
30
|
clearStreamingPartialJson,
|
|
31
31
|
getStreamingPartialJson,
|
|
@@ -78,6 +78,10 @@ export function wrapLeakedThinkingStream(inner: AssistantMessageEventStream): As
|
|
|
78
78
|
projector.thinking(event.delta, block?.type === "thinking" ? block.thinkingSignature : undefined);
|
|
79
79
|
break;
|
|
80
80
|
}
|
|
81
|
+
case "image_end":
|
|
82
|
+
projector ??= new LeakedThinkingProjector(out, event.partial);
|
|
83
|
+
projector.image(event.content);
|
|
84
|
+
break;
|
|
81
85
|
case "toolcall_start": {
|
|
82
86
|
projector ??= new LeakedThinkingProjector(out, event.partial);
|
|
83
87
|
const block = event.partial.content[event.contentIndex];
|
|
@@ -163,6 +167,20 @@ class LeakedThinkingProjector {
|
|
|
163
167
|
this.#out.push({ type: "thinking_delta", contentIndex: index, delta, partial: this.#partial });
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
/** Forward a completed native image after releasing held text. */
|
|
171
|
+
image(content: ImageContent): void {
|
|
172
|
+
this.#apply(this.#healer.flushEvents(), this.#lastTextSignature);
|
|
173
|
+
this.#closeText();
|
|
174
|
+
this.#closeThinking();
|
|
175
|
+
this.#partial.content.push(content);
|
|
176
|
+
this.#out.push({
|
|
177
|
+
type: "image_end",
|
|
178
|
+
contentIndex: this.#partial.content.length - 1,
|
|
179
|
+
content,
|
|
180
|
+
partial: this.#partial,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
/** Forward a native tool call's start, releasing any held-back text first. */
|
|
167
185
|
toolStart(srcIndex: number, source: StreamingToolCall | undefined): void {
|
|
168
186
|
if (!source) return;
|
package/src/utils/proxy.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function shouldBypassProxy(urlObj: URL): boolean {
|
|
|
61
61
|
.map(r => r.trim())
|
|
62
62
|
.filter(Boolean);
|
|
63
63
|
const targetHost = urlObj.hostname.toLowerCase();
|
|
64
|
-
const targetPort = urlObj.port || (urlObj.protocol === "https:" ? "443" : "80");
|
|
64
|
+
const targetPort = urlObj.port || (urlObj.protocol === "https:" || urlObj.protocol === "wss:" ? "443" : "80");
|
|
65
65
|
|
|
66
66
|
for (const rule of rules) {
|
|
67
67
|
if (rule === "*") {
|
|
@@ -69,6 +69,7 @@ Schemas sent on the Google JSON Schema path MUST follow:
|
|
|
69
69
|
- `minItems`, `maxItems`, `minLength`, `maxLength`
|
|
70
70
|
- `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
|
|
71
71
|
- `pattern`, `format`
|
|
72
|
+
- `dependencies`, `dependentSchemas`, `dependentRequired`
|
|
72
73
|
- Important: keys inside a `properties` object are treated as property names and MUST NOT be stripped by keyword match.
|
|
73
74
|
- Human-meaningful stripped keys (`pattern`, `format`, min/max constraints, `default`, `examples`, etc.) are appended to the sibling `description` as an Anthropic-style spill block: `{pattern: "^foo$", minimum: 0}`. Structural/meta keys such as `$ref`, `$defs`, and `additionalProperties` are not spilled.
|
|
74
75
|
|