@oh-my-pi/pi-ai 16.2.5 → 16.2.7
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 +28 -0
- package/dist/types/auth-storage.d.ts +11 -10
- package/dist/types/providers/__tests__/kimi-code-thinking.test.d.ts +1 -0
- package/dist/types/providers/google-auth.d.ts +8 -0
- package/dist/types/providers/google-interactions.d.ts +65 -0
- package/dist/types/providers/google-shared.d.ts +20 -5
- package/dist/types/providers/google-types.d.ts +5 -0
- package/dist/types/providers/openai-codex/request-transformer.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +1 -1
- package/dist/types/providers/openai-shared.d.ts +3 -3
- package/dist/types/types.d.ts +80 -30
- package/dist/types/utils/leaked-thinking-stream.d.ts +29 -0
- package/package.json +4 -4
- package/src/auth-storage.ts +56 -52
- package/src/providers/__tests__/kimi-code-thinking.test.ts +112 -0
- package/src/providers/anthropic.ts +11 -10
- package/src/providers/google-auth.ts +25 -0
- package/src/providers/google-gemini-cli.ts +90 -41
- package/src/providers/google-interactions.ts +753 -0
- package/src/providers/google-shared.ts +74 -13
- package/src/providers/google-types.ts +5 -1
- package/src/providers/google-vertex.ts +117 -26
- package/src/providers/google.ts +61 -19
- package/src/providers/openai-chat-server.ts +2 -2
- package/src/providers/openai-codex/request-transformer.ts +17 -4
- package/src/providers/openai-codex-responses.ts +23 -5
- package/src/providers/openai-shared.ts +5 -11
- package/src/stream.ts +34 -12
- package/src/types.ts +164 -51
- package/src/usage/google-antigravity.ts +59 -5
- package/src/utils/leaked-thinking-stream.ts +260 -0
package/src/stream.ts
CHANGED
|
@@ -71,6 +71,7 @@ import type {
|
|
|
71
71
|
ToolChoice,
|
|
72
72
|
} from "./types";
|
|
73
73
|
import { AssistantMessageEventStream } from "./utils/event-stream";
|
|
74
|
+
import { wrapLeakedThinkingStream } from "./utils/leaked-thinking-stream";
|
|
74
75
|
import { wrapFetchForProxy } from "./utils/proxy";
|
|
75
76
|
import { withRequestDebugFetch } from "./utils/request-debug";
|
|
76
77
|
import { withGeminiThinkingLoopGuard } from "./utils/thinking-loop";
|
|
@@ -367,14 +368,17 @@ async function tryAcquireProviderInFlightLease(
|
|
|
367
368
|
}
|
|
368
369
|
}
|
|
369
370
|
|
|
370
|
-
async function
|
|
371
|
+
async function signalProviderInFlightWaitersInDir(dir: string): Promise<void> {
|
|
371
372
|
try {
|
|
372
|
-
const dir = providerInFlightDir(provider);
|
|
373
373
|
await fs.mkdir(dir, { recursive: true });
|
|
374
|
-
await Bun.write(
|
|
374
|
+
await Bun.write(path.join(dir, ".wakeup"), String(Date.now()));
|
|
375
375
|
} catch {}
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
+
async function signalProviderInFlightWaiters(provider: string): Promise<void> {
|
|
379
|
+
await signalProviderInFlightWaitersInDir(providerInFlightDir(provider));
|
|
380
|
+
}
|
|
381
|
+
|
|
378
382
|
function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal): Promise<void> {
|
|
379
383
|
if (signal?.aborted)
|
|
380
384
|
return Promise.reject(signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch"));
|
|
@@ -434,11 +438,15 @@ async function removeProviderInFlightLeaseDir(leasePath: string): Promise<void>
|
|
|
434
438
|
}
|
|
435
439
|
}
|
|
436
440
|
|
|
437
|
-
|
|
441
|
+
// Signal into the lease's OWN provider directory (derived from `lease.path`)
|
|
442
|
+
// rather than recomputing it from the current root. A release that lands after
|
|
443
|
+
// the in-flight root has been repointed (only the test seam does that) must not
|
|
444
|
+
// write `.wakeup` into an unrelated provider directory.
|
|
445
|
+
async function releaseProviderInFlightLease(lease: ProviderInFlightLease): Promise<void> {
|
|
438
446
|
clearInterval(lease.heartbeat);
|
|
439
447
|
await lease.flushHeartbeat();
|
|
440
448
|
await removeProviderInFlightLeaseDir(lease.path);
|
|
441
|
-
await
|
|
449
|
+
await signalProviderInFlightWaitersInDir(path.dirname(lease.path));
|
|
442
450
|
}
|
|
443
451
|
|
|
444
452
|
async function acquireProviderInFlightSlot(
|
|
@@ -451,7 +459,7 @@ async function acquireProviderInFlightSlot(
|
|
|
451
459
|
while (true) {
|
|
452
460
|
if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
453
461
|
const lease = await tryAcquireProviderInFlightLease(provider, limit, signal);
|
|
454
|
-
if (lease) return () => releaseProviderInFlightLease(
|
|
462
|
+
if (lease) return () => releaseProviderInFlightLease(lease);
|
|
455
463
|
if (!loggedWait) {
|
|
456
464
|
loggedWait = true;
|
|
457
465
|
logger.debug("Provider in-flight limit blocked request", { provider, limit });
|
|
@@ -492,8 +500,11 @@ function withProviderInFlightLimit<TOptions extends Pick<StreamOptions, "signal"
|
|
|
492
500
|
options: TOptions | undefined,
|
|
493
501
|
dispatch: () => AssistantMessageEventStream,
|
|
494
502
|
): AssistantMessageEventStream {
|
|
503
|
+
// Leaked-thinking healing folds in here — the one shared provider-dispatch
|
|
504
|
+
// chokepoint — so the loop guard (which wraps this) sees healed events and all
|
|
505
|
+
// six provider exits are covered by one wrap. Healing is idempotent.
|
|
495
506
|
const limit = resolveProviderInFlightLimit(model.provider, options);
|
|
496
|
-
if (limit === undefined) return dispatch();
|
|
507
|
+
if (limit === undefined) return wrapLeakedThinkingStream(dispatch());
|
|
497
508
|
|
|
498
509
|
const outer = new AssistantMessageEventStream();
|
|
499
510
|
void (async () => {
|
|
@@ -513,7 +524,7 @@ function withProviderInFlightLimit<TOptions extends Pick<StreamOptions, "signal"
|
|
|
513
524
|
if (options?.signal?.aborted) {
|
|
514
525
|
throw options.signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
|
|
515
526
|
}
|
|
516
|
-
const inner = dispatch();
|
|
527
|
+
const inner = wrapLeakedThinkingStream(dispatch());
|
|
517
528
|
try {
|
|
518
529
|
for await (const event of inner) {
|
|
519
530
|
outer.push(event);
|
|
@@ -1098,10 +1109,13 @@ export function streamSimple<TApi extends Api>(
|
|
|
1098
1109
|
|
|
1099
1110
|
// GitLab Duo Workflow - IDE workflow protocol + WebSocket action bridge
|
|
1100
1111
|
if (model.api === "gitlab-duo-agent") {
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1112
|
+
// Does not route through withProviderInFlightLimit, so heal explicitly.
|
|
1113
|
+
return wrapLeakedThinkingStream(
|
|
1114
|
+
streamGitLabDuoWorkflow(model as Model<"gitlab-duo-agent">, context, {
|
|
1115
|
+
...requestOptions,
|
|
1116
|
+
apiKey,
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1105
1119
|
}
|
|
1106
1120
|
|
|
1107
1121
|
// Kimi Code - route to dedicated handler that wraps OpenAI or Anthropic API
|
|
@@ -1348,6 +1362,9 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1348
1362
|
streamFirstEventTimeoutMs: options?.streamFirstEventTimeoutMs,
|
|
1349
1363
|
streamIdleTimeoutMs: options?.streamIdleTimeoutMs,
|
|
1350
1364
|
providerSessionState: options?.providerSessionState,
|
|
1365
|
+
useInteractionsApi: options?.useInteractionsApi,
|
|
1366
|
+
storeInteraction: options?.storeInteraction,
|
|
1367
|
+
previousInteractionId: options?.previousInteractionId,
|
|
1351
1368
|
maxInFlightRequests: options?.maxInFlightRequests,
|
|
1352
1369
|
onPayload: options?.onPayload,
|
|
1353
1370
|
onResponse: options?.onResponse,
|
|
@@ -1558,6 +1575,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1558
1575
|
if (!reasoning || !model.reasoning) {
|
|
1559
1576
|
return castApi<"google-generative-ai">({
|
|
1560
1577
|
...base,
|
|
1578
|
+
serviceTier: options?.serviceTier,
|
|
1561
1579
|
thinking: { enabled: false },
|
|
1562
1580
|
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1563
1581
|
});
|
|
@@ -1571,6 +1589,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1571
1589
|
if (googleModel.thinking?.mode === "google-level") {
|
|
1572
1590
|
return castApi<"google-generative-ai">({
|
|
1573
1591
|
...base,
|
|
1592
|
+
serviceTier: options?.serviceTier,
|
|
1574
1593
|
thinking: {
|
|
1575
1594
|
enabled: true,
|
|
1576
1595
|
level: mapEffortToGoogleThinkingLevel(effort),
|
|
@@ -1654,6 +1673,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1654
1673
|
if (!reasoning || !model.reasoning) {
|
|
1655
1674
|
return castApi<"google-vertex">({
|
|
1656
1675
|
...base,
|
|
1676
|
+
serviceTier: options?.serviceTier,
|
|
1657
1677
|
thinking: { enabled: false },
|
|
1658
1678
|
toolChoice: mapGoogleToolChoice(options?.toolChoice),
|
|
1659
1679
|
});
|
|
@@ -1666,6 +1686,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1666
1686
|
if (geminiModel.thinking?.mode === "google-level") {
|
|
1667
1687
|
return castApi<"google-vertex">({
|
|
1668
1688
|
...base,
|
|
1689
|
+
serviceTier: options?.serviceTier,
|
|
1669
1690
|
thinking: {
|
|
1670
1691
|
enabled: true,
|
|
1671
1692
|
level: mapEffortToGoogleThinkingLevel(effort),
|
|
@@ -1676,6 +1697,7 @@ function mapOptionsForApi<TApi extends Api>(
|
|
|
1676
1697
|
|
|
1677
1698
|
return castApi<"google-vertex">({
|
|
1678
1699
|
...base,
|
|
1700
|
+
serviceTier: options?.serviceTier,
|
|
1679
1701
|
thinking: {
|
|
1680
1702
|
enabled: true,
|
|
1681
1703
|
budgetTokens: getGoogleBudget(geminiModel, effort, options?.thinkingBudgets),
|
package/src/types.ts
CHANGED
|
@@ -105,84 +105,181 @@ export type ToolChoice =
|
|
|
105
105
|
export type CacheRetention = "none" | "short" | "long";
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
|
-
* Service tier hint for processing priority / cost control.
|
|
108
|
+
* Service tier hint for processing priority / cost control. These are the
|
|
109
|
+
* values providers consume on the wire:
|
|
109
110
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* (
|
|
113
|
-
* `
|
|
111
|
+
* - OpenAI / OpenAI-Codex: sent verbatim as the `service_tier` field
|
|
112
|
+
* (`flex`/`scale`/`priority`).
|
|
113
|
+
* - Google (Gemini API + Vertex AI): sent as the top-level `serviceTier`
|
|
114
|
+
* field (`flex`/`priority`).
|
|
115
|
+
* - OpenRouter: passed through as `service_tier`; OpenRouter realizes it for
|
|
116
|
+
* the OpenAI- and Google-family upstreams it supports and ignores it
|
|
117
|
+
* otherwise.
|
|
118
|
+
* - Direct Anthropic: `"priority"` is translated into `speed: "fast"` plus the
|
|
119
|
+
* fast-mode beta on supported Opus models. Other tiers are ignored.
|
|
114
120
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* They let users opt into priority on one family without paying premium
|
|
118
|
-
* costs on the other when switching models mid-session.
|
|
119
|
-
*
|
|
120
|
-
* - `"openai-only"` → `"priority"` on `openai` and `openai-codex`; ignored elsewhere.
|
|
121
|
-
* - `"claude-only"` → `"priority"` on direct `anthropic` (not Bedrock/Vertex Claude).
|
|
121
|
+
* Per-family scoping is expressed by {@link ServiceTierByFamily}, not by
|
|
122
|
+
* scoped sentinel values — see {@link serviceTierFamily}.
|
|
122
123
|
*/
|
|
123
|
-
export type ServiceTier = "auto" | "default" | "flex" | "scale" | "priority"
|
|
124
|
+
export type ServiceTier = "auto" | "default" | "flex" | "scale" | "priority";
|
|
124
125
|
|
|
125
|
-
/**
|
|
126
|
-
export type
|
|
126
|
+
/** Provider families that expose an independent service-tier knob. */
|
|
127
|
+
export type ServiceTierFamily = "openai" | "anthropic" | "google";
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
130
|
+
* Per-family service-tier selection. A request consults only the entry for the
|
|
131
|
+
* family its model belongs to (see {@link resolveModelServiceTier}), so a user
|
|
132
|
+
* can opt one family into priority without affecting the others when switching
|
|
133
|
+
* models mid-session.
|
|
132
134
|
*/
|
|
133
|
-
export
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
export type ServiceTierByFamily = Partial<Record<ServiceTierFamily, ServiceTier>>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Classify a model into the service-tier family whose knob governs it, or
|
|
139
|
+
* `undefined` when the model exposes no serving-priority control.
|
|
140
|
+
*
|
|
141
|
+
* OpenRouter models are classified by id namespace (`anthropic/`, `google/`,
|
|
142
|
+
* `openai/`); Claude on Bedrock/Vertex (api `anthropic-messages`) is the
|
|
143
|
+
* anthropic family even though its provider is `amazon-bedrock`/`google-vertex`.
|
|
144
|
+
*/
|
|
145
|
+
export function serviceTierFamily(model: Pick<Model, "provider" | "api" | "id">): ServiceTierFamily | undefined {
|
|
146
|
+
const provider = model.provider;
|
|
147
|
+
if (provider === "openrouter") {
|
|
148
|
+
const id = model.id.toLowerCase();
|
|
149
|
+
if (id.startsWith("anthropic/")) return "anthropic";
|
|
150
|
+
if (id.startsWith("google/")) return "google";
|
|
151
|
+
if (id.startsWith("openai/")) return "openai";
|
|
152
|
+
return undefined;
|
|
145
153
|
}
|
|
154
|
+
if (provider === "openai" || provider === "openai-codex") return "openai";
|
|
155
|
+
if (model.api === "anthropic-messages") return "anthropic";
|
|
156
|
+
if (provider === "google" || provider === "google-vertex") return "google";
|
|
157
|
+
return undefined;
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
/**
|
|
149
|
-
*
|
|
150
|
-
* `
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
161
|
+
* Reduce a per-family tier map to the single wire tier for `model` — the entry
|
|
162
|
+
* for the model's family, or `undefined` when the model has no family.
|
|
163
|
+
*/
|
|
164
|
+
export function resolveModelServiceTier(
|
|
165
|
+
tiers: ServiceTierByFamily | null | undefined,
|
|
166
|
+
model: Pick<Model, "provider" | "api" | "id">,
|
|
167
|
+
): ServiceTier | undefined {
|
|
168
|
+
if (!tiers) return undefined;
|
|
169
|
+
const family = serviceTierFamily(model);
|
|
170
|
+
return family ? tiers[family] : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* True when the tier should be sent on the wire as the provider's service-tier
|
|
175
|
+
* request field. OpenAI / OpenAI-Codex accept `flex`/`scale`/`priority`; Google
|
|
176
|
+
* (Gemini API + Vertex) and OpenRouter accept `flex`/`priority`; Fireworks
|
|
177
|
+
* Serverless realizes only its Priority serving path. Anthropic is absent — it
|
|
178
|
+
* realizes `priority` via `speed: "fast"`, not a service-tier field.
|
|
155
179
|
*/
|
|
156
180
|
export function shouldSendServiceTier(
|
|
157
181
|
serviceTier: ServiceTier | null | undefined,
|
|
158
182
|
provider: Provider | undefined,
|
|
159
183
|
): boolean {
|
|
160
|
-
|
|
161
|
-
if (provider === "openai" || provider === "openai-codex") {
|
|
162
|
-
return
|
|
184
|
+
if (!serviceTier) return false;
|
|
185
|
+
if (provider === "openai" || provider === "openai-codex" || provider === "openrouter") {
|
|
186
|
+
return serviceTier === "flex" || serviceTier === "scale" || serviceTier === "priority";
|
|
187
|
+
}
|
|
188
|
+
if (provider === "google") {
|
|
189
|
+
return serviceTier === "flex" || serviceTier === "priority";
|
|
163
190
|
}
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
// Vertex realizes only priority (via header); flex has no documented control.
|
|
192
|
+
if (provider === "google-vertex" || provider === "fireworks") {
|
|
193
|
+
return serviceTier === "priority";
|
|
166
194
|
}
|
|
167
195
|
return false;
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
199
|
+
* True when `priority` will actually be realized on the wire for `model`.
|
|
200
|
+
* Direct Anthropic realizes fast mode; OpenAI/Google/Fireworks emit the
|
|
201
|
+
* service-tier field; OpenRouter realizes it only for its OpenAI- and
|
|
202
|
+
* Google-family upstreams. Bedrock/Vertex Claude and OpenRouter Anthropic
|
|
203
|
+
* models do not realize priority and return `false`.
|
|
204
|
+
*/
|
|
205
|
+
export function realizesPriorityServiceTier(
|
|
206
|
+
serviceTier: ServiceTier | null | undefined,
|
|
207
|
+
model: Pick<Model, "provider" | "api" | "id">,
|
|
208
|
+
): boolean {
|
|
209
|
+
if (serviceTier !== "priority") return false;
|
|
210
|
+
if (model.provider === "anthropic") return true;
|
|
211
|
+
if (model.provider === "openrouter") {
|
|
212
|
+
const family = serviceTierFamily(model);
|
|
213
|
+
return family === "openai" || family === "google";
|
|
214
|
+
}
|
|
215
|
+
if (model.api === "anthropic-messages") return false;
|
|
216
|
+
return shouldSendServiceTier(serviceTier, model.provider);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Premium-request weight contributed by a priority request to a provider that
|
|
221
|
+
* realizes it and bills extra. Mirrors GitHub Copilot's `premiumRequests`
|
|
222
|
+
* accounting so the "premium requests" stat aggregates priority traffic across
|
|
223
|
+
* the OpenAI family, direct Anthropic fast mode, and Google priority.
|
|
175
224
|
*
|
|
176
|
-
* Returns 1
|
|
225
|
+
* Returns 1 only when priority is actually realized on the wire for `model`
|
|
226
|
+
* (see {@link realizesPriorityServiceTier}) and the provider bills it as a
|
|
227
|
+
* premium request. OpenRouter is excluded — it bills per its own pricing, not
|
|
228
|
+
* Copilot-premium semantics — as are Bedrock/Vertex Claude, where priority is
|
|
229
|
+
* silently dropped.
|
|
177
230
|
*/
|
|
178
231
|
export function getPriorityPremiumRequests(
|
|
179
232
|
serviceTier: ServiceTier | null | undefined,
|
|
180
|
-
|
|
233
|
+
model: Pick<Model, "provider" | "api" | "id">,
|
|
181
234
|
): number {
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
235
|
+
if (!realizesPriorityServiceTier(serviceTier, model)) return 0;
|
|
236
|
+
const provider = model.provider;
|
|
237
|
+
return provider === "openai" ||
|
|
238
|
+
provider === "openai-codex" ||
|
|
239
|
+
provider === "anthropic" ||
|
|
240
|
+
provider === "google" ||
|
|
241
|
+
provider === "google-vertex"
|
|
242
|
+
? 1
|
|
243
|
+
: 0;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Coerce a persisted service-tier value to a {@link ServiceTierByFamily}. Newer
|
|
248
|
+
* sessions store the family map directly; legacy sessions stored a single
|
|
249
|
+
* scalar — `"priority"` applied everywhere, `"openai-only"`/`"claude-only"`
|
|
250
|
+
* scoped to one family, and the remaining values were OpenAI-only semantics.
|
|
251
|
+
*/
|
|
252
|
+
export function coerceServiceTierByFamily(value: unknown): ServiceTierByFamily | undefined {
|
|
253
|
+
if (value === null || value === undefined) return undefined;
|
|
254
|
+
if (typeof value === "object") {
|
|
255
|
+
const src = value as Record<string, unknown>;
|
|
256
|
+
const out: ServiceTierByFamily = {};
|
|
257
|
+
for (const family of ["openai", "anthropic", "google"] as const) {
|
|
258
|
+
const tier = src[family];
|
|
259
|
+
if (tier === "auto" || tier === "default" || tier === "flex" || tier === "scale" || tier === "priority") {
|
|
260
|
+
out[family] = tier;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
264
|
+
}
|
|
265
|
+
switch (value) {
|
|
266
|
+
case "priority":
|
|
267
|
+
return { openai: "priority", anthropic: "priority", google: "priority" };
|
|
268
|
+
case "openai-only":
|
|
269
|
+
return { openai: "priority" };
|
|
270
|
+
case "claude-only":
|
|
271
|
+
return { anthropic: "priority" };
|
|
272
|
+
case "auto":
|
|
273
|
+
return { openai: "auto" };
|
|
274
|
+
case "default":
|
|
275
|
+
return { openai: "default" };
|
|
276
|
+
case "flex":
|
|
277
|
+
return { openai: "flex" };
|
|
278
|
+
case "scale":
|
|
279
|
+
return { openai: "scale" };
|
|
280
|
+
default:
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
186
283
|
}
|
|
187
284
|
|
|
188
285
|
export interface ProviderSessionState {
|
|
@@ -277,6 +374,22 @@ export interface StreamOptions {
|
|
|
277
374
|
* Providers can use this to persist transport/session state between turns.
|
|
278
375
|
*/
|
|
279
376
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
377
|
+
/**
|
|
378
|
+
* Force Gemini model-mode Interactions API transport for providers that support it.
|
|
379
|
+
* When unset, those providers may still use Interactions to continue known
|
|
380
|
+
* server-side conversation lineage via `previousInteractionId` or stored state.
|
|
381
|
+
*/
|
|
382
|
+
useInteractionsApi?: boolean;
|
|
383
|
+
/**
|
|
384
|
+
* Whether supported Interactions transports should store server-side conversation
|
|
385
|
+
* state and return response ids for follow-up turns. Defaults to true.
|
|
386
|
+
*/
|
|
387
|
+
storeInteraction?: boolean;
|
|
388
|
+
/**
|
|
389
|
+
* Explicit Interactions response id to continue. Mutually exclusive with
|
|
390
|
+
* `storeInteraction: false` because the follow-up itself must be storable.
|
|
391
|
+
*/
|
|
392
|
+
previousInteractionId?: string;
|
|
280
393
|
/**
|
|
281
394
|
* Optional per-provider concurrent request cap for LLM stream calls. Keys are
|
|
282
395
|
* provider ids (`model.provider`); positive numeric values cap in-flight
|
|
@@ -68,6 +68,57 @@ function classifyWindow(id: string | undefined, label: string | undefined): Anti
|
|
|
68
68
|
return undefined;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function parseResetTime(info: AntigravityQuotaInfo): number | undefined {
|
|
72
|
+
const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
|
|
73
|
+
return resetAt !== undefined && Number.isFinite(resetAt) ? resetAt : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inferWindowFromReset(resetAt: number | undefined, nowMs: number): AntigravityWindowDescriptor {
|
|
77
|
+
if (resetAt !== undefined && resetAt - nowMs > ONE_DAY_MS) {
|
|
78
|
+
return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
|
|
79
|
+
}
|
|
80
|
+
return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function quotaInferenceKey(info: AntigravityQuotaInfo): string {
|
|
84
|
+
return [info.modelProvider ?? "", info.apiProvider ?? "", info.tier ?? ""].join("|");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function inferWindowDescriptors(
|
|
88
|
+
quotaInfos: AntigravityQuotaInfo[],
|
|
89
|
+
nowMs: number,
|
|
90
|
+
): WeakMap<AntigravityQuotaInfo, AntigravityWindowDescriptor> {
|
|
91
|
+
const descriptors = new WeakMap<AntigravityQuotaInfo, AntigravityWindowDescriptor>();
|
|
92
|
+
const groups = new Map<string, { info: AntigravityQuotaInfo; resetAt: number | undefined }[]>();
|
|
93
|
+
|
|
94
|
+
for (const info of quotaInfos) {
|
|
95
|
+
const explicitDescriptor = classifyWindow(info.windowId, info.windowLabel);
|
|
96
|
+
if (explicitDescriptor) {
|
|
97
|
+
descriptors.set(info, explicitDescriptor);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const group = groups.get(quotaInferenceKey(info)) ?? [];
|
|
101
|
+
group.push({ info, resetAt: parseResetTime(info) });
|
|
102
|
+
groups.set(quotaInferenceKey(info), group);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const group of groups.values()) {
|
|
106
|
+
const resetTimes = [...new Set(group.map(entry => entry.resetAt).filter(resetAt => resetAt !== undefined))].sort(
|
|
107
|
+
(a, b) => a - b,
|
|
108
|
+
);
|
|
109
|
+
const latestReset = resetTimes.length > 1 ? resetTimes.at(-1) : undefined;
|
|
110
|
+
for (const entry of group) {
|
|
111
|
+
const descriptor =
|
|
112
|
+
latestReset !== undefined && entry.resetAt === latestReset
|
|
113
|
+
? { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS }
|
|
114
|
+
: inferWindowFromReset(entry.resetAt, nowMs);
|
|
115
|
+
descriptors.set(entry.info, descriptor);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return descriptors;
|
|
120
|
+
}
|
|
121
|
+
|
|
71
122
|
function withWindowDescriptor(
|
|
72
123
|
info: AntigravityQuotaInfo,
|
|
73
124
|
descriptor: AntigravityWindowDescriptor | undefined,
|
|
@@ -94,10 +145,12 @@ function getUsageStatus(remainingFraction: number | undefined): UsageStatus | un
|
|
|
94
145
|
return "ok";
|
|
95
146
|
}
|
|
96
147
|
|
|
97
|
-
function parseWindow(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
148
|
+
function parseWindow(
|
|
149
|
+
info: AntigravityQuotaInfo,
|
|
150
|
+
descriptor: AntigravityWindowDescriptor | undefined,
|
|
151
|
+
): UsageWindow | undefined {
|
|
152
|
+
const resetAt = parseResetTime(info);
|
|
153
|
+
const hasResetAt = resetAt !== undefined;
|
|
101
154
|
if (!descriptor && !hasResetAt) return undefined;
|
|
102
155
|
return {
|
|
103
156
|
id: descriptor?.id ?? info.windowId ?? "default",
|
|
@@ -276,9 +329,10 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
276
329
|
|
|
277
330
|
for (const [_modelId, modelInfo] of Object.entries(data.models ?? {})) {
|
|
278
331
|
const quotaInfos = normalizeQuotaInfos(modelInfo);
|
|
332
|
+
const inferredDescriptors = inferWindowDescriptors(quotaInfos, nowMs);
|
|
279
333
|
for (const quotaInfo of quotaInfos) {
|
|
280
334
|
const amount = buildAmount(quotaInfo);
|
|
281
|
-
const window = parseWindow(quotaInfo);
|
|
335
|
+
const window = parseWindow(quotaInfo, inferredDescriptors.get(quotaInfo));
|
|
282
336
|
if (window?.resetsAt) {
|
|
283
337
|
earliestReset = earliestReset ? Math.min(earliestReset, window.resetsAt) : window.resetsAt;
|
|
284
338
|
}
|