@oh-my-pi/pi-ai 15.9.67 → 15.10.0
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 +16 -0
- package/dist/types/auth-gateway/types.d.ts +1 -1
- package/dist/types/effort.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model-thinking.d.ts +1 -9
- package/dist/types/providers/amazon-bedrock.d.ts +1 -1
- package/dist/types/providers/openai-responses-shared.d.ts +15 -0
- package/dist/types/stream.d.ts +1 -1
- package/dist/types/types.d.ts +1 -1
- package/dist/types/utils/schema/fields.d.ts +5 -0
- package/package.json +2 -2
- package/src/auth-gateway/server.ts +1 -1
- package/src/auth-gateway/types.ts +1 -1
- package/src/auth-storage.ts +37 -6
- package/src/effort.ts +16 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +1 -17
- package/src/provider-models/ollama.ts +1 -1
- package/src/provider-models/openai-compat.ts +1 -1
- package/src/providers/amazon-bedrock.ts +1 -1
- package/src/providers/anthropic.ts +22 -13
- package/src/providers/azure-openai-responses.ts +2 -1
- package/src/providers/openai-codex/request-transformer.ts +79 -33
- package/src/providers/openai-completions.ts +2 -1
- package/src/providers/openai-responses-shared.ts +53 -0
- package/src/providers/openai-responses.ts +2 -1
- package/src/stream.ts +1 -1
- package/src/types.ts +1 -1
- package/src/usage/google-antigravity.ts +77 -24
- package/src/utils/schema/fields.ts +16 -0
- package/src/utils/schema/normalize.ts +23 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [15.10.0] - 2026-06-06
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added a dependency-free `@oh-my-pi/pi-ai/effort` module exporting the `Effort` enum and `THINKING_EFFORTS`, split out of `model-thinking` so hot-path consumers can import the thinking levels without pulling in `model-thinking` and its provider-compat dependency graph. The package barrel still re-exports both names, so existing imports are unaffected.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed Antigravity usage provider emitting one bar per model instead of deduplicating by tier — a single account's 15+ model entries now collapse to one bar per tier, matching the shared-quota reality of the upstream API.
|
|
14
|
+
- Fixed Antigravity usage reports missing `email` and `accountId` in metadata, so the `/usage` display and the deduplicator can associate reports with their credentials.
|
|
15
|
+
- Fixed usage-report dedup ignoring `projectId` for Google Cloud providers, preventing duplicate credential entries from being recognized as the same account.
|
|
16
|
+
|
|
17
|
+
- Fixed Cloud Code Assist (Antigravity / Gemini CLI) rejecting the `github` tool with HTTP 400 when the `pr` parameter schema contained `anyOf: [string, array]`. The CCA mixed-type combiner collapse picked the first non-null type (`string`) but indiscriminately copied type-specific keys from variant branches — `items` from the array variant leaked onto the string-typed result, producing `{type: "string", items: {...}}` which Google's API rejects as invalid. The collapse now filters merged variant fields against the winning type's allowed key set. ([#2002](https://github.com/can1357/oh-my-pi/pull/2002))
|
|
18
|
+
- Fixed OpenAI Responses-family providers (Codex, OpenAI Responses, Azure Responses) rejecting requests with `400 No tool output found for function call …` after the user branched/navigated the session tree to a node that ends on a tool call (the tool-result child is dropped from the reconstructed history) or after a turn was aborted/crashed between the call streaming and its result persisting. The converters now synthesize a placeholder `function_call_output`/`custom_tool_call_output` immediately after any unpaired `function_call`/`custom_tool_call`, symmetric to the existing orphan-output repair, so the model still sees the call and can recover instead of the whole request 400ing.
|
|
19
|
+
- Fixed Anthropic-compatible reasoning endpoints losing prior-turn reasoning on continuation requests when they emit unsigned `thinking` blocks. `convertAnthropicMessages` treated unknown endpoints as signature-enforcing and demoted unsigned reasoning to `type: "text"`, which destabilized tool-call argument serialization on the next turn — the upstream symptom behind the `args?.ops?.map is not a function` crash reported against the `todo` tool. Official `api.anthropic.com` keeps the conservative text fallback; non-official `anthropic-messages` reasoning models now replay unsigned reasoning as native `type: "thinking"` ([#2005](https://github.com/can1357/oh-my-pi/issues/2005)).
|
|
20
|
+
|
|
5
21
|
## [15.9.67] - 2026-06-06
|
|
6
22
|
|
|
7
23
|
### Fixed
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Effort } from "../
|
|
1
|
+
import type { Effort } from "../effort";
|
|
2
2
|
import type { AssistantMessage, AssistantMessageEventStream, CacheRetention, Context, ServiceTier, TokenTaskBudget } from "../types";
|
|
3
3
|
/**
|
|
4
4
|
* Wire types for the omp auth-gateway.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./auth-broker";
|
|
|
4
4
|
export { type AuthGatewayBootOptions, type ModelResolver, startAuthGateway } from "./auth-gateway/server";
|
|
5
5
|
export * from "./auth-gateway/types";
|
|
6
6
|
export * from "./auth-storage";
|
|
7
|
+
export * from "./effort";
|
|
7
8
|
export * from "./model-cache";
|
|
8
9
|
export * from "./model-manager";
|
|
9
10
|
export * from "./model-thinking";
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
+
import { Effort } from "./effort";
|
|
1
2
|
import type { Api, Model as ApiModel } from "./types";
|
|
2
|
-
/** User-facing thinking levels, ordered least to most intensive. */
|
|
3
|
-
export declare const enum Effort {
|
|
4
|
-
Minimal = "minimal",
|
|
5
|
-
Low = "low",
|
|
6
|
-
Medium = "medium",
|
|
7
|
-
High = "high",
|
|
8
|
-
XHigh = "xhigh"
|
|
9
|
-
}
|
|
10
|
-
export declare const THINKING_EFFORTS: readonly Effort[];
|
|
11
3
|
/**
|
|
12
4
|
* Static fallback model injected when Cloudflare AI Gateway discovery
|
|
13
5
|
* returns no results. Ensures the provider always has at least one usable
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* No `@aws-sdk/*`, no `@smithy/*`, no `proxy-agent`. Proxies are honored via
|
|
7
7
|
* Bun's native `HTTPS_PROXY` support.
|
|
8
8
|
*/
|
|
9
|
-
import type { Effort } from "../
|
|
9
|
+
import type { Effort } from "../effort";
|
|
10
10
|
import type { StreamFunction, StreamOptions, ThinkingBudgets } from "../types";
|
|
11
11
|
export type BedrockThinkingDisplay = "summarized" | "omitted";
|
|
12
12
|
export interface BedrockOptions extends StreamOptions {
|
|
@@ -38,6 +38,21 @@ export declare function collectCustomCallIds(messages: ResponseInput): Set<strin
|
|
|
38
38
|
* codex provider — issue #1351 / regression of #472.
|
|
39
39
|
*/
|
|
40
40
|
export declare function repairOrphanResponsesToolOutputs(input: ResponseInput): ResponseInput;
|
|
41
|
+
/**
|
|
42
|
+
* Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
|
|
43
|
+
* for every `function_call` / `custom_tool_call` whose `call_id` has no matching
|
|
44
|
+
* output later in the same input. The Responses API rejects an unpaired call
|
|
45
|
+
* with `400 No tool output found for function call …`.
|
|
46
|
+
*
|
|
47
|
+
* Orphan calls surface when the user branches/navigates the session tree to a
|
|
48
|
+
* node that ends on a tool call (the tool-result child is excluded from the
|
|
49
|
+
* reconstructed history) or when a turn is aborted/crashes after the call
|
|
50
|
+
* streamed but before its result persisted. Dropping the call would erase the
|
|
51
|
+
* assistant's action; a placeholder output keeps the call visible so the model
|
|
52
|
+
* can recover (e.g. re-issue the call). Symmetric to
|
|
53
|
+
* {@link repairOrphanResponsesToolOutputs}.
|
|
54
|
+
*/
|
|
55
|
+
export declare function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput;
|
|
41
56
|
export declare function convertResponsesInputContent(content: string | Array<TextContent | ImageContent>, supportsImages: boolean): ResponseInputContent[] | undefined;
|
|
42
57
|
export declare function convertResponsesAssistantMessage<TApi extends Api>(assistantMsg: AssistantMessage, model: Model<TApi>, msgIndex: number, knownCallIds: Set<string>, includeThinkingSignatures?: boolean, customCallIds?: Set<string>): ResponseInput;
|
|
43
58
|
export declare function appendResponsesToolResultMessages<TApi extends Api>(messages: ResponseInput, toolResult: ToolResultMessage, model: Model<TApi>, strictResponsesPairing: boolean, knownCallIds: ReadonlySet<string>, customCallIds?: ReadonlySet<string>): void;
|
package/dist/types/stream.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Effort } from "./
|
|
1
|
+
import type { Effort } from "./effort";
|
|
2
2
|
import type { AnthropicOptions } from "./providers/anthropic";
|
|
3
3
|
import type { GoogleOptions } from "./providers/google";
|
|
4
4
|
import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
|
package/dist/types/types.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface ThinkingConfig {
|
|
|
50
50
|
}
|
|
51
51
|
export type KnownProvider = "alibaba-coding-plan" | "amazon-bedrock" | "anthropic" | "google" | "google-gemini-cli" | "google-antigravity" | "google-vertex" | "openai" | "openai-codex" | "kimi-code" | "minimax-code" | "minimax-code-cn" | "github-copilot" | "fireworks" | "firepass" | "gitlab-duo" | "cursor" | "deepseek" | "xai" | "xai-oauth" | "groq" | "cerebras" | "openrouter" | "kilo" | "vercel-ai-gateway" | "zai" | "zhipu-coding-plan" | "mistral" | "minimax" | "opencode-go" | "opencode-zen" | "synthetic" | "cloudflare-ai-gateway" | "huggingface" | "litellm" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "qianfan" | "qwen-portal" | "together" | "venice" | "vllm" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "wafer-pass" | "wafer-serverless" | "zenmux" | "lm-studio";
|
|
52
52
|
export type Provider = KnownProvider | string;
|
|
53
|
-
import type { Effort } from "./
|
|
53
|
+
import type { Effort } from "./effort";
|
|
54
54
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
55
55
|
export type ThinkingBudgets = {
|
|
56
56
|
[key in Effort]?: number;
|
|
@@ -30,6 +30,11 @@ export declare const NON_STRUCTURAL_SCHEMA_KEYS: Record<string, true>;
|
|
|
30
30
|
* Used when collapsing mixed-type combiner variants for CCA Claude.
|
|
31
31
|
*/
|
|
32
32
|
export declare const CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS: Record<string, Record<string, true>>;
|
|
33
|
+
/**
|
|
34
|
+
* Flat set of every type-specific key across all CCA types.
|
|
35
|
+
* Used to identify sibling keys that need filtering during mixed-type collapse.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ALL_CCA_TYPE_SPECIFIC_KEYS: Record<string, true>;
|
|
33
38
|
/**
|
|
34
39
|
* Cloud Code Assist shared schema keys allowed on any type.
|
|
35
40
|
* Used alongside CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS for CCA combiner collapsing.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.10.0",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@bufbuild/protobuf": "^2.12.0",
|
|
42
|
-
"@oh-my-pi/pi-utils": "15.
|
|
42
|
+
"@oh-my-pi/pi-utils": "15.10.0",
|
|
43
43
|
"openai": "^6.39.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "4.4.3"
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { extractRetryHint, logger } from "@oh-my-pi/pi-utils";
|
|
21
21
|
import type { AuthStorage } from "../auth-storage";
|
|
22
|
-
import { Effort } from "../
|
|
22
|
+
import { Effort } from "../effort";
|
|
23
23
|
import * as anthropicMessages from "../providers/anthropic-messages-server";
|
|
24
24
|
import * as openaiChat from "../providers/openai-chat-server";
|
|
25
25
|
import * as openaiResponses from "../providers/openai-responses-server";
|
package/src/auth-storage.ts
CHANGED
|
@@ -36,6 +36,8 @@ import { loginOpenAICodexDevice } from "./utils/oauth/openai-codex";
|
|
|
36
36
|
import type { OAuthController, OAuthCredentials, OAuthProvider, OAuthProviderId } from "./utils/oauth/types";
|
|
37
37
|
import { loginXiaomi, loginXiaomiTokenPlan } from "./utils/oauth/xiaomi";
|
|
38
38
|
|
|
39
|
+
const USAGE_RANKING_METRIC_EPSILON = 1e-9;
|
|
40
|
+
|
|
39
41
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
40
42
|
// Credential Types
|
|
41
43
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -606,6 +608,14 @@ function hasOpenAICodexProPlan(report: UsageReport | null): boolean {
|
|
|
606
608
|
return getUsagePlanType(report)?.includes("pro") === true;
|
|
607
609
|
}
|
|
608
610
|
|
|
611
|
+
function compareUsageRankingMetric(left: number, right: number): number {
|
|
612
|
+
if (left === right) return 0;
|
|
613
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return left < right ? -1 : 1;
|
|
614
|
+
const delta = left - right;
|
|
615
|
+
const tolerance = Math.max(USAGE_RANKING_METRIC_EPSILON, Math.max(Math.abs(left), Math.abs(right)) * 0.000001);
|
|
616
|
+
return Math.abs(delta) <= tolerance ? 0 : delta;
|
|
617
|
+
}
|
|
618
|
+
|
|
609
619
|
function resolveDefaultUsageProvider(provider: Provider): UsageProvider | undefined {
|
|
610
620
|
return DEFAULT_USAGE_PROVIDER_MAP.get(provider);
|
|
611
621
|
}
|
|
@@ -2288,6 +2298,16 @@ export class AuthStorage {
|
|
|
2288
2298
|
return undefined;
|
|
2289
2299
|
}
|
|
2290
2300
|
|
|
2301
|
+
#getUsageReportScopeProjectId(report: UsageReport): string | undefined {
|
|
2302
|
+
const ids = new Set<string>();
|
|
2303
|
+
for (const limit of report.limits) {
|
|
2304
|
+
const projectId = limit.scope.projectId?.trim();
|
|
2305
|
+
if (projectId) ids.add(projectId);
|
|
2306
|
+
}
|
|
2307
|
+
if (ids.size === 1) return [...ids][0];
|
|
2308
|
+
return undefined;
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2291
2311
|
#getUsageReportIdentifiers(report: UsageReport): string[] {
|
|
2292
2312
|
const identifiers: string[] = [];
|
|
2293
2313
|
const email = this.#getUsageReportMetadataValue(report, "email");
|
|
@@ -2295,6 +2315,11 @@ export class AuthStorage {
|
|
|
2295
2315
|
if (report.provider === "openai-codex" || report.provider === "anthropic") {
|
|
2296
2316
|
return identifiers.map(identifier => `${report.provider}:${identifier.toLowerCase()}`);
|
|
2297
2317
|
}
|
|
2318
|
+
const projectId =
|
|
2319
|
+
this.#getUsageReportMetadataValue(report, "projectId") ?? this.#getUsageReportScopeProjectId(report);
|
|
2320
|
+
// Only add project as a fallback when no email is available — two users
|
|
2321
|
+
// with different emails on the same GCP project must not merge.
|
|
2322
|
+
if (projectId && !email) identifiers.push(`project:${projectId}`);
|
|
2298
2323
|
const accountId = this.#getUsageReportMetadataValue(report, "accountId");
|
|
2299
2324
|
if (accountId) identifiers.push(`account:${accountId}`);
|
|
2300
2325
|
const account = this.#getUsageReportMetadataValue(report, "account");
|
|
@@ -2781,12 +2806,14 @@ export class AuthStorage {
|
|
|
2781
2806
|
return left.planPriority - right.planPriority;
|
|
2782
2807
|
}
|
|
2783
2808
|
if (left.hasPriorityBoost !== right.hasPriorityBoost) return left.hasPriorityBoost ? -1 : 1;
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2789
|
-
if (
|
|
2809
|
+
let metric = compareUsageRankingMetric(left.secondaryDrainRate, right.secondaryDrainRate);
|
|
2810
|
+
if (metric !== 0) return metric;
|
|
2811
|
+
metric = compareUsageRankingMetric(left.secondaryUsed, right.secondaryUsed);
|
|
2812
|
+
if (metric !== 0) return metric;
|
|
2813
|
+
metric = compareUsageRankingMetric(left.primaryDrainRate, right.primaryDrainRate);
|
|
2814
|
+
if (metric !== 0) return metric;
|
|
2815
|
+
metric = compareUsageRankingMetric(left.primaryUsed, right.primaryUsed);
|
|
2816
|
+
if (metric !== 0) return metric;
|
|
2790
2817
|
return 0;
|
|
2791
2818
|
}
|
|
2792
2819
|
|
|
@@ -3932,6 +3959,8 @@ function resolveProviderCredentialIdentityKey(provider: string, identifiers: str
|
|
|
3932
3959
|
const accountIdentifier = identifiers.find(identifier => identifier.startsWith("account:"));
|
|
3933
3960
|
if (accountIdentifier) return accountIdentifier;
|
|
3934
3961
|
if (emailIdentifier) return emailIdentifier;
|
|
3962
|
+
const projectIdentifier = identifiers.find(identifier => identifier.startsWith("project:"));
|
|
3963
|
+
if (projectIdentifier) return projectIdentifier;
|
|
3935
3964
|
return null;
|
|
3936
3965
|
}
|
|
3937
3966
|
|
|
@@ -3967,6 +3996,8 @@ function extractOAuthCredentialIdentifiers(credential: OAuthCredential): string[
|
|
|
3967
3996
|
if (accountId) identifiers.add(`account:${accountId}`);
|
|
3968
3997
|
const email = normalizeStoredEmail(credential.email);
|
|
3969
3998
|
if (email) identifiers.add(`email:${email}`);
|
|
3999
|
+
const projectId = normalizeStoredAccountId(credential.projectId);
|
|
4000
|
+
if (projectId) identifiers.add(`project:${projectId}`);
|
|
3970
4001
|
const accessIdentifiers = extractOAuthTokenIdentifiers(credential.access) ?? [];
|
|
3971
4002
|
for (const identifier of accessIdentifiers) {
|
|
3972
4003
|
identifiers.add(identifier);
|
package/src/effort.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** User-facing thinking levels, ordered least to most intensive. */
|
|
2
|
+
export const enum Effort {
|
|
3
|
+
Minimal = "minimal",
|
|
4
|
+
Low = "low",
|
|
5
|
+
Medium = "medium",
|
|
6
|
+
High = "high",
|
|
7
|
+
XHigh = "xhigh",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const THINKING_EFFORTS: readonly Effort[] = [
|
|
11
|
+
Effort.Minimal,
|
|
12
|
+
Effort.Low,
|
|
13
|
+
Effort.Medium,
|
|
14
|
+
Effort.High,
|
|
15
|
+
Effort.XHigh,
|
|
16
|
+
];
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from "./auth-broker";
|
|
|
4
4
|
export { type AuthGatewayBootOptions, type ModelResolver, startAuthGateway } from "./auth-gateway/server";
|
|
5
5
|
export * from "./auth-gateway/types";
|
|
6
6
|
export * from "./auth-storage";
|
|
7
|
+
export * from "./effort";
|
|
7
8
|
export * from "./model-cache";
|
|
8
9
|
export * from "./model-manager";
|
|
9
10
|
export * from "./model-thinking";
|
package/src/model-thinking.ts
CHANGED
|
@@ -1,23 +1,7 @@
|
|
|
1
|
+
import { Effort, THINKING_EFFORTS } from "./effort";
|
|
1
2
|
import { resolveOpenAICompat } from "./providers/openai-completions-compat";
|
|
2
3
|
import type { Api, Model as ApiModel, ThinkingConfig } from "./types";
|
|
3
4
|
|
|
4
|
-
/** User-facing thinking levels, ordered least to most intensive. */
|
|
5
|
-
export const enum Effort {
|
|
6
|
-
Minimal = "minimal",
|
|
7
|
-
Low = "low",
|
|
8
|
-
Medium = "medium",
|
|
9
|
-
High = "high",
|
|
10
|
-
XHigh = "xhigh",
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const THINKING_EFFORTS: readonly Effort[] = [
|
|
14
|
-
Effort.Minimal,
|
|
15
|
-
Effort.Low,
|
|
16
|
-
Effort.Medium,
|
|
17
|
-
Effort.High,
|
|
18
|
-
Effort.XHigh,
|
|
19
|
-
];
|
|
20
|
-
|
|
21
5
|
const DEFAULT_REASONING_EFFORTS: readonly Effort[] = [Effort.Minimal, Effort.Low, Effort.Medium, Effort.High];
|
|
22
6
|
const DEFAULT_REASONING_EFFORTS_WITH_XHIGH: readonly Effort[] = [
|
|
23
7
|
Effort.Minimal,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { Effort } from "../effort";
|
|
2
3
|
import type { ModelManagerOptions } from "../model-manager";
|
|
3
|
-
import { Effort } from "../model-thinking";
|
|
4
4
|
import type { ThinkingConfig } from "../types";
|
|
5
5
|
import { createBundledReferenceMap, createReferenceResolver } from "./bundled-references";
|
|
6
6
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { Effort } from "../effort";
|
|
1
2
|
import type { ModelManagerOptions } from "../model-manager";
|
|
2
|
-
import { Effort } from "../model-thinking";
|
|
3
3
|
import { getBundledModels } from "../models";
|
|
4
4
|
import type { Api, Model, Provider, ThinkingConfig } from "../types";
|
|
5
5
|
import { isAnthropicOAuthToken, isRecord, toBoolean, toNumber, toPositiveNumber } from "../utils";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { $env, $flag, extractHttpStatusFromError, fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
11
|
-
import type { Effort } from "../
|
|
11
|
+
import type { Effort } from "../effort";
|
|
12
12
|
import { mapEffortToAnthropicAdaptiveEffort, requireSupportedEffort } from "../model-thinking";
|
|
13
13
|
import { calculateCost } from "../models";
|
|
14
14
|
import type {
|
|
@@ -2427,22 +2427,31 @@ function isZaiAnthropicEndpoint(model: Model<"anthropic-messages">): boolean {
|
|
|
2427
2427
|
}
|
|
2428
2428
|
|
|
2429
2429
|
/**
|
|
2430
|
-
* Returns true
|
|
2431
|
-
*
|
|
2432
|
-
*
|
|
2433
|
-
*
|
|
2430
|
+
* Returns true when unsigned `thinking` blocks from prior assistant turns should
|
|
2431
|
+
* be replayed as Anthropic-native thinking instead of demoted to text.
|
|
2432
|
+
*
|
|
2433
|
+
* Official Anthropic (matched via `isAnthropicApiBaseUrl`, which intentionally
|
|
2434
|
+
* treats a missing baseUrl as official since `resolveAnthropicBaseUrl` routes
|
|
2435
|
+
* it to `https://api.anthropic.com`) enforces signature-based thinking-chain
|
|
2436
|
+
* integrity, so unsigned blocks must remain text there. Anthropic-compatible
|
|
2437
|
+
* reasoning endpoints commonly emit unsigned thinking blocks while still
|
|
2438
|
+
* expecting them back as `type: "thinking"` on continuation; demoting them
|
|
2439
|
+
* loses the model's reasoning chain and can destabilize the next tool-call
|
|
2440
|
+
* arguments (#2005). Known non-signing hosts are also preserved for
|
|
2441
|
+
* compatibility.
|
|
2434
2442
|
*/
|
|
2435
|
-
function
|
|
2436
|
-
// Known non-signing providers
|
|
2443
|
+
function shouldReplayUnsignedThinking(model: Model<"anthropic-messages">): boolean {
|
|
2437
2444
|
if (model.provider === "zai" || model.provider === "deepseek") return true;
|
|
2438
2445
|
const baseUrl = model.baseUrl;
|
|
2439
|
-
if (
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2446
|
+
if (baseUrl) {
|
|
2447
|
+
try {
|
|
2448
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
2449
|
+
if (hostname === "api.deepseek.com" || hostname.endsWith(".deepseek.com")) return true;
|
|
2450
|
+
} catch {
|
|
2451
|
+
// Fall through to the protocol-level reasoning rule below.
|
|
2452
|
+
}
|
|
2445
2453
|
}
|
|
2454
|
+
return model.reasoning && !isAnthropicApiBaseUrl(baseUrl);
|
|
2446
2455
|
}
|
|
2447
2456
|
|
|
2448
2457
|
function buildToolResultBlock(model: Model<"anthropic-messages">, msg: ToolResultMessage): ContentBlockParam {
|
|
@@ -2533,7 +2542,7 @@ export function convertAnthropicMessages(
|
|
|
2533
2542
|
}
|
|
2534
2543
|
if (block.thinking.trim().length === 0) continue;
|
|
2535
2544
|
if (!block.thinkingSignature || block.thinkingSignature.trim().length === 0) {
|
|
2536
|
-
if (
|
|
2545
|
+
if (shouldReplayUnsignedThinking(model)) {
|
|
2537
2546
|
blocks.push({
|
|
2538
2547
|
type: "thinking",
|
|
2539
2548
|
thinking: block.thinking.toWellFormed(),
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
isOpenAIResponsesProgressEvent,
|
|
41
41
|
normalizeResponsesToolCallIdForTransform,
|
|
42
42
|
processResponsesStream,
|
|
43
|
+
repairOrphanResponsesToolCalls,
|
|
43
44
|
} from "./openai-responses-shared";
|
|
44
45
|
import { transformMessages } from "./transform-messages";
|
|
45
46
|
|
|
@@ -347,7 +348,7 @@ function convertMessages(
|
|
|
347
348
|
msgIndex++;
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
return messages;
|
|
351
|
+
return repairOrphanResponsesToolCalls(messages);
|
|
351
352
|
}
|
|
352
353
|
|
|
353
354
|
function convertTools(tools: Tool[]): OpenAITool[] {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Effort } from "../../
|
|
1
|
+
import type { Effort } from "../../effort";
|
|
2
2
|
import { requireSupportedEffort } from "../../model-thinking";
|
|
3
3
|
import type { Api, Model } from "../../types";
|
|
4
4
|
|
|
@@ -76,6 +76,83 @@ function filterInput(input: InputItem[] | undefined): InputItem[] | undefined {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
const CODEX_ORPHAN_OUTPUT_LIMIT = 16_000;
|
|
80
|
+
/** Placeholder output for a tool call whose result never landed in the input. */
|
|
81
|
+
const CODEX_INTERRUPTED_TOOL_OUTPUT =
|
|
82
|
+
"[No tool output recorded: the tool call was interrupted before it produced a result.]";
|
|
83
|
+
|
|
84
|
+
function orphanFunctionOutputToMessage(item: InputItem, callId: string): InputItem {
|
|
85
|
+
const itemRecord = item as unknown as Record<string, unknown>;
|
|
86
|
+
const toolName = typeof itemRecord.name === "string" ? itemRecord.name : "tool";
|
|
87
|
+
let text = "";
|
|
88
|
+
try {
|
|
89
|
+
const output = itemRecord.output;
|
|
90
|
+
text = typeof output === "string" ? output : JSON.stringify(output);
|
|
91
|
+
} catch {
|
|
92
|
+
text = String(itemRecord.output ?? "");
|
|
93
|
+
}
|
|
94
|
+
if (text.length > CODEX_ORPHAN_OUTPUT_LIMIT) {
|
|
95
|
+
text = `${text.slice(0, CODEX_ORPHAN_OUTPUT_LIMIT)}\n...[truncated]`;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
type: "message",
|
|
99
|
+
role: "assistant",
|
|
100
|
+
content: `[Previous ${toolName} result; call_id=${callId}]: ${text}`,
|
|
101
|
+
} as InputItem;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Repair both halves of unpaired tool exchanges so the Responses input grammar
|
|
106
|
+
* stays valid — the API rejects either orphan with a 400:
|
|
107
|
+
*
|
|
108
|
+
* - `function_call_output` with no matching `function_call` → folded into an
|
|
109
|
+
* assistant message (`400 No tool call found for function call output …`).
|
|
110
|
+
* Regression of #472 / #1351.
|
|
111
|
+
* - `function_call` / `custom_tool_call` with no matching `*_output` → a
|
|
112
|
+
* placeholder output is synthesized immediately after the call
|
|
113
|
+
* (`400 No tool output found for function call …`). Hit when the user
|
|
114
|
+
* branches/navigates the session tree to a node that ends on a tool call (the
|
|
115
|
+
* tool-result child is dropped from the reconstructed history) or when a turn
|
|
116
|
+
* is aborted/crashes after the call streamed but before its result persisted.
|
|
117
|
+
*/
|
|
118
|
+
function repairToolCallPairs(input: InputItem[]): InputItem[] {
|
|
119
|
+
const callIds = new Set<string>();
|
|
120
|
+
const outputCallIds = new Set<string>();
|
|
121
|
+
for (const item of input) {
|
|
122
|
+
const callId = typeof item.call_id === "string" ? item.call_id : undefined;
|
|
123
|
+
if (callId === undefined) continue;
|
|
124
|
+
if (item.type === "function_call" || item.type === "custom_tool_call") callIds.add(callId);
|
|
125
|
+
else if (item.type === "function_call_output" || item.type === "custom_tool_call_output") {
|
|
126
|
+
outputCallIds.add(callId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const repaired: InputItem[] = [];
|
|
131
|
+
for (const item of input) {
|
|
132
|
+
const callId = typeof item.call_id === "string" ? item.call_id : undefined;
|
|
133
|
+
|
|
134
|
+
if (item.type === "function_call_output" && callId !== undefined && !callIds.has(callId)) {
|
|
135
|
+
repaired.push(orphanFunctionOutputToMessage(item, callId));
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
repaired.push(item);
|
|
140
|
+
|
|
141
|
+
if (
|
|
142
|
+
(item.type === "function_call" || item.type === "custom_tool_call") &&
|
|
143
|
+
callId !== undefined &&
|
|
144
|
+
!outputCallIds.has(callId)
|
|
145
|
+
) {
|
|
146
|
+
repaired.push({
|
|
147
|
+
type: item.type === "custom_tool_call" ? "custom_tool_call_output" : "function_call_output",
|
|
148
|
+
call_id: callId,
|
|
149
|
+
output: CODEX_INTERRUPTED_TOOL_OUTPUT,
|
|
150
|
+
} as InputItem);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return repaired;
|
|
154
|
+
}
|
|
155
|
+
|
|
79
156
|
export async function transformRequestBody(
|
|
80
157
|
body: RequestBody,
|
|
81
158
|
model: Model<Api>,
|
|
@@ -87,39 +164,8 @@ export async function transformRequestBody(
|
|
|
87
164
|
|
|
88
165
|
if (body.input && Array.isArray(body.input)) {
|
|
89
166
|
body.input = filterInput(body.input);
|
|
90
|
-
|
|
91
167
|
if (body.input) {
|
|
92
|
-
|
|
93
|
-
body.input
|
|
94
|
-
.filter(item => item.type === "function_call" && typeof item.call_id === "string")
|
|
95
|
-
.map(item => item.call_id as string),
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
body.input = body.input.map(item => {
|
|
99
|
-
if (item.type === "function_call_output" && typeof item.call_id === "string") {
|
|
100
|
-
const callId = item.call_id as string;
|
|
101
|
-
if (!functionCallIds.has(callId)) {
|
|
102
|
-
const itemRecord = item as unknown as Record<string, unknown>;
|
|
103
|
-
const toolName = typeof itemRecord.name === "string" ? itemRecord.name : "tool";
|
|
104
|
-
let text = "";
|
|
105
|
-
try {
|
|
106
|
-
const output = itemRecord.output;
|
|
107
|
-
text = typeof output === "string" ? output : JSON.stringify(output);
|
|
108
|
-
} catch {
|
|
109
|
-
text = String(itemRecord.output ?? "");
|
|
110
|
-
}
|
|
111
|
-
if (text.length > 16000) {
|
|
112
|
-
text = `${text.slice(0, 16000)}\n...[truncated]`;
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
type: "message",
|
|
116
|
-
role: "assistant",
|
|
117
|
-
content: `[Previous ${toolName} result; call_id=${callId}]: ${text}`,
|
|
118
|
-
} as InputItem;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return item;
|
|
122
|
-
});
|
|
168
|
+
body.input = repairToolCallPairs(body.input);
|
|
123
169
|
}
|
|
124
170
|
}
|
|
125
171
|
|
|
@@ -10,7 +10,8 @@ import type {
|
|
|
10
10
|
ChatCompletionToolMessageParam,
|
|
11
11
|
} from "openai/resources/chat/completions";
|
|
12
12
|
import packageJson from "../../package.json" with { type: "json" };
|
|
13
|
-
import {
|
|
13
|
+
import type { Effort } from "../effort";
|
|
14
|
+
import { getSupportedEfforts } from "../model-thinking";
|
|
14
15
|
import { calculateCost } from "../models";
|
|
15
16
|
import { getEnvApiKey } from "../stream";
|
|
16
17
|
import {
|
|
@@ -212,6 +212,59 @@ export function repairOrphanResponsesToolOutputs(input: ResponseInput): Response
|
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/** Placeholder output for a tool call whose result is absent from the input. */
|
|
216
|
+
const ORPHAN_TOOL_CALL_PLACEHOLDER =
|
|
217
|
+
"[No tool output recorded: the tool call was interrupted before it produced a result.]";
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Synthesize a placeholder `function_call_output` / `custom_tool_call_output`
|
|
221
|
+
* for every `function_call` / `custom_tool_call` whose `call_id` has no matching
|
|
222
|
+
* output later in the same input. The Responses API rejects an unpaired call
|
|
223
|
+
* with `400 No tool output found for function call …`.
|
|
224
|
+
*
|
|
225
|
+
* Orphan calls surface when the user branches/navigates the session tree to a
|
|
226
|
+
* node that ends on a tool call (the tool-result child is excluded from the
|
|
227
|
+
* reconstructed history) or when a turn is aborted/crashes after the call
|
|
228
|
+
* streamed but before its result persisted. Dropping the call would erase the
|
|
229
|
+
* assistant's action; a placeholder output keeps the call visible so the model
|
|
230
|
+
* can recover (e.g. re-issue the call). Symmetric to
|
|
231
|
+
* {@link repairOrphanResponsesToolOutputs}.
|
|
232
|
+
*/
|
|
233
|
+
export function repairOrphanResponsesToolCalls(input: ResponseInput): ResponseInput {
|
|
234
|
+
const outputCallIds = new Set<string>();
|
|
235
|
+
for (const item of input) {
|
|
236
|
+
const t = (item as { type?: string }).type;
|
|
237
|
+
if (t !== "function_call_output" && t !== "custom_tool_call_output") continue;
|
|
238
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
239
|
+
if (typeof callId === "string") outputCallIds.add(callId);
|
|
240
|
+
}
|
|
241
|
+
let hasOrphan = false;
|
|
242
|
+
for (const item of input) {
|
|
243
|
+
const t = (item as { type?: string }).type;
|
|
244
|
+
if (t !== "function_call" && t !== "custom_tool_call") continue;
|
|
245
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
246
|
+
if (typeof callId === "string" && !outputCallIds.has(callId)) {
|
|
247
|
+
hasOrphan = true;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!hasOrphan) return input;
|
|
252
|
+
const repaired: ResponseInput = [];
|
|
253
|
+
for (const item of input) {
|
|
254
|
+
repaired.push(item);
|
|
255
|
+
const t = (item as { type?: string }).type;
|
|
256
|
+
if (t !== "function_call" && t !== "custom_tool_call") continue;
|
|
257
|
+
const callId = (item as { call_id?: unknown }).call_id;
|
|
258
|
+
if (typeof callId !== "string" || outputCallIds.has(callId)) continue;
|
|
259
|
+
repaired.push({
|
|
260
|
+
type: t === "custom_tool_call" ? "custom_tool_call_output" : "function_call_output",
|
|
261
|
+
call_id: callId,
|
|
262
|
+
output: ORPHAN_TOOL_CALL_PLACEHOLDER,
|
|
263
|
+
} as ResponseInput[number]);
|
|
264
|
+
}
|
|
265
|
+
return repaired;
|
|
266
|
+
}
|
|
267
|
+
|
|
215
268
|
export function convertResponsesInputContent(
|
|
216
269
|
content: string | Array<TextContent | ImageContent>,
|
|
217
270
|
supportsImages: boolean,
|
|
@@ -62,6 +62,7 @@ import {
|
|
|
62
62
|
isOpenAIResponsesProgressEvent,
|
|
63
63
|
normalizeResponsesToolCallIdForTransform,
|
|
64
64
|
processResponsesStream,
|
|
65
|
+
repairOrphanResponsesToolCalls,
|
|
65
66
|
repairOrphanResponsesToolOutputs,
|
|
66
67
|
} from "./openai-responses-shared";
|
|
67
68
|
import { transformMessages } from "./transform-messages";
|
|
@@ -614,7 +615,7 @@ function convertConversationMessages(
|
|
|
614
615
|
msgIndex++;
|
|
615
616
|
}
|
|
616
617
|
|
|
617
|
-
return repairOrphanResponsesToolOutputs(messages);
|
|
618
|
+
return repairOrphanResponsesToolCalls(repairOrphanResponsesToolOutputs(messages));
|
|
618
619
|
}
|
|
619
620
|
|
|
620
621
|
/**
|
package/src/stream.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { $env, $pickenv, extractHttpStatusFromError } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import { getCustomApi } from "./api-registry";
|
|
6
|
-
import type { Effort } from "./
|
|
6
|
+
import type { Effort } from "./effort";
|
|
7
7
|
import {
|
|
8
8
|
mapEffortToAnthropicAdaptiveEffort,
|
|
9
9
|
mapEffortToGoogleThinkingLevel,
|
package/src/types.ts
CHANGED
|
@@ -151,7 +151,7 @@ export type KnownProvider =
|
|
|
151
151
|
| "lm-studio";
|
|
152
152
|
export type Provider = KnownProvider | string;
|
|
153
153
|
|
|
154
|
-
import type { Effort } from "./
|
|
154
|
+
import type { Effort } from "./effort";
|
|
155
155
|
|
|
156
156
|
/** Token budgets for each thinking level (token-based providers only) */
|
|
157
157
|
export type ThinkingBudgets = { [key in Effort]?: number };
|
|
@@ -148,10 +148,17 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
const data = (await response.json()) as AntigravityUsageResponse;
|
|
151
|
-
|
|
151
|
+
|
|
152
|
+
// The API returns per-model quota entries, but quota is shared across
|
|
153
|
+
// models within the same tier. Deduplicate by (tier, windowId) so one
|
|
154
|
+
// account doesn't produce 15 redundant bars.
|
|
155
|
+
const deduped = new Map<
|
|
156
|
+
string,
|
|
157
|
+
{ amount: UsageAmount; window: UsageWindow | undefined; tier: string | undefined }
|
|
158
|
+
>();
|
|
152
159
|
let earliestReset: number | undefined;
|
|
153
160
|
|
|
154
|
-
for (const [
|
|
161
|
+
for (const [_modelId, modelInfo] of Object.entries(data.models ?? {})) {
|
|
155
162
|
const quotaInfos = normalizeQuotaInfos(modelInfo);
|
|
156
163
|
for (const quotaInfo of quotaInfos) {
|
|
157
164
|
const amount = buildAmount(quotaInfo);
|
|
@@ -159,35 +166,81 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
|
|
|
159
166
|
if (window?.resetsAt) {
|
|
160
167
|
earliestReset = earliestReset ? Math.min(earliestReset, window.resetsAt) : window.resetsAt;
|
|
161
168
|
}
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
169
|
+
const tier = (quotaInfo.tier ?? "default").toLowerCase();
|
|
170
|
+
// Use quotaInfo.windowId even when parseWindow returns undefined
|
|
171
|
+
// (no resetTime) — separate windows must not collapse to "default".
|
|
172
|
+
const windowId = quotaInfo.windowId ?? window?.id ?? "default";
|
|
173
|
+
const key = `${tier}|${windowId}`;
|
|
174
|
+
const existing = deduped.get(key);
|
|
175
|
+
if (!existing) {
|
|
176
|
+
deduped.set(key, { amount, window, tier: quotaInfo.tier });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Merge: keep the entry with fraction data for the bar, but
|
|
180
|
+
// also keep any window with a reset time so "resets in…" survives.
|
|
181
|
+
const eFrac = existing.amount.remainingFraction;
|
|
182
|
+
const cFrac = amount.remainingFraction;
|
|
183
|
+
const eHasFrac = eFrac !== undefined;
|
|
184
|
+
const cHasFrac = cFrac !== undefined;
|
|
185
|
+
|
|
186
|
+
let bestAmount = existing.amount;
|
|
187
|
+
let bestWindow = existing.window?.resetsAt ? existing.window : (window ?? existing.window);
|
|
188
|
+
let bestTier = existing.tier ?? quotaInfo.tier;
|
|
189
|
+
|
|
190
|
+
if (!eHasFrac && cHasFrac) {
|
|
191
|
+
bestAmount = amount;
|
|
192
|
+
bestTier = quotaInfo.tier ?? existing.tier;
|
|
193
|
+
} else if (eHasFrac && cHasFrac && cFrac! < eFrac!) {
|
|
194
|
+
bestAmount = amount;
|
|
195
|
+
bestTier = quotaInfo.tier ?? existing.tier;
|
|
196
|
+
}
|
|
197
|
+
// Always merge in window with reset time if the current
|
|
198
|
+
// best doesn't have one.
|
|
199
|
+
if (!bestWindow?.resetsAt && window?.resetsAt) {
|
|
200
|
+
bestWindow = window;
|
|
201
|
+
}
|
|
202
|
+
deduped.set(key, { amount: bestAmount, window: bestWindow, tier: bestTier });
|
|
180
203
|
}
|
|
181
204
|
}
|
|
182
205
|
|
|
206
|
+
const limits: UsageLimit[] = [];
|
|
207
|
+
for (const [key, entry] of deduped) {
|
|
208
|
+
const [tier, windowId] = key.split("|") as [string, string];
|
|
209
|
+
const label = "Usage";
|
|
210
|
+
limits.push({
|
|
211
|
+
id: `${params.provider}:${tier}:${windowId}`,
|
|
212
|
+
label,
|
|
213
|
+
scope: {
|
|
214
|
+
provider: params.provider,
|
|
215
|
+
accountId: credential.accountId,
|
|
216
|
+
projectId: credential.projectId,
|
|
217
|
+
tier: entry.tier,
|
|
218
|
+
windowId,
|
|
219
|
+
},
|
|
220
|
+
window: entry.window,
|
|
221
|
+
amount: entry.amount,
|
|
222
|
+
status: getUsageStatus(entry.amount.remainingFraction),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
limits.sort((a, b) => {
|
|
227
|
+
const aFraction = a.amount.remainingFraction ?? 1;
|
|
228
|
+
const bFraction = b.amount.remainingFraction ?? 1;
|
|
229
|
+
return aFraction - bFraction;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const metadata: UsageReport["metadata"] = {
|
|
233
|
+
endpoint: url,
|
|
234
|
+
projectId: credential.projectId,
|
|
235
|
+
};
|
|
236
|
+
if (credential.email) metadata.email = credential.email;
|
|
237
|
+
if (credential.accountId) metadata.accountId = credential.accountId;
|
|
238
|
+
|
|
183
239
|
const report: UsageReport = {
|
|
184
240
|
provider: params.provider,
|
|
185
241
|
fetchedAt: nowMs,
|
|
186
242
|
limits,
|
|
187
|
-
metadata
|
|
188
|
-
endpoint: url,
|
|
189
|
-
projectId: credential.projectId,
|
|
190
|
-
},
|
|
243
|
+
metadata,
|
|
191
244
|
raw: data,
|
|
192
245
|
};
|
|
193
246
|
|
|
@@ -154,6 +154,22 @@ export const CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS: Record<string, Record<string,
|
|
|
154
154
|
null: {},
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Flat set of every type-specific key across all CCA types.
|
|
159
|
+
* Used to identify sibling keys that need filtering during mixed-type collapse.
|
|
160
|
+
*/
|
|
161
|
+
export const ALL_CCA_TYPE_SPECIFIC_KEYS: Record<string, true> = buildAllCcaTypeSpecificKeys();
|
|
162
|
+
|
|
163
|
+
function buildAllCcaTypeSpecificKeys(): Record<string, true> {
|
|
164
|
+
const all: Record<string, true> = {};
|
|
165
|
+
for (const typeKeys of Object.values(CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS)) {
|
|
166
|
+
for (const key in typeKeys) {
|
|
167
|
+
all[key] = true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return all;
|
|
171
|
+
}
|
|
172
|
+
|
|
157
173
|
/**
|
|
158
174
|
* Cloud Code Assist shared schema keys allowed on any type.
|
|
159
175
|
* Used alongside CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS for CCA combiner collapsing.
|
|
@@ -11,6 +11,7 @@ import { dereferenceJsonSchema } from "./dereference";
|
|
|
11
11
|
import { upgradeJsonSchemaTo202012 } from "./draft";
|
|
12
12
|
import { areJsonValuesEqual, mergePropertySchemas } from "./equality";
|
|
13
13
|
import {
|
|
14
|
+
ALL_CCA_TYPE_SPECIFIC_KEYS,
|
|
14
15
|
CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS,
|
|
15
16
|
CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS,
|
|
16
17
|
COMBINATOR_KEYS,
|
|
@@ -501,12 +502,32 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
|
|
|
501
502
|
if (variantTypes.length < 2 || variantTypes.every(type => type === "object")) {
|
|
502
503
|
return schema;
|
|
503
504
|
}
|
|
504
|
-
|
|
505
505
|
const nextSchema = copySchemaWithout(schema, combiner);
|
|
506
506
|
const nonNullTypes = variantTypes.filter(t => t !== "null");
|
|
507
|
-
|
|
507
|
+
const chosenType: string = nonNullTypes[0] ?? variantTypes[0];
|
|
508
|
+
nextSchema.type = chosenType;
|
|
509
|
+
const chosenTypeAllowedKeys = CLOUD_CODE_ASSIST_TYPE_SPECIFIC_KEYS[chosenType] ?? {};
|
|
510
|
+
|
|
511
|
+
// Strip sibling keys that were copied from the parent and belong to a
|
|
512
|
+
// different type (e.g. `items` sibling on a now-string-typed schema).
|
|
513
|
+
for (const key in nextSchema) {
|
|
514
|
+
if (!Object.hasOwn(nextSchema, key)) continue;
|
|
515
|
+
if (key === "type") continue;
|
|
516
|
+
if (
|
|
517
|
+
Object.hasOwn(ALL_CCA_TYPE_SPECIFIC_KEYS, key) &&
|
|
518
|
+
!Object.hasOwn(chosenTypeAllowedKeys, key) &&
|
|
519
|
+
!Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)
|
|
520
|
+
) {
|
|
521
|
+
delete nextSchema[key];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
508
525
|
for (const key in mergedVariantFields) {
|
|
509
526
|
if (!Object.hasOwn(mergedVariantFields, key)) continue;
|
|
527
|
+
// Drop type-specific keys that don't belong to the chosen type
|
|
528
|
+
if (!Object.hasOwn(chosenTypeAllowedKeys, key) && !Object.hasOwn(CLOUD_CODE_ASSIST_SHARED_SCHEMA_KEYS, key)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
510
531
|
const value = mergedVariantFields[key];
|
|
511
532
|
const existingValue = nextSchema[key];
|
|
512
533
|
if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
|