@oh-my-pi/pi-ai 17.0.1 → 17.0.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 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +13 -0
- package/dist/types/error/rate-limit.d.ts +9 -5
- package/dist/types/providers/cursor.d.ts +16 -0
- package/dist/types/providers/kimi.d.ts +3 -3
- package/dist/types/providers/openai-anthropic-shim.d.ts +3 -1
- package/dist/types/providers/openai-codex/request-transformer.d.ts +9 -3
- package/dist/types/providers/openai-shared.d.ts +9 -5
- package/dist/types/providers/transform-messages.d.ts +5 -9
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/event-stream.d.ts +2 -0
- package/package.json +4 -4
- package/src/auth-broker/wire-schemas.ts +1 -0
- package/src/dialect/thinking.ts +186 -16
- package/src/error/flags.ts +2 -2
- package/src/error/rate-limit.ts +12 -8
- package/src/providers/__tests__/kimi-code-thinking.test.ts +180 -8
- package/src/providers/cursor.ts +82 -30
- package/src/providers/gitlab-duo-workflow.ts +6 -2
- package/src/providers/kimi.ts +6 -8
- package/src/providers/openai-anthropic-shim.ts +7 -1
- package/src/providers/openai-codex/request-transformer.ts +12 -3
- package/src/providers/openai-codex-responses.ts +4 -3
- package/src/providers/openai-completions.ts +2 -2
- package/src/providers/openai-responses.ts +5 -1
- package/src/providers/openai-shared.ts +41 -11
- package/src/providers/transform-messages.ts +164 -1
- package/src/stream.ts +9 -4
- package/src/types.ts +1 -1
- package/src/usage/claude.ts +10 -24
- package/src/utils/event-stream.ts +5 -0
- package/src/utils.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [17.0.3] - 2026-07-17
|
|
6
|
+
|
|
5
7
|
### Fixed
|
|
6
8
|
|
|
9
|
+
- Replaced the opaque `h2 is not supported` failure on the Cursor run transport with an actionable error naming the ALPN-stripping proxy as the cause and pointing at the `providers.cursor.baseUrl` HTTP/2 bridge workaround. The run RPC is HTTP/2-only, so behind a TLS-intercepting proxy that strips ALPN (e.g. Zscaler) bun cannot negotiate `h2` and the completion cannot proceed ([#5828](https://github.com/can1357/oh-my-pi/issues/5828)).
|
|
10
|
+
- Restored the `createAssistantMessageEventStream()` root export used by legacy provider extensions ([#5879](https://github.com/can1357/oh-my-pi/issues/5879)).
|
|
11
|
+
- Fixed parallel Responses tool-result images interleaving synthetic user messages before all pending outputs, preventing strict OpenRouter/Moonshot backends from rejecting follow-up requests. ([#5850](https://github.com/can1357/oh-my-pi/issues/5850))
|
|
12
|
+
- Fixed Kimi Code K3 requests to send native named efforts (`low`, `high`, `max`) and use adaptive effort rather than generic token budgets on explicit Anthropic transport overrides ([#5893](https://github.com/can1357/oh-my-pi/issues/5893)).
|
|
7
13
|
- Automatically invalidate and rotate OAuth credentials when an "invalidated oauth token" error occurs
|
|
14
|
+
- Fixed Anthropic usage reports treating the organization response header as the account identity, which caused the 5h/7d status-line segment to disappear for OAuth credentials without stored organization metadata. ([#5698](https://github.com/can1357/oh-my-pi/issues/5698))
|
|
15
|
+
|
|
16
|
+
## [17.0.2] - 2026-07-17
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Automatically invalidate and rotate OAuth credentials when an "invalidated oauth token" error occurs.
|
|
21
|
+
- Fixed auth-broker snapshot validation rejecting API keys stored via the `/login` flow, restoring support for gateway/broker setups serving login-sourced keys on custom hosts.
|
|
22
|
+
- Fixed an issue where literal reasoning tags (e.g., `<think>`) inside Markdown code blocks or inline code were incorrectly treated as reasoning boundaries, which corrupted the rendered Markdown.
|
|
23
|
+
- Classified HTTP 402 and "balance exhausted" quota responses as persistent usage limits, enabling automatic rotation of multi-account requests to a sibling credential.
|
|
24
|
+
- Fixed `kimi-code` Anthropic-format requests ignoring custom provider base URLs.
|
|
25
|
+
- Fixed an issue where GPT-5.6 Codex Responses-Lite requests failed with an HTTP 400 error due to invalid `tool_choice` parameters after tools were rewritten, by automatically downgrading forced hosted choices to `tool_choice: "auto"` while preserving explicit tool-use constraints.
|
|
26
|
+
- Fixed Cursor streams prematurely reporting success before late CONNECT or gRPC terminal failures were observed, and resolved issues rejecting transport ends without a `turnEnded` signal.
|
|
8
27
|
|
|
9
28
|
## [17.0.1] - 2026-07-16
|
|
10
29
|
|
|
@@ -29,6 +29,7 @@ export declare const remoteOauthCredentialSchema: import("arktype/internal/varia
|
|
|
29
29
|
export declare const apiKeyCredentialSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
30
30
|
type: "api_key";
|
|
31
31
|
key: string;
|
|
32
|
+
source?: "login" | undefined;
|
|
32
33
|
}, {}>;
|
|
33
34
|
/** Discriminated union accepted on POST /v1/credential (writes). */
|
|
34
35
|
export declare const writableAuthCredentialSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
@@ -46,6 +47,7 @@ export declare const writableAuthCredentialSchema: import("arktype/internal/vari
|
|
|
46
47
|
} | {
|
|
47
48
|
type: "api_key";
|
|
48
49
|
key: string;
|
|
50
|
+
source?: "login" | undefined;
|
|
49
51
|
}, {}>;
|
|
50
52
|
/** Discriminated union returned in snapshots (refresh is sentinel for OAuth). */
|
|
51
53
|
export declare const snapshotCredentialSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
@@ -63,6 +65,7 @@ export declare const snapshotCredentialSchema: import("arktype/internal/variants
|
|
|
63
65
|
} | {
|
|
64
66
|
type: "api_key";
|
|
65
67
|
key: string;
|
|
68
|
+
source?: "login" | undefined;
|
|
66
69
|
}, {}>;
|
|
67
70
|
export declare const credentialSnapshotEntrySchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
68
71
|
id: number;
|
|
@@ -82,6 +85,7 @@ export declare const credentialSnapshotEntrySchema: import("arktype/internal/var
|
|
|
82
85
|
} | {
|
|
83
86
|
type: "api_key";
|
|
84
87
|
key: string;
|
|
88
|
+
source?: "login" | undefined;
|
|
85
89
|
};
|
|
86
90
|
identityKey: string | null;
|
|
87
91
|
}, {}>;
|
|
@@ -109,6 +113,7 @@ export declare const snapshotEntrySchema: import("arktype/internal/variants/obje
|
|
|
109
113
|
} | {
|
|
110
114
|
type: "api_key";
|
|
111
115
|
key: string;
|
|
116
|
+
source?: "login" | undefined;
|
|
112
117
|
};
|
|
113
118
|
identityKey: string | null;
|
|
114
119
|
rotatesInMs: number | null;
|
|
@@ -153,6 +158,7 @@ export declare const snapshotResponseSchema: import("arktype/internal/variants/o
|
|
|
153
158
|
} | {
|
|
154
159
|
type: "api_key";
|
|
155
160
|
key: string;
|
|
161
|
+
source?: "login" | undefined;
|
|
156
162
|
};
|
|
157
163
|
identityKey: string | null;
|
|
158
164
|
rotatesInMs: number | null;
|
|
@@ -193,6 +199,7 @@ export declare const snapshotStreamSnapshotEventSchema: import("arktype/internal
|
|
|
193
199
|
} | {
|
|
194
200
|
type: "api_key";
|
|
195
201
|
key: string;
|
|
202
|
+
source?: "login" | undefined;
|
|
196
203
|
};
|
|
197
204
|
identityKey: string | null;
|
|
198
205
|
rotatesInMs: number | null;
|
|
@@ -234,6 +241,7 @@ export declare const snapshotStreamEntryEventSchema: import("arktype/internal/va
|
|
|
234
241
|
} | {
|
|
235
242
|
type: "api_key";
|
|
236
243
|
key: string;
|
|
244
|
+
source?: "login" | undefined;
|
|
237
245
|
};
|
|
238
246
|
identityKey: string | null;
|
|
239
247
|
rotatesInMs: number | null;
|
|
@@ -287,6 +295,7 @@ export declare const snapshotStreamEventSchema: import("arktype/internal/variant
|
|
|
287
295
|
} | {
|
|
288
296
|
type: "api_key";
|
|
289
297
|
key: string;
|
|
298
|
+
source?: "login" | undefined;
|
|
290
299
|
};
|
|
291
300
|
identityKey: string | null;
|
|
292
301
|
rotatesInMs: number | null;
|
|
@@ -326,6 +335,7 @@ export declare const snapshotStreamEventSchema: import("arktype/internal/variant
|
|
|
326
335
|
} | {
|
|
327
336
|
type: "api_key";
|
|
328
337
|
key: string;
|
|
338
|
+
source?: "login" | undefined;
|
|
329
339
|
};
|
|
330
340
|
identityKey: string | null;
|
|
331
341
|
rotatesInMs: number | null;
|
|
@@ -427,6 +437,7 @@ export declare const credentialRefreshResponseSchema: import("arktype/internal/v
|
|
|
427
437
|
} | {
|
|
428
438
|
type: "api_key";
|
|
429
439
|
key: string;
|
|
440
|
+
source?: "login" | undefined;
|
|
430
441
|
};
|
|
431
442
|
identityKey: string | null;
|
|
432
443
|
};
|
|
@@ -469,6 +480,7 @@ export declare const credentialUploadRequestSchema: import("arktype/internal/var
|
|
|
469
480
|
} | {
|
|
470
481
|
type: "api_key";
|
|
471
482
|
key: string;
|
|
483
|
+
source?: "login" | undefined;
|
|
472
484
|
};
|
|
473
485
|
}, {}>;
|
|
474
486
|
export declare const credentialUploadResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
|
|
@@ -490,6 +502,7 @@ export declare const credentialUploadResponseSchema: import("arktype/internal/va
|
|
|
490
502
|
} | {
|
|
491
503
|
type: "api_key";
|
|
492
504
|
key: string;
|
|
505
|
+
source?: "login" | undefined;
|
|
493
506
|
};
|
|
494
507
|
identityKey: string | null;
|
|
495
508
|
}[];
|
|
@@ -20,6 +20,9 @@ export declare function calculateRateLimitBackoffMs(reason: RateLimitReason): nu
|
|
|
20
20
|
/**
|
|
21
21
|
* HTTP status codes that, absent richer body classification, represent an
|
|
22
22
|
* account-local usage cap rather than a bad credential or a transient blip.
|
|
23
|
+
* HTTP 402 Payment Required is categorically an account-billing cap (xAI
|
|
24
|
+
* Grok Build "usage balance exhausted", DeepSeek "Insufficient Balance",
|
|
25
|
+
* OpenRouter credit exhaustion) — never a transient blip or bad credential.
|
|
23
26
|
* Always combine with {@link isUsageLimitOutcome} when a message is available
|
|
24
27
|
* — a 429 carrying transient rate-limit wording is NOT a usage cap.
|
|
25
28
|
*/
|
|
@@ -31,7 +34,7 @@ export declare function isUsageLimitStatus(status: number | undefined): boolean;
|
|
|
31
34
|
* 1. Body matches {@link isUsageLimitError} (Codex `usage_limit_reached`,
|
|
32
35
|
* Anthropic account rate-limit, Google `resource_exhausted`, OpenAI
|
|
33
36
|
* `insufficient_quota`, …) → rotate.
|
|
34
|
-
* 2. Status is not 429 → backoff (caller's domain).
|
|
37
|
+
* 2. Status is not a usage-limit status (429/402) → backoff (caller's domain).
|
|
35
38
|
* 3. Body is absent or {@link isOpaqueStatusBody opaque} (just the status,
|
|
36
39
|
* empty JSON, HTTP framing only) → rotate conservatively: the server
|
|
37
40
|
* gave us nothing else to go on.
|
|
@@ -44,10 +47,11 @@ export declare function isUsageLimitStatus(status: number | undefined): boolean;
|
|
|
44
47
|
*/
|
|
45
48
|
export declare function isUsageLimitOutcome(status: number | undefined, message: string | undefined): boolean;
|
|
46
49
|
/**
|
|
47
|
-
* A
|
|
48
|
-
* empty, whitespace-only, the status digits with HTTP/JSON
|
|
49
|
-
* generic punctuation. Anything else (retry hints, capacity
|
|
50
|
-
* descriptions) is informative enough to defer to the
|
|
50
|
+
* A usage-limit status body is opaque when it carries no signal beyond the
|
|
51
|
+
* status itself — empty, whitespace-only, the status digits with HTTP/JSON
|
|
52
|
+
* framing, or generic punctuation. Anything else (retry hints, capacity
|
|
53
|
+
* wording, error descriptions) is informative enough to defer to the
|
|
54
|
+
* classifier.
|
|
51
55
|
*/
|
|
52
56
|
export declare function isOpaqueStatusBody(message: string): boolean;
|
|
53
57
|
/**
|
|
@@ -13,6 +13,20 @@ export interface CursorOptions extends StreamOptions {
|
|
|
13
13
|
execHandlers?: CursorExecHandlers;
|
|
14
14
|
onToolResult?: CursorToolResultHandler;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Maps an opaque HTTP/2 negotiation failure into an actionable error.
|
|
18
|
+
*
|
|
19
|
+
* bun only opens an HTTP/2 session when TLS-ALPN negotiates `h2`. Behind a
|
|
20
|
+
* TLS-intercepting proxy that strips ALPN (e.g. Zscaler), the handshake yields
|
|
21
|
+
* no `h2` protocol and bun throws `ERR_HTTP2_ERROR: h2 is not supported`. The
|
|
22
|
+
* Cursor run RPC is HTTP/2-only (the ALB rejects HTTP/1.1 with 464), so there
|
|
23
|
+
* is no h1 fallback the way model discovery has one — the run simply cannot
|
|
24
|
+
* proceed. Replace the opaque message with one that names the cause and points
|
|
25
|
+
* at the `providers.cursor.baseUrl` workaround.
|
|
26
|
+
*
|
|
27
|
+
* Non-ALPN errors pass through untouched.
|
|
28
|
+
*/
|
|
29
|
+
export declare function mapH2TransportError(error: unknown, baseUrl: string): unknown;
|
|
16
30
|
export declare const streamCursor: StreamFunction<"cursor-agent">;
|
|
17
31
|
export type ToolCallState = ToolCall & {
|
|
18
32
|
[kStreamingBlockIndex]: number;
|
|
@@ -29,6 +43,8 @@ export interface BlockState {
|
|
|
29
43
|
[kStreamingBlockIndex]: number;
|
|
30
44
|
}) | null;
|
|
31
45
|
currentToolCall: ToolCallState | null;
|
|
46
|
+
/** MCP call IDs executed through Cursor's exec channel before their stream block arrives. */
|
|
47
|
+
resolvedMcpToolCallIds: Set<string>;
|
|
32
48
|
firstTokenTime: number | undefined;
|
|
33
49
|
setTextBlock: (b: (TextContent & {
|
|
34
50
|
[kStreamingBlockIndex]: number;
|
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
* - OpenAI: https://api.kimi.com/coding/v1/chat/completions
|
|
6
6
|
* - Anthropic: https://api.kimi.com/coding/v1/messages
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Each discovered model selects its server-declared protocol; legacy models
|
|
9
|
+
* without protocol metadata retain the Anthropic-compatible default.
|
|
10
10
|
*/
|
|
11
11
|
import type { Api, Context, Model } from "../types.js";
|
|
12
12
|
import type { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
13
13
|
import { type OpenAIAnthropicApiFormat, type OpenAIAnthropicShimOptions } from "./openai-anthropic-shim.js";
|
|
14
14
|
export type KimiApiFormat = OpenAIAnthropicApiFormat;
|
|
15
15
|
export interface KimiOptions extends OpenAIAnthropicShimOptions {
|
|
16
|
-
/** API format
|
|
16
|
+
/** Explicit API format override. Defaults to the model's discovered protocol. */
|
|
17
17
|
format?: KimiApiFormat;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* format, optional extra headers); the streaming/forwarding plumbing lives
|
|
8
8
|
* here once.
|
|
9
9
|
*/
|
|
10
|
-
import type { Context, Model, SimpleStreamOptions } from "../types.js";
|
|
10
|
+
import type { Context, Model, SimpleStreamOptions, ThinkingControlMode } from "../types.js";
|
|
11
11
|
import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|
12
12
|
export type OpenAIAnthropicApiFormat = "openai" | "anthropic";
|
|
13
13
|
export interface OpenAIAnthropicShimOptions extends SimpleStreamOptions {
|
|
@@ -21,6 +21,8 @@ export interface OpenAIAnthropicShimConfig {
|
|
|
21
21
|
openaiBaseUrl?: string;
|
|
22
22
|
/** Default API format when caller does not specify one. */
|
|
23
23
|
defaultFormat: OpenAIAnthropicApiFormat;
|
|
24
|
+
/** Thinking transport used when this provider's Anthropic endpoint differs from generic budget semantics. */
|
|
25
|
+
anthropicThinkingMode?: ThinkingControlMode;
|
|
24
26
|
/** Provider-specific headers (e.g. auth/session) merged ahead of user-supplied headers. */
|
|
25
27
|
extraHeaders?: () => Record<string, string>;
|
|
26
28
|
}
|
|
@@ -77,6 +77,7 @@ export declare function resolveCodexResponsesLite(model: Model<"openai-codex-res
|
|
|
77
77
|
export interface CodexLiteShapedBody {
|
|
78
78
|
instructions?: unknown;
|
|
79
79
|
tools?: unknown;
|
|
80
|
+
tool_choice?: unknown;
|
|
80
81
|
input?: unknown;
|
|
81
82
|
parallel_tool_calls?: unknown;
|
|
82
83
|
}
|
|
@@ -85,9 +86,14 @@ export interface CodexLiteShapedBody {
|
|
|
85
86
|
* `build_responses_request` with `use_responses_lite`): strips pinned image
|
|
86
87
|
* detail, forces parallel tool calling off, moves tools into a leading
|
|
87
88
|
* `additional_tools` developer item and the base instructions into a
|
|
88
|
-
* developer message, then omits top-level `instructions`/`tools`.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
89
|
+
* developer message, then omits top-level `instructions`/`tools`. Because the
|
|
90
|
+
* rewrite removes top-level `tools`, a forced hosted-tool choice (e.g.
|
|
91
|
+
* `{ type: "web_search" }`) would leave the backend unable to validate the
|
|
92
|
+
* choice against a tools collection and it rejects the request with HTTP 400
|
|
93
|
+
* (#5771). Such choices must fall back to `"auto"`; explicit string constraints
|
|
94
|
+
* such as `"none"` and `"required"` remain valid. Shared by normal turns and
|
|
95
|
+
* both remote-compaction paths — codex-rs routes `/responses/compact` through
|
|
96
|
+
* the same builder.
|
|
91
97
|
*/
|
|
92
98
|
export declare function applyCodexResponsesLiteShape(body: CodexLiteShapedBody): void;
|
|
93
99
|
export declare function transformRequestBody(body: RequestBody, model: Model<"openai-codex-responses">, options?: CodexRequestOptions, prompt?: {
|
|
@@ -200,6 +200,7 @@ export type OpenAICompletionsParams = Omit<ChatCompletionCreateParamsStreaming,
|
|
|
200
200
|
repetition_penalty?: number;
|
|
201
201
|
thinking?: {
|
|
202
202
|
type: "enabled" | "disabled";
|
|
203
|
+
effort?: string;
|
|
203
204
|
keep?: "all";
|
|
204
205
|
};
|
|
205
206
|
enable_thinking?: boolean;
|
|
@@ -297,12 +298,14 @@ export declare function applyChatCompletionsReasoningParams(params: OpenAIComple
|
|
|
297
298
|
}) | undefined): void;
|
|
298
299
|
export declare function disableChatCompletionsReasoningForDialect(params: OpenAICompletionsParams, compat: ResolvedOpenAICompat): void;
|
|
299
300
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
301
|
+
* Provider-specific Chat Completions output clamp.
|
|
302
|
+
*
|
|
303
|
+
* Most OpenAI-compatible endpoints retain the conservative 64k ceiling from
|
|
304
|
+
* {@link resolveOpenAIOutputTokenParam}. Z.AI/GLM-5.2 reasoning and native
|
|
305
|
+
* Moonshot K3 explicitly accept their full advertised model caps, so those
|
|
306
|
+
* routes clamp to `model.maxTokens` instead.
|
|
304
307
|
*/
|
|
305
|
-
export declare function
|
|
308
|
+
export declare function resolveOpenAICompletionsOutputClamp(model: Model<"openai-completions">, compat: ResolvedOpenAICompat): number | undefined;
|
|
306
309
|
/**
|
|
307
310
|
* Enable `tool_stream` for Z.AI/GLM-5.2 reasoning models when tools are present
|
|
308
311
|
* (GLM-5.2 streams tool-call arguments incrementally and needs the flag to do so).
|
|
@@ -380,6 +383,7 @@ export interface BuildResponsesInputOptions<TApi extends Api> {
|
|
|
380
383
|
}
|
|
381
384
|
export declare function buildResponsesInput<TApi extends Api>(options: BuildResponsesInputOptions<TApi>): ResponseInput;
|
|
382
385
|
export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>, preserveMessageIds?: boolean, supportsCustomToolCalls?: boolean, customToolWireNameMap?: ReadonlyMap<string, string>): ResponseInput;
|
|
386
|
+
/** Appends one tool result while keeping consecutive outputs ahead of its synthetic image messages. */
|
|
383
387
|
export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, supportsImageDetailOriginal: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>, supportsCustomToolCalls?: boolean): void;
|
|
384
388
|
/**
|
|
385
389
|
* Per-block accumulation helpers shared by the two Responses decode loops —
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { Api, AssistantMessage, Message, Model } from "../types.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* For aborted/errored turns, this function:
|
|
8
|
-
* - Preserves tool call structure (unlike converting to text summaries)
|
|
9
|
-
* - Injects synthetic "aborted" tool results
|
|
10
|
-
*/
|
|
2
|
+
export declare function redactSensitiveCredentials(text: string): string;
|
|
3
|
+
export declare function redactSensitiveInObject(val: unknown): {
|
|
4
|
+
result: unknown;
|
|
5
|
+
changed: boolean;
|
|
6
|
+
};
|
|
11
7
|
export declare function transformMessages<TApi extends Api>(messages: Message[], model: Model<TApi>, normalizeToolCallId?: (id: string, model: Model<TApi>, source: AssistantMessage) => string, maxNormalizedToolCallIdLength?: number, duplicateToolCallIdSuffixPrefix?: string, targetCompat?: Model<TApi>["compat"]): Message[];
|
package/dist/types/types.d.ts
CHANGED
|
@@ -376,7 +376,7 @@ export interface SimpleStreamOptions extends Omit<StreamOptions, "apiKey"> {
|
|
|
376
376
|
toolChoice?: ToolChoice;
|
|
377
377
|
/** OpenAI service tier for processing priority/cost control. Ignored by non-OpenAI providers. */
|
|
378
378
|
serviceTier?: ServiceTier;
|
|
379
|
-
/** API format
|
|
379
|
+
/** Explicit Kimi Code API format override; omitted uses live per-model protocol metadata. */
|
|
380
380
|
kimiApiFormat?: "openai" | "anthropic";
|
|
381
381
|
/** API format for Synthetic provider: "openai" or "anthropic" (default: "openai") */
|
|
382
382
|
syntheticApiFormat?: "openai" | "anthropic";
|
|
@@ -35,3 +35,5 @@ export declare class AssistantMessageEventStream extends EventStream<AssistantMe
|
|
|
35
35
|
push(event: AssistantMessageEvent): void;
|
|
36
36
|
end(result?: AssistantMessage): void;
|
|
37
37
|
}
|
|
38
|
+
/** Create an assistant-message event stream for legacy extension providers. */
|
|
39
|
+
export declare function createAssistantMessageEventStream(): AssistantMessageEventStream;
|
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.0.
|
|
4
|
+
"version": "17.0.3",
|
|
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,9 +38,9 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.1",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "17.0.3",
|
|
42
|
+
"@oh-my-pi/pi-utils": "17.0.3",
|
|
43
|
+
"@oh-my-pi/pi-wire": "17.0.3",
|
|
44
44
|
"arktype": "2.2.3",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/dialect/thinking.ts
CHANGED
|
@@ -32,6 +32,16 @@ export class ThinkingInbandScanner implements InbandScanner {
|
|
|
32
32
|
#thinking = "";
|
|
33
33
|
/** Fence-aware close-matcher while inside a ` ```thinking ` block; undefined otherwise. */
|
|
34
34
|
#fenced: FencedThinkingScanner | undefined;
|
|
35
|
+
/** Backtick count that opened the Markdown code span/fence we are inside; 0 when not in code. */
|
|
36
|
+
#codeTicks = 0;
|
|
37
|
+
/** True when {@link #codeTicks} opened a fenced block (closes on a fence line), not an inline span. */
|
|
38
|
+
#codeFenced = false;
|
|
39
|
+
/**
|
|
40
|
+
* Leading-space count on the current output line, or -1 once a non-space
|
|
41
|
+
* character has appeared. Starts at 0 (line start) so a fence opening the
|
|
42
|
+
* stream — or one indented ≤3 spaces, as CommonMark allows — is recognized.
|
|
43
|
+
*/
|
|
44
|
+
#lineIndent = 0;
|
|
35
45
|
|
|
36
46
|
feed(text: string): InbandScanEvent[] {
|
|
37
47
|
if (text.length === 0) return [];
|
|
@@ -86,25 +96,93 @@ export class ThinkingInbandScanner implements InbandScanner {
|
|
|
86
96
|
this.#closeTag = "";
|
|
87
97
|
continue;
|
|
88
98
|
}
|
|
99
|
+
if (this.#codeTicks > 0) {
|
|
100
|
+
if (this.#emitCode(final, events)) continue;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
89
103
|
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
const hit = scanVisible(this.#buffer, final);
|
|
105
|
+
if (hit.kind === "none") {
|
|
106
|
+
this.#emitText(this.#buffer, events);
|
|
107
|
+
this.#buffer = "";
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
if (hit.index > 0) this.#emitText(this.#buffer.slice(0, hit.index), events);
|
|
111
|
+
if (hit.kind === "hold") {
|
|
112
|
+
this.#buffer = this.#buffer.slice(hit.index);
|
|
96
113
|
break;
|
|
97
114
|
}
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
115
|
+
if (hit.kind === "code") {
|
|
116
|
+
const fenced = hit.ticks >= 3 && this.#lineIndent >= 0 && this.#lineIndent <= 3;
|
|
117
|
+
this.#emitText(this.#buffer.slice(hit.index, hit.index + hit.ticks), events);
|
|
118
|
+
this.#buffer = this.#buffer.slice(hit.index + hit.ticks);
|
|
119
|
+
this.#codeTicks = hit.ticks;
|
|
120
|
+
this.#codeFenced = fenced;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
this.#buffer = this.#buffer.slice(hit.index + hit.tag.open.length);
|
|
124
|
+
this.#closeTag = hit.tag.close;
|
|
101
125
|
this.#thinking = "";
|
|
102
|
-
if (tag.fenced) this.#fenced = new FencedThinkingScanner();
|
|
126
|
+
if (hit.tag.fenced) this.#fenced = new FencedThinkingScanner();
|
|
103
127
|
events.push({ type: "thinkingStart" });
|
|
104
128
|
}
|
|
105
129
|
return events;
|
|
106
130
|
}
|
|
107
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Emit buffered content while inside a Markdown code region, suppressing
|
|
134
|
+
* reasoning-tag detection. A fenced block closes only on a fence line (a line
|
|
135
|
+
* of backticks ≥ the opener); an inline span closes on the first backtick run
|
|
136
|
+
* of exactly the opener length. Returns true when the region closed and the
|
|
137
|
+
* loop should continue, false when it held back and should break.
|
|
138
|
+
*/
|
|
139
|
+
#emitCode(final: boolean, events: InbandScanEvent[]): boolean {
|
|
140
|
+
if (this.#codeFenced) {
|
|
141
|
+
const end = findFenceCloseEnd(this.#buffer, this.#codeTicks, final);
|
|
142
|
+
if (end !== -1) {
|
|
143
|
+
this.#emitText(this.#buffer.slice(0, end), events);
|
|
144
|
+
this.#buffer = this.#buffer.slice(end);
|
|
145
|
+
this.#codeTicks = 0;
|
|
146
|
+
this.#codeFenced = false;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (final) {
|
|
150
|
+
this.#emitText(this.#buffer, events);
|
|
151
|
+
this.#buffer = "";
|
|
152
|
+
this.#codeTicks = 0;
|
|
153
|
+
this.#codeFenced = false;
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
// Stream committed lines; hold only the last (possibly partial) fence line.
|
|
157
|
+
const lastNl = this.#buffer.lastIndexOf("\n");
|
|
158
|
+
if (lastNl !== -1) {
|
|
159
|
+
this.#emitText(this.#buffer.slice(0, lastNl + 1), events);
|
|
160
|
+
this.#buffer = this.#buffer.slice(lastNl + 1);
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const close = findBacktickRun(this.#buffer, 0, this.#codeTicks);
|
|
165
|
+
if (close !== -1 && (final || close + this.#codeTicks < this.#buffer.length)) {
|
|
166
|
+
this.#emitText(this.#buffer.slice(0, close + this.#codeTicks), events);
|
|
167
|
+
this.#buffer = this.#buffer.slice(close + this.#codeTicks);
|
|
168
|
+
this.#codeTicks = 0;
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
// No committed close yet: emit text, holding a trailing backtick run that
|
|
172
|
+
// may still grow into — or past — the closing delimiter.
|
|
173
|
+
const hold = final ? 0 : trailingBacktickRun(this.#buffer);
|
|
174
|
+
this.#emitText(this.#buffer.slice(0, this.#buffer.length - hold), events);
|
|
175
|
+
this.#buffer = this.#buffer.slice(this.#buffer.length - hold);
|
|
176
|
+
if (final) this.#codeTicks = 0;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#emitText(text: string, events: InbandScanEvent[]): void {
|
|
181
|
+
if (text.length === 0) return;
|
|
182
|
+
events.push({ type: "text", text });
|
|
183
|
+
this.#lineIndent = trailingLineIndent(text, this.#lineIndent);
|
|
184
|
+
}
|
|
185
|
+
|
|
108
186
|
#emitThinking(delta: string, events: InbandScanEvent[]): void {
|
|
109
187
|
if (delta.length === 0) return;
|
|
110
188
|
this.#thinking += delta;
|
|
@@ -112,11 +190,103 @@ export class ThinkingInbandScanner implements InbandScanner {
|
|
|
112
190
|
}
|
|
113
191
|
}
|
|
114
192
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
193
|
+
/** Outcome of scanning idle visible text for the next reasoning-tag or code-span boundary. */
|
|
194
|
+
type VisibleHit =
|
|
195
|
+
| { readonly kind: "tag"; readonly index: number; readonly tag: Tag }
|
|
196
|
+
| { readonly kind: "code"; readonly index: number; readonly ticks: number }
|
|
197
|
+
| { readonly kind: "hold"; readonly index: number }
|
|
198
|
+
| { readonly kind: "none" };
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Walk idle visible text for the earliest boundary: a leaked reasoning-tag open,
|
|
202
|
+
* a Markdown code-span/fence opener (a backtick run), or — when more chunks may
|
|
203
|
+
* follow — a held partial delimiter at the buffer tail.
|
|
204
|
+
*
|
|
205
|
+
* Reasoning tags win at any position so the gemini ` ```thinking ` fence is
|
|
206
|
+
* healed instead of being read as a code fence. Backtick runs enter code mode so
|
|
207
|
+
* a literal `<think>` inside inline code or a fenced block stays visible text.
|
|
208
|
+
*/
|
|
209
|
+
function scanVisible(buffer: string, final: boolean): VisibleHit {
|
|
210
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
211
|
+
const tag = TAGS.find(candidate => buffer.startsWith(candidate.open, i));
|
|
212
|
+
if (tag) return { kind: "tag", index: i, tag };
|
|
213
|
+
if (!final) {
|
|
214
|
+
const rest = buffer.slice(i);
|
|
215
|
+
if (OPENS.some(open => open.length > rest.length && open.startsWith(rest))) {
|
|
216
|
+
return { kind: "hold", index: i };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (buffer[i] === "`") {
|
|
220
|
+
const ticks = backtickRun(buffer, i);
|
|
221
|
+
if (!final && i + ticks === buffer.length) return { kind: "hold", index: i };
|
|
222
|
+
return { kind: "code", index: i, ticks };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return { kind: "none" };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Length of the maximal backtick run beginning at `from`. */
|
|
229
|
+
function backtickRun(buffer: string, from: number): number {
|
|
230
|
+
let end = from;
|
|
231
|
+
while (end < buffer.length && buffer[end] === "`") end++;
|
|
232
|
+
return end - from;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Index of the first maximal backtick run of exactly `ticks` at/after `from`, else -1. */
|
|
236
|
+
function findBacktickRun(buffer: string, from: number, ticks: number): number {
|
|
237
|
+
for (let i = buffer.indexOf("`", from); i !== -1; i = buffer.indexOf("`", i)) {
|
|
238
|
+
const run = backtickRun(buffer, i);
|
|
239
|
+
if (run === ticks) return i;
|
|
240
|
+
i += run;
|
|
120
241
|
}
|
|
121
|
-
return
|
|
242
|
+
return -1;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Length of a backtick run that ends at the buffer tail; 0 when the tail is not a backtick. */
|
|
246
|
+
function trailingBacktickRun(buffer: string): number {
|
|
247
|
+
let start = buffer.length;
|
|
248
|
+
while (start > 0 && buffer[start - 1] === "`") start--;
|
|
249
|
+
return buffer.length - start;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Leading-space count of the line at the tail of `text`, continuing from the
|
|
254
|
+
* prior line's `indent` state (see {@link ThinkingInbandScanner.#lineIndent}).
|
|
255
|
+
* Returns -1 once any non-space character has appeared on the current line.
|
|
256
|
+
*/
|
|
257
|
+
function trailingLineIndent(text: string, prior: number): number {
|
|
258
|
+
const lastNl = text.lastIndexOf("\n");
|
|
259
|
+
let indent = lastNl === -1 ? prior : 0;
|
|
260
|
+
for (let i = lastNl + 1; i < text.length; i++) {
|
|
261
|
+
if (indent === -1) break;
|
|
262
|
+
indent = text[i] === " " ? indent + 1 : -1;
|
|
263
|
+
}
|
|
264
|
+
return indent;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Index just past the first closing fence line for a fenced block opened with
|
|
269
|
+
* `ticks` backticks, or -1 when none is committed yet. A closing fence is a whole
|
|
270
|
+
* line whose trimmed content is only backticks, at least `ticks` of them. A line
|
|
271
|
+
* without a terminating newline is committed only when `final` (no more input can
|
|
272
|
+
* extend it into a non-fence line).
|
|
273
|
+
*/
|
|
274
|
+
function findFenceCloseEnd(buffer: string, ticks: number, final: boolean): number {
|
|
275
|
+
for (let start = 0; start <= buffer.length; ) {
|
|
276
|
+
const nl = buffer.indexOf("\n", start);
|
|
277
|
+
const terminated = nl !== -1;
|
|
278
|
+
const line = buffer.slice(start, terminated ? nl : buffer.length).trim();
|
|
279
|
+
if (line.length >= ticks && isAllBackticks(line) && (terminated || final)) {
|
|
280
|
+
return terminated ? nl + 1 : buffer.length;
|
|
281
|
+
}
|
|
282
|
+
if (!terminated) break;
|
|
283
|
+
start = nl + 1;
|
|
284
|
+
}
|
|
285
|
+
return -1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** True when `text` is non-empty and every character is a backtick. */
|
|
289
|
+
function isAllBackticks(text: string): boolean {
|
|
290
|
+
for (let i = 0; i < text.length; i++) if (text[i] !== "`") return false;
|
|
291
|
+
return text.length > 0;
|
|
122
292
|
}
|
package/src/error/flags.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ProviderHttpError,
|
|
8
8
|
STREAM_ENVELOPE_ERROR_PREFIX,
|
|
9
9
|
} from "./classes";
|
|
10
|
-
import { isOpaqueStatusBody, matchesUsageLimitText, parseRateLimitReason } from "./rate-limit";
|
|
10
|
+
import { isOpaqueStatusBody, isUsageLimitStatus, matchesUsageLimitText, parseRateLimitReason } from "./rate-limit";
|
|
11
11
|
|
|
12
12
|
export const Flag = {
|
|
13
13
|
Class: 0x1000,
|
|
@@ -318,7 +318,7 @@ function classifyText(errorMessage: string | undefined, errorStatus: number | un
|
|
|
318
318
|
const cleanMessage = errorMessage;
|
|
319
319
|
const isOpaque = isOpaqueStatusBody(cleanMessage);
|
|
320
320
|
|
|
321
|
-
const isLimitStatus = statusClean
|
|
321
|
+
const isLimitStatus = isUsageLimitStatus(statusClean);
|
|
322
322
|
if (
|
|
323
323
|
matchesUsageLimitText(cleanMessage) ||
|
|
324
324
|
(isLimitStatus && (isOpaque || parseRateLimitReason(cleanMessage) === "QUOTA_EXHAUSTED"))
|