@oh-my-pi/pi-ai 18.4.1 → 18.4.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 +19 -25
- package/dist/types/error/flags.d.ts +9 -2
- package/dist/types/images/openai-hosted.d.ts +1 -1
- package/dist/types/providers/cursor/exec-modern.d.ts +1 -1
- package/dist/types/providers/cursor-pi-args.d.ts +13 -18
- package/dist/types/usage/commandcode.d.ts +4 -0
- package/dist/types/utils/event-stream.d.ts +7 -0
- package/dist/types/utils/openai-http.d.ts +7 -1
- package/package.json +6 -6
- package/src/dialect/thinking.ts +24 -6
- package/src/error/flags.ts +10 -3
- package/src/images/openai-hosted.ts +5 -2
- package/src/providers/amazon-bedrock.ts +58 -16
- package/src/providers/aws-eventstream.ts +4 -3
- package/src/providers/azure-openai-responses.ts +3 -4
- package/src/providers/cursor/exec-modern.ts +1 -1
- package/src/providers/cursor-pi-args.ts +13 -18
- package/src/providers/cursor.ts +179 -57
- package/src/providers/google-gemini-cli.ts +9 -2
- package/src/providers/google-shared.ts +8 -4
- package/src/providers/openai-codex-responses.ts +8 -2
- package/src/providers/openai-completions.ts +51 -36
- package/src/providers/openai-responses.ts +3 -4
- package/src/providers/openai-shared.ts +126 -18
- package/src/registry/engine/api-key.ts +8 -3
- package/src/stream.ts +25 -15
- package/src/usage/commandcode.ts +209 -0
- package/src/usage/registry.ts +3 -0
- package/src/utils/event-stream.ts +38 -2
- package/src/utils/openai-http.ts +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.4.3] - 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added Command Code usage limits (5-hour, weekly, and credit balance) to /usage and the status line ([#13666](https://github.com/can1357/oh-my-pi/pull/13666) by [@riicodespretty](https://github.com/riicodespretty))
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Reduced per-token CPU and allocations while streaming: the leaked-thinking scanner used for OpenAI-compatible and custom endpoints no longer allocates per character, chat-completions and Bedrock look up a delta's content block in constant time, Google, Gemini CLI, Codex, and chat-completions streams skip raw SSE line capture unless an `onSseEvent` listener is attached, and event streams drain backlogs without `Array#shift` ([#13650](https://github.com/can1357/oh-my-pi/pull/13650) by [@H4vC](https://github.com/H4vC)).
|
|
14
|
+
|
|
15
|
+
## [18.4.2] - 2026-09-28
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Fixed successful Cursor agent turns being treated as context overflows, which ran overflow compaction and showed "Compaction freed too little context to make progress" while `/context` read well under the window; overflow detection now uses the reported context size instead of input totals summed across a turn's model calls ([#13608](https://github.com/can1357/oh-my-pi/pull/13608) by [@H4vC](https://github.com/H4vC))
|
|
20
|
+
- Fixed Anthropic requests with thinking enabled failing on models whose output ceiling cannot fit the minimum thinking budget; thinking is now disabled for those requests instead ([#13359](https://github.com/can1357/oh-my-pi/pull/13359) by [@jchanghong023](https://github.com/jchanghong023))
|
|
21
|
+
- Fixed Cursor native Grep/Glob results showing no matches or raw output, Write failing to create files, StrReplace missing edits beyond the read limit, and Read/Shell/Delete results misreporting content or metadata ([#13600](https://github.com/can1357/oh-my-pi/issues/13600)).
|
|
22
|
+
|
|
5
23
|
## [18.4.1] - 2026-09-28
|
|
6
24
|
|
|
7
25
|
### Fixed
|
|
@@ -2298,28 +2316,4 @@
|
|
|
2298
2316
|
|
|
2299
2317
|
- Fixed `google-gemini-cli` ignoring `Model.requestModelId` when serializing the request model id
|
|
2300
2318
|
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
### Added
|
|
2304
|
-
|
|
2305
|
-
- Added `AuthStorage.listUsageHistory` to retrieve historical usage snapshots with optional `provider` and `sinceMs` filtering
|
|
2306
|
-
- Added durable usage-history persistence in the sqlite auth store so successful usage reports are recorded as time-series snapshots of limit utilization for later trend inspection
|
|
2307
|
-
- Added `AuthStorage.redeemResetCredit` to redeem stored OpenAI Codex saved rate-limit reset credits for a target account by `credentialId`, `accountId`, or `email`
|
|
2308
|
-
- Added `listCodexResetCredits` and `consumeCodexResetCredit` exports for OpenAI Codex saved reset-credit listing and redemption
|
|
2309
|
-
- Added `resetCredits` with `availableCount` to `UsageReport` so OpenAI Codex usage data now exposes redeemable rate-limit resets
|
|
2310
|
-
- Added `openai-codex-reset` exports via package barrel for out-of-band tooling usage
|
|
2311
|
-
- Added a one-shot request-debug target that writes the next provider HTTP request JSON to an explicit path.
|
|
2312
|
-
|
|
2313
|
-
### Changed
|
|
2314
|
-
|
|
2315
|
-
- Changed `AuthStorage.redeemResetCredit` to invalidate cached usage data after a successful redemption so the next usage report reflects the reset immediately
|
|
2316
|
-
|
|
2317
|
-
### Fixed
|
|
2318
|
-
|
|
2319
|
-
- Fixed temporary credential block state so redeemed reset credits immediately make the affected account selectable again after `redeemResetCredit` succeeds
|
|
2320
|
-
- Fixed one-shot request-debug path handling so an explicit request log target is consumed after the next request and no longer affects subsequent calls
|
|
2321
|
-
- Fixed explicit request-debug path mode to create missing parent directories before writing request logs
|
|
2322
|
-
- Fixed explicit request-debug mode to overwrite existing `.res.log` files for the requested path instead of failing when they already exist
|
|
2323
|
-
- Fixed OpenAI Responses `previous_response_id` chaining on Zero Data Retention orgs: the in-provider retry classifier missed the ZDR-specific 400 ("Previous response cannot be used for this organization due to Zero Data Retention"), so chained turns kept failing every other request after a brief recovery — the chain was reset but not disabled, so the next successful full-replay turn re-armed it. The ZDR phrasing is now classified categorically: one strike disables chaining for the session (skipping the three-strike circuit breaker) and the in-call retry drops `store: true`/`previous_response_id` and replays the full transcript instead ([#2341](https://github.com/can1357/oh-my-pi/issues/2341)).
|
|
2324
|
-
|
|
2325
|
-
Older entries are archived in [packages/ai/CHANGELOG.md@dfbf3cc34eeb](https://github.com/can1357/oh-my-pi/blob/dfbf3cc34eeb5653580f51bfbcae558a9840f697/packages/ai/CHANGELOG.md).
|
|
2319
|
+
Older entries are archived in [packages/ai/CHANGELOG.md@689a3418cb45](https://github.com/can1357/oh-my-pi/blob/689a3418cb45d54a459cde2e1abf3f66f50e47a4/packages/ai/CHANGELOG.md).
|
|
@@ -126,9 +126,16 @@ export declare function classifyMessage(message: {
|
|
|
126
126
|
export declare function attach<E extends object>(error: E, id: number): E;
|
|
127
127
|
/** Overflow-classification evidence, including errors received before token usage is available. */
|
|
128
128
|
export interface ContextOverflowMessage extends Pick<AssistantMessage, "errorId" | "stopReason" | "errorMessage"> {
|
|
129
|
-
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite">;
|
|
129
|
+
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite" | "contextTokens">;
|
|
130
130
|
}
|
|
131
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235).
|
|
133
|
+
*
|
|
134
|
+
* Prefers `contextTokens` when the provider reports it: providers that run
|
|
135
|
+
* several model calls per turn (Cursor's server-side tool loop) report
|
|
136
|
+
* `input`/`cacheRead` summed across those calls, which can exceed the window
|
|
137
|
+
* many times over while the conversation itself stays small.
|
|
138
|
+
*/
|
|
132
139
|
export declare function isUsageBackedContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean;
|
|
133
140
|
/** Classify overflow from error flags, available token usage, or provider error text. */
|
|
134
141
|
export declare function isContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Model } from "@oh-my-pi/pi-catalog/types";
|
|
2
2
|
import type { ImageGenerationOptions, ImageGenerationRequest, ImageGenerationResult } from "./types.js";
|
|
3
3
|
export declare function generateHostedImage(model: Model, request: ImageGenerationRequest, options: ImageGenerationOptions): Promise<ImageGenerationResult>;
|
|
@@ -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 {
|
|
20
|
+
export { cursorExecReadPath, cursorRawReadPath, omitUndefinedArgs, piEscapeRegexLiteral, piGrepSkip, piJoinPath, piLimit, piLsPath, piReadDisplayPath, piReadPath, piReadPathHasRange, piTimeout, 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
|
/**
|
|
@@ -22,18 +22,14 @@
|
|
|
22
22
|
* A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
|
|
23
23
|
* selector.
|
|
24
24
|
*
|
|
25
|
-
* `read` exposes no range kwargs
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* wrong for a caller that asked for exactly `limit` lines from `offset`. The
|
|
34
|
-
* wire result is an opaque `output` string, so the hashline and line-number
|
|
35
|
-
* gutter that `raw` also drops carry nothing the frame's contract needs.
|
|
36
|
-
* A range-free read keeps the ordinary form — whole-file reads want them.
|
|
25
|
+
* `read` exposes no range kwargs; `offset` and `limit` are composed onto the
|
|
26
|
+
* path. A negative offset needs the source line count and is resolved by the
|
|
27
|
+
* coding-agent bridge before calling this helper. `limit: 0` has no selector
|
|
28
|
+
* representation and returns `null`.
|
|
29
|
+
*
|
|
30
|
+
* Range selectors are raw because plain ranges add context lines. Cursor
|
|
31
|
+
* numbers the returned text itself, so it cannot use read's hashline gutter.
|
|
32
|
+
* Use [`cursorExecReadPath`] for a range-free read, which also needs `:raw`.
|
|
37
33
|
*/
|
|
38
34
|
export declare function piReadPath(readPath: string, offset?: number, limit?: number): string | null;
|
|
39
35
|
/**
|
|
@@ -55,14 +51,13 @@ export declare function piReadPathHasRange(readPath: string): boolean;
|
|
|
55
51
|
*/
|
|
56
52
|
export declare function cursorRawReadPath(readPath: string): string;
|
|
57
53
|
/**
|
|
58
|
-
*
|
|
54
|
+
* Raw selector for a Cursor exec read, including edit-owned materialization.
|
|
59
55
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* second `:raw` onto the already-composed selector.
|
|
56
|
+
* Compose a requested window before forcing `:raw` on whole-file reads. The
|
|
57
|
+
* caller drops `offset`/`limit` after composing so the handler cannot append
|
|
58
|
+
* another selector.
|
|
64
59
|
*/
|
|
65
|
-
export declare function
|
|
60
|
+
export declare function cursorExecReadPath(readPath: string, offset?: number, limit?: number): string | null;
|
|
66
61
|
/**
|
|
67
62
|
* The same range as {@link piReadPath}, rendered for a transcript block rather
|
|
68
63
|
* than for execution.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CredentialRankingStrategy, UsageProvider } from "../usage.js";
|
|
2
|
+
export declare const commandCodeUsageProvider: UsageProvider;
|
|
3
|
+
/** Ranks Command Code accounts by the 5-hour and weekly credit windows. */
|
|
4
|
+
export declare const commandCodeRankingStrategy: CredentialRankingStrategy;
|
|
@@ -5,6 +5,13 @@ export interface LocalWorkSource {
|
|
|
5
5
|
}
|
|
6
6
|
export declare class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
7
7
|
#private;
|
|
8
|
+
/**
|
|
9
|
+
* Events pushed while no consumer was waiting. The iterator dequeues by
|
|
10
|
+
* advancing {@link #queueHead} instead of `shift()` — O(remaining) per event,
|
|
11
|
+
* quadratic for a consumer draining a backlog — so while it drains, the
|
|
12
|
+
* slots before the head are consumed (cleared) placeholders. Do not mutate
|
|
13
|
+
* the array while the stream is being iterated.
|
|
14
|
+
*/
|
|
8
15
|
queue: T[];
|
|
9
16
|
waiting: Array<{
|
|
10
17
|
resolve: (value: IteratorResult<T>) => void;
|
|
@@ -28,8 +28,14 @@ export interface OpenAIStreamRequestInit {
|
|
|
28
28
|
fetch?: FetchImpl;
|
|
29
29
|
/** Optional caller-specific gate composed with shared transport retry exclusions. */
|
|
30
30
|
shouldRetryResponse?: (response: Response, bodyText: string) => boolean | Promise<boolean>;
|
|
31
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Raw wire-frame observer (`onSseEvent` debug pipeline). Leave it unset
|
|
33
|
+
* when no diagnostic listener exists: any observer turns on per-line raw
|
|
34
|
+
* capture for every frame.
|
|
35
|
+
*/
|
|
32
36
|
onSseEvent?: SseEventObserver;
|
|
37
|
+
/** Called when the stream ends on the OpenAI `[DONE]` sentinel; independent of {@link onSseEvent}. */
|
|
38
|
+
onDoneSentinel?: () => void;
|
|
33
39
|
}
|
|
34
40
|
export interface OpenAIStreamHandle<TEvent> {
|
|
35
41
|
/** Decoded `data:` payloads; terminates on `[DONE]` or stream end. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "18.4.
|
|
3
|
+
"version": "18.4.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.4.
|
|
159
|
-
"@oh-my-pi/pi-catalog": "18.4.
|
|
160
|
-
"@oh-my-pi/pi-natives": "18.4.
|
|
161
|
-
"@oh-my-pi/pi-utils": "18.4.
|
|
162
|
-
"@oh-my-pi/pi-wire": "18.4.
|
|
158
|
+
"@oh-my-pi/omptype": "18.4.3",
|
|
159
|
+
"@oh-my-pi/pi-catalog": "18.4.3",
|
|
160
|
+
"@oh-my-pi/pi-natives": "18.4.3",
|
|
161
|
+
"@oh-my-pi/pi-utils": "18.4.3",
|
|
162
|
+
"@oh-my-pi/pi-wire": "18.4.3"
|
|
163
163
|
},
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@types/bun": "^1.3.14"
|
package/src/dialect/thinking.ts
CHANGED
|
@@ -36,6 +36,18 @@ const TAGS: readonly Tag[] = [
|
|
|
36
36
|
const OPENS = TAGS.map(tag => tag.open);
|
|
37
37
|
const IMPLIED_OPEN_TAGS = TAGS.filter(tag => tag.impliedOpen);
|
|
38
38
|
const IMPLIED_OPEN_DELIMITERS = [...OPENS, ...IMPLIED_OPEN_TAGS.map(tag => tag.close)];
|
|
39
|
+
/** A hold needs the buffer tail to be a proper prefix of some delimiter, so it must be shorter than this. */
|
|
40
|
+
const MAX_DELIMITER_LENGTH = Math.max(...IMPLIED_OPEN_DELIMITERS.map(delimiter => delimiter.length));
|
|
41
|
+
const BACKTICK = 0x60;
|
|
42
|
+
const BOUNDARY_LEAD_CODES = [...IMPLIED_OPEN_DELIMITERS.map(delimiter => delimiter.charCodeAt(0)), BACKTICK];
|
|
43
|
+
/**
|
|
44
|
+
* `1` at the char code of every character a {@link scanVisible} boundary can
|
|
45
|
+
* start on: the first character of any delimiter (a tag open, an implied close,
|
|
46
|
+
* or a partial of either) and the backtick. Every other character — including
|
|
47
|
+
* any code past the table — is skipped without comparing.
|
|
48
|
+
*/
|
|
49
|
+
const BOUNDARY_LEAD = new Uint8Array(Math.max(...BOUNDARY_LEAD_CODES) + 1);
|
|
50
|
+
for (const code of BOUNDARY_LEAD_CODES) BOUNDARY_LEAD[code] = 1;
|
|
39
51
|
|
|
40
52
|
export interface ThinkingInbandScannerOptions {
|
|
41
53
|
/**
|
|
@@ -240,20 +252,26 @@ type VisibleHit =
|
|
|
240
252
|
*/
|
|
241
253
|
function scanVisible(buffer: string, final: boolean, impliedOpen: boolean): VisibleHit {
|
|
242
254
|
const delimiters = impliedOpen ? IMPLIED_OPEN_DELIMITERS : OPENS;
|
|
255
|
+
// Only the tail can be a proper prefix of a delimiter.
|
|
256
|
+
const holdFrom = final ? buffer.length : buffer.length - MAX_DELIMITER_LENGTH + 1;
|
|
243
257
|
for (let i = 0; i < buffer.length; i++) {
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
258
|
+
const code = buffer.charCodeAt(i);
|
|
259
|
+
if (code >= BOUNDARY_LEAD.length || BOUNDARY_LEAD[code] === 0) continue;
|
|
260
|
+
for (const tag of TAGS) {
|
|
261
|
+
if (buffer.startsWith(tag.open, i)) return { kind: "tag", index: i, tag };
|
|
262
|
+
}
|
|
246
263
|
if (impliedOpen) {
|
|
247
|
-
const
|
|
248
|
-
|
|
264
|
+
for (const tag of IMPLIED_OPEN_TAGS) {
|
|
265
|
+
if (buffer.startsWith(tag.close, i)) return { kind: "impliedClose", index: i, tag };
|
|
266
|
+
}
|
|
249
267
|
}
|
|
250
|
-
if (
|
|
268
|
+
if (i >= holdFrom) {
|
|
251
269
|
const rest = buffer.slice(i);
|
|
252
270
|
if (delimiters.some(delimiter => delimiter.length > rest.length && delimiter.startsWith(rest))) {
|
|
253
271
|
return { kind: "hold", index: i };
|
|
254
272
|
}
|
|
255
273
|
}
|
|
256
|
-
if (
|
|
274
|
+
if (code === BACKTICK) {
|
|
257
275
|
const ticks = backtickRun(buffer, i);
|
|
258
276
|
if (!final && i + ticks === buffer.length) return { kind: "hold", index: i };
|
|
259
277
|
return { kind: "code", index: i, ticks };
|
package/src/error/flags.ts
CHANGED
|
@@ -875,14 +875,21 @@ export function attach<E extends object>(error: E, id: number): E {
|
|
|
875
875
|
|
|
876
876
|
/** Overflow-classification evidence, including errors received before token usage is available. */
|
|
877
877
|
export interface ContextOverflowMessage extends Pick<AssistantMessage, "errorId" | "stopReason" | "errorMessage"> {
|
|
878
|
-
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite">;
|
|
878
|
+
readonly usage?: Pick<Usage, "input" | "cacheRead" | "cacheWrite" | "contextTokens">;
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
-
/**
|
|
881
|
+
/**
|
|
882
|
+
* Provider-reported usage proves context-window excess — authoritative, compaction-owned (#9235).
|
|
883
|
+
*
|
|
884
|
+
* Prefers `contextTokens` when the provider reports it: providers that run
|
|
885
|
+
* several model calls per turn (Cursor's server-side tool loop) report
|
|
886
|
+
* `input`/`cacheRead` summed across those calls, which can exceed the window
|
|
887
|
+
* many times over while the conversation itself stays small.
|
|
888
|
+
*/
|
|
882
889
|
export function isUsageBackedContextOverflow(message: ContextOverflowMessage, contextWindow?: number): boolean {
|
|
883
890
|
const usage = message.usage;
|
|
884
891
|
if (!contextWindow || !usage) return false;
|
|
885
|
-
const inputTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
892
|
+
const inputTokens = usage.contextTokens ?? usage.input + usage.cacheRead + usage.cacheWrite;
|
|
886
893
|
return inputTokens > contextWindow;
|
|
887
894
|
}
|
|
888
895
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Api, type Model, modelKind } from "@oh-my-pi/pi-catalog/types";
|
|
2
2
|
import {
|
|
3
3
|
applyCodexResidencyHeader,
|
|
4
4
|
CODEX_BASE_URL,
|
|
@@ -155,7 +155,10 @@ export async function generateHostedImage(
|
|
|
155
155
|
action: content.length > 1 ? "edit" : "generate",
|
|
156
156
|
output_format: "webp",
|
|
157
157
|
...(size ? { size } : {}),
|
|
158
|
-
|
|
158
|
+
// A chat model generating on its own lets the host pick the image model.
|
|
159
|
+
...(model.api === "openai-responses" && modelKind(model) === "image"
|
|
160
|
+
? { model: model.requestModelId ?? model.id }
|
|
161
|
+
: {}),
|
|
159
162
|
};
|
|
160
163
|
const body = {
|
|
161
164
|
model: carrier.requestModelId ?? carrier.id,
|
|
@@ -446,6 +446,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
446
446
|
};
|
|
447
447
|
|
|
448
448
|
const blocks = output.content as Block[];
|
|
449
|
+
const contentIndexByBlockIndex = new Map<number, number>();
|
|
449
450
|
let rawRequestDump: RawHttpRequestDump | undefined;
|
|
450
451
|
const region = resolveBedrockRegion(model.id, options);
|
|
451
452
|
|
|
@@ -640,7 +641,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
640
641
|
if (messageType === "exception") {
|
|
641
642
|
const exceptionType = message.headers[":exception-type"] || "Exception";
|
|
642
643
|
const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
|
|
643
|
-
const errorMessage = payload?.message ||
|
|
644
|
+
const errorMessage = payload?.message || PAYLOAD_DECODER.decode(message.payload);
|
|
644
645
|
const text = `${exceptionType}: ${errorMessage}`;
|
|
645
646
|
throw new AIError.BedrockApiError(text, bedrockStreamExceptionStatus(exceptionType), {
|
|
646
647
|
code: exceptionType,
|
|
@@ -648,7 +649,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
648
649
|
}
|
|
649
650
|
if (messageType === "error") {
|
|
650
651
|
const code = message.headers[":error-code"] || "UnknownError";
|
|
651
|
-
const errorMessage = message.headers[":error-message"] ||
|
|
652
|
+
const errorMessage = message.headers[":error-message"] || PAYLOAD_DECODER.decode(message.payload);
|
|
652
653
|
throw new AIError.BedrockApiError(`${code}: ${errorMessage}`, bedrockStreamExceptionStatus(code), {
|
|
653
654
|
code,
|
|
654
655
|
});
|
|
@@ -673,16 +674,35 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
673
674
|
}
|
|
674
675
|
case "contentBlockStart": {
|
|
675
676
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
676
|
-
handleContentBlockStart(
|
|
677
|
+
handleContentBlockStart(
|
|
678
|
+
payload as ContentBlockStartEvent,
|
|
679
|
+
blocks,
|
|
680
|
+
contentIndexByBlockIndex,
|
|
681
|
+
output,
|
|
682
|
+
stream,
|
|
683
|
+
sentinelInjected,
|
|
684
|
+
);
|
|
677
685
|
break;
|
|
678
686
|
}
|
|
679
687
|
case "contentBlockDelta": {
|
|
680
688
|
if (!firstTokenTime) firstTokenTime = performance.now();
|
|
681
|
-
handleContentBlockDelta(
|
|
689
|
+
handleContentBlockDelta(
|
|
690
|
+
payload as ContentBlockDeltaEvent,
|
|
691
|
+
blocks,
|
|
692
|
+
contentIndexByBlockIndex,
|
|
693
|
+
output,
|
|
694
|
+
stream,
|
|
695
|
+
);
|
|
682
696
|
break;
|
|
683
697
|
}
|
|
684
698
|
case "contentBlockStop": {
|
|
685
|
-
handleContentBlockStop(
|
|
699
|
+
handleContentBlockStop(
|
|
700
|
+
payload as ContentBlockStopEvent,
|
|
701
|
+
blocks,
|
|
702
|
+
contentIndexByBlockIndex,
|
|
703
|
+
output,
|
|
704
|
+
stream,
|
|
705
|
+
);
|
|
686
706
|
break;
|
|
687
707
|
}
|
|
688
708
|
case "messageStop": {
|
|
@@ -782,18 +802,40 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
|
|
|
782
802
|
return stream;
|
|
783
803
|
};
|
|
784
804
|
|
|
805
|
+
/** Shared across events: every payload decode is a complete, non-streaming call. */
|
|
806
|
+
const PAYLOAD_DECODER = new TextDecoder();
|
|
807
|
+
|
|
785
808
|
function safeParsePayload(payload: Uint8Array): unknown {
|
|
786
809
|
if (payload.length === 0) return {};
|
|
787
810
|
try {
|
|
788
|
-
return JSON.parse(
|
|
811
|
+
return JSON.parse(PAYLOAD_DECODER.decode(payload));
|
|
789
812
|
} catch {
|
|
790
813
|
return undefined;
|
|
791
814
|
}
|
|
792
815
|
}
|
|
793
816
|
|
|
817
|
+
/**
|
|
818
|
+
* Append a streamed block and index it by Bedrock's `contentBlockIndex`, so
|
|
819
|
+
* per-delta routing is an O(1) lookup instead of a scan over every block
|
|
820
|
+
* (quadratic over a long turn). The first block registered for an index wins,
|
|
821
|
+
* as the first-match scan did. Returns the block's content index.
|
|
822
|
+
*/
|
|
823
|
+
function pushStreamBlock(
|
|
824
|
+
blocks: Block[],
|
|
825
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
826
|
+
block: Block,
|
|
827
|
+
contentBlockIndex: number,
|
|
828
|
+
): number {
|
|
829
|
+
const contentIndex = blocks.length;
|
|
830
|
+
blocks.push(block);
|
|
831
|
+
if (!contentIndexByBlockIndex.has(contentBlockIndex)) contentIndexByBlockIndex.set(contentBlockIndex, contentIndex);
|
|
832
|
+
return contentIndex;
|
|
833
|
+
}
|
|
834
|
+
|
|
794
835
|
function handleContentBlockStart(
|
|
795
836
|
event: ContentBlockStartEvent,
|
|
796
837
|
blocks: Block[],
|
|
838
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
797
839
|
output: AssistantMessage,
|
|
798
840
|
stream: AssistantMessageEventStream,
|
|
799
841
|
sentinelInjected: boolean,
|
|
@@ -815,29 +857,29 @@ function handleContentBlockStart(
|
|
|
815
857
|
[kStreamingPartialJson]: "",
|
|
816
858
|
[kStreamingBlockIndex]: index,
|
|
817
859
|
};
|
|
818
|
-
|
|
819
|
-
stream.push({ type: "toolcall_start", contentIndex
|
|
860
|
+
const contentIndex = pushStreamBlock(blocks, contentIndexByBlockIndex, block, index);
|
|
861
|
+
stream.push({ type: "toolcall_start", contentIndex, partial: output });
|
|
820
862
|
}
|
|
821
863
|
}
|
|
822
864
|
|
|
823
865
|
function handleContentBlockDelta(
|
|
824
866
|
event: ContentBlockDeltaEvent,
|
|
825
867
|
blocks: Block[],
|
|
868
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
826
869
|
output: AssistantMessage,
|
|
827
870
|
stream: AssistantMessageEventStream,
|
|
828
871
|
): void {
|
|
829
872
|
const contentBlockIndex = event.contentBlockIndex;
|
|
830
873
|
const delta = event.delta;
|
|
831
|
-
let index =
|
|
874
|
+
let index = contentIndexByBlockIndex.get(contentBlockIndex) ?? -1;
|
|
832
875
|
let block = blocks[index];
|
|
833
876
|
|
|
834
877
|
if (delta?.text !== undefined) {
|
|
835
878
|
// If no text block exists yet, create one — `handleContentBlockStart` is not sent for text blocks
|
|
836
879
|
if (!block) {
|
|
837
880
|
const newBlock: Block = { type: "text", text: "", [kStreamingBlockIndex]: contentBlockIndex };
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
block = blocks[index];
|
|
881
|
+
index = pushStreamBlock(blocks, contentIndexByBlockIndex, newBlock, contentBlockIndex);
|
|
882
|
+
block = newBlock;
|
|
841
883
|
stream.push({ type: "text_start", contentIndex: index, partial: output });
|
|
842
884
|
}
|
|
843
885
|
if (block.type === "text") {
|
|
@@ -863,9 +905,8 @@ function handleContentBlockDelta(
|
|
|
863
905
|
thinkingSignature: "",
|
|
864
906
|
[kStreamingBlockIndex]: contentBlockIndex,
|
|
865
907
|
};
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
thinkingBlock = blocks[thinkingIndex];
|
|
908
|
+
thinkingIndex = pushStreamBlock(blocks, contentIndexByBlockIndex, newBlock, contentBlockIndex);
|
|
909
|
+
thinkingBlock = newBlock;
|
|
869
910
|
stream.push({ type: "thinking_start", contentIndex: thinkingIndex, partial: output });
|
|
870
911
|
}
|
|
871
912
|
|
|
@@ -901,10 +942,11 @@ function handleMetadata(event: MetadataEvent, model: Model<"bedrock-converse-str
|
|
|
901
942
|
function handleContentBlockStop(
|
|
902
943
|
event: ContentBlockStopEvent,
|
|
903
944
|
blocks: Block[],
|
|
945
|
+
contentIndexByBlockIndex: Map<number, number>,
|
|
904
946
|
output: AssistantMessage,
|
|
905
947
|
stream: AssistantMessageEventStream,
|
|
906
948
|
): void {
|
|
907
|
-
const index =
|
|
949
|
+
const index = contentIndexByBlockIndex.get(event.contentBlockIndex) ?? -1;
|
|
908
950
|
const block = blocks[index];
|
|
909
951
|
if (!block) return;
|
|
910
952
|
|
|
@@ -24,6 +24,8 @@ const PRELUDE_CRC_LEN = 4;
|
|
|
24
24
|
const MESSAGE_CRC_LEN = 4;
|
|
25
25
|
const HEADER_BLOCK_OFFSET = PRELUDE_LEN + PRELUDE_CRC_LEN;
|
|
26
26
|
const MIN_MESSAGE_LEN = HEADER_BLOCK_OFFSET + MESSAGE_CRC_LEN;
|
|
27
|
+
/** Shared across messages: every header decode is a complete, non-streaming call. */
|
|
28
|
+
const HEADER_DECODER = new TextDecoder();
|
|
27
29
|
|
|
28
30
|
export interface EventStreamMessage {
|
|
29
31
|
/** Lower-cased copy is *not* applied — Bedrock uses casing like `:event-type` verbatim. */
|
|
@@ -64,12 +66,11 @@ export function decodeMessage(frame: Uint8Array): EventStreamMessage {
|
|
|
64
66
|
function parseHeaders(buf: Uint8Array): Record<string, string> {
|
|
65
67
|
const out: Record<string, string> = {};
|
|
66
68
|
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
67
|
-
const decoder = new TextDecoder();
|
|
68
69
|
let p = 0;
|
|
69
70
|
while (p < buf.length) {
|
|
70
71
|
const nameLen = view.getUint8(p);
|
|
71
72
|
p += 1;
|
|
72
|
-
const name =
|
|
73
|
+
const name = HEADER_DECODER.decode(buf.subarray(p, p + nameLen));
|
|
73
74
|
p += nameLen;
|
|
74
75
|
const type = view.getUint8(p);
|
|
75
76
|
p += 1;
|
|
@@ -108,7 +109,7 @@ function parseHeaders(buf: Uint8Array): Record<string, string> {
|
|
|
108
109
|
// string
|
|
109
110
|
const len = view.getUint16(p, false);
|
|
110
111
|
p += 2;
|
|
111
|
-
out[name] =
|
|
112
|
+
out[name] = HEADER_DECODER.decode(buf.subarray(p, p + len));
|
|
112
113
|
p += len;
|
|
113
114
|
break;
|
|
114
115
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { $env } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { $env, type ServerSentEvent } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import * as AIError from "../error";
|
|
3
3
|
import { getEnvApiKey } from "../stream";
|
|
4
4
|
import type {
|
|
5
5
|
AssistantMessage,
|
|
6
6
|
Context,
|
|
7
7
|
Model,
|
|
8
|
-
RawSseEvent,
|
|
9
8
|
ServiceTier,
|
|
10
9
|
StreamFunction,
|
|
11
10
|
StreamOptions,
|
|
@@ -104,7 +103,7 @@ const streamAzureOpenAIResponsesOnce = (
|
|
|
104
103
|
const { requestAbortController, requestSignal } = abortTracker;
|
|
105
104
|
const onSseEvent = options?.onSseEvent;
|
|
106
105
|
const rawSseObserver = onSseEvent
|
|
107
|
-
? (event:
|
|
106
|
+
? (event: ServerSentEvent) => {
|
|
108
107
|
if (!event.event && event.data && event.data !== "[DONE]") {
|
|
109
108
|
try {
|
|
110
109
|
const parsed = JSON.parse(event.data);
|
|
@@ -120,7 +119,7 @@ const streamAzureOpenAIResponsesOnce = (
|
|
|
120
119
|
}
|
|
121
120
|
} catch {}
|
|
122
121
|
}
|
|
123
|
-
onSseEvent(event, model);
|
|
122
|
+
onSseEvent({ event: event.event, data: event.data, raw: [...event.raw] }, model);
|
|
124
123
|
}
|
|
125
124
|
: undefined;
|
|
126
125
|
|
|
@@ -25,18 +25,14 @@ import * as path from "node:path";
|
|
|
25
25
|
* A `pi_read` range composed onto the path as `read`'s inline `:raw:N+K`
|
|
26
26
|
* selector.
|
|
27
27
|
*
|
|
28
|
-
* `read` exposes no range kwargs
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* wrong for a caller that asked for exactly `limit` lines from `offset`. The
|
|
37
|
-
* wire result is an opaque `output` string, so the hashline and line-number
|
|
38
|
-
* gutter that `raw` also drops carry nothing the frame's contract needs.
|
|
39
|
-
* A range-free read keeps the ordinary form — whole-file reads want them.
|
|
28
|
+
* `read` exposes no range kwargs; `offset` and `limit` are composed onto the
|
|
29
|
+
* path. A negative offset needs the source line count and is resolved by the
|
|
30
|
+
* coding-agent bridge before calling this helper. `limit: 0` has no selector
|
|
31
|
+
* representation and returns `null`.
|
|
32
|
+
*
|
|
33
|
+
* Range selectors are raw because plain ranges add context lines. Cursor
|
|
34
|
+
* numbers the returned text itself, so it cannot use read's hashline gutter.
|
|
35
|
+
* Use [`cursorExecReadPath`] for a range-free read, which also needs `:raw`.
|
|
40
36
|
*/
|
|
41
37
|
export function piReadPath(readPath: string, offset?: number, limit?: number): string | null {
|
|
42
38
|
if (limit !== undefined && Math.floor(limit) <= 0) return null;
|
|
@@ -100,14 +96,13 @@ export function cursorRawReadPath(readPath: string): string {
|
|
|
100
96
|
}
|
|
101
97
|
|
|
102
98
|
/**
|
|
103
|
-
*
|
|
99
|
+
* Raw selector for a Cursor exec read, including edit-owned materialization.
|
|
104
100
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* second `:raw` onto the already-composed selector.
|
|
101
|
+
* Compose a requested window before forcing `:raw` on whole-file reads. The
|
|
102
|
+
* caller drops `offset`/`limit` after composing so the handler cannot append
|
|
103
|
+
* another selector.
|
|
109
104
|
*/
|
|
110
|
-
export function
|
|
105
|
+
export function cursorExecReadPath(readPath: string, offset?: number, limit?: number): string | null {
|
|
111
106
|
const ranged = piReadPath(readPath, offset, limit);
|
|
112
107
|
if (ranged === null) return null;
|
|
113
108
|
return cursorRawReadPath(ranged);
|