@oh-my-pi/pi-ai 17.0.0 → 17.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/error/auth-classify.d.ts +2 -0
- package/dist/types/providers/openai-shared.d.ts +5 -1
- 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-retry.ts +2 -2
- package/src/auth-storage.ts +120 -79
- package/src/dialect/owned-stream.ts +11 -0
- package/src/error/auth-classify.ts +13 -0
- package/src/error/rate-limit.ts +16 -1
- package/src/providers/anthropic.ts +82 -25
- package/src/providers/openai-codex-responses.ts +18 -1
- package/src/providers/openai-completions.ts +26 -20
- package/src/providers/openai-shared.ts +51 -7
- package/src/providers/transform-messages.ts +7 -0
- 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
|
@@ -1167,6 +1167,25 @@ function resolveAnthropicBaseUrl(model: Model<"anthropic-messages">, apiKey?: st
|
|
|
1167
1167
|
return normalizeAnthropicBaseUrl(model.baseUrl);
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
|
+
function resolveEagerToolInputStreamingSupport(
|
|
1171
|
+
model: Model<"anthropic-messages">,
|
|
1172
|
+
effectiveBaseUrl: string | undefined,
|
|
1173
|
+
): boolean {
|
|
1174
|
+
if (!model.compat.supportsEagerToolInputStreaming) return false;
|
|
1175
|
+
// First-party Anthropic endpoints accept the per-tool flag.
|
|
1176
|
+
if (isOfficialAnthropicApiUrl(effectiveBaseUrl)) return true;
|
|
1177
|
+
// Non-official effective endpoint. `supportsEagerToolInputStreaming` may be
|
|
1178
|
+
// stale-true here because compat is materialized once at build time and is
|
|
1179
|
+
// never rebuilt for a baseUrl-only reroute — either a runtime provider
|
|
1180
|
+
// override (`pi.registerProvider("anthropic", { baseUrl })`) or Foundry
|
|
1181
|
+
// (`CLAUDE_CODE_USE_FOUNDRY`). Both leave the canonical model's resolved
|
|
1182
|
+
// compat in place. `officialEndpoint` records whether compat was built for
|
|
1183
|
+
// the canonical Anthropic URL, so only endpoints whose compat was authored
|
|
1184
|
+
// for a non-official host (an explicit `compat.supportsEagerToolInputStreaming`
|
|
1185
|
+
// opt-in on a custom `baseUrl`) still send the field.
|
|
1186
|
+
return !model.compat.officialEndpoint;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1170
1189
|
function parseAnthropicCustomHeaders(rawHeaders: string | undefined): Record<string, string> | undefined {
|
|
1171
1190
|
const source = rawHeaders?.trim();
|
|
1172
1191
|
if (!source) return undefined;
|
|
@@ -1741,6 +1760,7 @@ const streamAnthropicOnce = (
|
|
|
1741
1760
|
}
|
|
1742
1761
|
const apiKey = options?.apiKey ?? getEnvApiKey(model.provider) ?? "";
|
|
1743
1762
|
const baseUrl = resolveAnthropicBaseUrl(model, apiKey) ?? "https://api.anthropic.com";
|
|
1763
|
+
const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
|
|
1744
1764
|
const providerSessionState = getAnthropicProviderSessionState(
|
|
1745
1765
|
options?.providerSessionState,
|
|
1746
1766
|
baseUrl,
|
|
@@ -1752,6 +1772,23 @@ const streamAnthropicOnce = (
|
|
|
1752
1772
|
let forceDemoteUnsignedThinking = providerSessionState?.replayUnsignedThinkingDisabled ?? false;
|
|
1753
1773
|
const mergedCallerHeaders = mergeHeaders(model.headers, options?.headers);
|
|
1754
1774
|
const umansGatewayWebSearchHeader = getUmansWebSearchHeader(model, mergedCallerHeaders);
|
|
1775
|
+
// Keep fallback payloads aligned with the top-level Vertex effort gate:
|
|
1776
|
+
// no nested effort field means the fallback scan cannot re-add its beta.
|
|
1777
|
+
let fallbacks = options?.fallbacks;
|
|
1778
|
+
if (
|
|
1779
|
+
model.provider === "google-vertex" &&
|
|
1780
|
+
fallbacks?.some(entry => entry.output_config?.effort !== undefined)
|
|
1781
|
+
) {
|
|
1782
|
+
fallbacks = fallbacks.map(entry => {
|
|
1783
|
+
const outputConfig = entry.output_config;
|
|
1784
|
+
if (outputConfig?.effort === undefined) return entry;
|
|
1785
|
+
return {
|
|
1786
|
+
...entry,
|
|
1787
|
+
output_config:
|
|
1788
|
+
outputConfig.task_budget === undefined ? undefined : { task_budget: outputConfig.task_budget },
|
|
1789
|
+
};
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1755
1792
|
|
|
1756
1793
|
let client: AnthropicMessagesClientLike;
|
|
1757
1794
|
let isOAuthToken: boolean;
|
|
@@ -1776,6 +1813,10 @@ const streamAnthropicOnce = (
|
|
|
1776
1813
|
// the toggle cannot 400); the beta must accompany the field in both.
|
|
1777
1814
|
// MiniMax uses `thinking.type:"adaptive"` itself as the control surface,
|
|
1778
1815
|
// so the sentinel "adaptive" value intentionally sends no output_config.
|
|
1816
|
+
// Skip Vertex rawPredict: that adapter needs betas in the body
|
|
1817
|
+
// (`anthropic_beta`), not as an `anthropic-beta` HTTP header, so the
|
|
1818
|
+
// effort field is dropped from the body there too (see buildParams) and
|
|
1819
|
+
// advertising the beta would only earn a 400 (#5614).
|
|
1779
1820
|
const sendsAdaptiveEffortPin =
|
|
1780
1821
|
options?.thinkingEnabled === false &&
|
|
1781
1822
|
model.thinking?.mode === "anthropic-adaptive" &&
|
|
@@ -1783,6 +1824,7 @@ const streamAnthropicOnce = (
|
|
|
1783
1824
|
!usesAdaptiveThinkingTagOnly(model);
|
|
1784
1825
|
if (
|
|
1785
1826
|
model.reasoning &&
|
|
1827
|
+
model.provider !== "google-vertex" &&
|
|
1786
1828
|
((options?.thinkingEnabled && options.effort !== "adaptive") || sendsAdaptiveEffortPin) &&
|
|
1787
1829
|
!extraBetas.includes(effortBeta)
|
|
1788
1830
|
) {
|
|
@@ -1816,11 +1858,11 @@ const streamAnthropicOnce = (
|
|
|
1816
1858
|
// `output_config.task_budget`) reuse the same top-level betas
|
|
1817
1859
|
// Anthropic requires for the primary request, so scan the chain
|
|
1818
1860
|
// and add every companion beta the fallback entries touch.
|
|
1819
|
-
if (
|
|
1861
|
+
if (fallbacks?.length) {
|
|
1820
1862
|
if (!extraBetas.includes(serverSideFallbackBeta)) {
|
|
1821
1863
|
extraBetas.push(serverSideFallbackBeta);
|
|
1822
1864
|
}
|
|
1823
|
-
for (const entry of
|
|
1865
|
+
for (const entry of fallbacks) {
|
|
1824
1866
|
if (entry.speed === "fast" && !extraBetas.includes(fastModeBeta)) {
|
|
1825
1867
|
extraBetas.push(fastModeBeta);
|
|
1826
1868
|
}
|
|
@@ -1854,15 +1896,13 @@ const streamAnthropicOnce = (
|
|
|
1854
1896
|
}
|
|
1855
1897
|
const preparedContext = await prepareAnthropicManyImageContext(context, model.input.includes("image"));
|
|
1856
1898
|
const prepareParams = async (): Promise<MessageCreateParamsStreaming> => {
|
|
1857
|
-
let nextParams = buildParams(
|
|
1858
|
-
model,
|
|
1859
|
-
preparedContext,
|
|
1860
|
-
isOAuthToken,
|
|
1861
|
-
options,
|
|
1899
|
+
let nextParams = buildParams(model, preparedContext, isOAuthToken, options, {
|
|
1862
1900
|
disableStrictTools,
|
|
1863
|
-
umansGatewayWebSearchHeader !== undefined,
|
|
1901
|
+
useUmansGatewayWebSearch: umansGatewayWebSearchHeader !== undefined,
|
|
1864
1902
|
forceDemoteUnsignedThinking,
|
|
1865
|
-
|
|
1903
|
+
supportsEagerToolInputStreaming,
|
|
1904
|
+
fallbacks,
|
|
1905
|
+
});
|
|
1866
1906
|
if (disableStrictTools) {
|
|
1867
1907
|
dropAnthropicStrictTools(nextParams);
|
|
1868
1908
|
}
|
|
@@ -1888,9 +1928,9 @@ const streamAnthropicOnce = (
|
|
|
1888
1928
|
|
|
1889
1929
|
// Opt-in flag: the response parser only honors `fallback` content
|
|
1890
1930
|
// blocks and `usage.iterations` when the current request opted into
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
const serverSideFallback = !!
|
|
1931
|
+
// server-side-fallback beta chain. Leaving `fallbacks` unset preserves
|
|
1932
|
+
// the pre-fallback stream shape on every event.
|
|
1933
|
+
const serverSideFallback = !!fallbacks?.length;
|
|
1894
1934
|
type Block = (
|
|
1895
1935
|
| ThinkingContent
|
|
1896
1936
|
| RedactedThinkingContent
|
|
@@ -2682,9 +2722,11 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2682
2722
|
const compat = model.compat;
|
|
2683
2723
|
const disableStrictTools = disableStrictToolsOverride ?? compat.disableStrictTools;
|
|
2684
2724
|
const needsInterleavedBeta = interleavedThinking && !model.thinking?.supportsDisplay;
|
|
2685
|
-
const needsFineGrainedToolStreamingBeta = hasTools && !compat.supportsEagerToolInputStreaming;
|
|
2686
2725
|
const oauthToken = isOAuth ?? isAnthropicOAuthToken(apiKey);
|
|
2687
2726
|
const baseUrl = resolveAnthropicBaseUrl(model, apiKey);
|
|
2727
|
+
const supportsEagerToolInputStreaming = resolveEagerToolInputStreamingSupport(model, baseUrl);
|
|
2728
|
+
const needsFineGrainedToolStreamingBeta =
|
|
2729
|
+
hasTools && isOfficialAnthropicApiUrl(baseUrl) && !supportsEagerToolInputStreaming;
|
|
2688
2730
|
const foundryCustomHeaders = resolveAnthropicCustomHeaders(model);
|
|
2689
2731
|
const tlsFetchOptions = buildClaudeCodeTlsFetchOptions(model, baseUrl);
|
|
2690
2732
|
// Disable Bun's native ~300s pre-response fetch timeout (issue #2422).
|
|
@@ -2699,9 +2741,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
|
|
|
2699
2741
|
if (model.provider === "github-copilot") {
|
|
2700
2742
|
const copilotApiKey = parseGitHubCopilotApiKey(apiKey).accessToken;
|
|
2701
2743
|
// The GitHub Copilot Anthropic proxy doesn't accept Anthropic beta
|
|
2702
|
-
// features
|
|
2703
|
-
// = false` for this host, so `needsFineGrainedToolStreamingBeta` is true
|
|
2704
|
-
// whenever tools are present). Forward only caller-supplied betas.
|
|
2744
|
+
// features. Forward only caller-supplied betas.
|
|
2705
2745
|
const betaFeatures = [...extraBetas];
|
|
2706
2746
|
const defaultHeaders = mergeHeaders(
|
|
2707
2747
|
{
|
|
@@ -3129,15 +3169,29 @@ function extractClaudeCodeFirstUserMessageText(messages: readonly Message[]): st
|
|
|
3129
3169
|
return "";
|
|
3130
3170
|
}
|
|
3131
3171
|
|
|
3172
|
+
type AnthropicParamBuildOptions = {
|
|
3173
|
+
disableStrictTools: boolean;
|
|
3174
|
+
useUmansGatewayWebSearch: boolean;
|
|
3175
|
+
forceDemoteUnsignedThinking: boolean;
|
|
3176
|
+
supportsEagerToolInputStreaming: boolean;
|
|
3177
|
+
/** Sanitized server-side fallback entries; defaults to `options?.fallbacks` when omitted. */
|
|
3178
|
+
fallbacks?: AnthropicOptions["fallbacks"];
|
|
3179
|
+
};
|
|
3180
|
+
|
|
3132
3181
|
function buildParams(
|
|
3133
3182
|
model: Model<"anthropic-messages">,
|
|
3134
3183
|
context: Context,
|
|
3135
3184
|
isOAuthToken: boolean,
|
|
3136
|
-
options
|
|
3137
|
-
|
|
3138
|
-
useUmansGatewayWebSearch = false,
|
|
3139
|
-
forceDemoteUnsignedThinking = false,
|
|
3185
|
+
options: AnthropicOptions | undefined,
|
|
3186
|
+
buildOptions: AnthropicParamBuildOptions,
|
|
3140
3187
|
): MessageCreateParamsStreaming {
|
|
3188
|
+
const {
|
|
3189
|
+
disableStrictTools,
|
|
3190
|
+
useUmansGatewayWebSearch,
|
|
3191
|
+
forceDemoteUnsignedThinking,
|
|
3192
|
+
supportsEagerToolInputStreaming,
|
|
3193
|
+
fallbacks = options?.fallbacks,
|
|
3194
|
+
} = buildOptions;
|
|
3141
3195
|
// A session-scoped auto-demote (learned from a live signing 400) clones the
|
|
3142
3196
|
// resolved compat with `replayUnsignedThinking: false` so every subsequent
|
|
3143
3197
|
// downstream read (convertAnthropicMessages, transformMessages) sees the
|
|
@@ -3165,7 +3219,7 @@ function buildParams(
|
|
|
3165
3219
|
context.tools,
|
|
3166
3220
|
isOAuthToken,
|
|
3167
3221
|
disableStrictTools || model.provider === "github-copilot",
|
|
3168
|
-
|
|
3222
|
+
supportsEagerToolInputStreaming,
|
|
3169
3223
|
model.compat.escapeBuiltinToolNames,
|
|
3170
3224
|
useUmansGatewayWebSearch,
|
|
3171
3225
|
);
|
|
@@ -3255,9 +3309,12 @@ function buildParams(
|
|
|
3255
3309
|
? { edits: [{ type: "clear_thinking_20251015" as const, keep: "all" as const }] }
|
|
3256
3310
|
: undefined;
|
|
3257
3311
|
|
|
3258
|
-
// Pre-compute output_config.
|
|
3312
|
+
// Pre-compute output_config. Skip `effort` on Vertex rawPredict: it requires
|
|
3313
|
+
// the `effort-2025-11-24` beta, which that adapter can only accept in the body
|
|
3314
|
+
// (`anthropic_beta`), never as the `anthropic-beta` HTTP header this path sets
|
|
3315
|
+
// — so the field is dropped alongside the beta to avoid a 400 (#5614).
|
|
3259
3316
|
const outputConfigEntries: AnthropicOutputConfig = {};
|
|
3260
|
-
if (outputConfigEffort) outputConfigEntries.effort = outputConfigEffort;
|
|
3317
|
+
if (outputConfigEffort && model.provider !== "google-vertex") outputConfigEntries.effort = outputConfigEffort;
|
|
3261
3318
|
if (options?.taskBudget) outputConfigEntries.task_budget = options.taskBudget;
|
|
3262
3319
|
const outputConfig = Object.keys(outputConfigEntries).length ? outputConfigEntries : undefined;
|
|
3263
3320
|
|
|
@@ -3272,7 +3329,7 @@ function buildParams(
|
|
|
3272
3329
|
const params: MessageCreateParamsStreaming = {
|
|
3273
3330
|
model: options?.requestModelId ?? model.requestModelId ?? model.id,
|
|
3274
3331
|
messages: convertAnthropicMessages(context.messages, effectiveModel, isOAuthToken, {
|
|
3275
|
-
serverSideFallbackEnabled: !!
|
|
3332
|
+
serverSideFallbackEnabled: !!fallbacks?.length,
|
|
3276
3333
|
}),
|
|
3277
3334
|
...(systemBlocks && { system: systemBlocks }),
|
|
3278
3335
|
...(tools !== undefined && { tools }),
|
|
@@ -3281,7 +3338,7 @@ function buildParams(
|
|
|
3281
3338
|
...(thinking && { thinking }),
|
|
3282
3339
|
...(contextManagement && { context_management: contextManagement }),
|
|
3283
3340
|
...(outputConfig && { output_config: outputConfig }),
|
|
3284
|
-
...(
|
|
3341
|
+
...(fallbacks?.length ? { fallbacks } : {}),
|
|
3285
3342
|
stream: true,
|
|
3286
3343
|
};
|
|
3287
3344
|
|
|
@@ -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";
|
|
@@ -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;
|
|
@@ -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,
|
|
@@ -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) {
|
|
@@ -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
|
|
|
@@ -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;
|
|
@@ -2532,6 +2556,19 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2532
2556
|
}
|
|
2533
2557
|
closeOpenItem(event.output_index, item.id, entry, item.call_id, prefixedFunctionCallItemKey(item.call_id));
|
|
2534
2558
|
stream.push({ type: "toolcall_end", contentIndex, toolCall, partial: output });
|
|
2559
|
+
} else if (item.type === "image_generation_call" && item.status === "completed" && item.result) {
|
|
2560
|
+
const image: ImageContent = {
|
|
2561
|
+
type: "image",
|
|
2562
|
+
data: item.result,
|
|
2563
|
+
mimeType: parseImageMetadata(Buffer.from(item.result, "base64"))?.mimeType ?? "image/png",
|
|
2564
|
+
};
|
|
2565
|
+
output.content.push(image);
|
|
2566
|
+
stream.push({
|
|
2567
|
+
type: "image_end",
|
|
2568
|
+
contentIndex: output.content.length - 1,
|
|
2569
|
+
content: image,
|
|
2570
|
+
partial: output,
|
|
2571
|
+
});
|
|
2535
2572
|
}
|
|
2536
2573
|
} else if (terminalEvent) {
|
|
2537
2574
|
const response = terminalEvent.response;
|
|
@@ -2541,6 +2578,7 @@ export async function processResponsesStream<TApi extends Api>(
|
|
|
2541
2578
|
}
|
|
2542
2579
|
populateResponsesUsageFromResponse(output, response?.usage);
|
|
2543
2580
|
calculateCost(model, output.usage);
|
|
2581
|
+
applyOpenRouterReportedCost(model, output.usage, response?.usage);
|
|
2544
2582
|
applyOpenAIResponsesServiceTierCost(
|
|
2545
2583
|
model,
|
|
2546
2584
|
output.usage,
|
|
@@ -2717,7 +2755,9 @@ type CommonSamplingOptions = Pick<
|
|
|
2717
2755
|
export function applyCommonResponsesSamplingParams<P extends CommonResponsesParams>(
|
|
2718
2756
|
params: P,
|
|
2719
2757
|
options: CommonSamplingOptions | undefined,
|
|
2720
|
-
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"
|
|
2758
|
+
model: Pick<Model, "provider" | "api" | "id" | "omitMaxOutputTokens" | "maxTokens"> & {
|
|
2759
|
+
compat: Pick<ResolvedOpenAISharedCompat, "supportsSamplingParams">;
|
|
2760
|
+
},
|
|
2721
2761
|
): void {
|
|
2722
2762
|
if (options?.maxTokens && !model.omitMaxOutputTokens) {
|
|
2723
2763
|
params.max_output_tokens = Math.min(
|
|
@@ -2726,12 +2766,16 @@ export function applyCommonResponsesSamplingParams<P extends CommonResponsesPara
|
|
|
2726
2766
|
OPENAI_MAX_OUTPUT_TOKENS,
|
|
2727
2767
|
);
|
|
2728
2768
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
if (
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2769
|
+
// OpenAI proprietary reasoning models (o-series, gpt-5+) reject explicit
|
|
2770
|
+
// sampling params with a 400 on every serving host (#5606).
|
|
2771
|
+
if (model.compat.supportsSamplingParams) {
|
|
2772
|
+
if (options?.temperature !== undefined) params.temperature = options.temperature;
|
|
2773
|
+
if (options?.topP !== undefined) params.top_p = options.topP;
|
|
2774
|
+
if (options?.topK !== undefined) params.top_k = options.topK;
|
|
2775
|
+
if (options?.minP !== undefined) params.min_p = options.minP;
|
|
2776
|
+
if (options?.presencePenalty !== undefined) params.presence_penalty = options.presencePenalty;
|
|
2777
|
+
if (options?.repetitionPenalty !== undefined) params.repetition_penalty = options.repetitionPenalty;
|
|
2778
|
+
}
|
|
2735
2779
|
applyOpenAIServiceTier(params, options?.serviceTier, model);
|
|
2736
2780
|
}
|
|
2737
2781
|
|
|
@@ -554,6 +554,13 @@ export function transformMessages<TApi extends Api>(
|
|
|
554
554
|
return [];
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
if (block.type === "image") {
|
|
558
|
+
// Assistant images are display artifacts. No provider accepts them
|
|
559
|
+
// in an assistant replay turn; the native Responses result remains
|
|
560
|
+
// in providerPayload for OpenAI replay.
|
|
561
|
+
return [];
|
|
562
|
+
}
|
|
563
|
+
|
|
557
564
|
if (block.type === "text") {
|
|
558
565
|
if (isSameModel) return block;
|
|
559
566
|
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
|
|