@bitkyc08/opencodex 2.12.0 → 2.13.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/README.md +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-BHldBl6_.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-BNVYzdn0.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Based on Anthropic SDK's transformJSONSchema, preserving root $defs required by root $ref:
|
|
2
|
+
// https://github.com/anthropics/anthropic-sdk-typescript/blob/main/src/lib/transform-json-schema.ts
|
|
3
|
+
const SUPPORTED_STRING_FORMATS = new Set([
|
|
4
|
+
"date-time",
|
|
5
|
+
"time",
|
|
6
|
+
"date",
|
|
7
|
+
"duration",
|
|
8
|
+
"email",
|
|
9
|
+
"hostname",
|
|
10
|
+
"uri",
|
|
11
|
+
"ipv4",
|
|
12
|
+
"ipv6",
|
|
13
|
+
"uuid",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function take(schema: Record<string, unknown>, key: string): unknown {
|
|
21
|
+
const value = schema[key];
|
|
22
|
+
delete schema[key];
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeSubschema(value: unknown): unknown {
|
|
27
|
+
return isRecord(value) ? normalizeSchema(value) : value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
|
31
|
+
const normalized: Record<string, unknown> = {};
|
|
32
|
+
|
|
33
|
+
const defs = take(schema, "$defs");
|
|
34
|
+
if (isRecord(defs)) {
|
|
35
|
+
normalized.$defs = Object.fromEntries(
|
|
36
|
+
Object.entries(defs).map(([name, definition]) => [name, normalizeSubschema(definition)]),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ref = take(schema, "$ref");
|
|
41
|
+
if (ref !== undefined) {
|
|
42
|
+
normalized.$ref = ref;
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const type = take(schema, "type");
|
|
47
|
+
const anyOf = schema.anyOf;
|
|
48
|
+
const oneOf = schema.oneOf;
|
|
49
|
+
const allOf = schema.allOf;
|
|
50
|
+
|
|
51
|
+
if (Array.isArray(anyOf)) {
|
|
52
|
+
take(schema, "anyOf");
|
|
53
|
+
normalized.anyOf = anyOf.map(normalizeSubschema);
|
|
54
|
+
} else if (Array.isArray(oneOf)) {
|
|
55
|
+
take(schema, "oneOf");
|
|
56
|
+
normalized.anyOf = oneOf.map(normalizeSubschema);
|
|
57
|
+
} else if (Array.isArray(allOf)) {
|
|
58
|
+
take(schema, "allOf");
|
|
59
|
+
normalized.allOf = allOf.map(normalizeSubschema);
|
|
60
|
+
} else {
|
|
61
|
+
if (type === undefined) {
|
|
62
|
+
throw new Error("JSON schema must have a type defined if anyOf/oneOf/allOf are not used");
|
|
63
|
+
}
|
|
64
|
+
normalized.type = type;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const description = take(schema, "description");
|
|
68
|
+
if (description !== undefined) {
|
|
69
|
+
normalized.description = description;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const title = take(schema, "title");
|
|
73
|
+
if (title !== undefined) {
|
|
74
|
+
normalized.title = title;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (type === "object") {
|
|
78
|
+
const properties = take(schema, "properties");
|
|
79
|
+
normalized.properties = isRecord(properties)
|
|
80
|
+
? Object.fromEntries(
|
|
81
|
+
Object.entries(properties).map(([name, property]) => [name, normalizeSubschema(property)]),
|
|
82
|
+
)
|
|
83
|
+
: {};
|
|
84
|
+
take(schema, "additionalProperties");
|
|
85
|
+
normalized.additionalProperties = false;
|
|
86
|
+
|
|
87
|
+
const required = take(schema, "required");
|
|
88
|
+
if (required !== undefined) {
|
|
89
|
+
normalized.required = required;
|
|
90
|
+
}
|
|
91
|
+
} else if (type === "string") {
|
|
92
|
+
const format = take(schema, "format");
|
|
93
|
+
if (typeof format === "string" && SUPPORTED_STRING_FORMATS.has(format)) {
|
|
94
|
+
normalized.format = format;
|
|
95
|
+
} else if (format !== undefined) {
|
|
96
|
+
schema.format = format;
|
|
97
|
+
}
|
|
98
|
+
} else if (type === "array") {
|
|
99
|
+
const items = take(schema, "items");
|
|
100
|
+
if (items !== undefined) {
|
|
101
|
+
normalized.items = normalizeSubschema(items);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const minItems = take(schema, "minItems");
|
|
105
|
+
if (minItems === 0 || minItems === 1) {
|
|
106
|
+
normalized.minItems = minItems;
|
|
107
|
+
} else if (minItems !== undefined) {
|
|
108
|
+
schema.minItems = minItems;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const unsupported = Object.entries(schema);
|
|
113
|
+
if (unsupported.length > 0) {
|
|
114
|
+
const existingDescription =
|
|
115
|
+
typeof normalized.description === "string" ? `${normalized.description}\n\n` : "";
|
|
116
|
+
normalized.description = `${existingDescription}{${unsupported
|
|
117
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
|
118
|
+
.join(", ")}}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return normalized;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeAnthropicOutputSchema(
|
|
125
|
+
schema: Record<string, unknown>,
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
return normalizeSchema(structuredClone(schema));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function isAnthropicOutputSchema(schema: Record<string, unknown>): boolean {
|
|
131
|
+
try {
|
|
132
|
+
normalizeAnthropicOutputSchema(schema);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -18,6 +18,7 @@ import { ANTHROPIC_OAUTH_BETA, CLAUDE_CODE_SYSTEM_INSTRUCTION, applyClaudeToolPr
|
|
|
18
18
|
import { parseDataUrl } from "./image";
|
|
19
19
|
import { enforceAnthropicImageLimits } from "./anthropic-image-guard";
|
|
20
20
|
import { normalizeAnthropicImages } from "./anthropic-image-normalize";
|
|
21
|
+
import { normalizeAnthropicOutputSchema } from "./anthropic-output-schema";
|
|
21
22
|
import { identifyRoutedModel } from "./identity";
|
|
22
23
|
import { redactSecretString } from "../lib/redact";
|
|
23
24
|
import { CLAUDE_CODE_HEADERS, claudeCodeSessionId } from "./client-fingerprint";
|
|
@@ -898,6 +899,20 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
|
|
|
898
899
|
delete body.top_p;
|
|
899
900
|
}
|
|
900
901
|
|
|
902
|
+
const textFormat = parsed.options.textFormat;
|
|
903
|
+
if (textFormat?.type === "json_schema" && textFormat.schema) {
|
|
904
|
+
const outputConfig = body.output_config;
|
|
905
|
+
body.output_config = {
|
|
906
|
+
...(outputConfig && typeof outputConfig === "object" && !Array.isArray(outputConfig)
|
|
907
|
+
? outputConfig
|
|
908
|
+
: {}),
|
|
909
|
+
format: {
|
|
910
|
+
type: "json_schema",
|
|
911
|
+
schema: normalizeAnthropicOutputSchema(textFormat.schema),
|
|
912
|
+
},
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
901
916
|
if (parsed.options.toolChoice && (tools || parsed.options.toolChoice === "none")) {
|
|
902
917
|
const tc = parsed.options.toolChoice;
|
|
903
918
|
if (tc === "auto") body.tool_choice = { type: "auto" };
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { atomicWriteFileAsync, getConfigDir, type AtomicWriteAsyncTestSeam } from "../config";
|
|
2
5
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
3
6
|
|
|
4
7
|
/**
|
|
@@ -26,6 +29,8 @@ interface ReplayEntry {
|
|
|
26
29
|
bytes: number;
|
|
27
30
|
expiresAtMs: number;
|
|
28
31
|
oldestAtMs: number | null;
|
|
32
|
+
/** Most recent observe/apply activity; orders sessions under the snapshot cap. */
|
|
33
|
+
lastActiveAtMs: number;
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
const MIN_SIGNATURE_LEN = 16;
|
|
@@ -38,6 +43,16 @@ export const ANTIGRAVITY_REPLAY_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
|
38
43
|
const REPLAY_MAX_SIGNATURE_BYTES = 64 * 1024;
|
|
39
44
|
/** Fixed 64-hex outer key length, counted once per session entry. */
|
|
40
45
|
const REPLAY_SESSION_KEY_BYTES = 64;
|
|
46
|
+
const REPLAY_SNAPSHOT_FILE = "antigravity-replay.json";
|
|
47
|
+
const REPLAY_SNAPSHOT_VERSION = 1;
|
|
48
|
+
const REPLAY_SNAPSHOT_DEBOUNCE_MS = 2_000;
|
|
49
|
+
/** Upper bound on flush retries: mutations arriving faster than writes complete
|
|
50
|
+
* must not let shutdown hang; the last completed write is already on disk. */
|
|
51
|
+
const REPLAY_FLUSH_MAX_ATTEMPTS = 8;
|
|
52
|
+
/** Write bound for the durable snapshot (mirrors the responses-state cap). */
|
|
53
|
+
const REPLAY_SNAPSHOT_MAX_BYTES = 24 * 1024 * 1024;
|
|
54
|
+
/** Refuse-to-parse ceiling for an existing snapshot file. */
|
|
55
|
+
const REPLAY_SNAPSHOT_REFUSE_BYTES = 32 * 1024 * 1024;
|
|
41
56
|
|
|
42
57
|
interface ReplayLimits {
|
|
43
58
|
maxCallsPerSession: number;
|
|
@@ -57,6 +72,216 @@ let replayLimits = { ...DEFAULT_REPLAY_LIMITS };
|
|
|
57
72
|
let replayBytes = 0;
|
|
58
73
|
let replayOldestSessionKey: string | undefined;
|
|
59
74
|
let replayOldestAt: number | null = null;
|
|
75
|
+
let replaySnapshotLoaded = false;
|
|
76
|
+
let replaySnapshotPersistTimer: ReturnType<typeof setTimeout> | null = null;
|
|
77
|
+
let replaySnapshotPersistGate: Promise<void> = Promise.resolve();
|
|
78
|
+
/** Mutation generation: bumped on every cache change that needs persisting. */
|
|
79
|
+
let replayMutationGeneration = 0;
|
|
80
|
+
/** Generation of the data the last successful snapshot write actually captured. */
|
|
81
|
+
let replayWrittenGeneration = 0;
|
|
82
|
+
let replaySnapshotWriteSeam: AtomicWriteAsyncTestSeam | undefined;
|
|
83
|
+
let replaySnapshotLoadDiscarded = false;
|
|
84
|
+
let replaySnapshotMaxBytes = REPLAY_SNAPSHOT_MAX_BYTES;
|
|
85
|
+
|
|
86
|
+
function replaySnapshotPath(): string {
|
|
87
|
+
return join(getConfigDir(), REPLAY_SNAPSHOT_FILE);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function loadReplaySnapshotEntry(key: string, value: unknown): void {
|
|
91
|
+
if (!/^[0-9a-f]{64}$/.test(key)) return;
|
|
92
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return;
|
|
93
|
+
const rec = value as { byCall?: unknown; expiresAtMs?: unknown; lastActiveAtMs?: unknown };
|
|
94
|
+
if (typeof rec.expiresAtMs !== "number" || !Number.isFinite(rec.expiresAtMs)) return;
|
|
95
|
+
// Expired sessions are dropped at load; a stale snapshot is self-healing.
|
|
96
|
+
if (rec.expiresAtMs <= Date.now()) {
|
|
97
|
+
replaySnapshotLoadDiscarded = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(rec.byCall)) return;
|
|
101
|
+
const byCall = new Map<string, ReplayCall>();
|
|
102
|
+
let bytes = REPLAY_SESSION_KEY_BYTES;
|
|
103
|
+
for (const pair of rec.byCall) {
|
|
104
|
+
if (!Array.isArray(pair) || pair.length !== 2 || typeof pair[0] !== "string") continue;
|
|
105
|
+
if (!/^[0-9a-f]{64}$/.test(pair[0])) continue;
|
|
106
|
+
const call = pair[1] as { signature?: unknown; touchedAtMs?: unknown } | null;
|
|
107
|
+
if (!call || typeof call !== "object" || Array.isArray(call)) continue;
|
|
108
|
+
if (typeof call.signature !== "string" || call.signature.length < MIN_SIGNATURE_LEN) continue;
|
|
109
|
+
if (typeof call.touchedAtMs !== "number" || !Number.isFinite(call.touchedAtMs)) continue;
|
|
110
|
+
// Never trust a serialized sizeBytes: a forged snapshot could claim a tiny
|
|
111
|
+
// size for a huge signature and bypass every byte cap. Recompute from the
|
|
112
|
+
// signature itself, exactly like the live write path.
|
|
113
|
+
const signatureBytes = utf8.encode(call.signature).byteLength;
|
|
114
|
+
if (signatureBytes > replayLimits.maxSignatureBytes) continue;
|
|
115
|
+
const callBytes = utf8.encode(pair[0]).byteLength + signatureBytes;
|
|
116
|
+
if (callBytes > replayLimits.maxBytesPerSession) continue;
|
|
117
|
+
// A duplicated call key would overstate entry.bytes (the map keeps only the
|
|
118
|
+
// last value) and could evict valid sessions; keep the first occurrence.
|
|
119
|
+
if (byCall.has(pair[0])) {
|
|
120
|
+
replaySnapshotLoadDiscarded = true;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
byCall.set(pair[0], {
|
|
124
|
+
signature: call.signature,
|
|
125
|
+
sizeBytes: callBytes,
|
|
126
|
+
touchedAtMs: call.touchedAtMs,
|
|
127
|
+
});
|
|
128
|
+
bytes += callBytes;
|
|
129
|
+
}
|
|
130
|
+
if (byCall.size === 0) {
|
|
131
|
+
replaySnapshotLoadDiscarded = true;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const lastActiveAtMs = typeof rec.lastActiveAtMs === "number" && Number.isFinite(rec.lastActiveAtMs)
|
|
135
|
+
? rec.lastActiveAtMs
|
|
136
|
+
: rec.expiresAtMs;
|
|
137
|
+
const entry: ReplayEntry = { byCall, bytes, expiresAtMs: rec.expiresAtMs, oldestAtMs: null, lastActiveAtMs };
|
|
138
|
+
const loadedCallCount = entry.byCall.size;
|
|
139
|
+
// Account BEFORE trimming: evictInnerCalls decrements the global byte count
|
|
140
|
+
// through deleteReplayCall, so the entry must already be on the books.
|
|
141
|
+
replayCache.set(key, entry);
|
|
142
|
+
replayBytes += entry.bytes;
|
|
143
|
+
// Same per-session caps as live writes, in case limits changed across versions.
|
|
144
|
+
evictInnerCalls(entry);
|
|
145
|
+
if (entry.byCall.size < loadedCallCount) replaySnapshotLoadDiscarded = true;
|
|
146
|
+
if (entry.byCall.size === 0) {
|
|
147
|
+
deleteReplaySession(key);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
refreshReplaySessionCandidate(key, entry);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Lazy load of the durable snapshot on first cache access, so signatures observed
|
|
155
|
+
* before a proxy restart are available again once the session id re-derives to the
|
|
156
|
+
* same key (the session id is anchored on the first user message text). Load is
|
|
157
|
+
* best-effort: missing, corrupt, or oversized files start the cache empty.
|
|
158
|
+
*/
|
|
159
|
+
function ensureReplaySnapshotLoaded(): void {
|
|
160
|
+
if (replaySnapshotLoaded) return;
|
|
161
|
+
replaySnapshotLoaded = true;
|
|
162
|
+
replaySnapshotLoadDiscarded = false;
|
|
163
|
+
try {
|
|
164
|
+
const path = replaySnapshotPath();
|
|
165
|
+
if (!existsSync(path)) return;
|
|
166
|
+
const stat = statSync(path);
|
|
167
|
+
// Bound the read BEFORE parse: the 24 MiB write cap constrains snapshots this
|
|
168
|
+
// process wrote, not a pre-existing oversized file.
|
|
169
|
+
if (!stat.isFile() || stat.size > REPLAY_SNAPSHOT_REFUSE_BYTES) return;
|
|
170
|
+
const raw = JSON.parse(readFileSync(path, "utf8")) as { version?: unknown; sessions?: unknown };
|
|
171
|
+
if (raw.version !== REPLAY_SNAPSHOT_VERSION || !Array.isArray(raw.sessions)) return;
|
|
172
|
+
for (const entry of raw.sessions) {
|
|
173
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") continue;
|
|
174
|
+
loadReplaySnapshotEntry(entry[0], entry[1]);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Missing/corrupt snapshot: start empty.
|
|
178
|
+
}
|
|
179
|
+
const sessionsAfterLoad = replayCache.size;
|
|
180
|
+
// Load-time admission must respect the same global caps as live writes.
|
|
181
|
+
evictIfNeeded();
|
|
182
|
+
if (replayCache.size < sessionsAfterLoad) replaySnapshotLoadDiscarded = true;
|
|
183
|
+
// Persist the cleaned state once: expired/over-limit sessions must not stay
|
|
184
|
+
// on disk and be re-parsed (and re-dropped) after every restart.
|
|
185
|
+
if (replaySnapshotLoadDiscarded) markReplayDirty();
|
|
186
|
+
enforceAppOwnedMemoryBudget();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function markReplayDirty(): void {
|
|
190
|
+
replayMutationGeneration += 1;
|
|
191
|
+
if (replaySnapshotPersistTimer) return;
|
|
192
|
+
replaySnapshotPersistTimer = setTimeout(() => {
|
|
193
|
+
void persistReplaySnapshotNow().catch(() => {
|
|
194
|
+
// Redacted static message only: never echo the underlying error, which
|
|
195
|
+
// can carry paths or other environment details.
|
|
196
|
+
console.warn("[antigravity] replay snapshot persist failed; cached signatures will not survive a restart");
|
|
197
|
+
});
|
|
198
|
+
}, REPLAY_SNAPSHOT_DEBOUNCE_MS);
|
|
199
|
+
(replaySnapshotPersistTimer as { unref?: () => void }).unref?.();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function persistReplaySnapshotNow(): Promise<void> {
|
|
203
|
+
if (replaySnapshotPersistTimer) {
|
|
204
|
+
clearTimeout(replaySnapshotPersistTimer);
|
|
205
|
+
replaySnapshotPersistTimer = null;
|
|
206
|
+
}
|
|
207
|
+
// Serialize writers so a flush and a debounced write cannot race on temps/ACL.
|
|
208
|
+
const previous = replaySnapshotPersistGate;
|
|
209
|
+
let release!: () => void;
|
|
210
|
+
replaySnapshotPersistGate = new Promise<void>(resolve => { release = resolve; });
|
|
211
|
+
await previous;
|
|
212
|
+
const writeGeneration = replayMutationGeneration;
|
|
213
|
+
try {
|
|
214
|
+
const sessions: Array<[string, unknown]> = [];
|
|
215
|
+
// Account for the bytes actually written, not just the entries: the document
|
|
216
|
+
// is `{"version":N,"sessions":[...]}`, so the framing and the comma between
|
|
217
|
+
// entries count against the cap too. Summing per-entry sizes alone let the
|
|
218
|
+
// written file exceed replaySnapshotMaxBytes by a margin that grew with every
|
|
219
|
+
// additional session.
|
|
220
|
+
const prefix = `{"version":${JSON.stringify(REPLAY_SNAPSHOT_VERSION)},"sessions":[`;
|
|
221
|
+
const suffix = "]}";
|
|
222
|
+
const serialized: string[] = [];
|
|
223
|
+
let total = Buffer.byteLength(prefix, "utf8") + Buffer.byteLength(suffix, "utf8");
|
|
224
|
+
// Most-recently-active first so the sessions that survive the snapshot
|
|
225
|
+
// byte cap are the ones actually in use (Map order is insertion order).
|
|
226
|
+
for (const [key, entry] of [...replayCache].sort((a, b) => b[1].lastActiveAtMs - a[1].lastActiveAtMs)) {
|
|
227
|
+
const byCall = [...entry.byCall].map(([callKey, call]) => [
|
|
228
|
+
callKey,
|
|
229
|
+
{ signature: call.signature, touchedAtMs: call.touchedAtMs },
|
|
230
|
+
]);
|
|
231
|
+
const persistEntry: [string, unknown] = [key, {
|
|
232
|
+
byCall,
|
|
233
|
+
expiresAtMs: entry.expiresAtMs,
|
|
234
|
+
lastActiveAtMs: entry.lastActiveAtMs,
|
|
235
|
+
}];
|
|
236
|
+
const encoded = JSON.stringify(persistEntry);
|
|
237
|
+
const size = Buffer.byteLength(encoded, "utf8") + (serialized.length > 0 ? 1 : 0);
|
|
238
|
+
if (total + size > replaySnapshotMaxBytes) break;
|
|
239
|
+
total += size;
|
|
240
|
+
serialized.push(encoded);
|
|
241
|
+
sessions.push(persistEntry);
|
|
242
|
+
}
|
|
243
|
+
serialized.reverse();
|
|
244
|
+
const document = `${prefix}${serialized.join(",")}${suffix}`;
|
|
245
|
+
// Defensive: the admitted entries are what we serialize, so this can only
|
|
246
|
+
// trip if the accounting above and the payload below ever drift apart.
|
|
247
|
+
if (Buffer.byteLength(document, "utf8") > replaySnapshotMaxBytes) {
|
|
248
|
+
throw new Error("Antigravity replay snapshot exceeded its configured byte cap.");
|
|
249
|
+
}
|
|
250
|
+
mkdirSync(dirname(replaySnapshotPath()), { recursive: true, mode: 0o700 });
|
|
251
|
+
try { chmodSync(dirname(replaySnapshotPath()), 0o700); } catch { /* best-effort (e.g. Windows) */ }
|
|
252
|
+
await atomicWriteFileAsync(
|
|
253
|
+
replaySnapshotPath(),
|
|
254
|
+
document,
|
|
255
|
+
undefined,
|
|
256
|
+
replaySnapshotWriteSeam,
|
|
257
|
+
);
|
|
258
|
+
replayWrittenGeneration = writeGeneration;
|
|
259
|
+
} finally {
|
|
260
|
+
release();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Flush any pending debounced snapshot write (graceful shutdown / deterministic tests). */
|
|
265
|
+
export async function flushAntigravityReplay(): Promise<void> {
|
|
266
|
+
// Persist until the durable generation catches up with the latest mutation:
|
|
267
|
+
// a change that lands while a writer is in flight must not be lost when
|
|
268
|
+
// shutdown exits right after the first write completes.
|
|
269
|
+
for (let attempt = 0; attempt < REPLAY_FLUSH_MAX_ATTEMPTS; attempt += 1) {
|
|
270
|
+
if (replaySnapshotPersistTimer || replayMutationGeneration > replayWrittenGeneration) {
|
|
271
|
+
await persistReplaySnapshotNow();
|
|
272
|
+
}
|
|
273
|
+
// No pending timer: still await any in-flight write so shutdown does not race it.
|
|
274
|
+
await replaySnapshotPersistGate;
|
|
275
|
+
if (replayMutationGeneration === replayWrittenGeneration && replaySnapshotPersistTimer === null) return;
|
|
276
|
+
}
|
|
277
|
+
// Budget exhausted without convergence. Resolving here would tell
|
|
278
|
+
// drainAndShutdown() the snapshot is durable while the latest thought
|
|
279
|
+
// signature may never have reached disk, so shutdown diagnostics would claim
|
|
280
|
+
// a durability we cannot demonstrate. Reject instead, with fixed text: the
|
|
281
|
+
// shutdown path logs this message, so it must not carry session, model, or
|
|
282
|
+
// signature detail.
|
|
283
|
+
throw new Error("Antigravity replay snapshot flush did not converge.");
|
|
284
|
+
}
|
|
60
285
|
|
|
61
286
|
/**
|
|
62
287
|
* Fixed-size identity for a (model, sessionId) pair: SHA-256 over
|
|
@@ -372,6 +597,7 @@ export function antigravityUsesReplayCache(model: string): boolean {
|
|
|
372
597
|
*/
|
|
373
598
|
export function observeAntigravityReplay(model: string, sessionId: string, parts: unknown[]): void {
|
|
374
599
|
if (!antigravityUsesReplayCache(model) || !Array.isArray(parts) || parts.length === 0) return;
|
|
600
|
+
ensureReplaySnapshotLoaded();
|
|
375
601
|
const now = Date.now();
|
|
376
602
|
deleteExpiredReplaySessionsThrottled(now);
|
|
377
603
|
const key = replayKey(model, sessionId);
|
|
@@ -381,6 +607,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
|
|
|
381
607
|
bytes: REPLAY_SESSION_KEY_BYTES,
|
|
382
608
|
expiresAtMs: 0,
|
|
383
609
|
oldestAtMs: null,
|
|
610
|
+
lastActiveAtMs: 0,
|
|
384
611
|
};
|
|
385
612
|
let inserted = false;
|
|
386
613
|
let pendingThoughtSig: string | undefined;
|
|
@@ -418,15 +645,21 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
|
|
|
418
645
|
// The fixed session overhead can exceed the per-session cap on its own
|
|
419
646
|
// (test-sized limits): an entry holding zero calls is unusable — drop it
|
|
420
647
|
// instead of retaining an unevictable shell.
|
|
421
|
-
if (existing)
|
|
422
|
-
|
|
648
|
+
if (existing) {
|
|
649
|
+
deleteReplaySession(key);
|
|
650
|
+
markReplayDirty();
|
|
651
|
+
} else {
|
|
652
|
+
replayBytes -= REPLAY_SESSION_KEY_BYTES;
|
|
653
|
+
}
|
|
423
654
|
return;
|
|
424
655
|
}
|
|
425
656
|
entry.expiresAtMs = now + REPLAY_TTL_MS;
|
|
657
|
+
entry.lastActiveAtMs = now;
|
|
426
658
|
replayCache.set(key, entry);
|
|
427
659
|
refreshReplaySessionCandidate(key, entry);
|
|
428
660
|
evictIfNeeded();
|
|
429
661
|
enforceAppOwnedMemoryBudget();
|
|
662
|
+
markReplayDirty();
|
|
430
663
|
}
|
|
431
664
|
|
|
432
665
|
/**
|
|
@@ -436,6 +669,7 @@ export function observeAntigravityReplay(model: string, sessionId: string, parts
|
|
|
436
669
|
*/
|
|
437
670
|
export function applyAntigravityReplay(model: string, sessionId: string, contents: unknown[]): unknown[] {
|
|
438
671
|
if (!antigravityUsesReplayCache(model) || !Array.isArray(contents)) return contents;
|
|
672
|
+
ensureReplaySnapshotLoaded();
|
|
439
673
|
const now = Date.now();
|
|
440
674
|
deleteExpiredReplaySessionsThrottled(now);
|
|
441
675
|
const entry = replayCache.get(replayKey(model, sessionId));
|
|
@@ -461,13 +695,18 @@ export function applyAntigravityReplay(model: string, sessionId: string, content
|
|
|
461
695
|
}
|
|
462
696
|
}
|
|
463
697
|
}
|
|
464
|
-
if (touched)
|
|
698
|
+
if (touched) {
|
|
699
|
+
entry.lastActiveAtMs = now;
|
|
700
|
+
refreshReplaySessionCandidate(replayKey(model, sessionId), entry);
|
|
701
|
+
markReplayDirty();
|
|
702
|
+
}
|
|
465
703
|
return contents;
|
|
466
704
|
}
|
|
467
705
|
|
|
468
706
|
/** Drop the cache entry when upstream rejects a signature (clear-on-invalid). */
|
|
469
707
|
export function clearAntigravityReplay(model: string, sessionId: string): void {
|
|
470
|
-
|
|
708
|
+
ensureReplaySnapshotLoaded();
|
|
709
|
+
if (deleteReplaySession(replayKey(model, sessionId)) > 0) markReplayDirty();
|
|
471
710
|
}
|
|
472
711
|
|
|
473
712
|
export function antigravityReplayMetrics(): {
|
|
@@ -476,6 +715,7 @@ export function antigravityReplayMetrics(): {
|
|
|
476
715
|
totalBytes: number;
|
|
477
716
|
largestSessionBytes: number;
|
|
478
717
|
} {
|
|
718
|
+
ensureReplaySnapshotLoaded();
|
|
479
719
|
let calls = 0;
|
|
480
720
|
let largestSessionBytes = 0;
|
|
481
721
|
for (const entry of replayCache.values()) {
|
|
@@ -492,6 +732,7 @@ export function antigravityReplayRetainedStoreSnapshot(): {
|
|
|
492
732
|
pinnedBytes: number;
|
|
493
733
|
oldestAt: number | null;
|
|
494
734
|
} {
|
|
735
|
+
ensureReplaySnapshotLoaded();
|
|
495
736
|
return {
|
|
496
737
|
count: replayCache.size,
|
|
497
738
|
bytes: replayBytes,
|
|
@@ -502,7 +743,10 @@ export function antigravityReplayRetainedStoreSnapshot(): {
|
|
|
502
743
|
}
|
|
503
744
|
|
|
504
745
|
export function evictOldestAntigravityReplayForBudget(): number {
|
|
505
|
-
|
|
746
|
+
ensureReplaySnapshotLoaded();
|
|
747
|
+
const removed = replayOldestSessionKey === undefined ? 0 : deleteReplaySession(replayOldestSessionKey);
|
|
748
|
+
if (removed > 0) markReplayDirty();
|
|
749
|
+
return removed;
|
|
506
750
|
}
|
|
507
751
|
|
|
508
752
|
export function setAntigravityReplayLimitsForTests(limits?: Partial<ReplayLimits>): void {
|
|
@@ -510,11 +754,31 @@ export function setAntigravityReplayLimitsForTests(limits?: Partial<ReplayLimits
|
|
|
510
754
|
replayLimits = limits ? { ...DEFAULT_REPLAY_LIMITS, ...limits } : { ...DEFAULT_REPLAY_LIMITS };
|
|
511
755
|
}
|
|
512
756
|
|
|
757
|
+
/** Test-only write seam (mirrors AtomicWriteAsyncTestSeam usage elsewhere). */
|
|
758
|
+
export function setAntigravityReplayWriteSeamForTests(seam: AtomicWriteAsyncTestSeam | undefined): void {
|
|
759
|
+
replaySnapshotWriteSeam = seam;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** Test-only snapshot byte cap (mirrors the limits seam). */
|
|
763
|
+
export function setAntigravityReplaySnapshotMaxBytesForTests(maxBytes: number): void {
|
|
764
|
+
replaySnapshotMaxBytes = maxBytes;
|
|
765
|
+
}
|
|
766
|
+
|
|
513
767
|
/** Test seam. */
|
|
514
768
|
export function __resetAntigravityReplayCache(): void {
|
|
769
|
+
if (replaySnapshotPersistTimer) {
|
|
770
|
+
clearTimeout(replaySnapshotPersistTimer);
|
|
771
|
+
replaySnapshotPersistTimer = null;
|
|
772
|
+
}
|
|
773
|
+
replaySnapshotLoaded = false;
|
|
515
774
|
lastLazySweepAt = Number.NEGATIVE_INFINITY;
|
|
516
775
|
replayCache.clear();
|
|
517
776
|
replayBytes = 0;
|
|
518
777
|
replayOldestSessionKey = undefined;
|
|
519
778
|
replayOldestAt = null;
|
|
779
|
+
replayMutationGeneration = 0;
|
|
780
|
+
replayWrittenGeneration = 0;
|
|
781
|
+
replaySnapshotWriteSeam = undefined;
|
|
782
|
+
replaySnapshotLoadDiscarded = false;
|
|
783
|
+
replaySnapshotMaxBytes = REPLAY_SNAPSHOT_MAX_BYTES;
|
|
520
784
|
}
|
|
@@ -226,7 +226,7 @@ function toolResultImageChatParts(content: string | OcxContentPart[]): unknown[]
|
|
|
226
226
|
function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
|
|
227
227
|
const out: unknown[] = [];
|
|
228
228
|
const { context, options } = parsed;
|
|
229
|
-
const replayCacheScope = parsed.
|
|
229
|
+
const replayCacheScope = parsed._reasoningReplayScope;
|
|
230
230
|
|
|
231
231
|
interface PendingToolCall { id: string; name: string }
|
|
232
232
|
let pendingToolCalls: PendingToolCall[] = [];
|
|
@@ -813,19 +813,25 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
813
813
|
if (provider.promptCacheKey && parsed.options.promptCacheKey !== undefined) {
|
|
814
814
|
body.prompt_cache_key = parsed.options.promptCacheKey;
|
|
815
815
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
816
|
+
// Structured-output support varies by the physical upstream model even when one
|
|
817
|
+
// gateway exposes a uniform OpenAI-compatible endpoint. Keep the #1137 translation
|
|
818
|
+
// as the default, but let an exact model opt out instead of forcing a provider-wide
|
|
819
|
+
// rollback that would silently return prose for siblings that support JSON Schema.
|
|
820
|
+
if (!provider.noStructuredOutputModels?.includes(parsed.modelId)) {
|
|
821
|
+
const textFormat = parsed.options.textFormat;
|
|
822
|
+
if (textFormat?.type === "json_object") {
|
|
823
|
+
body.response_format = { type: "json_object" };
|
|
824
|
+
} else if (textFormat?.type === "json_schema") {
|
|
825
|
+
body.response_format = {
|
|
826
|
+
type: "json_schema",
|
|
827
|
+
json_schema: {
|
|
828
|
+
name: textFormat.name ?? "response",
|
|
829
|
+
...(textFormat.description !== undefined ? { description: textFormat.description } : {}),
|
|
830
|
+
...(textFormat.schema !== undefined ? { schema: textFormat.schema } : {}),
|
|
831
|
+
...(textFormat.strict !== undefined ? { strict: textFormat.strict } : {}),
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
}
|
|
829
835
|
}
|
|
830
836
|
|
|
831
837
|
if (tools) {
|
|
@@ -969,10 +975,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
969
975
|
}
|
|
970
976
|
|
|
971
977
|
const rawToolCalls = delta.tool_calls;
|
|
972
|
-
if (rawToolCalls !== undefined) {
|
|
973
|
-
// A claimed tool-call payload is not benign padding. Dropping it can leave the
|
|
978
|
+
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
979
|
+
// A non-null claimed tool-call payload is not benign padding. Dropping it can leave the
|
|
974
980
|
// matching result permanently orphaned, so malformed nested shapes fail closed
|
|
975
|
-
// through the adapter error channel instead of escaping as TypeError (#1325).
|
|
981
|
+
// through the adapter error channel instead of escaping as TypeError (#1325). Null is
|
|
982
|
+
// tolerated as absent because OpenAI-compatible providers may emit it as stream padding.
|
|
976
983
|
if (!Array.isArray(rawToolCalls)) {
|
|
977
984
|
return yield* terminateWithError(invalidToolCallsEvent(pendingUsage));
|
|
978
985
|
}
|
|
@@ -1140,7 +1147,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
|
|
|
1140
1147
|
if (reasoningText !== undefined) events.push({ type: "reasoning_raw_delta", text: reasoningText });
|
|
1141
1148
|
if (typeof msg.content === "string") events.push({ type: "text_delta", text: msg.content });
|
|
1142
1149
|
const rawToolCalls = msg.tool_calls;
|
|
1143
|
-
if (rawToolCalls !== undefined) {
|
|
1150
|
+
if (rawToolCalls !== undefined && rawToolCalls !== null) {
|
|
1144
1151
|
if (!Array.isArray(rawToolCalls)) return [invalidToolCallsEvent(usage)];
|
|
1145
1152
|
for (const rawToolCall of rawToolCalls) {
|
|
1146
1153
|
if (!isRecord(rawToolCall) || !isRecord(rawToolCall.function)) {
|