@oh-my-pi/pi-ai 18.11.2 → 18.11.3
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 -21
- package/README.md +2 -2
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -0
- package/dist/types/types.d.ts +5 -0
- package/dist/types/utils/openai-http.d.ts +2 -0
- package/package.json +6 -6
- package/src/providers/anthropic.ts +10 -4
- package/src/providers/cowork-fetch.ts +5 -1
- package/src/providers/cursor/exec-modern.ts +1 -0
- package/src/providers/cursor-pi-args.ts +19 -0
- package/src/providers/cursor.ts +4 -4
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server.ts +29 -0
- package/src/providers/openai-completions.ts +2 -1
- package/src/providers/pi-native-server.ts +1 -0
- package/src/registry/oauth/google-antigravity.ts +1 -1
- package/src/stream.ts +1 -0
- package/src/types.ts +5 -0
- package/src/utils/openai-http.ts +3 -1
- package/src/utils/schema/dereference.ts +44 -11
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.11.3] - 2026-10-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed JSON Schema dereferencing rewriting literal values in `const`, `enum`, `default`, examples, and custom annotations, or treating property names as schema keywords ([#15927](https://github.com/can1357/oh-my-pi/pull/15927) by [@jchanghong023](https://github.com/jchanghong023)).
|
|
10
|
+
- Fixed Anthropic requests hanging forever on Bun 1.3 when the upstream accepted the request but never sent headers: aborting the request (including the first-event timeout) now fails it, so the turn retries instead of waiting until the user interrupts it ([#16010](https://github.com/can1357/oh-my-pi/pull/16010) by [@ca-io-systems](https://github.com/ca-io-systems))
|
|
11
|
+
- Fixed the auth gateway's `POST /v1/chat/completions` streams being dropped (`terminated`) after ~255 s without upstream output, e.g. while xAI generates a large tool call's arguments, which arrive only when complete; the stream now sends an SSE `: keepalive` comment every 15 s ([#15940](https://github.com/can1357/oh-my-pi/pull/15940) by [@radkawar](https://github.com/radkawar))
|
|
12
|
+
- Fixed Anthropic models with mid-conversation system messages (e.g. Claude Opus 5.5) receiving `@file` mention contents and hidden extension messages as a trailing `system` message instead of ending the request on the user's turn ([#15881](https://github.com/can1357/oh-my-pi/issues/15881))
|
|
13
|
+
|
|
5
14
|
## [18.11.2] - 2026-10-16
|
|
6
15
|
|
|
7
16
|
### Breaking Changes
|
|
@@ -2359,24 +2368,4 @@
|
|
|
2359
2368
|
- Fixed Ollama chat requests sending image payloads to text-only models. Image blocks are now omitted and replaced with the standard non-vision placeholder for models without vision support, while vision-capable Ollama models continue to receive images. ([#3009](https://github.com/can1357/oh-my-pi/pull/3009) by [@serverinspector](https://github.com/serverinspector))
|
|
2360
2369
|
- Fixed `SqliteAuthCredentialStore.close()` leaking one-off prepared statements created by inline `this.#db.prepare()` calls in `#authCredentialsTableExists`, `#readAuthSchemaVersion`, `#inferAuthSchemaVersion`, `#migrateAuthSchemaV0ToV1`, `#backfillCredentialIdentityKeys`, and `updateAuthCredential`. Each statement is now wrapped in `try/finally` with `stmt.finalize()`, and the `close()` method finalizes `#insertUsageCostStmt` and `#listUsageCostsStmt` which were previously missed. This caused EBUSY on Windows when tests tried to delete temp dirs containing open SQLite handles.
|
|
2361
2370
|
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
### Added
|
|
2365
|
-
|
|
2366
|
-
- Added improved JSON repair capabilities for Anthropic tool arguments
|
|
2367
|
-
- Added authentication broker discovery to sync credentials between local SQLite and remote state
|
|
2368
|
-
|
|
2369
|
-
### Fixed
|
|
2370
|
-
|
|
2371
|
-
- Improved error feedback and transparency for malformed Anthropic tool call arguments
|
|
2372
|
-
- Added automatic fallback for unsupported OpenAI reasoning effort levels
|
|
2373
|
-
- Improved reliability when handling invalid reasoning parameter errors across OpenAI-compatible APIs
|
|
2374
|
-
- Fixed OpenAI-compatible Chat Completions, Responses, and Azure Responses requests to retry once with the nearest provider-supported reasoning effort when an endpoint rejects `xhigh`/`minimal`-style effort values.
|
|
2375
|
-
|
|
2376
|
-
## [16.1.0] - 2026-06-19
|
|
2377
|
-
|
|
2378
|
-
### Added
|
|
2379
|
-
|
|
2380
|
-
- Added utility functions to strip schema descriptions for optimized LLM context usage
|
|
2381
|
-
|
|
2382
|
-
Older entries are archived in [packages/ai/CHANGELOG.md@aa43b0057f82](https://github.com/can1357/oh-my-pi/blob/aa43b0057f82cb167459f8e6b41c5ec9f4e0f91d/packages/ai/CHANGELOG.md).
|
|
2371
|
+
Older entries are archived in [packages/ai/CHANGELOG.md@0595d346917d](https://github.com/can1357/oh-my-pi/blob/0595d346917d152ea2b710332817de599f749626/packages/ai/CHANGELOG.md).
|
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Unified LLM API with automatic model discovery, provider configuration, token an
|
|
|
84
84
|
- **vLLM** (OpenAI-compatible server; `VLLM_API_KEY` for secured deployments)
|
|
85
85
|
- **GitHub Copilot** (requires OAuth, see below)
|
|
86
86
|
- **Google Gemini CLI** (requires OAuth, see below)
|
|
87
|
-
- **Antigravity** (requires OAuth, see below)
|
|
87
|
+
- **Google Antigravity** (requires OAuth, see below)
|
|
88
88
|
- **Any OpenAI-compatible API**: LM Studio, custom proxies, etc.
|
|
89
89
|
|
|
90
90
|
## Installation
|
|
@@ -1031,7 +1031,7 @@ Several providers support OAuth authentication (some also support static API key
|
|
|
1031
1031
|
- **OpenAI Codex** (ChatGPT Plus/Pro subscription, access to GPT-5.x Codex models)
|
|
1032
1032
|
- **GitHub Copilot** (Copilot subscription)
|
|
1033
1033
|
- **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)
|
|
1034
|
-
- **Antigravity** (
|
|
1034
|
+
- **Google Antigravity** (Gemini, Claude, GPT-OSS via Google Cloud)
|
|
1035
1035
|
- **Qwen Portal** (Qwen OAuth token or API key)
|
|
1036
1036
|
- **Snowflake Cortex** (Snowflake account OAuth with PKCE and refresh, subject to the account's local-application integration)
|
|
1037
1037
|
|
|
@@ -17,7 +17,7 @@ import type { ToolResultMessage } from "../../types.js";
|
|
|
17
17
|
* virtual registry. Re-exported here because this is where the frame builders
|
|
18
18
|
* and their translation are consumed together.
|
|
19
19
|
*/
|
|
20
|
-
export { cursorExecReadPath, cursorRawReadPath, omitUndefinedArgs, piEscapeRegexLiteral, piGrepSkip, piJoinPath, piLimit, piLsPath, piReadDisplayPath, piReadPath, piReadPathHasRange, piTimeout, shellTimeoutSeconds, } from "../cursor-pi-args.js";
|
|
20
|
+
export { cursorExecReadPath, cursorRawReadPath, omitUndefinedArgs, piEscapeRegexLiteral, piGrepSkip, piJoinPath, piLimit, piLsPath, piReadDisplayPath, piReadPath, piReadPathHasRange, piTimeout, shellDeadlineSeconds, shellTimeoutSeconds, } from "../cursor-pi-args.js";
|
|
21
21
|
/** Flatten a tool result's content into the single `output` string the Pi frames carry. */
|
|
22
22
|
export declare function piOutputText(toolResult: ToolResultMessage): string;
|
|
23
23
|
/**
|
|
@@ -129,6 +129,19 @@ export declare function piTimeout(timeout: number | undefined): number | undefin
|
|
|
129
129
|
* budgets round up: 0 seconds means "no deadline" to the bash tool.
|
|
130
130
|
*/
|
|
131
131
|
export declare function shellTimeoutSeconds(timeoutMs: number | undefined): number | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* The bash-tool kill deadline, in seconds, for a Cursor `ShellArgs`/`ShellStreamArgs` frame.
|
|
134
|
+
*
|
|
135
|
+
* Cursor's `timeout` carries the model's `block_until_ms`, which Cursor's Shell tool documents as
|
|
136
|
+
* how long to block before moving the command to the background, not as a kill deadline; the kill
|
|
137
|
+
* deadline is `hard_timeout`. Without one, a short wait (`block_until_ms: 5000` on a build) must not
|
|
138
|
+
* kill the command: the deadline stays the bash default (`undefined`), raised to the wait only when
|
|
139
|
+
* the wait is longer.
|
|
140
|
+
*/
|
|
141
|
+
export declare function shellDeadlineSeconds(args: {
|
|
142
|
+
timeout: number;
|
|
143
|
+
hardTimeout?: number;
|
|
144
|
+
}): number | undefined;
|
|
132
145
|
/**
|
|
133
146
|
* Drop keys whose value is `undefined` so optional local-tool kwargs stay
|
|
134
147
|
* absent rather than present-as-undefined.
|
package/dist/types/types.d.ts
CHANGED
|
@@ -267,6 +267,11 @@ export interface StreamOptions {
|
|
|
267
267
|
frequencyPenalty?: number;
|
|
268
268
|
maxTokens?: number;
|
|
269
269
|
signal?: AbortSignal;
|
|
270
|
+
/**
|
|
271
|
+
* OpenAI chat-completions HTTP attempt budget. When set, transient provider
|
|
272
|
+
* errors are not replayed above the transport, so retries share this limit.
|
|
273
|
+
*/
|
|
274
|
+
maxHttpAttempts?: number;
|
|
270
275
|
apiKey?: string;
|
|
271
276
|
/** @internal Stored credential row serving this request, when known. */
|
|
272
277
|
credentialId?: number;
|
|
@@ -26,6 +26,8 @@ export interface OpenAIStreamRequestInit {
|
|
|
26
26
|
body: unknown;
|
|
27
27
|
signal: AbortSignal;
|
|
28
28
|
fetch?: FetchImpl;
|
|
29
|
+
/** Override the default transport attempt budget for this request. */
|
|
30
|
+
maxAttempts?: number;
|
|
29
31
|
/** Optional caller-specific gate composed with shared transport retry exclusions. */
|
|
30
32
|
shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
|
|
31
33
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.11.
|
|
3
|
+
"version": "18.11.3",
|
|
4
4
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -155,11 +155,11 @@
|
|
|
155
155
|
"fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
|
|
156
156
|
},
|
|
157
157
|
"dependencies": {
|
|
158
|
-
"@oh-my-pi/omptype": "18.11.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.11.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.11.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.11.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.11.
|
|
158
|
+
"@oh-my-pi/omptype": "18.11.3",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.11.3",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.11.3",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.11.3",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.11.3"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
|
@@ -5777,9 +5777,9 @@ export function convertAnthropicMessages(
|
|
|
5777
5777
|
compactionRequest?: AnthropicCompactionRequest;
|
|
5778
5778
|
},
|
|
5779
5779
|
): AnthropicMessageParam[] {
|
|
5780
|
-
// Indices of params emitted from `developer` messages. After the
|
|
5781
|
-
// the ones whose placement satisfies Anthropic's mid-conversation
|
|
5782
|
-
// upgraded from the `user` role to the authoritative `system` role.
|
|
5780
|
+
// Indices of params emitted from agent-owned `developer` messages. After the
|
|
5781
|
+
// main pass, the ones whose placement satisfies Anthropic's mid-conversation
|
|
5782
|
+
// rules are upgraded from the `user` role to the authoritative `system` role.
|
|
5783
5783
|
const developerParams: Array<{ index: number; payload?: AnthropicMessagePayload }> = [];
|
|
5784
5784
|
const params: AnthropicMessageParam[] = [];
|
|
5785
5785
|
// Controls rendered from request-control markers. The assistant repairs
|
|
@@ -5906,7 +5906,13 @@ export function convertAnthropicMessages(
|
|
|
5906
5906
|
blocks.push(...anthropicToolChangeBlocks(payload.toolChanges, isOAuthToken, model));
|
|
5907
5907
|
content = blocks;
|
|
5908
5908
|
}
|
|
5909
|
-
|
|
5909
|
+
// User-attributed developer turns (text `@file` mentions, hidden extension
|
|
5910
|
+
// messages) only borrow the slot: their content is the user's, so it stays
|
|
5911
|
+
// on `user` instead of gaining system authority or ending the request in
|
|
5912
|
+
// place of the user's turn. Provider controls only exist on `system`.
|
|
5913
|
+
if (msg.role === "developer" && (msg.attribution !== "user" || hasProviderControls)) {
|
|
5914
|
+
developerParams.push({ index: params.length, payload });
|
|
5915
|
+
}
|
|
5910
5916
|
const param: AnthropicMessageParam & ConversationalUserCarrier = { role: "user", content };
|
|
5911
5917
|
// Record that this wire `user` came from a real conversational turn, so
|
|
5912
5918
|
// prompt-cache decimation can tell it apart from everything else that
|
|
@@ -198,7 +198,11 @@ async function sendCoworkRequest(
|
|
|
198
198
|
};
|
|
199
199
|
const abort = (): void => {
|
|
200
200
|
const reason = signal?.reason;
|
|
201
|
-
|
|
201
|
+
const error = reason instanceof Error ? reason : new DOMException("The operation was aborted.", "AbortError");
|
|
202
|
+
request?.destroy(error);
|
|
203
|
+
// Bun before 1.4 emits 'close' but not 'error' for destroy(error), so the
|
|
204
|
+
// 'error' listener below never settles a request still waiting for headers.
|
|
205
|
+
result.reject(error);
|
|
202
206
|
};
|
|
203
207
|
if (signal?.aborted) {
|
|
204
208
|
release();
|
|
@@ -209,6 +209,25 @@ export function shellTimeoutSeconds(timeoutMs: number | undefined): number | und
|
|
|
209
209
|
return Math.max(1, Math.round(timeoutMs / 1000));
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
+
/** The bash tool's deadline when its `timeout` is omitted. */
|
|
213
|
+
const BASH_DEFAULT_DEADLINE_SECONDS = 300;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The bash-tool kill deadline, in seconds, for a Cursor `ShellArgs`/`ShellStreamArgs` frame.
|
|
217
|
+
*
|
|
218
|
+
* Cursor's `timeout` carries the model's `block_until_ms`, which Cursor's Shell tool documents as
|
|
219
|
+
* how long to block before moving the command to the background, not as a kill deadline; the kill
|
|
220
|
+
* deadline is `hard_timeout`. Without one, a short wait (`block_until_ms: 5000` on a build) must not
|
|
221
|
+
* kill the command: the deadline stays the bash default (`undefined`), raised to the wait only when
|
|
222
|
+
* the wait is longer.
|
|
223
|
+
*/
|
|
224
|
+
export function shellDeadlineSeconds(args: { timeout: number; hardTimeout?: number }): number | undefined {
|
|
225
|
+
const hardSeconds = shellTimeoutSeconds(args.hardTimeout);
|
|
226
|
+
if (hardSeconds !== undefined) return hardSeconds;
|
|
227
|
+
const waitSeconds = shellTimeoutSeconds(args.timeout);
|
|
228
|
+
return waitSeconds !== undefined && waitSeconds > BASH_DEFAULT_DEADLINE_SECONDS ? waitSeconds : undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
212
231
|
/**
|
|
213
232
|
* Drop keys whose value is `undefined` so optional local-tool kwargs stay
|
|
214
233
|
* absent rather than present-as-undefined.
|
package/src/providers/cursor.ts
CHANGED
|
@@ -261,7 +261,7 @@ import {
|
|
|
261
261
|
piReadDisplayPath,
|
|
262
262
|
piReadPathHasRange,
|
|
263
263
|
piTimeout,
|
|
264
|
-
|
|
264
|
+
shellDeadlineSeconds,
|
|
265
265
|
} from "./cursor/exec-modern";
|
|
266
266
|
import { handleInteractionQuery, protoUnknownFields } from "./cursor/interaction-query";
|
|
267
267
|
|
|
@@ -2681,7 +2681,7 @@ async function handleExecServerMessage(
|
|
|
2681
2681
|
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
2682
2682
|
command: args.command,
|
|
2683
2683
|
cwd: args.workingDirectory || undefined,
|
|
2684
|
-
timeout:
|
|
2684
|
+
timeout: shellDeadlineSeconds(args),
|
|
2685
2685
|
});
|
|
2686
2686
|
const { execResult } = await resolveExecHandler(
|
|
2687
2687
|
args,
|
|
@@ -2702,7 +2702,7 @@ async function handleExecServerMessage(
|
|
|
2702
2702
|
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
2703
2703
|
command: args.command,
|
|
2704
2704
|
cwd: args.workingDirectory || undefined,
|
|
2705
|
-
timeout:
|
|
2705
|
+
timeout: shellDeadlineSeconds(args),
|
|
2706
2706
|
});
|
|
2707
2707
|
await handleShellStreamArgs(args, execMsg, runTransport, execHandlers, onToolResult);
|
|
2708
2708
|
return;
|
|
@@ -3180,7 +3180,7 @@ async function handleExecServerMessage(
|
|
|
3180
3180
|
synthesizeCursorExecToolCall(output, stream, state, args.toolCallId, "bash", {
|
|
3181
3181
|
command: args.command,
|
|
3182
3182
|
cwd: args.workingDirectory || undefined,
|
|
3183
|
-
timeout:
|
|
3183
|
+
timeout: shellDeadlineSeconds(args),
|
|
3184
3184
|
});
|
|
3185
3185
|
const { execResult } = await resolveExecHandler(
|
|
3186
3186
|
normalizedArgs,
|
|
@@ -133,6 +133,7 @@ export function streamOpenAIAnthropicShim(
|
|
|
133
133
|
presencePenalty: options?.presencePenalty,
|
|
134
134
|
repetitionPenalty: options?.repetitionPenalty,
|
|
135
135
|
maxTokens: options?.maxTokens ?? model.maxTokens ?? undefined,
|
|
136
|
+
maxHttpAttempts: options?.maxHttpAttempts,
|
|
136
137
|
signal: options?.signal,
|
|
137
138
|
headers: mergedHeaders,
|
|
138
139
|
cacheRetention: config.forwardCacheOptions ? options?.cacheRetention : undefined,
|
|
@@ -531,6 +531,13 @@ function makeId(): string {
|
|
|
531
531
|
// encodeStream (SSE)
|
|
532
532
|
// ---------------------------------------------------------------------------
|
|
533
533
|
|
|
534
|
+
// Keepalive cadence for the SSE encoder. Some upstreams go silent for minutes
|
|
535
|
+
// mid-stream (e.g. xAI emits a large tool call's arguments only when complete);
|
|
536
|
+
// without bytes on the wire the listener's idle timeout and client watchdogs
|
|
537
|
+
// drop a healthy stream. SSE comment lines are ignored by spec-compliant clients.
|
|
538
|
+
const STREAM_KEEPALIVE_INTERVAL_MS = 15_000;
|
|
539
|
+
const SSE_KEEPALIVE = ": keepalive\n\n";
|
|
540
|
+
|
|
534
541
|
export function encodeStream(
|
|
535
542
|
events: AssistantMessageEventStream,
|
|
536
543
|
requestedModelId: string,
|
|
@@ -546,6 +553,13 @@ export function encodeStream(
|
|
|
546
553
|
cancelled = true;
|
|
547
554
|
};
|
|
548
555
|
control?.signal?.addEventListener("abort", markCancelled, { once: true });
|
|
556
|
+
let keepaliveTimer: NodeJS.Timeout | undefined;
|
|
557
|
+
const stopKeepalive = () => {
|
|
558
|
+
if (keepaliveTimer !== undefined) {
|
|
559
|
+
clearInterval(keepaliveTimer);
|
|
560
|
+
keepaliveTimer = undefined;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
549
563
|
|
|
550
564
|
const baseChunk = (delta: Record<string, unknown>, finishReason: string | null) => ({
|
|
551
565
|
id,
|
|
@@ -584,6 +598,19 @@ export function encodeStream(
|
|
|
584
598
|
let hasToolCalls = false;
|
|
585
599
|
let finishReason: string = "stop";
|
|
586
600
|
|
|
601
|
+
keepaliveTimer = setInterval(() => {
|
|
602
|
+
try {
|
|
603
|
+
if (cancelled) {
|
|
604
|
+
stopKeepalive();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
controller.enqueue(encoder.encode(SSE_KEEPALIVE));
|
|
608
|
+
} catch {
|
|
609
|
+
// Controller already closed/errored (client gone); stop the timer.
|
|
610
|
+
stopKeepalive();
|
|
611
|
+
}
|
|
612
|
+
}, STREAM_KEEPALIVE_INTERVAL_MS);
|
|
613
|
+
|
|
587
614
|
try {
|
|
588
615
|
if (cancelled) {
|
|
589
616
|
controller.close();
|
|
@@ -733,11 +760,13 @@ export function encodeStream(
|
|
|
733
760
|
controller.close();
|
|
734
761
|
}
|
|
735
762
|
} finally {
|
|
763
|
+
stopKeepalive();
|
|
736
764
|
control?.signal?.removeEventListener("abort", markCancelled);
|
|
737
765
|
}
|
|
738
766
|
},
|
|
739
767
|
cancel(reason) {
|
|
740
768
|
cancelled = true;
|
|
769
|
+
stopKeepalive();
|
|
741
770
|
control?.signal?.removeEventListener("abort", markCancelled);
|
|
742
771
|
control?.onCancel?.(reason);
|
|
743
772
|
},
|
|
@@ -936,6 +936,7 @@ const streamOpenAICompletionsOnce = (
|
|
|
936
936
|
headers: headersWithTimeout,
|
|
937
937
|
body: params,
|
|
938
938
|
signal: requestSignal,
|
|
939
|
+
maxAttempts: options?.maxHttpAttempts,
|
|
939
940
|
fetch: wrapFetchForCopilotFallback(
|
|
940
941
|
options?.fetch,
|
|
941
942
|
model.provider === "github-copilot",
|
|
@@ -1866,7 +1867,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (mo
|
|
|
1866
1867
|
const resolvedModel = resolveOpenAICompletionsCompat(model);
|
|
1867
1868
|
return withReplaySafeStreamRetry(resolvedModel, context, options, streamOpenAICompletionsOnce, {
|
|
1868
1869
|
retryEmptyCompletion: true,
|
|
1869
|
-
retryProviderErrors:
|
|
1870
|
+
retryProviderErrors: options?.maxHttpAttempts === undefined,
|
|
1870
1871
|
maxProviderErrorRetries: 1,
|
|
1871
1872
|
});
|
|
1872
1873
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Antigravity OAuth flow (Gemini
|
|
2
|
+
* Google Antigravity OAuth flow (Gemini, Claude, GPT-OSS via Google Cloud)
|
|
3
3
|
* Uses different OAuth credentials than google-gemini-cli for access to additional models.
|
|
4
4
|
*/
|
|
5
5
|
import { type } from "@oh-my-pi/omptype";
|
package/src/stream.ts
CHANGED
|
@@ -1816,6 +1816,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1816
1816
|
repetitionPenalty: options?.repetitionPenalty,
|
|
1817
1817
|
maxTokens: options?.maxTokens ?? model.maxTokens ?? undefined,
|
|
1818
1818
|
signal: options?.signal,
|
|
1819
|
+
maxHttpAttempts: options?.maxHttpAttempts,
|
|
1819
1820
|
apiKey: apiKey ?? (typeof options?.apiKey === "string" ? options.apiKey : undefined),
|
|
1820
1821
|
credentialId: options?.credentialId,
|
|
1821
1822
|
oauthIdentity: options?.oauthIdentity,
|
package/src/types.ts
CHANGED
|
@@ -497,6 +497,11 @@ export interface StreamOptions {
|
|
|
497
497
|
frequencyPenalty?: number;
|
|
498
498
|
maxTokens?: number;
|
|
499
499
|
signal?: AbortSignal;
|
|
500
|
+
/**
|
|
501
|
+
* OpenAI chat-completions HTTP attempt budget. When set, transient provider
|
|
502
|
+
* errors are not replayed above the transport, so retries share this limit.
|
|
503
|
+
*/
|
|
504
|
+
maxHttpAttempts?: number;
|
|
500
505
|
apiKey?: string;
|
|
501
506
|
/** @internal Stored credential row serving this request, when known. */
|
|
502
507
|
credentialId?: number;
|
package/src/utils/openai-http.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface OpenAIStreamRequestInit {
|
|
|
68
68
|
body: unknown;
|
|
69
69
|
signal: AbortSignal;
|
|
70
70
|
fetch?: FetchImpl;
|
|
71
|
+
/** Override the default transport attempt budget for this request. */
|
|
72
|
+
maxAttempts?: number;
|
|
71
73
|
/** Optional caller-specific gate composed with shared transport retry exclusions. */
|
|
72
74
|
shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
|
|
73
75
|
/**
|
|
@@ -102,7 +104,7 @@ export async function postOpenAIStream<TEvent>(init: OpenAIStreamRequestInit): P
|
|
|
102
104
|
body: JSON.stringify(init.body),
|
|
103
105
|
signal: init.signal,
|
|
104
106
|
fetch: init.fetch,
|
|
105
|
-
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
|
107
|
+
maxAttempts: init.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
106
108
|
// A proxy concurrency-admission 429 (`rate_limit_type: max_parallel_requests`)
|
|
107
109
|
// surfaces immediately instead of being slept-and-retried here; session
|
|
108
110
|
// recovery owns its backoff/fallback (issue #8854).
|
|
@@ -11,6 +11,43 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { isJsonObject, type JsonObject } from "./types";
|
|
13
13
|
|
|
14
|
+
// Only schema-valued keywords may contain references; literal JSON is opaque.
|
|
15
|
+
const SCHEMA_MAP_KEYS: Record<string, true> = {
|
|
16
|
+
properties: true,
|
|
17
|
+
patternProperties: true,
|
|
18
|
+
dependencies: true,
|
|
19
|
+
dependentSchemas: true,
|
|
20
|
+
};
|
|
21
|
+
const SCHEMA_ARRAY_KEYS: Record<string, true> = { anyOf: true, oneOf: true, allOf: true, prefixItems: true };
|
|
22
|
+
const SCHEMA_VALUE_KEYS: Record<string, true> = {
|
|
23
|
+
items: true,
|
|
24
|
+
additionalItems: true,
|
|
25
|
+
contains: true,
|
|
26
|
+
contentSchema: true,
|
|
27
|
+
propertyNames: true,
|
|
28
|
+
if: true,
|
|
29
|
+
// oxlint-disable-next-line unicorn/no-thenable -- JSON Schema keyword
|
|
30
|
+
then: true,
|
|
31
|
+
else: true,
|
|
32
|
+
not: true,
|
|
33
|
+
additionalProperties: true,
|
|
34
|
+
unevaluatedItems: true,
|
|
35
|
+
unevaluatedProperties: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function dereferenceKeyword(key: string, value: unknown, root: JsonObject, visiting: Set<string>): unknown {
|
|
39
|
+
if (Object.hasOwn(SCHEMA_MAP_KEYS, key) && isJsonObject(value)) {
|
|
40
|
+
const result: JsonObject = {};
|
|
41
|
+
for (const name in value) result[name] = dereferenceNode(value[name], root, visiting);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
if ((Object.hasOwn(SCHEMA_ARRAY_KEYS, key) || key === "items") && Array.isArray(value)) {
|
|
45
|
+
return value.map(item => dereferenceNode(item, root, visiting));
|
|
46
|
+
}
|
|
47
|
+
if (Object.hasOwn(SCHEMA_VALUE_KEYS, key)) return dereferenceNode(value, root, visiting);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
/**
|
|
15
52
|
* Resolve a JSON-pointer-style `$ref` against the root schema's `$defs`
|
|
16
53
|
* or `definitions` block. Returns `undefined` for external or unresolvable refs.
|
|
@@ -33,7 +70,6 @@ function resolveLocalRef(ref: string, root: JsonObject): JsonObject | undefined
|
|
|
33
70
|
*/
|
|
34
71
|
function dereferenceNode(node: unknown, root: JsonObject, visiting: Set<string>): unknown {
|
|
35
72
|
if (!isJsonObject(node)) return node;
|
|
36
|
-
if (Array.isArray(node)) return node.map(item => dereferenceNode(item, root, visiting));
|
|
37
73
|
|
|
38
74
|
const ref = node.$ref;
|
|
39
75
|
if (typeof ref === "string") {
|
|
@@ -49,14 +85,17 @@ function dereferenceNode(node: unknown, root: JsonObject, visiting: Set<string>)
|
|
|
49
85
|
// referencing node. In draft 2020-12 these are valid alongside $ref.
|
|
50
86
|
let hasSiblings = false;
|
|
51
87
|
for (const k in node) {
|
|
52
|
-
if (k !== "$ref") {
|
|
88
|
+
if (k !== "$ref" && k !== "$defs" && k !== "definitions") {
|
|
53
89
|
hasSiblings = true;
|
|
54
90
|
break;
|
|
55
91
|
}
|
|
56
92
|
}
|
|
57
93
|
if (!hasSiblings || !isJsonObject(inlined)) return inlined;
|
|
58
|
-
const merged: JsonObject = { ...inlined
|
|
59
|
-
|
|
94
|
+
const merged: JsonObject = { ...inlined };
|
|
95
|
+
for (const key in node) {
|
|
96
|
+
if (key === "$ref" || key === "$defs" || key === "definitions") continue;
|
|
97
|
+
merged[key] = dereferenceKeyword(key, node[key], root, visiting);
|
|
98
|
+
}
|
|
60
99
|
return merged;
|
|
61
100
|
}
|
|
62
101
|
|
|
@@ -66,13 +105,7 @@ function dereferenceNode(node: unknown, root: JsonObject, visiting: Set<string>)
|
|
|
66
105
|
// Skip $defs/definitions — they get inlined into consumers
|
|
67
106
|
if (key === "$defs" || key === "definitions") continue;
|
|
68
107
|
|
|
69
|
-
|
|
70
|
-
result[key] = value.map(item => dereferenceNode(item, root, visiting));
|
|
71
|
-
} else if (isJsonObject(value)) {
|
|
72
|
-
result[key] = dereferenceNode(value, root, visiting);
|
|
73
|
-
} else {
|
|
74
|
-
result[key] = value;
|
|
75
|
-
}
|
|
108
|
+
result[key] = dereferenceKeyword(key, value, root, visiting);
|
|
76
109
|
}
|
|
77
110
|
return result;
|
|
78
111
|
}
|