@oh-my-pi/pi-ai 16.2.2 → 16.2.4
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 +22 -1
- package/dist/types/error/flags.d.ts +2 -0
- package/dist/types/providers/openai-responses.d.ts +2 -2
- package/dist/types/usage/google-antigravity.d.ts +8 -9
- package/dist/types/usage/openai-codex-base-url.d.ts +17 -0
- package/dist/types/utils/stream-markup-healing.d.ts +14 -5
- package/package.json +4 -4
- package/src/auth-storage.ts +1 -0
- package/src/error/flags.ts +6 -0
- package/src/providers/ollama.ts +6 -1
- package/src/providers/openai-responses.ts +4 -2
- package/src/usage/google-antigravity.ts +91 -39
- package/src/usage/openai-codex-base-url.ts +30 -10
- package/src/usage/zai.ts +91 -18
- package/src/utils/stream-markup-healing.ts +41 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.2.4] - 2026-06-28
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Enabled freeform tool patch support for Azure OpenAI and Codex models
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed usage reporting for Antigravity and Z.AI to correctly surface and preserve distinct quota windows (daily, weekly, monthly) instead of collapsing or duplicating them
|
|
14
|
+
- Fixed an issue where `/usage show` returned "No usage data available" when using a custom proxy base URL for Codex
|
|
15
|
+
- Fixed OpenAI stream read errors being incorrectly classified as non-transient, enabling the coding agent to automatically retry after recoverable stream failures
|
|
16
|
+
|
|
17
|
+
## [16.2.3] - 2026-06-28
|
|
18
|
+
|
|
5
19
|
### Changed
|
|
6
20
|
|
|
7
|
-
-
|
|
21
|
+
- Enabled automatic removal of leaked reasoning tags for all models
|
|
22
|
+
- Prevented reasoning text duplication when models emit both structured and inline thinking
|
|
23
|
+
- Defaulted reasoning context to all turns for all Codex requests.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Enabled freeform tool patch support for Azure OpenAI and Codex models.
|
|
28
|
+
- Fixed an issue where the `/usage show` command returned "No usage data available" when using a custom proxy base URL for Codex.
|
|
8
29
|
|
|
9
30
|
## [16.2.2] - 2026-06-27
|
|
10
31
|
|
|
@@ -21,6 +21,7 @@ export declare const Flag: {
|
|
|
21
21
|
readonly OAuthExpiry: 1073741824;
|
|
22
22
|
};
|
|
23
23
|
export type Flag = (typeof Flag)[keyof typeof Flag];
|
|
24
|
+
export declare const STREAM_READ_ERROR_PATTERN: RegExp;
|
|
24
25
|
export declare const TRANSIENT_TRANSPORT_PATTERN: RegExp;
|
|
25
26
|
/** Whether an OAuth refresh error message means the grant is definitively dead. */
|
|
26
27
|
export declare function isOAuthExpiry(errorMessage: string): boolean;
|
|
@@ -30,6 +31,7 @@ export declare function retriable(id: number | undefined, opts?: {
|
|
|
30
31
|
replayUnsafe?: boolean;
|
|
31
32
|
}): boolean;
|
|
32
33
|
export declare function status(error: unknown): number | undefined;
|
|
34
|
+
export declare function isStreamReadErrorText(text: string): boolean;
|
|
33
35
|
export declare function classify(error: unknown, api?: Api): number;
|
|
34
36
|
/**
|
|
35
37
|
* Whether an error (or message string) classifies as an account usage/quota
|
|
@@ -118,9 +118,9 @@ export declare function buildParams(model: Model<"openai-responses">, context: C
|
|
|
118
118
|
* runtime path only consumes that metadata.
|
|
119
119
|
* @internal Exported for tests.
|
|
120
120
|
*/
|
|
121
|
-
export declare function supportsFreeformApplyPatch(model: Model<"openai-responses">): boolean;
|
|
121
|
+
export declare function supportsFreeformApplyPatch(model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">): boolean;
|
|
122
122
|
/** @internal Exported for tests. */
|
|
123
123
|
export declare function mapOpenAIResponsesToolChoiceForTools(choice: ToolChoice | undefined, tools: Tool[], model: Model<"openai-responses">): OpenAIResponsesToolChoice;
|
|
124
124
|
/** @internal Exported for tests. */
|
|
125
|
-
export declare function convertTools(tools: Tool[], strictMode: boolean, model: Model<"openai-responses">, onQuarantine?: (toolName: string, schemaPath: string) => void): OpenAITool[];
|
|
125
|
+
export declare function convertTools(tools: Tool[], strictMode: boolean, model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">, onQuarantine?: (toolName: string, schemaPath: string) => void): OpenAITool[];
|
|
126
126
|
export {};
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { CredentialRankingStrategy, UsageProvider } from "../usage";
|
|
2
2
|
export declare const antigravityUsageProvider: UsageProvider;
|
|
3
3
|
/**
|
|
4
|
-
* Antigravity quotas
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* relevant counter is index 0.
|
|
4
|
+
* Antigravity quotas are returned per backend counter (Anthropic / Google /
|
|
5
|
+
* OpenAI) and can include both daily and weekly windows. `fetchAntigravityUsage`
|
|
6
|
+
* sorts `limits` ascending by `remainingFraction`; after model-family scoping,
|
|
7
|
+
* the most-pressured relevant counter/window is index 0.
|
|
9
8
|
*
|
|
10
9
|
* Leave `secondary` unset: AuthStorage compares secondary metrics before
|
|
11
|
-
* primary metrics, which is correct for providers with
|
|
12
|
-
*
|
|
13
|
-
* avoids preferring an account at 95% Gemini / 0% Claude over one
|
|
14
|
-
*
|
|
10
|
+
* primary metrics, which is correct for providers with a fixed short/long
|
|
11
|
+
* split but wrong here. Ranking Antigravity by the bottleneck counter first
|
|
12
|
+
* avoids preferring an account at 95% Gemini daily / 0% Claude weekly over one
|
|
13
|
+
* with healthier Gemini headroom.
|
|
15
14
|
*/
|
|
16
15
|
export declare const antigravityRankingStrategy: CredentialRankingStrategy;
|
|
@@ -1 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the base URL for ChatGPT account-API requests (`wham/usage`,
|
|
3
|
+
* `wham/rate-limit-reset-credits`).
|
|
4
|
+
*
|
|
5
|
+
* These endpoints live on the canonical ChatGPT origin and authenticate with
|
|
6
|
+
* the Codex OAuth bearer minted for that origin. They are NOT part of the
|
|
7
|
+
* `/responses` API surface that streaming proxies (Headroom, 9router, etc.)
|
|
8
|
+
* forward, so a `providers.openai-codex.baseUrl` override pointed at such a
|
|
9
|
+
* proxy MUST NOT be used here — doing so 404s and silently breaks
|
|
10
|
+
* `/usage show` (issue #3679).
|
|
11
|
+
*
|
|
12
|
+
* Accepted overrides are the canonical `chatgpt.com` / `chat.openai.com`
|
|
13
|
+
* origins. Any extra path (e.g. a streaming override like
|
|
14
|
+
* `/backend-api/codex/responses`) is normalized to `${origin}/backend-api`,
|
|
15
|
+
* since the account endpoints always live directly under `/backend-api`. Any
|
|
16
|
+
* other host falls back to {@link CODEX_BASE_URL}.
|
|
17
|
+
*/
|
|
1
18
|
export declare function normalizeCodexBaseUrl(baseUrl?: string): string;
|
|
@@ -29,6 +29,12 @@ export type StreamMarkupHealingEvent = {
|
|
|
29
29
|
* State machine that consumes streamed visible text and emits cleaned text,
|
|
30
30
|
* thinking deltas, and reconstructed tool calls.
|
|
31
31
|
*
|
|
32
|
+
* A {@link ThinkingInbandScanner} always heals leaked reasoning idioms
|
|
33
|
+
* (`<think>`, `<thinking>`, ` ```thinking `, Gemma/Harmony channels, …) out of
|
|
34
|
+
* the visible channel. For Kimi / DeepSeek-DSML the provider tool-call grammar
|
|
35
|
+
* runs first and its cleaned text is piped through that thinking healer, so a
|
|
36
|
+
* model can leak tool-call markup and reasoning in the same stream.
|
|
37
|
+
*
|
|
32
38
|
* Feed only one stream channel (usually `delta.content` / `message.content`).
|
|
33
39
|
* Mixing reasoning and visible text into the same instance can corrupt held-back
|
|
34
40
|
* partial tag buffers.
|
|
@@ -71,8 +77,11 @@ export declare class StreamMarkupHealing {
|
|
|
71
77
|
export declare function modelMayLeakKimiToolCalls(provider: string, modelId: string): boolean;
|
|
72
78
|
/** Cheap model/provider gate for DeepSeek DSML envelope leaks. */
|
|
73
79
|
export declare function modelMayLeakDsmlToolCalls(provider: string, modelId: string): boolean;
|
|
74
|
-
/**
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
80
|
+
/**
|
|
81
|
+
* Pick the leaked-markup healer for an OpenAI-compatible / Ollama visible-text
|
|
82
|
+
* stream. Kimi chat-template tokens and DeepSeek DSML envelopes need their
|
|
83
|
+
* dedicated tool-call grammars; every other model uses `"thinking"`. All three
|
|
84
|
+
* patterns run the generic {@link ThinkingInbandScanner}, so leaked reasoning
|
|
85
|
+
* idioms (e.g. a Gemini ` ```thinking ` fence on OpenRouter) are always healed.
|
|
86
|
+
*/
|
|
87
|
+
export declare function getStreamMarkupHealingPattern(provider: string, modelId: string): StreamMarkupHealingPattern;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.2.
|
|
4
|
+
"version": "16.2.4",
|
|
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.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.2.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.2.
|
|
43
|
-
"@oh-my-pi/pi-wire": "16.2.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.2.4",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.2.4",
|
|
43
|
+
"@oh-my-pi/pi-wire": "16.2.4",
|
|
44
44
|
"arktype": "^2.2.0",
|
|
45
45
|
"zod": "^4"
|
|
46
46
|
},
|
package/src/auth-storage.ts
CHANGED
|
@@ -537,6 +537,7 @@ const USAGE_FAILURE_BACKOFF_MS = 10_000;
|
|
|
537
537
|
const DEFAULT_USAGE_REQUEST_TIMEOUT_MS = 10_000;
|
|
538
538
|
const USAGE_REPORT_CACHE_KEY_VERSION_OVERRIDES: Partial<Record<Provider, number>> = {
|
|
539
539
|
"google-antigravity": 2,
|
|
540
|
+
zai: 2,
|
|
540
541
|
};
|
|
541
542
|
const DEFAULT_OAUTH_REFRESH_TIMEOUT_MS = 10_000;
|
|
542
543
|
/**
|
package/src/error/flags.ts
CHANGED
|
@@ -85,6 +85,7 @@ const OVERFLOW_NO_BODY_PATTERN = /\b4(00|13)\s*(status code)?\s*\(no body\)/i;
|
|
|
85
85
|
const TIMEOUT_PATTERN = /\b(?:operation\s+)?timed?\s*out\b|\btimeout\b|\bstream stall\b/i;
|
|
86
86
|
const TRANSIENT_ENVELOPE_PATTERN = /anthropic stream envelope error:/i;
|
|
87
87
|
const TRANSIENT_ENVELOPE_BEFORE_START_PATTERN = /before message_start/i;
|
|
88
|
+
export const STREAM_READ_ERROR_PATTERN = /stream[_ -]?read[_ -]?error/i;
|
|
88
89
|
export const TRANSIENT_TRANSPORT_PATTERN =
|
|
89
90
|
/overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|retry your request|network.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|upstream.?request.?failed|reset before headers|socket hang up|timed? out|timeout|terminated|retry delay|stream stall|no error details in response|HTTP2(?:StreamReset|RefusedStream|EnhanceYourCalm)|malformed.?function.?call/i;
|
|
90
91
|
const AUTH_FAILURE_PATTERN =
|
|
@@ -240,9 +241,14 @@ function statusInternal(error: unknown, depth: number): number | undefined {
|
|
|
240
241
|
return undefined;
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
export function isStreamReadErrorText(text: string): boolean {
|
|
245
|
+
return STREAM_READ_ERROR_PATTERN.test(text);
|
|
246
|
+
}
|
|
247
|
+
|
|
243
248
|
function isTransientErrorText(text: string): boolean {
|
|
244
249
|
return (
|
|
245
250
|
isUnexpectedSocketCloseMessage(text) ||
|
|
251
|
+
isStreamReadErrorText(text) ||
|
|
246
252
|
(TRANSIENT_ENVELOPE_PATTERN.test(text) && TRANSIENT_ENVELOPE_BEFORE_START_PATTERN.test(text)) ||
|
|
247
253
|
TRANSIENT_TRANSPORT_PATTERN.test(text)
|
|
248
254
|
);
|
package/src/providers/ollama.ts
CHANGED
|
@@ -465,6 +465,10 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
465
465
|
? new StreamMarkupHealing({ pattern: streamMarkupHealingPattern })
|
|
466
466
|
: undefined;
|
|
467
467
|
let healedToolCallEmitted = false;
|
|
468
|
+
// Once the provider streams native reasoning (`message.thinking`), drop any
|
|
469
|
+
// thinking the text-channel healer also recovers so a model that emits both
|
|
470
|
+
// does not double-count its reasoning.
|
|
471
|
+
let suppressHealedThinking = false;
|
|
468
472
|
const endActiveTextBlock = (): void => {
|
|
469
473
|
if (activeTextIndex === undefined) return;
|
|
470
474
|
endTextBlock(stream, output, activeTextIndex);
|
|
@@ -542,7 +546,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
542
546
|
if (event.type === "text") {
|
|
543
547
|
appendVisibleText(event.text);
|
|
544
548
|
} else if (event.type === "thinking") {
|
|
545
|
-
appendVisibleThinking(event.thinking);
|
|
549
|
+
if (!suppressHealedThinking) appendVisibleThinking(event.thinking);
|
|
546
550
|
} else {
|
|
547
551
|
emitHealedToolCall(event.call);
|
|
548
552
|
}
|
|
@@ -614,6 +618,7 @@ export const streamOllama: StreamFunction<"ollama-chat"> = (
|
|
|
614
618
|
stream.push({ type: "start", partial: output });
|
|
615
619
|
for await (const chunk of iterateNdjson(response.body)) {
|
|
616
620
|
if (chunk.message?.thinking) {
|
|
621
|
+
suppressHealedThinking = true;
|
|
617
622
|
endActiveTextBlock();
|
|
618
623
|
if (activeThinkingIndex === undefined) {
|
|
619
624
|
output.content.push({ type: "thinking", thinking: "" });
|
|
@@ -936,7 +936,9 @@ export function buildParams(
|
|
|
936
936
|
* runtime path only consumes that metadata.
|
|
937
937
|
* @internal Exported for tests.
|
|
938
938
|
*/
|
|
939
|
-
export function supportsFreeformApplyPatch(
|
|
939
|
+
export function supportsFreeformApplyPatch(
|
|
940
|
+
model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">,
|
|
941
|
+
): boolean {
|
|
940
942
|
return model.applyPatchToolType === "freeform";
|
|
941
943
|
}
|
|
942
944
|
|
|
@@ -970,7 +972,7 @@ export function mapOpenAIResponsesToolChoiceForTools(
|
|
|
970
972
|
export function convertTools(
|
|
971
973
|
tools: Tool[],
|
|
972
974
|
strictMode: boolean,
|
|
973
|
-
model: Model<"openai-responses">,
|
|
975
|
+
model: Model<"openai-responses" | "azure-openai-responses" | "openai-codex-responses">,
|
|
974
976
|
onQuarantine: (toolName: string, schemaPath: string) => void = (toolName, schemaPath) =>
|
|
975
977
|
logger.warn(
|
|
976
978
|
`Tool "${toolName}" omitted from the openai-responses request: its parameter schema is invalid for this provider at ${schemaPath} (an enum/const value cannot match its declared type). Other tools are unaffected.`,
|
|
@@ -29,7 +29,13 @@ interface AntigravityModelInfo {
|
|
|
29
29
|
displayName?: string;
|
|
30
30
|
quotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
|
31
31
|
quotaInfos?: AntigravityQuotaInfo[];
|
|
32
|
+
dailyQuotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
|
33
|
+
dailyQuotaInfos?: AntigravityQuotaInfo[];
|
|
34
|
+
weeklyQuotaInfo?: AntigravityQuotaInfo | AntigravityQuotaInfo[];
|
|
35
|
+
weeklyQuotaInfos?: AntigravityQuotaInfo[];
|
|
32
36
|
quotaInfoByTier?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
|
37
|
+
quotaInfoByWindow?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
|
38
|
+
quotaInfosByWindow?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>;
|
|
33
39
|
apiProvider?: string;
|
|
34
40
|
modelProvider?: string;
|
|
35
41
|
}
|
|
@@ -41,6 +47,39 @@ interface AntigravityUsageResponse {
|
|
|
41
47
|
const DEFAULT_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
42
48
|
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
|
43
49
|
|
|
50
|
+
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
51
|
+
const ONE_WEEK_MS = 7 * ONE_DAY_MS;
|
|
52
|
+
|
|
53
|
+
interface AntigravityWindowDescriptor {
|
|
54
|
+
id: string;
|
|
55
|
+
label: string;
|
|
56
|
+
durationMs?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function classifyWindow(id: string | undefined, label: string | undefined): AntigravityWindowDescriptor | undefined {
|
|
60
|
+
const source = `${id ?? ""} ${label ?? ""}`.toLowerCase();
|
|
61
|
+
if (source.includes("week") || source.includes("7d") || /7[\s_-]*day/.test(source)) {
|
|
62
|
+
return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
|
|
63
|
+
}
|
|
64
|
+
if (source.includes("day") || source.includes("daily") || source.includes("24h")) {
|
|
65
|
+
return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
|
|
66
|
+
}
|
|
67
|
+
if (id || label) return { id: id ?? label ?? "default", label: label ?? id ?? "Default" };
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function withWindowDescriptor(
|
|
72
|
+
info: AntigravityQuotaInfo,
|
|
73
|
+
descriptor: AntigravityWindowDescriptor | undefined,
|
|
74
|
+
): AntigravityQuotaInfo {
|
|
75
|
+
if (!descriptor) return info;
|
|
76
|
+
return {
|
|
77
|
+
...info,
|
|
78
|
+
windowId: info.windowId ?? descriptor.id,
|
|
79
|
+
windowLabel: info.windowLabel ?? descriptor.label,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
function clampFraction(value: number | undefined): number | undefined {
|
|
45
84
|
if (value === undefined || !Number.isFinite(value)) return undefined;
|
|
46
85
|
if (value < 0) return 0;
|
|
@@ -56,13 +95,15 @@ function getUsageStatus(remainingFraction: number | undefined): UsageStatus | un
|
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
function parseWindow(info: AntigravityQuotaInfo): UsageWindow | undefined {
|
|
59
|
-
|
|
60
|
-
const resetAt = Date.parse(info.resetTime);
|
|
61
|
-
|
|
98
|
+
const descriptor = classifyWindow(info.windowId, info.windowLabel);
|
|
99
|
+
const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
|
|
100
|
+
const hasResetAt = resetAt !== undefined && Number.isFinite(resetAt);
|
|
101
|
+
if (!descriptor && !hasResetAt) return undefined;
|
|
62
102
|
return {
|
|
63
|
-
id: info.windowId ?? "default",
|
|
64
|
-
label: info.windowLabel ?? "Default",
|
|
65
|
-
|
|
103
|
+
id: descriptor?.id ?? info.windowId ?? "default",
|
|
104
|
+
label: info.windowLabel ?? descriptor?.label ?? "Default",
|
|
105
|
+
...(descriptor?.durationMs !== undefined ? { durationMs: descriptor.durationMs } : {}),
|
|
106
|
+
...(hasResetAt ? { resetsAt: resetAt } : {}),
|
|
66
107
|
};
|
|
67
108
|
}
|
|
68
109
|
|
|
@@ -106,31 +147,44 @@ function normalizeQuotaInfos(info: AntigravityModelInfo): AntigravityQuotaInfo[]
|
|
|
106
147
|
...(info.apiProvider ? { apiProvider: info.apiProvider } : {}),
|
|
107
148
|
...(info.modelProvider ? { modelProvider: info.modelProvider } : {}),
|
|
108
149
|
};
|
|
109
|
-
const addInfo = (value: AntigravityQuotaInfo, tier?: string) => {
|
|
110
|
-
results.push({ ...source, ...value, ...(tier ? { tier } : {}) });
|
|
150
|
+
const addInfo = (value: AntigravityQuotaInfo, tier?: string, windowDescriptor?: AntigravityWindowDescriptor) => {
|
|
151
|
+
results.push({ ...source, ...withWindowDescriptor(value, windowDescriptor), ...(tier ? { tier } : {}) });
|
|
111
152
|
};
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
153
|
+
const addValue = (
|
|
154
|
+
value: AntigravityQuotaInfo | AntigravityQuotaInfo[] | undefined,
|
|
155
|
+
tier?: string,
|
|
156
|
+
windowDescriptor?: AntigravityWindowDescriptor,
|
|
157
|
+
) => {
|
|
158
|
+
if (!value) return;
|
|
159
|
+
if (Array.isArray(value)) {
|
|
160
|
+
for (const entry of value) addInfo(entry, tier, windowDescriptor);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
addInfo(value, tier, windowDescriptor);
|
|
115
164
|
};
|
|
116
165
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
166
|
+
addValue(info.quotaInfo);
|
|
167
|
+
addValue(info.quotaInfos);
|
|
168
|
+
addValue(info.dailyQuotaInfo, undefined, classifyWindow("daily", "Daily"));
|
|
169
|
+
addValue(info.dailyQuotaInfos, undefined, classifyWindow("daily", "Daily"));
|
|
170
|
+
addValue(info.weeklyQuotaInfo, undefined, classifyWindow("weekly", "Weekly"));
|
|
171
|
+
addValue(info.weeklyQuotaInfos, undefined, classifyWindow("weekly", "Weekly"));
|
|
123
172
|
|
|
124
173
|
if (info.quotaInfoByTier) {
|
|
125
174
|
for (const [tier, value] of Object.entries(info.quotaInfoByTier)) {
|
|
126
|
-
|
|
127
|
-
for (const entry of value) addInfo(entry, tier);
|
|
128
|
-
} else if (value) {
|
|
129
|
-
addInfo(value, tier);
|
|
130
|
-
}
|
|
175
|
+
addValue(value, tier);
|
|
131
176
|
}
|
|
132
177
|
}
|
|
133
178
|
|
|
179
|
+
const addWindowMap = (values?: Record<string, AntigravityQuotaInfo | AntigravityQuotaInfo[]>) => {
|
|
180
|
+
if (!values) return;
|
|
181
|
+
for (const [windowId, value] of Object.entries(values)) {
|
|
182
|
+
addValue(value, undefined, classifyWindow(windowId, undefined));
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
addWindowMap(info.quotaInfoByWindow);
|
|
186
|
+
addWindowMap(info.quotaInfosByWindow);
|
|
187
|
+
|
|
134
188
|
return results;
|
|
135
189
|
}
|
|
136
190
|
|
|
@@ -231,9 +285,10 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
231
285
|
const tierKey = (quotaInfo.tier ?? "default").toLowerCase();
|
|
232
286
|
const counterName = formatCounterName(quotaInfo);
|
|
233
287
|
const counterKey = counterName?.toLowerCase() ?? "default";
|
|
234
|
-
// Use
|
|
235
|
-
//
|
|
236
|
-
|
|
288
|
+
// Use the parsed window id when available so provider enum names like
|
|
289
|
+
// WINDOW_WEEKLY normalize into the same visible `/usage` group as
|
|
290
|
+
// weeklyQuotaInfo entries.
|
|
291
|
+
const windowId = window?.id ?? quotaInfo.windowId ?? "default";
|
|
237
292
|
const key = `${counterKey}|${tierKey}|${windowId}`;
|
|
238
293
|
const existing = deduped.get(key);
|
|
239
294
|
if (!existing) {
|
|
@@ -324,8 +379,6 @@ export const antigravityUsageProvider: UsageProvider = {
|
|
|
324
379
|
supports: params => params.provider === "google-antigravity",
|
|
325
380
|
};
|
|
326
381
|
|
|
327
|
-
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
328
|
-
|
|
329
382
|
function getAntigravityCounterKeyForModel(context: CredentialRankingContext | undefined): string | undefined {
|
|
330
383
|
const modelId = context?.modelId?.toLowerCase();
|
|
331
384
|
if (!modelId) return undefined;
|
|
@@ -361,17 +414,16 @@ function rankAntigravityLimits(report: UsageReport, context: CredentialRankingCo
|
|
|
361
414
|
}
|
|
362
415
|
|
|
363
416
|
/**
|
|
364
|
-
* Antigravity quotas
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
* relevant counter is index 0.
|
|
417
|
+
* Antigravity quotas are returned per backend counter (Anthropic / Google /
|
|
418
|
+
* OpenAI) and can include both daily and weekly windows. `fetchAntigravityUsage`
|
|
419
|
+
* sorts `limits` ascending by `remainingFraction`; after model-family scoping,
|
|
420
|
+
* the most-pressured relevant counter/window is index 0.
|
|
369
421
|
*
|
|
370
422
|
* Leave `secondary` unset: AuthStorage compares secondary metrics before
|
|
371
|
-
* primary metrics, which is correct for providers with
|
|
372
|
-
*
|
|
373
|
-
* avoids preferring an account at 95% Gemini / 0% Claude over one
|
|
374
|
-
*
|
|
423
|
+
* primary metrics, which is correct for providers with a fixed short/long
|
|
424
|
+
* split but wrong here. Ranking Antigravity by the bottleneck counter first
|
|
425
|
+
* avoids preferring an account at 95% Gemini daily / 0% Claude weekly over one
|
|
426
|
+
* with healthier Gemini headroom.
|
|
375
427
|
*/
|
|
376
428
|
export const antigravityRankingStrategy: CredentialRankingStrategy = {
|
|
377
429
|
findWindowLimits(report, context) {
|
|
@@ -384,8 +436,8 @@ export const antigravityRankingStrategy: CredentialRankingStrategy = {
|
|
|
384
436
|
const counterKey = getAntigravityCounterKeyForModel(context);
|
|
385
437
|
return `counter:${counterKey ?? "unknown"}`;
|
|
386
438
|
},
|
|
387
|
-
// Antigravity windows
|
|
388
|
-
//
|
|
389
|
-
//
|
|
439
|
+
// Antigravity windows carry `durationMs` when the response identifies them
|
|
440
|
+
// as daily/weekly. Fall back to daily for legacy unlabelled quotaInfo
|
|
441
|
+
// entries from `daily-cloudcode-pa.googleapis.com`.
|
|
390
442
|
windowDefaults: { primaryMs: ONE_DAY_MS, secondaryMs: ONE_DAY_MS },
|
|
391
443
|
};
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import { CODEX_BASE_URL } from "@oh-my-pi/pi-catalog/wire/codex";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the base URL for ChatGPT account-API requests (`wham/usage`,
|
|
5
|
+
* `wham/rate-limit-reset-credits`).
|
|
6
|
+
*
|
|
7
|
+
* These endpoints live on the canonical ChatGPT origin and authenticate with
|
|
8
|
+
* the Codex OAuth bearer minted for that origin. They are NOT part of the
|
|
9
|
+
* `/responses` API surface that streaming proxies (Headroom, 9router, etc.)
|
|
10
|
+
* forward, so a `providers.openai-codex.baseUrl` override pointed at such a
|
|
11
|
+
* proxy MUST NOT be used here — doing so 404s and silently breaks
|
|
12
|
+
* `/usage show` (issue #3679).
|
|
13
|
+
*
|
|
14
|
+
* Accepted overrides are the canonical `chatgpt.com` / `chat.openai.com`
|
|
15
|
+
* origins. Any extra path (e.g. a streaming override like
|
|
16
|
+
* `/backend-api/codex/responses`) is normalized to `${origin}/backend-api`,
|
|
17
|
+
* since the account endpoints always live directly under `/backend-api`. Any
|
|
18
|
+
* other host falls back to {@link CODEX_BASE_URL}.
|
|
19
|
+
*/
|
|
3
20
|
export function normalizeCodexBaseUrl(baseUrl?: string): string {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
) {
|
|
12
|
-
return `${base}/backend-api`;
|
|
21
|
+
const trimmed = baseUrl?.trim().replace(/\/+$/, "");
|
|
22
|
+
if (!trimmed) return CODEX_BASE_URL;
|
|
23
|
+
let parsed: URL;
|
|
24
|
+
try {
|
|
25
|
+
parsed = new URL(trimmed);
|
|
26
|
+
} catch {
|
|
27
|
+
return CODEX_BASE_URL;
|
|
13
28
|
}
|
|
14
|
-
|
|
29
|
+
const host = parsed.host.toLowerCase();
|
|
30
|
+
if (host !== "chatgpt.com" && host !== "chat.openai.com") return CODEX_BASE_URL;
|
|
31
|
+
// wham/usage always lives at `${origin}/backend-api/...` on the canonical
|
|
32
|
+
// ChatGPT origin, so ignore any extra path (e.g. `/backend-api/codex/responses`
|
|
33
|
+
// from a streaming baseUrl) — otherwise we'd build `.../codex/responses/wham/usage`.
|
|
34
|
+
return `${parsed.origin}/backend-api`;
|
|
15
35
|
}
|
package/src/usage/zai.ts
CHANGED
|
@@ -15,6 +15,15 @@ const DEFAULT_ENDPOINT = "https://api.z.ai";
|
|
|
15
15
|
const QUOTA_PATH = "/api/monitor/usage/quota/limit";
|
|
16
16
|
const MODEL_USAGE_PATH = "/api/monitor/usage/model-usage";
|
|
17
17
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
|
18
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
19
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
20
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
21
|
+
const MONTH_MS = 30 * DAY_MS;
|
|
22
|
+
|
|
23
|
+
interface ZaiUsageDetail {
|
|
24
|
+
modelCode?: string;
|
|
25
|
+
usage?: number;
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
function normalizeZaiBaseUrl(baseUrl?: string): string {
|
|
20
29
|
if (!baseUrl?.trim()) return DEFAULT_ENDPOINT;
|
|
@@ -32,6 +41,9 @@ interface ZaiUsageLimitItem {
|
|
|
32
41
|
percentage?: number;
|
|
33
42
|
remaining?: number;
|
|
34
43
|
nextResetTime?: number;
|
|
44
|
+
unit?: number;
|
|
45
|
+
number?: number;
|
|
46
|
+
usageDetails?: ZaiUsageDetail[];
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
interface ZaiQuotaPayload {
|
|
@@ -49,6 +61,21 @@ function parseMillis(value: unknown): number | undefined {
|
|
|
49
61
|
return parsed > 1_000_000_000_000 ? parsed : parsed * 1000;
|
|
50
62
|
}
|
|
51
63
|
|
|
64
|
+
function parseUsageDetails(value: unknown): ZaiUsageDetail[] | undefined {
|
|
65
|
+
if (!Array.isArray(value)) return undefined;
|
|
66
|
+
const details: ZaiUsageDetail[] = [];
|
|
67
|
+
for (const item of value) {
|
|
68
|
+
if (!isRecord(item)) continue;
|
|
69
|
+
const modelCode = typeof item.modelCode === "string" && item.modelCode ? item.modelCode : undefined;
|
|
70
|
+
const usage = toNumber(item.usage);
|
|
71
|
+
details.push({
|
|
72
|
+
...(modelCode !== undefined ? { modelCode } : {}),
|
|
73
|
+
...(usage !== undefined ? { usage } : {}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return details.length > 0 ? details : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
|
|
53
80
|
if (!isRecord(value)) return null;
|
|
54
81
|
const type = typeof value.type === "string" ? value.type : undefined;
|
|
@@ -60,6 +87,9 @@ function parseLimitItem(value: unknown): ZaiUsageLimitItem | null {
|
|
|
60
87
|
percentage: toNumber(value.percentage),
|
|
61
88
|
remaining: toNumber(value.remaining),
|
|
62
89
|
nextResetTime: parseMillis(value.nextResetTime),
|
|
90
|
+
unit: toNumber(value.unit),
|
|
91
|
+
number: toNumber(value.number),
|
|
92
|
+
usageDetails: parseUsageDetails(value.usageDetails),
|
|
63
93
|
};
|
|
64
94
|
}
|
|
65
95
|
|
|
@@ -101,6 +131,59 @@ function formatDate(value: Date): string {
|
|
|
101
131
|
)}:${pad(value.getSeconds())}`;
|
|
102
132
|
}
|
|
103
133
|
|
|
134
|
+
function formatCountedUnit(count: number, singular: string): string {
|
|
135
|
+
const suffix = count === 1 ? "" : "s";
|
|
136
|
+
return `${count} ${singular}${suffix}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildZaiWindow(parsed: ZaiUsageLimitItem): UsageWindow {
|
|
140
|
+
const count = parsed.number !== undefined && parsed.number > 0 ? parsed.number : 1;
|
|
141
|
+
let id: string;
|
|
142
|
+
let label: string;
|
|
143
|
+
let durationMs: number | undefined;
|
|
144
|
+
switch (parsed.unit) {
|
|
145
|
+
case 3:
|
|
146
|
+
id = `${count}h`;
|
|
147
|
+
label = formatCountedUnit(count, "Hour");
|
|
148
|
+
durationMs = count * HOUR_MS;
|
|
149
|
+
break;
|
|
150
|
+
case 4:
|
|
151
|
+
id = `${count}d`;
|
|
152
|
+
label = formatCountedUnit(count, "Day");
|
|
153
|
+
durationMs = count * DAY_MS;
|
|
154
|
+
break;
|
|
155
|
+
case 5:
|
|
156
|
+
id = `${count}mo`;
|
|
157
|
+
label = count === 1 ? "Monthly" : formatCountedUnit(count, "Month");
|
|
158
|
+
durationMs = count * MONTH_MS;
|
|
159
|
+
break;
|
|
160
|
+
case 6:
|
|
161
|
+
id = "1w";
|
|
162
|
+
label = "Weekly";
|
|
163
|
+
durationMs = WEEK_MS;
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
id = parsed.unit !== undefined ? `${count}u${parsed.unit}` : "quota";
|
|
167
|
+
label = "Quota";
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
id,
|
|
172
|
+
label,
|
|
173
|
+
...(durationMs !== undefined ? { durationMs } : {}),
|
|
174
|
+
...(parsed.nextResetTime !== undefined ? { resetsAt: parsed.nextResetTime } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function requestQuotaLabel(parsed: ZaiUsageLimitItem): string {
|
|
179
|
+
const detailCodes =
|
|
180
|
+
parsed.usageDetails?.map(detail => detail.modelCode).filter((code): code is string => !!code) ?? [];
|
|
181
|
+
if (detailCodes.includes("search-prime") && detailCodes.includes("web-reader") && detailCodes.includes("zread")) {
|
|
182
|
+
return "ZAI Web Search / Reader / Zread Quota";
|
|
183
|
+
}
|
|
184
|
+
return "ZAI Request Quota";
|
|
185
|
+
}
|
|
186
|
+
|
|
104
187
|
function buildModelUsageUrl(baseUrl: string, now: Date): string {
|
|
105
188
|
const start = new Date(now.getTime() - SEVEN_DAYS_MS);
|
|
106
189
|
const startTime = formatDate(start);
|
|
@@ -157,18 +240,13 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
157
240
|
percentage: parsed.percentage,
|
|
158
241
|
unit: "tokens",
|
|
159
242
|
});
|
|
160
|
-
const window
|
|
161
|
-
id: "quota",
|
|
162
|
-
label: "Quota",
|
|
163
|
-
durationMs: SEVEN_DAYS_MS,
|
|
164
|
-
resetsAt: parsed.nextResetTime,
|
|
165
|
-
};
|
|
243
|
+
const window = buildZaiWindow(parsed);
|
|
166
244
|
limits.push({
|
|
167
|
-
id:
|
|
168
|
-
label:
|
|
245
|
+
id: `zai:tokens:${window.id}`,
|
|
246
|
+
label: `ZAI ${window.label} Token Quota`,
|
|
169
247
|
scope: {
|
|
170
248
|
provider: params.provider,
|
|
171
|
-
windowId: window
|
|
249
|
+
windowId: window.id,
|
|
172
250
|
shared: true,
|
|
173
251
|
},
|
|
174
252
|
window,
|
|
@@ -177,12 +255,7 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
177
255
|
});
|
|
178
256
|
}
|
|
179
257
|
if (parsed.type === "TIME_LIMIT") {
|
|
180
|
-
const window
|
|
181
|
-
id: "quota",
|
|
182
|
-
label: "Quota",
|
|
183
|
-
durationMs: SEVEN_DAYS_MS,
|
|
184
|
-
resetsAt: parsed.nextResetTime,
|
|
185
|
-
};
|
|
258
|
+
const window = buildZaiWindow(parsed);
|
|
186
259
|
const amount = buildUsageAmount({
|
|
187
260
|
used: parsed.currentValue,
|
|
188
261
|
limit: parsed.usage,
|
|
@@ -191,11 +264,11 @@ async function fetchZaiUsage(params: UsageFetchParams, ctx: UsageFetchContext):
|
|
|
191
264
|
unit: "requests",
|
|
192
265
|
});
|
|
193
266
|
limits.push({
|
|
194
|
-
id:
|
|
195
|
-
label:
|
|
267
|
+
id: `zai:requests:${window.id}`,
|
|
268
|
+
label: requestQuotaLabel(parsed),
|
|
196
269
|
scope: {
|
|
197
270
|
provider: params.provider,
|
|
198
|
-
windowId:
|
|
271
|
+
windowId: window.id,
|
|
199
272
|
shared: true,
|
|
200
273
|
},
|
|
201
274
|
window,
|
|
@@ -38,24 +38,33 @@ export type StreamMarkupHealingEvent =
|
|
|
38
38
|
* State machine that consumes streamed visible text and emits cleaned text,
|
|
39
39
|
* thinking deltas, and reconstructed tool calls.
|
|
40
40
|
*
|
|
41
|
+
* A {@link ThinkingInbandScanner} always heals leaked reasoning idioms
|
|
42
|
+
* (`<think>`, `<thinking>`, ` ```thinking `, Gemma/Harmony channels, …) out of
|
|
43
|
+
* the visible channel. For Kimi / DeepSeek-DSML the provider tool-call grammar
|
|
44
|
+
* runs first and its cleaned text is piped through that thinking healer, so a
|
|
45
|
+
* model can leak tool-call markup and reasoning in the same stream.
|
|
46
|
+
*
|
|
41
47
|
* Feed only one stream channel (usually `delta.content` / `message.content`).
|
|
42
48
|
* Mixing reasoning and visible text into the same instance can corrupt held-back
|
|
43
49
|
* partial tag buffers.
|
|
44
50
|
*/
|
|
45
51
|
export class StreamMarkupHealing {
|
|
46
52
|
readonly #pattern: StreamMarkupHealingPattern;
|
|
47
|
-
|
|
53
|
+
/** Provider tool-call grammar (Kimi tokens / DSML envelope); absent for plain text streams. */
|
|
54
|
+
readonly #toolScanner: InbandScanner | undefined;
|
|
55
|
+
/** Always-on healer for leaked reasoning idioms in the visible text channel. */
|
|
56
|
+
readonly #thinkingScanner = new ThinkingInbandScanner();
|
|
48
57
|
#sectionTerminated = false;
|
|
49
58
|
readonly #completed: HealedToolCall[] = [];
|
|
50
59
|
|
|
51
60
|
constructor(options: StreamMarkupHealingOptions) {
|
|
52
61
|
this.#pattern = options.pattern;
|
|
53
|
-
this.#
|
|
62
|
+
this.#toolScanner =
|
|
54
63
|
options.pattern === "kimi"
|
|
55
64
|
? createInbandScanner("kimi")
|
|
56
65
|
: options.pattern === "dsml"
|
|
57
66
|
? createInbandScanner("xml", { xmlTagset: "dsml" })
|
|
58
|
-
:
|
|
67
|
+
: undefined;
|
|
59
68
|
}
|
|
60
69
|
|
|
61
70
|
get pattern(): StreamMarkupHealingPattern {
|
|
@@ -84,7 +93,8 @@ export class StreamMarkupHealing {
|
|
|
84
93
|
feedEvents(text: string): StreamMarkupHealingEvent[] {
|
|
85
94
|
if (text.length === 0) return [];
|
|
86
95
|
this.#markSectionClosed(text);
|
|
87
|
-
return this.#convertScannerEvents(this.#
|
|
96
|
+
if (!this.#toolScanner) return this.#convertScannerEvents(this.#thinkingScanner.feed(text));
|
|
97
|
+
return this.#convertScannerEvents(this.#healThinking(this.#toolScanner.feed(text)));
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
/**
|
|
@@ -120,7 +130,9 @@ export class StreamMarkupHealing {
|
|
|
120
130
|
* behavior.
|
|
121
131
|
*/
|
|
122
132
|
flushEvents(): StreamMarkupHealingEvent[] {
|
|
123
|
-
|
|
133
|
+
const tail = this.#toolScanner ? this.#healThinking(this.#toolScanner.flush()) : [];
|
|
134
|
+
tail.push(...this.#thinkingScanner.flush());
|
|
135
|
+
return this.#convertScannerEvents(tail);
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
/** Flush held-back text only. Reconstructed calls are retained for {@link drainCompleted}. */
|
|
@@ -142,7 +154,7 @@ export class StreamMarkupHealing {
|
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
#markSectionClosed(text: string): void {
|
|
145
|
-
if (this.#sectionTerminated) return;
|
|
157
|
+
if (this.#sectionTerminated || !this.#toolScanner) return;
|
|
146
158
|
if (this.#pattern === "kimi") {
|
|
147
159
|
this.#sectionTerminated = text.includes(KIMI_SECTION_END);
|
|
148
160
|
return;
|
|
@@ -151,6 +163,20 @@ export class StreamMarkupHealing {
|
|
|
151
163
|
text.includes(DSML_TOOL_CALLS_CLOSE_FULLWIDTH) || text.includes(DSML_TOOL_CALLS_CLOSE_ASCII);
|
|
152
164
|
}
|
|
153
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Re-scan the tool scanner's visible text through the always-on thinking
|
|
168
|
+
* healer: `text` events are healed for leaked reasoning idioms, while the tool
|
|
169
|
+
* scanner's own thinking / tool-call events pass through in stream order.
|
|
170
|
+
*/
|
|
171
|
+
#healThinking(toolEvents: readonly InbandScanEvent[]): InbandScanEvent[] {
|
|
172
|
+
const out: InbandScanEvent[] = [];
|
|
173
|
+
for (const event of toolEvents) {
|
|
174
|
+
if (event.type === "text") out.push(...this.#thinkingScanner.feed(event.text));
|
|
175
|
+
else out.push(event);
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
154
180
|
#convertScannerEvents(events: readonly InbandScanEvent[]): StreamMarkupHealingEvent[] {
|
|
155
181
|
const out: StreamMarkupHealingEvent[] = [];
|
|
156
182
|
for (const event of events) {
|
|
@@ -207,18 +233,15 @@ export function modelMayLeakDsmlToolCalls(provider: string, modelId: string): bo
|
|
|
207
233
|
);
|
|
208
234
|
}
|
|
209
235
|
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
options?: { readonly parseThinkingTags?: boolean },
|
|
219
|
-
): StreamMarkupHealingPattern | undefined {
|
|
220
|
-
if (options?.parseThinkingTags || modelMayLeakThinkingTags(provider, modelId)) return "thinking";
|
|
236
|
+
/**
|
|
237
|
+
* Pick the leaked-markup healer for an OpenAI-compatible / Ollama visible-text
|
|
238
|
+
* stream. Kimi chat-template tokens and DeepSeek DSML envelopes need their
|
|
239
|
+
* dedicated tool-call grammars; every other model uses `"thinking"`. All three
|
|
240
|
+
* patterns run the generic {@link ThinkingInbandScanner}, so leaked reasoning
|
|
241
|
+
* idioms (e.g. a Gemini ` ```thinking ` fence on OpenRouter) are always healed.
|
|
242
|
+
*/
|
|
243
|
+
export function getStreamMarkupHealingPattern(provider: string, modelId: string): StreamMarkupHealingPattern {
|
|
221
244
|
if (modelMayLeakKimiToolCalls(provider, modelId)) return "kimi";
|
|
222
245
|
if (modelMayLeakDsmlToolCalls(provider, modelId)) return "dsml";
|
|
223
|
-
return
|
|
246
|
+
return "thinking";
|
|
224
247
|
}
|