@oh-my-pi/pi-ai 17.3.7 → 17.3.8
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 +23 -0
- package/README.md +1 -1
- package/dist/types/auth/sqlite-credential-store.d.ts +2 -15
- package/dist/types/auth-storage.d.ts +4 -2
- package/dist/types/dialect/thinking-fence-strip.d.ts +39 -0
- package/dist/types/error/finalize.d.ts +2 -0
- package/dist/types/error/flags.d.ts +9 -0
- package/dist/types/providers/cursor/interaction-query.d.ts +15 -0
- package/dist/types/providers/cursor.d.ts +11 -2
- package/dist/types/providers/openai-shared.d.ts +18 -0
- package/dist/types/registry/oauth/opencode.d.ts +16 -9
- package/dist/types/types.d.ts +7 -0
- package/dist/types/usage.d.ts +10 -0
- package/package.json +5 -5
- package/src/auth/sqlite-credential-store.ts +11 -24
- package/src/auth-storage.ts +62 -10
- package/src/dialect/thinking-fence-strip.ts +92 -0
- package/src/error/finalize.ts +4 -0
- package/src/error/flags.ts +77 -4
- package/src/oneshot-retry.ts +4 -0
- package/src/providers/amazon-bedrock.ts +3 -2
- package/src/providers/anthropic.ts +6 -0
- package/src/providers/cursor/interaction-query.ts +212 -0
- package/src/providers/cursor/proto/agent.proto +25 -3
- package/src/providers/cursor.ts +106 -10
- package/src/providers/google-shared.ts +24 -7
- package/src/providers/openai-codex-responses.ts +2 -0
- package/src/providers/openai-completions.ts +3 -7
- package/src/providers/openai-shared.ts +97 -3
- package/src/registry/oauth/callback-server.ts +31 -0
- package/src/registry/oauth/opencode.ts +27 -13
- package/src/registry/oauth/perplexity.ts +6 -3
- package/src/registry/opencode-go.ts +1 -1
- package/src/registry/opencode-zen.ts +1 -1
- package/src/stream.ts +34 -9
- package/src/types.ts +7 -0
- package/src/usage/claude.ts +9 -2
- package/src/usage.ts +10 -0
- package/src/utils/openai-http.ts +30 -0
- package/src/utils/validation.ts +188 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.3.8] - 2026-08-19
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Fixed Gemini thought summaries occasionally leaking a raw `` ```thinking `` / `` ``````thinking `` fence delimiter into the reasoning block, so it no longer shows up as fence spam in the thinking display or persisted transcripts ([#8719](https://github.com/can1357/oh-my-pi/issues/8719)).
|
|
10
|
+
- Fixed the OpenCode Go login prompting for an "OpenCode Zen API key": the shared login flow now names the provider you selected, so connecting OpenCode Go asks for an OpenCode Go key (the `opencode.ai/auth` console is still shared, as documented upstream) ([#8738](https://github.com/can1357/oh-my-pi/issues/8738)).
|
|
11
|
+
- Fixed Anthropic-compatible endpoints with strict prompt validation (e.g. Z.AI GLM `api.z.ai/api/anthropic`, which rejects the whole request with `400 code 1213 "The prompt parameter was not received normally"`) failing sessions once a tool returned empty output on a vision-capable model: empty successful `tool_result` blocks now encode as `content: ""` instead of `content: []`, which both the official API and strict compatible endpoints accept.
|
|
12
|
+
- Fixed `retry.usageReservePct` (Reserve Margin) ignoring Claude Fable/Mythos weekly tier usage until it hit 100%, so a Fable model kept serving turns past the configured reserve; reserve health now honors the mapped tier row while credential-wide hard blocks still require confirmed exhaustion ([#8773](https://github.com/can1357/oh-my-pi/issues/8773)).
|
|
13
|
+
- Fixed `cursor-agent` streams stalling with "Provider stream stalled while waiting for the next event" when Cursor asked the client to approve a hosted WebFetch / web search (reproduced on `cursor-grok-4.6-xhigh` after "I'll fetch the page…"). Those `interaction_query` frames — including the newer WebFetch field 9 this proto did not name — were dropped, so the server waited forever and the idle watchdog aborted a live connection. Permission queries are now answered; hosted search/fetch is approved, unnamed permission fields get an `approved` reply on the same field number, and prompts this client cannot serve are rejected so the turn can continue.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Fixed thinking effort selections being ignored for local Qwen 3.8+ models on llama.cpp and vLLM: the Qwen chat-completions dialects only toggled `enable_thinking`, so the chat template always reasoned at its `xhigh` default no matter which level was selected. The encoder now routes the requested effort onto the template's `reasoning_effort` kwarg (`chat_template_kwargs` for both Qwen dialects, plus the top-level field newer llama.cpp builds map natively).
|
|
18
|
+
- Fixed OpenAI Completions, Amazon Bedrock, and Cursor providers ignoring `onPayload` replacement payloads. The hook now transforms the actual request body sent upstream on these providers, matching the Anthropic/Gemini/OpenAI Responses replacement contract. `devin-agent` still does not fire the hook (its payload is a protobuf object).
|
|
19
|
+
- Fixed Codex requests failing outright when the signed-in ChatGPT account is not entitled to the requested model; the exact model denial is now classified as an account-policy error so credential rotation can reach an entitled sibling account
|
|
20
|
+
- Fixed Perplexity email-OTP login after its verification response renamed the encrypted session token from `token` to `challenge_token`.
|
|
21
|
+
- Cloud Code Assist Gemini 3.6/3.7 Flash requests at `minimal` now send `thinkingLevel: LOW` on the aliased `-low` SKU instead of `MINIMAL`, which the API rejects with HTTP 400.
|
|
22
|
+
- Answer Cursor `interaction_query` permission gates (hosted web search, Exa, unnamed field-9 WebFetch) so the Run RPC continues instead of sitting silent until the 300s idle watchdog.
|
|
23
|
+
- Fixed provider tool calls arriving with flattened array argument paths (e.g. Gemini's `questions[0].id`) being stripped and rejected by argument validation; well-formed flattened paths are now rebuilt into the nested arrays the tool schema expects ([#8886](https://github.com/can1357/oh-my-pi/issues/8886)).
|
|
24
|
+
- Fixed opencode-go (Console Go) rejecting Responses turns with `400 No tool output found for tool call …` (naming a random call of the batch on each retry) when a model streamed a trailing text/thinking block after its tool calls: `buildResponsesInput` emitted that block as an assistant `message` item wedged between the `function_call` batch and its `function_call_output` items. Such interleaved messages are now hoisted ahead of their call batch (canonical `message(s) → calls → outputs`), which the strict gateway validator accepts; content is unchanged ([#8789](https://github.com/can1357/oh-my-pi/issues/8789)).
|
|
25
|
+
- Fixed the OpenAI-wire transport sleeping on a LiteLLM concurrency-admission 429 (`rate_limit_type: max_parallel_requests`, `Retry-After: 60`) and retrying it up to 6 times (~300s) before session recovery saw the error. Because a 60s hint equals the transport's `maxDelayMs` cap, `fetchWithRetry` kept sleeping and retrying; the request now surfaces on the first attempt so `TurnRecovery`'s concurrency backoff/model fallback runs promptly. Genuine RPM/quota 429s (no such marker) still honor `Retry-After` ([#8854](https://github.com/can1357/oh-my-pi/issues/8854)).
|
|
26
|
+
- Fixed OAuth login (Codex `localhost:1455`, and any `localhost` callback flow) failing on hosts with IPv6 disabled at the kernel (`ipv6.disable=1`). The `::1` companion listener added in #8081 fails there with Bun's generic "Is port X in use?" message (oven-sh/bun#7187), which the in-use check misread as a real collision — tearing down the healthy IPv4 listener and surfacing a bogus "port 1455 is in use" error. The dual-bind path now detects the missing IPv6 loopback up front and serves IPv4 alone ([#8814](https://github.com/can1357/oh-my-pi/issues/8814)).
|
|
27
|
+
|
|
5
28
|
## [17.3.7] - 2026-08-17
|
|
6
29
|
|
|
7
30
|
### Changed
|
package/README.md
CHANGED
|
@@ -634,7 +634,7 @@ All providers accept the base `StreamOptions` (in addition to provider-specific
|
|
|
634
634
|
- `headers`: Extra request headers merged on top of model-defined headers
|
|
635
635
|
- `sessionId`: Provider-specific session identifier (prompt caching/routing)
|
|
636
636
|
- `signal`: Abort in-flight requests
|
|
637
|
-
- `onPayload`: Callback invoked with the provider request payload just before sending
|
|
637
|
+
- `onPayload`: Callback invoked with the provider request payload just before sending. Return a replacement payload object (sync or async) to send it instead of the original; return `undefined` to keep the original. The replacement is applied by every provider that fires the hook — all of them except `devin-agent`, whose payload is a protobuf object and does not fire the hook yet.
|
|
638
638
|
|
|
639
639
|
Example:
|
|
640
640
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* and remote stores share the same contract.
|
|
6
6
|
*/
|
|
7
7
|
import { Database } from "bun:sqlite";
|
|
8
|
+
import { isSqliteBusyError, isSqliteCorruptionError } from "@oh-my-pi/pi-utils";
|
|
8
9
|
import type { AuthCredential, AuthCredentialStore, CredentialRefreshLeaseFence, DisabledCredentialSummary, StoredAuthCredential, StoredCredentialBlock } from "../auth-storage.js";
|
|
9
10
|
import type { OAuthCredentials } from "../registry/oauth/types.js";
|
|
10
11
|
import type { ClientUsageReport, ClientUsageSummary, UsageHistoryEntry, UsageHistoryQuery } from "../usage.js";
|
|
@@ -14,20 +15,7 @@ type SerializedCredentialRecord = {
|
|
|
14
15
|
data: string;
|
|
15
16
|
identityKey: string | null;
|
|
16
17
|
};
|
|
17
|
-
|
|
18
|
-
* SQLite's busy result code family — base `SQLITE_BUSY` plus the extended
|
|
19
|
-
* variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
|
|
20
|
-
* and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
|
|
21
|
-
*/
|
|
22
|
-
export declare function isSqliteBusyError(err: unknown): boolean;
|
|
23
|
-
/**
|
|
24
|
-
* SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
|
|
25
|
-
* (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
|
|
26
|
-
* and `SQLITE_NOTADB` (the file header is not a database). Unlike
|
|
27
|
-
* {@link isSqliteBusyError}, these never clear by retrying: the store must be
|
|
28
|
-
* repaired or replaced, so callers latch and stop touching it.
|
|
29
|
-
*/
|
|
30
|
-
export declare function isSqliteCorruptionError(err: unknown): boolean;
|
|
18
|
+
export { isSqliteBusyError, isSqliteCorruptionError };
|
|
31
19
|
export declare function serializeCredential(provider: string, credential: AuthCredential): SerializedCredentialRecord | null;
|
|
32
20
|
export declare function resolveCredentialIdentityKey(provider: string, credential: AuthCredential): string | null;
|
|
33
21
|
/**
|
|
@@ -112,4 +100,3 @@ export declare class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
112
100
|
acknowledgeLocalChanges(): void;
|
|
113
101
|
close(): void;
|
|
114
102
|
}
|
|
115
|
-
export {};
|
|
@@ -1142,8 +1142,10 @@ export declare class AuthStorage {
|
|
|
1142
1142
|
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
1143
1143
|
* (temporary block via its own backoff — default plus server usage-report
|
|
1144
1144
|
* reset; sticky left intact so the next resolve re-ranks around the block).
|
|
1145
|
-
* -
|
|
1146
|
-
*
|
|
1145
|
+
* - exact Codex model-entitlement denial → temporarily block only that
|
|
1146
|
+
* requested model after provider/model identity matches, then rotate.
|
|
1147
|
+
* - other account-scoped policy denial → temporarily block that account
|
|
1148
|
+
* without marking its credential suspect, then rotate through siblings.
|
|
1147
1149
|
* - otherwise (hard 401 / auth failure) → mark the credential suspect (or
|
|
1148
1150
|
* reload when no broker hook is wired) and block it, then drop matching
|
|
1149
1151
|
* sticky state.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip self-referential reasoning-fence delimiters that a provider leaks
|
|
3
|
+
* *inside* a structured thinking part.
|
|
4
|
+
*
|
|
5
|
+
* The visible-channel healers ({@link ../utils/leaked-thinking-stream},
|
|
6
|
+
* {@link ./fenced-thinking}) split leaked ` ```thinking ` fences out of the
|
|
7
|
+
* *text* stream. They never run over parts a provider already flags as
|
|
8
|
+
* thinking, so when a model (observed on Gemini thought summaries — issue
|
|
9
|
+
* #8719) emits a bare ` ```thinking ` / ` ``````thinking ` opener line between
|
|
10
|
+
* summaries, that delimiter reaches display and persistence verbatim as fence
|
|
11
|
+
* spam inside the reasoning block.
|
|
12
|
+
*
|
|
13
|
+
* This stripper removes only a *standalone* reasoning-fence opener line — a line
|
|
14
|
+
* that is nothing but a run of ≥3 backticks immediately followed by the info
|
|
15
|
+
* string `thinking` or `reasoning`. Such a line is always redundant inside an
|
|
16
|
+
* already-structured thinking block and never carries content. Inline mentions
|
|
17
|
+
* (prose on the same line), language-tagged code fences (` ```rs `), and bare
|
|
18
|
+
* closers (` ``` `) are left untouched so legitimate fenced code inside the
|
|
19
|
+
* reasoning survives.
|
|
20
|
+
*
|
|
21
|
+
* Streaming-safe: deltas may split a line anywhere. A trailing partial line is
|
|
22
|
+
* held only while it remains a viable opener prefix; the moment it cannot be an
|
|
23
|
+
* opener it is flushed and the rest of the line passes through character-level.
|
|
24
|
+
* Correctness never depends on the prefix heuristic — every held line is
|
|
25
|
+
* classified strictly on its newline (or on {@link ThinkingFenceStripper.flush})
|
|
26
|
+
* before it is dropped.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Stateful, line-oriented stripper for leaked reasoning-fence openers in one
|
|
30
|
+
* structured thinking block. One instance per thinking block; feed every
|
|
31
|
+
* thinking delta through {@link push} and drain the tail with {@link flush}.
|
|
32
|
+
*/
|
|
33
|
+
export declare class ThinkingFenceStripper {
|
|
34
|
+
#private;
|
|
35
|
+
/** Consume one thinking delta; returns the sanitized text to emit (may be empty). */
|
|
36
|
+
push(chunk: string): string;
|
|
37
|
+
/** Drain any held partial line at block end; returns text to emit (may be empty). */
|
|
38
|
+
flush(): string;
|
|
39
|
+
}
|
|
@@ -7,6 +7,8 @@ export interface FinalizeOptions {
|
|
|
7
7
|
api?: Api;
|
|
8
8
|
/** Provider id; forwarded to the message formatter for copilot rewrites. */
|
|
9
9
|
provider?: string;
|
|
10
|
+
/** Requested model id; paired with provider for model-entitlement classification. */
|
|
11
|
+
model?: string;
|
|
10
12
|
/** Caller signal, for providers that don't run an abort tracker. */
|
|
11
13
|
signal?: AbortSignal;
|
|
12
14
|
/** Abort tracker, preferred over `signal`: distinguishes caller vs. local aborts. */
|
|
@@ -58,6 +58,13 @@ export declare function classify(error: unknown, api?: Api): number;
|
|
|
58
58
|
export declare function isUsageLimit(error: unknown, api?: Api): boolean;
|
|
59
59
|
/** Whether an upstream rejection is an account-scoped policy denial worth retrying with a sibling credential. */
|
|
60
60
|
export declare function isAccountPolicyError(error: unknown, api?: Api): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Model id from Codex's exact ChatGPT-account entitlement denial. Generic
|
|
63
|
+
* unsupported-model invalid requests deliberately do not match.
|
|
64
|
+
*/
|
|
65
|
+
export declare function codexChatGPTAccountPolicyModel(error: unknown, depth?: number): string | undefined;
|
|
66
|
+
/** Whether the exact Codex entitlement denial applies to this provider and requested model. */
|
|
67
|
+
export declare function isCodexChatGPTAccountPolicyError(error: unknown, provider: string, modelId: string | undefined): boolean;
|
|
61
68
|
/**
|
|
62
69
|
* Strict-tool rejection: grammar too large, schema too complex, or structured
|
|
63
70
|
* outputs unsupported by the model/endpoint.
|
|
@@ -78,6 +85,8 @@ export declare function isFastModeUnsupported(error: unknown): boolean;
|
|
|
78
85
|
export declare function isCopilotTransientModelError(error: unknown): boolean;
|
|
79
86
|
export declare function classifyMessage(message: {
|
|
80
87
|
api?: Api;
|
|
88
|
+
provider?: string;
|
|
89
|
+
model?: string;
|
|
81
90
|
errorId?: number;
|
|
82
91
|
errorMessage?: string;
|
|
83
92
|
errorStatus?: number;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type http2 from "node:http2";
|
|
2
|
+
import { type InteractionQuery } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
3
|
+
/**
|
|
4
|
+
* Answer a Cursor `interaction_query` so the Run RPC can continue.
|
|
5
|
+
*
|
|
6
|
+
* Hosted web search / Exa / unnamed permission gates (field 9 = WebFetch on
|
|
7
|
+
* current Cursor builds) block the turn until the client writes an
|
|
8
|
+
* `interaction_response`. Dropping the frame leaves the HTTP/2 stream alive
|
|
9
|
+
* on heartbeats that are not semantic progress; the lazy idle watchdog then
|
|
10
|
+
* aborts with "Provider stream stalled while waiting for the next event".
|
|
11
|
+
*
|
|
12
|
+
* Unsupported interactive queries are rejected so the server is not stranded.
|
|
13
|
+
* VM setup is left unanswered rather than reporting a fake success.
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleInteractionQuery(query: InteractionQuery, h2Request: http2.ClientHttp2Stream): void;
|
|
@@ -2,7 +2,7 @@ import http2 from "node:http2";
|
|
|
2
2
|
import { type JsonValue } from "@bufbuild/protobuf";
|
|
3
3
|
import type { McpToolDefinition } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
4
4
|
import { type AgentServerMessage, type ConversationStateStructure } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb";
|
|
5
|
-
import type { AssistantMessage, CursorExecHandlerResult, CursorExecHandlers, CursorExecPairing, CursorTodoSyncHandler, CursorToolResultHandler, Message, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
|
|
5
|
+
import type { AssistantMessage, Context, CursorExecHandlerResult, CursorExecHandlers, CursorExecPairing, CursorTodoSyncHandler, CursorToolResultHandler, Message, Model, StreamFunction, StreamOptions, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage } from "../types.js";
|
|
6
6
|
import { kCursorExecResolved, kStreamingBlockIndex, kStreamingBlockKind, kStreamingEnvelopeId, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols.js";
|
|
7
7
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
8
8
|
export declare const CURSOR_API_URL = "https://api2.cursor.sh";
|
|
@@ -32,7 +32,7 @@ export type ToolCallState = ToolCall & {
|
|
|
32
32
|
[kStreamingBlockIndex]: number;
|
|
33
33
|
[kStreamingPartialJson]?: string;
|
|
34
34
|
[kStreamingLastParseLen]?: number;
|
|
35
|
-
[kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm";
|
|
35
|
+
[kStreamingBlockKind]: "mcp" | "todo" | "cursor-exec" | "connect-scm" | "web-fetch";
|
|
36
36
|
[kStreamingEnvelopeId]?: string;
|
|
37
37
|
[kCursorExecResolved]?: true;
|
|
38
38
|
};
|
|
@@ -219,3 +219,12 @@ export declare function buildCursorHistoryForTest(messages: Message[], activeUse
|
|
|
219
219
|
turnUserMessagesJson: JsonValue[];
|
|
220
220
|
turnStepMessagesJson: JsonValue[][];
|
|
221
221
|
};
|
|
222
|
+
export declare function buildGrpcRequest(model: Model<"cursor-agent">, context: Context, options: CursorOptions | undefined, state: {
|
|
223
|
+
conversationId: string;
|
|
224
|
+
blobStore: Map<string, Uint8Array>;
|
|
225
|
+
conversationState?: ConversationStateStructure;
|
|
226
|
+
}): Promise<{
|
|
227
|
+
requestBytes: Uint8Array;
|
|
228
|
+
blobStore: Map<string, Uint8Array>;
|
|
229
|
+
conversationState: ConversationStateStructure;
|
|
230
|
+
}>;
|
|
@@ -231,6 +231,7 @@ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming,
|
|
|
231
231
|
chat_template_kwargs?: {
|
|
232
232
|
enable_thinking?: boolean;
|
|
233
233
|
preserve_thinking?: boolean;
|
|
234
|
+
reasoning_effort?: string;
|
|
234
235
|
};
|
|
235
236
|
reasoning?: {
|
|
236
237
|
effort?: string;
|
|
@@ -403,6 +404,23 @@ export declare function repairOrphanResponsesToolOutputs(input: ResponseInput):
|
|
|
403
404
|
* {@link repairOrphanResponsesToolOutputs}.
|
|
404
405
|
*/
|
|
405
406
|
export declare function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput;
|
|
407
|
+
/**
|
|
408
|
+
* Relocate assistant `message` items wedged inside a tool-call → tool-output
|
|
409
|
+
* batch to before the batch, yielding canonical `message(s) → calls → outputs`
|
|
410
|
+
* order. Idempotent; returns the same array reference when nothing moves.
|
|
411
|
+
*
|
|
412
|
+
* OpenAI's Responses API pairs tool outputs by `call_id` and tolerates any item
|
|
413
|
+
* order, but stricter gateways (notably opencode-go's "Console Go") reject a
|
|
414
|
+
* shape where an assistant message interrupts a `function_call` →
|
|
415
|
+
* `function_call_output` run, 400ing with `No tool output found for tool call …`
|
|
416
|
+
* (naming a random call of the batch on each retry). This arises whenever a
|
|
417
|
+
* model streams a trailing text / demoted-thinking block *after* its tool calls:
|
|
418
|
+
* the block-encode path preserves stream order, emitting the message between the
|
|
419
|
+
* calls and the outputs appended afterward. Moving the already-model-owned
|
|
420
|
+
* message ahead of its call batch keeps content identical while satisfying the
|
|
421
|
+
* strict validator. See #8789.
|
|
422
|
+
*/
|
|
423
|
+
export declare function hoistInterleavedResponsesToolBatchMessages<T extends object>(items: readonly T[]): T[];
|
|
406
424
|
export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean, supportsImageDetailOriginal: boolean, escapeControlTokens?: boolean): ResponseInputContent[] | undefined;
|
|
407
425
|
export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
408
426
|
model: Model<TApi>;
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* OpenCode Zen
|
|
2
|
+
* OpenCode login flow, shared by the OpenCode Zen and OpenCode Go providers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Both are subscription services whose API keys are issued from the same
|
|
5
|
+
* OpenCode Zen console at https://opencode.ai/auth — OpenCode Go keys are
|
|
6
|
+
* minted there after subscribing to Go (see https://opencode.ai/docs/go).
|
|
7
|
+
* This is not OAuth; it's a simple paste-the-API-key flow:
|
|
7
8
|
* 1. Open browser to https://opencode.ai/auth
|
|
8
|
-
* 2. User logs in and copies
|
|
9
|
+
* 2. User logs in (and subscribes to Go, for OpenCode Go) and copies the key
|
|
9
10
|
* 3. User pastes the API key back into the CLI
|
|
10
11
|
*/
|
|
11
12
|
import type { OAuthController } from "./types.js";
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
+
* Log in to an OpenCode subscription provider.
|
|
14
15
|
*
|
|
15
|
-
* Opens browser to
|
|
16
|
-
*
|
|
16
|
+
* Opens the browser to the OpenCode Zen console, prompts the user to paste
|
|
17
|
+
* their API key, and returns it directly (not OAuthCredentials — this isn't
|
|
18
|
+
* OAuth).
|
|
19
|
+
*
|
|
20
|
+
* @param providerName Display name of the provider being connected
|
|
21
|
+
* ("OpenCode Zen" or "OpenCode Go"). Used verbatim in the paste prompt so
|
|
22
|
+
* the CLI reflects the provider the user actually selected instead of always
|
|
23
|
+
* asking for a Zen key.
|
|
17
24
|
*/
|
|
18
|
-
export declare function loginOpenCode(options: OAuthController): Promise<string>;
|
|
25
|
+
export declare function loginOpenCode(options: OAuthController, providerName?: string): Promise<string>;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -701,6 +701,13 @@ export interface ContextSnapshot {
|
|
|
701
701
|
nonMessageTokens: number;
|
|
702
702
|
/** Estimated prompt tokens removed by local history rewrites after this provider snapshot was recorded. */
|
|
703
703
|
historyRewriteTokensRemoved?: number;
|
|
704
|
+
/**
|
|
705
|
+
* Compaction epoch current when this snapshot's provider request was recorded.
|
|
706
|
+
* A later compaction bumps the session epoch, so an anchor whose epoch is
|
|
707
|
+
* older than the current in-flight snapshot describes pre-compaction history
|
|
708
|
+
* and must not override the rebased estimate.
|
|
709
|
+
*/
|
|
710
|
+
compactionEpoch?: number;
|
|
704
711
|
lastMessageTimestamp?: number;
|
|
705
712
|
}
|
|
706
713
|
export interface AssistantMessage {
|
package/dist/types/usage.d.ts
CHANGED
|
@@ -482,6 +482,16 @@ export interface CredentialRankingStrategy {
|
|
|
482
482
|
* account-wide quotas can omit this and use all limits.
|
|
483
483
|
*/
|
|
484
484
|
scopeLimits?(report: UsageReport, context?: CredentialRankingContext): UsageLimit[];
|
|
485
|
+
/**
|
|
486
|
+
* Restrict limits for the opt-in, non-destructive usage-reserve health
|
|
487
|
+
* check ({@link AuthStorage.getModelUsageHealth}). Distinct from
|
|
488
|
+
* {@link scopeLimits}, which gates credential-wide hard blocks: a provider
|
|
489
|
+
* whose model/tier counters are trusted only at confirmed exhaustion for
|
|
490
|
+
* hard-blocking can still expose them here so the reserve margin protects
|
|
491
|
+
* the mapped quota before it hits the cap. Falls back to {@link scopeLimits}
|
|
492
|
+
* when omitted.
|
|
493
|
+
*/
|
|
494
|
+
scopeLimitsForReserve?(report: UsageReport, context?: CredentialRankingContext): UsageLimit[];
|
|
485
495
|
/**
|
|
486
496
|
* Return a provider-local backoff scope for the requested model. Providers
|
|
487
497
|
* with backend-specific quotas use this so one exhausted model family does
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.8",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/omptype": "17.3.
|
|
42
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
43
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
44
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
41
|
+
"@oh-my-pi/omptype": "17.3.8",
|
|
42
|
+
"@oh-my-pi/pi-catalog": "17.3.8",
|
|
43
|
+
"@oh-my-pi/pi-utils": "17.3.8",
|
|
44
|
+
"@oh-my-pi/pi-wire": "17.3.8"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@bufbuild/protoc-gen-es": "^2.12.1",
|
|
@@ -8,7 +8,13 @@ import { Database, type Statement } from "bun:sqlite";
|
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
getAgentDbPath,
|
|
13
|
+
getDbBusyTimeoutMs,
|
|
14
|
+
isSqliteBusyError,
|
|
15
|
+
isSqliteCorruptionError,
|
|
16
|
+
logger,
|
|
17
|
+
} from "@oh-my-pi/pi-utils";
|
|
12
18
|
import type {
|
|
13
19
|
AuthCredential,
|
|
14
20
|
AuthCredentialStore,
|
|
@@ -87,29 +93,10 @@ const LEGACY_CODEX_BLOCK_PROVIDER_KEY = "openai-codex:oauth";
|
|
|
87
93
|
const LEGACY_CODEX_BLOCK_SCOPE = "shared";
|
|
88
94
|
const CODEX_METER_BLOCK_SCOPES = ["chat", "spark"] as const;
|
|
89
95
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*/
|
|
95
|
-
export function isSqliteBusyError(err: unknown): boolean {
|
|
96
|
-
if (err === null || typeof err !== "object") return false;
|
|
97
|
-
const code = (err as { code?: unknown }).code;
|
|
98
|
-
return typeof code === "string" && code.startsWith("SQLITE_BUSY");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
|
|
103
|
-
* (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
|
|
104
|
-
* and `SQLITE_NOTADB` (the file header is not a database). Unlike
|
|
105
|
-
* {@link isSqliteBusyError}, these never clear by retrying: the store must be
|
|
106
|
-
* repaired or replaced, so callers latch and stop touching it.
|
|
107
|
-
*/
|
|
108
|
-
export function isSqliteCorruptionError(err: unknown): boolean {
|
|
109
|
-
if (err === null || typeof err !== "object" || !("code" in err)) return false;
|
|
110
|
-
const code = err.code;
|
|
111
|
-
return typeof code === "string" && (code.startsWith("SQLITE_CORRUPT") || code === "SQLITE_NOTADB");
|
|
112
|
-
}
|
|
96
|
+
// SQLite error classifiers live in pi-utils so the credential store and the
|
|
97
|
+
// model cache share one implementation; re-exported here to preserve the
|
|
98
|
+
// pre-existing `@oh-my-pi/pi-ai/auth-storage` surface.
|
|
99
|
+
export { isSqliteBusyError, isSqliteCorruptionError };
|
|
113
100
|
|
|
114
101
|
function normalizeStoredAccountId(accountId: string | null | undefined): string | null {
|
|
115
102
|
const normalized = accountId?.trim();
|
package/src/auth-storage.ts
CHANGED
|
@@ -1026,6 +1026,28 @@ function resolveOpenAICodexPlanRequirement(provider: string, modelId: string | u
|
|
|
1026
1026
|
return "none";
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
1029
|
+
const MODEL_ACCOUNT_POLICY_BLOCK_SCOPE_PREFIX = "model-policy:";
|
|
1030
|
+
|
|
1031
|
+
function modelAccountPolicyBlockScope(provider: string, modelId: string | undefined): string | undefined {
|
|
1032
|
+
if (provider !== "openai-codex" || typeof modelId !== "string") return undefined;
|
|
1033
|
+
const separator = modelId.lastIndexOf("/");
|
|
1034
|
+
const bareModelId = (separator === -1 ? modelId : modelId.slice(separator + 1)).trim().toLowerCase();
|
|
1035
|
+
if (!bareModelId || bareModelId.includes("\0")) return undefined;
|
|
1036
|
+
return `${MODEL_ACCOUNT_POLICY_BLOCK_SCOPE_PREFIX}${bareModelId}`;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function credentialBlockScopesForRequest(
|
|
1040
|
+
provider: string,
|
|
1041
|
+
strategy: CredentialRankingStrategy | undefined,
|
|
1042
|
+
rankingContext: CredentialRankingContext,
|
|
1043
|
+
blockScope: string | undefined,
|
|
1044
|
+
): readonly string[] {
|
|
1045
|
+
const scopes = strategy?.blockScopes?.(rankingContext) ?? (blockScope ? [blockScope] : []);
|
|
1046
|
+
const modelPolicyScope = modelAccountPolicyBlockScope(provider, rankingContext.modelId);
|
|
1047
|
+
if (!modelPolicyScope || scopes.includes(modelPolicyScope)) return scopes;
|
|
1048
|
+
return [...scopes, modelPolicyScope];
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1029
1051
|
function getUsagePlanType(report: UsageReport | null): string | undefined {
|
|
1030
1052
|
const metadata = report?.metadata;
|
|
1031
1053
|
if (!metadata) return undefined;
|
|
@@ -2169,7 +2191,7 @@ export class AuthStorage {
|
|
|
2169
2191
|
|
|
2170
2192
|
const rankingContext: CredentialRankingContext = { modelId: options?.modelId };
|
|
2171
2193
|
const blockScope = strategy.blockScope?.(rankingContext);
|
|
2172
|
-
const blockScopes =
|
|
2194
|
+
const blockScopes = credentialBlockScopesForRequest(provider, strategy, rankingContext, blockScope);
|
|
2173
2195
|
const candidates = await this.#rankApiKeySelections({
|
|
2174
2196
|
providerKey,
|
|
2175
2197
|
provider,
|
|
@@ -3911,7 +3933,7 @@ export class AuthStorage {
|
|
|
3911
3933
|
const planRequirement = resolveOpenAICodexPlanRequirement(provider, options.modelId);
|
|
3912
3934
|
const planEligibilityByCredential = new Map<number, boolean | undefined>();
|
|
3913
3935
|
const blockScope = strategy.blockScope?.(rankingContext);
|
|
3914
|
-
const blockScopes =
|
|
3936
|
+
const blockScopes = credentialBlockScopesForRequest(provider, strategy, rankingContext, blockScope);
|
|
3915
3937
|
const reserveFraction = Number.isFinite(options.reserveFraction)
|
|
3916
3938
|
? Math.max(0, Math.min(1, options.reserveFraction))
|
|
3917
3939
|
: 0;
|
|
@@ -3961,7 +3983,12 @@ export class AuthStorage {
|
|
|
3961
3983
|
}
|
|
3962
3984
|
if (!report) return { credentialId: entry.id, credentialType, state: "unknown" };
|
|
3963
3985
|
|
|
3964
|
-
|
|
3986
|
+
// Reserve health is opt-in and non-destructive: prefer the strategy's
|
|
3987
|
+
// reserve scoping, which may expose mapped model/tier rows that the
|
|
3988
|
+
// hard-block scoper withholds until confirmed exhaustion.
|
|
3989
|
+
const limits =
|
|
3990
|
+
strategy.scopeLimitsForReserve?.(report, rankingContext) ??
|
|
3991
|
+
this.#getScopedUsageLimits(strategy, report, rankingContext);
|
|
3965
3992
|
if (limits.length === 0) return { credentialId: entry.id, credentialType, state: "unknown" };
|
|
3966
3993
|
|
|
3967
3994
|
const currentLimits = limits.filter(limit => {
|
|
@@ -4378,17 +4405,24 @@ export class AuthStorage {
|
|
|
4378
4405
|
provider: string,
|
|
4379
4406
|
credentialType: AuthCredential["type"],
|
|
4380
4407
|
modelId: string | undefined,
|
|
4408
|
+
blockScopeOverride?: string,
|
|
4381
4409
|
): CredentialBlockRouting {
|
|
4382
4410
|
const providerKey = this.#getProviderTypeKey(provider, credentialType);
|
|
4383
4411
|
const strategy = this.#rankingStrategyResolver?.(provider);
|
|
4384
4412
|
const rankingContext: CredentialRankingContext = { modelId };
|
|
4385
|
-
const
|
|
4413
|
+
const defaultBlockScope = strategy?.blockScope?.(rankingContext);
|
|
4414
|
+
const blockScope = blockScopeOverride ?? defaultBlockScope;
|
|
4415
|
+
const requestBlockScopes = credentialBlockScopesForRequest(provider, strategy, rankingContext, defaultBlockScope);
|
|
4416
|
+
const siblingBlockScopes =
|
|
4417
|
+
blockScopeOverride && !requestBlockScopes.includes(blockScopeOverride)
|
|
4418
|
+
? [...requestBlockScopes, blockScopeOverride]
|
|
4419
|
+
: requestBlockScopes;
|
|
4386
4420
|
return {
|
|
4387
4421
|
providerKey,
|
|
4388
4422
|
strategy,
|
|
4389
4423
|
rankingContext,
|
|
4390
4424
|
blockScope,
|
|
4391
|
-
siblingBlockScopes
|
|
4425
|
+
siblingBlockScopes,
|
|
4392
4426
|
};
|
|
4393
4427
|
}
|
|
4394
4428
|
|
|
@@ -4755,7 +4789,7 @@ export class AuthStorage {
|
|
|
4755
4789
|
const rankingContext: CredentialRankingContext = { modelId: options?.modelId };
|
|
4756
4790
|
const blockScope = strategy?.blockScope?.(rankingContext);
|
|
4757
4791
|
// Reads honour every scope that applies; the scalar above is for args that persist.
|
|
4758
|
-
const blockScopes =
|
|
4792
|
+
const blockScopes = credentialBlockScopesForRequest(provider, strategy, rankingContext, blockScope);
|
|
4759
4793
|
const planRequirement = resolveOpenAICodexPlanRequirement(provider, options?.modelId);
|
|
4760
4794
|
const hasPlanRequirement = planRequirement !== "none";
|
|
4761
4795
|
const checkUsage = strategy !== undefined && (credentials.length > 1 || hasPlanRequirement);
|
|
@@ -6205,8 +6239,10 @@ export class AuthStorage {
|
|
|
6205
6239
|
* - usage-limit / account-rate-limit error → {@link AuthStorage.markUsageLimitReached}
|
|
6206
6240
|
* (temporary block via its own backoff — default plus server usage-report
|
|
6207
6241
|
* reset; sticky left intact so the next resolve re-ranks around the block).
|
|
6208
|
-
* -
|
|
6209
|
-
*
|
|
6242
|
+
* - exact Codex model-entitlement denial → temporarily block only that
|
|
6243
|
+
* requested model after provider/model identity matches, then rotate.
|
|
6244
|
+
* - other account-scoped policy denial → temporarily block that account
|
|
6245
|
+
* without marking its credential suspect, then rotate through siblings.
|
|
6210
6246
|
* - otherwise (hard 401 / auth failure) → mark the credential suspect (or
|
|
6211
6247
|
* reload when no broker hook is wired) and block it, then drop matching
|
|
6212
6248
|
* sticky state.
|
|
@@ -6243,8 +6279,24 @@ export class AuthStorage {
|
|
|
6243
6279
|
});
|
|
6244
6280
|
if (!sessionCredential) return false;
|
|
6245
6281
|
|
|
6246
|
-
|
|
6247
|
-
|
|
6282
|
+
const deniedModel = AIError.codexChatGPTAccountPolicyModel(error);
|
|
6283
|
+
const exactCodexModelPolicy =
|
|
6284
|
+
deniedModel !== undefined && AIError.isCodexChatGPTAccountPolicyError(error, provider, options?.modelId);
|
|
6285
|
+
// The exact sentence is provider-controlled input. A non-Codex provider,
|
|
6286
|
+
// absent request model, or mismatched model must not turn it into either a
|
|
6287
|
+
// global block or a hard-auth invalidation.
|
|
6288
|
+
if (deniedModel !== undefined && !exactCodexModelPolicy) return false;
|
|
6289
|
+
if (exactCodexModelPolicy || AIError.isAccountPolicyError(error)) {
|
|
6290
|
+
const modelPolicyScope = exactCodexModelPolicy
|
|
6291
|
+
? modelAccountPolicyBlockScope(provider, options?.modelId)
|
|
6292
|
+
: undefined;
|
|
6293
|
+
if (exactCodexModelPolicy && modelPolicyScope === undefined) return false;
|
|
6294
|
+
const routing = this.#credentialBlockRouting(
|
|
6295
|
+
provider,
|
|
6296
|
+
sessionCredential.type,
|
|
6297
|
+
options?.modelId,
|
|
6298
|
+
modelPolicyScope,
|
|
6299
|
+
);
|
|
6248
6300
|
return this.#blockCredentialForRotation(
|
|
6249
6301
|
provider,
|
|
6250
6302
|
sessionCredential.type,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip self-referential reasoning-fence delimiters that a provider leaks
|
|
3
|
+
* *inside* a structured thinking part.
|
|
4
|
+
*
|
|
5
|
+
* The visible-channel healers ({@link ../utils/leaked-thinking-stream},
|
|
6
|
+
* {@link ./fenced-thinking}) split leaked ` ```thinking ` fences out of the
|
|
7
|
+
* *text* stream. They never run over parts a provider already flags as
|
|
8
|
+
* thinking, so when a model (observed on Gemini thought summaries — issue
|
|
9
|
+
* #8719) emits a bare ` ```thinking ` / ` ``````thinking ` opener line between
|
|
10
|
+
* summaries, that delimiter reaches display and persistence verbatim as fence
|
|
11
|
+
* spam inside the reasoning block.
|
|
12
|
+
*
|
|
13
|
+
* This stripper removes only a *standalone* reasoning-fence opener line — a line
|
|
14
|
+
* that is nothing but a run of ≥3 backticks immediately followed by the info
|
|
15
|
+
* string `thinking` or `reasoning`. Such a line is always redundant inside an
|
|
16
|
+
* already-structured thinking block and never carries content. Inline mentions
|
|
17
|
+
* (prose on the same line), language-tagged code fences (` ```rs `), and bare
|
|
18
|
+
* closers (` ``` `) are left untouched so legitimate fenced code inside the
|
|
19
|
+
* reasoning survives.
|
|
20
|
+
*
|
|
21
|
+
* Streaming-safe: deltas may split a line anywhere. A trailing partial line is
|
|
22
|
+
* held only while it remains a viable opener prefix; the moment it cannot be an
|
|
23
|
+
* opener it is flushed and the rest of the line passes through character-level.
|
|
24
|
+
* Correctness never depends on the prefix heuristic — every held line is
|
|
25
|
+
* classified strictly on its newline (or on {@link ThinkingFenceStripper.flush})
|
|
26
|
+
* before it is dropped.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A complete standalone reasoning-fence opener: ≤3 lead spaces, ≥3 backticks,
|
|
31
|
+
* `thinking`/`reasoning`, optional trailing spaces, tolerating a trailing CR
|
|
32
|
+
* from a CRLF newline.
|
|
33
|
+
*/
|
|
34
|
+
const OPENER_LINE = /^ {0,3}`{3,}(?:thinking|reasoning)[ \t]*\r?$/i;
|
|
35
|
+
|
|
36
|
+
/** Could `line` (a partial, newline-not-yet-seen) still grow into {@link OPENER_LINE}? */
|
|
37
|
+
function couldBeOpenerPrefix(line: string): boolean {
|
|
38
|
+
// Tolerate a pending CR from a split CRLF.
|
|
39
|
+
const s = line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
40
|
+
const m = /^ {0,3}(`*)([\s\S]*)$/.exec(s);
|
|
41
|
+
if (!m) return false;
|
|
42
|
+
const ticks = m[1]!.length;
|
|
43
|
+
const rest = m[2]!;
|
|
44
|
+
if (rest === "") return true; // still consuming leading spaces / backticks
|
|
45
|
+
if (ticks < 3) return false; // a non-backtick char appeared before 3 backticks: never a fence
|
|
46
|
+
const word = rest.replace(/[ \t]+$/, "").toLowerCase();
|
|
47
|
+
return "thinking".startsWith(word) || "reasoning".startsWith(word);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Stateful, line-oriented stripper for leaked reasoning-fence openers in one
|
|
52
|
+
* structured thinking block. One instance per thinking block; feed every
|
|
53
|
+
* thinking delta through {@link push} and drain the tail with {@link flush}.
|
|
54
|
+
*/
|
|
55
|
+
export class ThinkingFenceStripper {
|
|
56
|
+
/** Buffered content of the current line still being classified. */
|
|
57
|
+
#carry = "";
|
|
58
|
+
/** True once the current line is known not to be an opener; passes through until newline. */
|
|
59
|
+
#passthrough = false;
|
|
60
|
+
|
|
61
|
+
/** Consume one thinking delta; returns the sanitized text to emit (may be empty). */
|
|
62
|
+
push(chunk: string): string {
|
|
63
|
+
let out = "";
|
|
64
|
+
for (const ch of chunk) {
|
|
65
|
+
if (this.#passthrough) {
|
|
66
|
+
out += ch;
|
|
67
|
+
if (ch === "\n") this.#passthrough = false;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === "\n") {
|
|
71
|
+
if (!OPENER_LINE.test(this.#carry)) out += `${this.#carry}\n`;
|
|
72
|
+
this.#carry = "";
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
this.#carry += ch;
|
|
76
|
+
if (!couldBeOpenerPrefix(this.#carry)) {
|
|
77
|
+
out += this.#carry;
|
|
78
|
+
this.#carry = "";
|
|
79
|
+
this.#passthrough = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Drain any held partial line at block end; returns text to emit (may be empty). */
|
|
86
|
+
flush(): string {
|
|
87
|
+
const carry = this.#carry;
|
|
88
|
+
this.#carry = "";
|
|
89
|
+
this.#passthrough = false;
|
|
90
|
+
return OPENER_LINE.test(carry) ? "" : carry;
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/error/finalize.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface FinalizeOptions {
|
|
|
10
10
|
api?: Api;
|
|
11
11
|
/** Provider id; forwarded to the message formatter for copilot rewrites. */
|
|
12
12
|
provider?: string;
|
|
13
|
+
/** Requested model id; paired with provider for model-entitlement classification. */
|
|
14
|
+
model?: string;
|
|
13
15
|
/** Caller signal, for providers that don't run an abort tracker. */
|
|
14
16
|
signal?: AbortSignal;
|
|
15
17
|
/** Abort tracker, preferred over `signal`: distinguishes caller vs. local aborts. */
|
|
@@ -55,6 +57,8 @@ export async function finalize(error: unknown, opts: FinalizeOptions = {}): Prom
|
|
|
55
57
|
|
|
56
58
|
const id = classifyMessage({
|
|
57
59
|
api: opts.api,
|
|
60
|
+
provider: opts.provider,
|
|
61
|
+
model: opts.model,
|
|
58
62
|
errorId: classify(error, opts.api),
|
|
59
63
|
errorMessage: message,
|
|
60
64
|
errorStatus: currentStatus,
|