@bitkyc08/opencodex 2.84.0 → 2.86.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 +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingest of canonical `usage.jsonl` rows into a request-history index file.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the serving thread (tails, small rebuilds) and the rebuild worker
|
|
5
|
+
* (large rebuilds, #6990), so both produce identical rows and meta. This
|
|
6
|
+
* module imports no config or indexer state; the worker loads it on its own.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Database } from "bun:sqlite";
|
|
10
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
11
|
+
import {
|
|
12
|
+
encodePersistedRequestedModel,
|
|
13
|
+
type PersistedUsageEntry,
|
|
14
|
+
type UsageLogRevision,
|
|
15
|
+
} from "../../usage/log";
|
|
16
|
+
import { HISTORY_DDL, HISTORY_META_KEYS, HISTORY_SCHEMA_VERSION } from "./schema";
|
|
17
|
+
|
|
18
|
+
export const REQUEST_HISTORY_INSERT_BATCH = 500;
|
|
19
|
+
export const REQUEST_HISTORY_READ_CHUNK_BYTES = 64 * 1024;
|
|
20
|
+
// The SQLite index is a disposable projection. Complete JSONL records above
|
|
21
|
+
// this bound are omitted from the projection; the canonical usage.jsonl is
|
|
22
|
+
// never truncated or rewritten by the indexer.
|
|
23
|
+
export const REQUEST_HISTORY_MAX_RECORD_BYTES = 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/** The ledger at the recorded path is no longer the file the rebuild started from. */
|
|
26
|
+
export class RequestHistoryIndexStaleError extends Error {
|
|
27
|
+
constructor(message: string) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "RequestHistoryIndexStaleError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function metaValue(dbHandle: Database, key: string): string | null {
|
|
34
|
+
const row = dbHandle.query("SELECT value FROM schema_meta WHERE key = ?").get(key) as
|
|
35
|
+
| { value: string }
|
|
36
|
+
| undefined;
|
|
37
|
+
return row?.value ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setMeta(dbHandle: Database, key: string, value: string | number): void {
|
|
41
|
+
dbHandle.query(
|
|
42
|
+
"INSERT INTO schema_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
43
|
+
).run(key, String(value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function metaNumber(dbHandle: Database, key: string): number {
|
|
47
|
+
const value = metaValue(dbHandle, key);
|
|
48
|
+
const parsed = value === null ? NaN : Number(value);
|
|
49
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resetAndCreateSchema(dbHandle: Database): void {
|
|
53
|
+
dbHandle.exec("DROP TABLE IF EXISTS requests");
|
|
54
|
+
dbHandle.exec("DROP TABLE IF EXISTS schema_meta");
|
|
55
|
+
dbHandle.exec(HISTORY_DDL);
|
|
56
|
+
setMeta(dbHandle, HISTORY_META_KEYS.schemaVersion, HISTORY_SCHEMA_VERSION);
|
|
57
|
+
setMeta(dbHandle, HISTORY_META_KEYS.indexedOffset, 0);
|
|
58
|
+
setMeta(dbHandle, HISTORY_META_KEYS.indexedRows, 0);
|
|
59
|
+
setMeta(dbHandle, HISTORY_META_KEYS.builtAtMs, Date.now());
|
|
60
|
+
setMeta(dbHandle, HISTORY_META_KEYS.lastError, "rebuilt");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function recordSourceMeta(dbHandle: Database, revision: UsageLogRevision | null): void {
|
|
64
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourcePath, revision?.path ?? "");
|
|
65
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceDev, revision?.dev ?? 0);
|
|
66
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceIno, revision?.ino ?? 0);
|
|
67
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceBirthtimeMs, revision?.birthtimeMs ?? 0);
|
|
68
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceSize, revision?.size ?? 0);
|
|
69
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceMtimeMs, revision?.mtimeMs ?? 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Extract the `requests` row columns from a canonical persisted entry. */
|
|
73
|
+
function extractRow(entry: PersistedUsageEntry): Array<string | number | null> {
|
|
74
|
+
const attempts = entry.attempts;
|
|
75
|
+
return [
|
|
76
|
+
entry.requestId,
|
|
77
|
+
entry.timestamp,
|
|
78
|
+
entry.provider,
|
|
79
|
+
entry.model,
|
|
80
|
+
// Old canonical JSONL rows can predate bounded selector persistence. Encode
|
|
81
|
+
// the disposable projection on rebuild so exact filters match across versions.
|
|
82
|
+
typeof entry.requestedModel === "string" ? encodePersistedRequestedModel(entry.requestedModel) : null,
|
|
83
|
+
entry.status,
|
|
84
|
+
entry.surface ?? null,
|
|
85
|
+
entry.inboundProtocol ?? null,
|
|
86
|
+
entry.apiKeyId ?? null,
|
|
87
|
+
entry.conversationId ?? null,
|
|
88
|
+
entry.routeDecision?.routeKind ?? null,
|
|
89
|
+
entry.routeDecision?.profile?.id ?? null,
|
|
90
|
+
entry.routeDecision?.profile?.revision ?? null,
|
|
91
|
+
(attempts?.length ?? 0) > 1 ? 1 : 0,
|
|
92
|
+
entry.durationMs,
|
|
93
|
+
entry.firstOutputMs ?? null,
|
|
94
|
+
entry.usageStatus,
|
|
95
|
+
entry.usage ? JSON.stringify(entry.usage) : null,
|
|
96
|
+
entry.totalTokens ?? null,
|
|
97
|
+
entry.errorCode ?? null,
|
|
98
|
+
entry.terminalStatus ?? null,
|
|
99
|
+
entry.closeReason ?? null,
|
|
100
|
+
attempts?.length ?? 1,
|
|
101
|
+
entry.routeDecision ? JSON.stringify(entry.routeDecision) : null,
|
|
102
|
+
JSON.stringify(entry),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const ROW_INSERT = `
|
|
107
|
+
INSERT OR IGNORE INTO requests (
|
|
108
|
+
request_id, timestamp, provider, model, requested_model, status, surface,
|
|
109
|
+
inbound_protocol, api_key_id, conversation_id, route_kind, profile_id,
|
|
110
|
+
profile_revision, fallback, duration_ms, first_output_ms, usage_status,
|
|
111
|
+
usage_json, total_tokens, error_code, terminal_status, close_reason,
|
|
112
|
+
attempt_count, decision_json, row_json
|
|
113
|
+
) VALUES (
|
|
114
|
+
?, ?, ?, ?, ?, ?, ?,
|
|
115
|
+
?, ?, ?, ?, ?, ?,
|
|
116
|
+
?, ?, ?, ?, ?, ?, ?,
|
|
117
|
+
?, ?, ?, ?, ?
|
|
118
|
+
)`;
|
|
119
|
+
|
|
120
|
+
function parsedEntryFromLine(line: string): PersistedUsageEntry | null {
|
|
121
|
+
if (!line.trim()) return null;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(line) as PersistedUsageEntry;
|
|
124
|
+
if (parsed && typeof parsed === "object"
|
|
125
|
+
&& typeof parsed.requestId === "string"
|
|
126
|
+
&& typeof parsed.timestamp === "number"
|
|
127
|
+
&& typeof parsed.provider === "string"
|
|
128
|
+
&& typeof parsed.model === "string"
|
|
129
|
+
&& typeof parsed.status === "number"
|
|
130
|
+
&& typeof parsed.durationMs === "number") {
|
|
131
|
+
return parsed;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
/* skip partial / hand-edited lines, same as every other reader */
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface IngestOptions {
|
|
140
|
+
/** Ledger file the caller recorded; a different file at the path makes the ingest stale. */
|
|
141
|
+
expected?: Pick<UsageLogRevision, "dev" | "ino">;
|
|
142
|
+
/**
|
|
143
|
+
* Stop at this byte offset instead of the size observed when the ingest
|
|
144
|
+
* opens the ledger, so a worker rebuild indexes the snapshot its refresh
|
|
145
|
+
* recorded and the next refresh picks up later appends as a tail.
|
|
146
|
+
*/
|
|
147
|
+
endOffset?: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function ingestSourceTail(
|
|
151
|
+
dbHandle: Database,
|
|
152
|
+
path: string,
|
|
153
|
+
fromOffset: number,
|
|
154
|
+
options: IngestOptions = {},
|
|
155
|
+
): number {
|
|
156
|
+
let inserted = 0;
|
|
157
|
+
let pending: Array<Array<string | number | null>> = [];
|
|
158
|
+
const insert = dbHandle.prepare(ROW_INSERT);
|
|
159
|
+
let fd: number | undefined;
|
|
160
|
+
try {
|
|
161
|
+
const commitBatch = () => {
|
|
162
|
+
dbHandle.transaction((rows: Array<Array<string | number | null>>) => {
|
|
163
|
+
for (const row of rows) {
|
|
164
|
+
// `changes` counts real inserts only; INSERT OR IGNORE replays add 0.
|
|
165
|
+
inserted += insert.run(...row).changes;
|
|
166
|
+
}
|
|
167
|
+
})(pending);
|
|
168
|
+
pending = [];
|
|
169
|
+
};
|
|
170
|
+
const ingestLine = (line: Buffer) => {
|
|
171
|
+
const entry = parsedEntryFromLine(line.toString("utf-8"));
|
|
172
|
+
if (!entry) return;
|
|
173
|
+
pending.push(extractRow(entry));
|
|
174
|
+
if (pending.length >= REQUEST_HISTORY_INSERT_BATCH) commitBatch();
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
fd = openSync(path, "r");
|
|
178
|
+
const stat = fstatSync(fd);
|
|
179
|
+
const expected = options.expected;
|
|
180
|
+
if (expected && (Number(stat.dev) !== Number(expected.dev) || Number(stat.ino) !== Number(expected.ino))) {
|
|
181
|
+
throw new RequestHistoryIndexStaleError("usage log changed before indexing");
|
|
182
|
+
}
|
|
183
|
+
const size = options.endOffset === undefined
|
|
184
|
+
? Number(stat.size)
|
|
185
|
+
: Math.min(Number(stat.size), options.endOffset);
|
|
186
|
+
const readBuffer = Buffer.allocUnsafe(REQUEST_HISTORY_READ_CHUNK_BYTES);
|
|
187
|
+
let position = fromOffset;
|
|
188
|
+
let nextOffset = fromOffset;
|
|
189
|
+
let fragments: Buffer[] = [];
|
|
190
|
+
let fragmentBytes = 0;
|
|
191
|
+
let oversized = false;
|
|
192
|
+
while (position < size) {
|
|
193
|
+
const requested = Math.min(readBuffer.length, size - position);
|
|
194
|
+
const bytesRead = readSync(fd, readBuffer, 0, requested, position);
|
|
195
|
+
if (bytesRead === 0) throw new RequestHistoryIndexStaleError("usage log changed while indexing");
|
|
196
|
+
let lineStart = 0;
|
|
197
|
+
for (let index = 0; index < bytesRead; index++) {
|
|
198
|
+
if (readBuffer[index] !== 0x0a) continue;
|
|
199
|
+
const segment = readBuffer.subarray(lineStart, index);
|
|
200
|
+
if (!oversized && fragmentBytes + segment.length <= REQUEST_HISTORY_MAX_RECORD_BYTES) {
|
|
201
|
+
const line = fragments.length === 0
|
|
202
|
+
? segment
|
|
203
|
+
: Buffer.concat([...fragments, segment], fragmentBytes + segment.length);
|
|
204
|
+
ingestLine(line);
|
|
205
|
+
}
|
|
206
|
+
fragments = [];
|
|
207
|
+
fragmentBytes = 0;
|
|
208
|
+
oversized = false;
|
|
209
|
+
lineStart = index + 1;
|
|
210
|
+
nextOffset = position + index + 1;
|
|
211
|
+
}
|
|
212
|
+
const remainder = readBuffer.subarray(lineStart, bytesRead);
|
|
213
|
+
if (!oversized && fragmentBytes + remainder.length <= REQUEST_HISTORY_MAX_RECORD_BYTES) {
|
|
214
|
+
// Copy because readBuffer is reused on the next iteration.
|
|
215
|
+
fragments.push(Buffer.from(remainder));
|
|
216
|
+
fragmentBytes += remainder.length;
|
|
217
|
+
} else if (remainder.length > 0) {
|
|
218
|
+
fragments = [];
|
|
219
|
+
fragmentBytes = 0;
|
|
220
|
+
oversized = true;
|
|
221
|
+
}
|
|
222
|
+
position += bytesRead;
|
|
223
|
+
}
|
|
224
|
+
if (pending.length > 0) commitBatch();
|
|
225
|
+
const indexedRows = metaNumber(dbHandle, HISTORY_META_KEYS.indexedRows);
|
|
226
|
+
setMeta(dbHandle, HISTORY_META_KEYS.indexedOffset, nextOffset);
|
|
227
|
+
setMeta(dbHandle, HISTORY_META_KEYS.indexedRows, indexedRows + inserted);
|
|
228
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceSize, size);
|
|
229
|
+
setMeta(dbHandle, HISTORY_META_KEYS.sourceMtimeMs, Number(stat.mtimeMs));
|
|
230
|
+
setMeta(dbHandle, HISTORY_META_KEYS.builtAtMs, Date.now());
|
|
231
|
+
return inserted;
|
|
232
|
+
} finally {
|
|
233
|
+
if (fd !== undefined) closeSync(fd);
|
|
234
|
+
// Windows file locks: an unterminated prepared statement keeps the DB
|
|
235
|
+
// file busy after close (verified on Bun 1.3.14). Finalize always.
|
|
236
|
+
insert.finalize();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The indexer's side of one rebuild worker (#6990): spawn it, send the build
|
|
3
|
+
* request, and take it down again. Every exit settles: a worker that errors or
|
|
4
|
+
* exits rejects the pending build. dispose() is bounded; afterExit() runs
|
|
5
|
+
* cleanup only once the thread has really exited, so build files the thread
|
|
6
|
+
* may still hold are never removed under it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawnWorker } from "../../lib/worker-embed";
|
|
10
|
+
import { storageWorkerOsJoinSettleMs } from "../../storage/worker-lifecycle";
|
|
11
|
+
import type { UsageLogRevision } from "../../usage/log";
|
|
12
|
+
import { RequestHistoryIndexStaleError } from "./ingest";
|
|
13
|
+
import type { RebuildWorkerReply, RebuildWorkerRequest } from "./rebuild-protocol";
|
|
14
|
+
|
|
15
|
+
const CLOSE_TIMEOUT_MS = 5_000;
|
|
16
|
+
|
|
17
|
+
function delay(ms: number): Promise<void> {
|
|
18
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True when `promise` settles within `ms`; the timer is cleared either way so it never keeps a process alive. */
|
|
22
|
+
function settlesWithin(promise: Promise<unknown>, ms: number): Promise<boolean> {
|
|
23
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
24
|
+
const expired = new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(false), ms); });
|
|
25
|
+
return Promise.race([promise.then(() => true, () => true), expired]).finally(() => clearTimeout(timer));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function errorFromReply(reply: Extract<RebuildWorkerReply, { type: "error" }>): Error {
|
|
29
|
+
if (reply.name === "RequestHistoryIndexStaleError") return new RequestHistoryIndexStaleError(reply.message);
|
|
30
|
+
const error = new Error(reply.message);
|
|
31
|
+
error.name = reply.name;
|
|
32
|
+
return error;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class RebuildWorkerJob {
|
|
36
|
+
private readonly worker: Worker;
|
|
37
|
+
private pending: { resolve: (rows: number) => void; reject: (error: unknown) => void } | null = null;
|
|
38
|
+
private exited = false;
|
|
39
|
+
private exitedCleanly = false;
|
|
40
|
+
private disposal: Promise<void> | null = null;
|
|
41
|
+
private readonly closed: Promise<void>;
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
this.worker = spawnWorker(new URL("./rebuild-worker.ts", import.meta.url).href, "request-history-rebuild-worker");
|
|
45
|
+
let markClosed!: () => void;
|
|
46
|
+
this.closed = new Promise(resolve => { markClosed = resolve; });
|
|
47
|
+
this.worker.addEventListener("message", (event: MessageEvent<RebuildWorkerReply>) => {
|
|
48
|
+
const pending = this.pending;
|
|
49
|
+
this.pending = null;
|
|
50
|
+
if (!pending) return;
|
|
51
|
+
const reply = event.data;
|
|
52
|
+
if (reply.type === "built") pending.resolve(reply.rows);
|
|
53
|
+
else pending.reject(errorFromReply(reply));
|
|
54
|
+
});
|
|
55
|
+
this.worker.addEventListener("error", (event: Event) => {
|
|
56
|
+
this.fail(new Error((event as ErrorEvent).message || "request-history rebuild worker failed"));
|
|
57
|
+
});
|
|
58
|
+
this.worker.addEventListener("close", () => {
|
|
59
|
+
this.exited = true;
|
|
60
|
+
this.fail(new Error("request-history rebuild worker exited"));
|
|
61
|
+
markClosed();
|
|
62
|
+
}, { once: true });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fail(error: unknown): void {
|
|
66
|
+
const pending = this.pending;
|
|
67
|
+
this.pending = null;
|
|
68
|
+
pending?.reject(error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Build the complete private file; resolves with the ingested row count. */
|
|
72
|
+
build(indexPath: string, buildPath: string, reason: string, revision: UsageLogRevision): Promise<number> {
|
|
73
|
+
if (this.exited || this.disposal) return Promise.reject(new Error("request-history rebuild worker is not running"));
|
|
74
|
+
if (this.pending) return Promise.reject(new Error("request-history rebuild worker is busy"));
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
this.pending = { resolve, reject };
|
|
77
|
+
this.worker.postMessage({ type: "build", indexPath, buildPath, reason, revision } satisfies RebuildWorkerRequest);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Stop the thread; waits a bounded time for it to exit. */
|
|
82
|
+
dispose(): Promise<void> {
|
|
83
|
+
this.disposal ??= this.shutDown();
|
|
84
|
+
return this.disposal;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Dispose, then run `cleanup` once the thread has exited. A thread that
|
|
89
|
+
* overran the bound (blocked in native I/O) gets its cleanup when it does
|
|
90
|
+
* exit; the returned promise does not wait for that.
|
|
91
|
+
*/
|
|
92
|
+
async afterExit(cleanup: () => void): Promise<void> {
|
|
93
|
+
await this.dispose();
|
|
94
|
+
if (this.exitedCleanly) {
|
|
95
|
+
cleanup();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
void this.closed.then(() => delay(storageWorkerOsJoinSettleMs())).then(cleanup).catch(() => undefined);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async shutDown(): Promise<void> {
|
|
102
|
+
this.fail(new Error("request-history rebuild worker stopped"));
|
|
103
|
+
try { this.worker.terminate(); } catch { /* already gone */ }
|
|
104
|
+
if (!await settlesWithin(this.closed, CLOSE_TIMEOUT_MS)) return;
|
|
105
|
+
// Bun does not expose the OS thread join; Windows needs this gap before
|
|
106
|
+
// files a terminated thread held can be removed.
|
|
107
|
+
const settleMs = storageWorkerOsJoinSettleMs();
|
|
108
|
+
if (settleMs > 0) await delay(settleMs);
|
|
109
|
+
this.exitedCleanly = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Messages between the indexer and its rebuild worker (#6990). Types only. */
|
|
2
|
+
|
|
3
|
+
import type { UsageLogRevision } from "../../usage/log";
|
|
4
|
+
|
|
5
|
+
export type RebuildWorkerRequest = {
|
|
6
|
+
type: "build";
|
|
7
|
+
indexPath: string;
|
|
8
|
+
buildPath: string;
|
|
9
|
+
reason: string;
|
|
10
|
+
revision: UsageLogRevision;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RebuildWorkerReply =
|
|
14
|
+
| { type: "built"; rows: number }
|
|
15
|
+
| { type: "error"; name: string; message: string };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rebuild worker for the request-history index (#6990).
|
|
3
|
+
*
|
|
4
|
+
* A full rebuild reads the whole ledger and commits every row; one rebuild of
|
|
5
|
+
* a 2.34 GB ledger held the serving event loop for 487 s. This thread does
|
|
6
|
+
* that work instead: it sweeps abandoned builds, creates the private build,
|
|
7
|
+
* ingests the ledger snapshot the refresh recorded, switches it to WAL and
|
|
8
|
+
* closes it. The serving thread then swaps the finished file in.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { buildIndexFile, removeAbandonedBuilds } from "./build-file";
|
|
12
|
+
import type { RebuildWorkerReply, RebuildWorkerRequest } from "./rebuild-protocol";
|
|
13
|
+
|
|
14
|
+
declare const self: Worker;
|
|
15
|
+
|
|
16
|
+
function handle(request: RebuildWorkerRequest): RebuildWorkerReply {
|
|
17
|
+
removeAbandonedBuilds(request.indexPath);
|
|
18
|
+
const rows = buildIndexFile(request.buildPath, request.reason, request.revision);
|
|
19
|
+
return { type: "built", rows };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
self.onmessage = (event: MessageEvent<RebuildWorkerRequest>) => {
|
|
23
|
+
let reply: RebuildWorkerReply;
|
|
24
|
+
try {
|
|
25
|
+
reply = handle(event.data);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
reply = {
|
|
28
|
+
type: "error",
|
|
29
|
+
name: error instanceof Error ? error.name : "Error",
|
|
30
|
+
message: error instanceof Error ? error.message : String(error),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
self.postMessage(reply);
|
|
34
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DataPlaneAdmission } from "./auth-cors";
|
|
2
2
|
import type { OcxApiKeyEntry, OcxConfig } from "../types";
|
|
3
|
+
import { markResponseNonReplayable } from "../lib/upstream-retry";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Per-admission-key model and provider scope.
|
|
@@ -155,10 +156,12 @@ export function admissionModelDeniedBody(error: AdmissionModelDeniedError): {
|
|
|
155
156
|
* indistinguishable from a typo in the model name.
|
|
156
157
|
*/
|
|
157
158
|
export function admissionModelDeniedResponse(error: AdmissionModelDeniedError): Response {
|
|
158
|
-
|
|
159
|
+
const response = new Response(JSON.stringify(admissionModelDeniedBody(error)), {
|
|
159
160
|
status: 403,
|
|
160
161
|
headers: { "Content-Type": "application/json" },
|
|
161
162
|
});
|
|
163
|
+
markResponseNonReplayable(response);
|
|
164
|
+
return response;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
/**
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a settled Chat or Messages route may take the Antigravity direct lane.
|
|
3
|
+
*
|
|
4
|
+
* On that lane the ingress hands the Responses core a body that carries only the request
|
|
5
|
+
* settings (model, stream, reasoning, limits, tool choice, tier) and decodes the conversation
|
|
6
|
+
* straight from the client body into the IR at the parse seam (`src/protocols/codecs/chat-ir.ts`,
|
|
7
|
+
* `messages-ir.ts`), so the Google adapter compiles from the caller's own messages. Everything
|
|
8
|
+
* that is decided from those settings keeps its current owner in the core: routing, account
|
|
9
|
+
* rotation, effort policy, usage and logging.
|
|
10
|
+
*
|
|
11
|
+
* PURE: reads the route, the body's structure and config. The ingresses and the protocol planner
|
|
12
|
+
* (`src/protocols/plan-snapshot.ts`) both ask here, so a preview and the real send cannot
|
|
13
|
+
* disagree about the lane.
|
|
14
|
+
*/
|
|
15
|
+
import { chatBodyCarriesImage } from "../chat/image-parts";
|
|
16
|
+
import { contentToText } from "../chat/inbound";
|
|
17
|
+
import { isClaudeWebSearchToolName } from "../claude/outbound";
|
|
18
|
+
import { isShadowSourceModel } from "../lib/shadow-call";
|
|
19
|
+
import type { ProtocolReasonCode } from "../protocols/contract";
|
|
20
|
+
import { resolveProtocolSettings } from "../protocols/settings";
|
|
21
|
+
import { effectiveGoogleMode } from "../providers/registry";
|
|
22
|
+
import type { RouteResult } from "../router";
|
|
23
|
+
import type { OcxConfig } from "../types";
|
|
24
|
+
import { requiresVisionPreprocessing } from "../vision";
|
|
25
|
+
|
|
26
|
+
/** Why the Antigravity direct lane declines a route, as a protocol reason code. */
|
|
27
|
+
export type AntigravityDirectDeclineReason = Extract<
|
|
28
|
+
ProtocolReasonCode,
|
|
29
|
+
| "rollout-disabled"
|
|
30
|
+
| "not-migrated"
|
|
31
|
+
| "combo-or-policy-route"
|
|
32
|
+
| "hosted-tool"
|
|
33
|
+
| "vision-preprocessing"
|
|
34
|
+
| "bridge-only-policy"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
type Rec = Record<string, unknown>;
|
|
38
|
+
function isRec(value: unknown): value is Rec {
|
|
39
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type RouteShape = Pick<RouteResult, "providerName" | "modelId" | "provider" | "combo" | "routeKind">;
|
|
43
|
+
|
|
44
|
+
/** A Google adapter route in Cloud Code Assist mode: the Antigravity wire. */
|
|
45
|
+
export function isAntigravityRoute(route: Pick<RouteResult, "providerName" | "provider">): boolean {
|
|
46
|
+
return effectiveGoogleMode(route.providerName, route.provider) === "cloud-code-assist";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A hosted web-search tool the bridge would hand to the web-search sidecar. The sidecar reads
|
|
51
|
+
* the hosted tool from the parsed Responses body, which this lane does not build.
|
|
52
|
+
*/
|
|
53
|
+
function declaresHostedWebSearch(body: Readonly<Rec>): boolean {
|
|
54
|
+
return Array.isArray(body.tools)
|
|
55
|
+
&& body.tools.some(tool => isRec(tool) && (tool.type === "web_search" || tool.type === "web_search_preview"));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The shadow-call intercept rewrites the model to another target (often a combo) before the
|
|
60
|
+
* core parses the body, so the request would leave the route this lane was decided for.
|
|
61
|
+
*/
|
|
62
|
+
function shadowInterceptMayApply(model: unknown, config: OcxConfig): boolean {
|
|
63
|
+
const intercept = config.shadowCallIntercept;
|
|
64
|
+
return intercept?.enabled === true && typeof intercept.model === "string" && intercept.model.length > 0
|
|
65
|
+
&& typeof model === "string" && isShadowSourceModel(model, intercept.sourceModels);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const SKILLS_CATALOG_MARKER = "<skills_instructions>";
|
|
69
|
+
const GENERATED_IMAGE_LINK = " and a generated-image
|
|
74
|
+
* link in assistant history (hosted display paths are redacted to the artifact URL). The lane's
|
|
75
|
+
* settings-only body has neither, so such a request keeps the bridge.
|
|
76
|
+
*/
|
|
77
|
+
function carriesBridgeRewrittenText(body: Readonly<Rec>): boolean {
|
|
78
|
+
if (!Array.isArray(body.messages)) return false;
|
|
79
|
+
return body.messages.some(message => {
|
|
80
|
+
if (!isRec(message)) return false;
|
|
81
|
+
if (message.role === "system" || message.role === "developer") {
|
|
82
|
+
return contentToText(message.content).includes(SKILLS_CATALOG_MARKER);
|
|
83
|
+
}
|
|
84
|
+
if (message.role === "assistant") return contentToText(message.content).includes(GENERATED_IMAGE_LINK);
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Request facts the body and config cannot carry, supplied by a caller that is about to send. */
|
|
90
|
+
export interface AntigravityDirectRuntime {
|
|
91
|
+
/**
|
|
92
|
+
* The cache diagnostic recorder is observing inbound bodies. It records the body the core
|
|
93
|
+
* receives, which on the lane has no conversation, so the request keeps the bridge while it
|
|
94
|
+
* observes. The planner never supplies this.
|
|
95
|
+
*/
|
|
96
|
+
cacheDiagnostics?: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The first rule that keeps a Chat request off the Antigravity direct lane, or `undefined` when
|
|
101
|
+
* the route is eligible.
|
|
102
|
+
*
|
|
103
|
+
* - the `protocols.rollout.antigravityDirect` switch is explicitly off (it defaults on);
|
|
104
|
+
* - the final adapter is not Google in Cloud Code Assist mode;
|
|
105
|
+
* - a combo or policy route owns multi-candidate execution in the Responses pipeline;
|
|
106
|
+
* - a hosted web-search tool would engage the sidecar, which reads the Responses body;
|
|
107
|
+
* - an image would reach a model the operator declared unable to read it (the vision sidecar
|
|
108
|
+
* rewrites images on the Responses body as well as the IR);
|
|
109
|
+
* - the shadow-call intercept could move the request to another target before parsing.
|
|
110
|
+
* - the core would rewrite conversation text on the bridge body (a skills catalog, a generated
|
|
111
|
+
* image link), or the cache diagnostic recorder is observing.
|
|
112
|
+
*/
|
|
113
|
+
export function antigravityDirectDeclineReason(
|
|
114
|
+
route: RouteShape,
|
|
115
|
+
body: Readonly<Rec>,
|
|
116
|
+
config: OcxConfig,
|
|
117
|
+
runtime: AntigravityDirectRuntime = {},
|
|
118
|
+
): AntigravityDirectDeclineReason | undefined {
|
|
119
|
+
const routeDecline = antigravityRouteDeclineReason(route, config);
|
|
120
|
+
if (routeDecline) return routeDecline;
|
|
121
|
+
if (declaresHostedWebSearch(body)) return "hosted-tool";
|
|
122
|
+
if (chatBodyCarriesImage(body) && requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName)) {
|
|
123
|
+
return "vision-preprocessing";
|
|
124
|
+
}
|
|
125
|
+
if (shadowInterceptMayApply(body.model, config)) return "bridge-only-policy";
|
|
126
|
+
if (carriesBridgeRewrittenText(body) || runtime.cacheDiagnostics === true) return "bridge-only-policy";
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The declines that read only the route and config, shared by both ingresses. */
|
|
131
|
+
function antigravityRouteDeclineReason(route: RouteShape, config: OcxConfig): AntigravityDirectDeclineReason | undefined {
|
|
132
|
+
if (!resolveProtocolSettings(config).rollout.antigravityDirect) return "rollout-disabled";
|
|
133
|
+
if (!isAntigravityRoute(route)) return "not-migrated";
|
|
134
|
+
if (route.combo !== undefined || route.routeKind === "combo" || route.routeKind === "policy") return "combo-or-policy-route";
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function anthropicBlocks(content: unknown): Rec[] {
|
|
139
|
+
return Array.isArray(content) ? content.filter(isRec) : [];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Text of an Anthropic `system` or message content: a string or its text blocks. */
|
|
143
|
+
function anthropicText(content: unknown): string {
|
|
144
|
+
if (typeof content === "string") return content;
|
|
145
|
+
return anthropicBlocks(content)
|
|
146
|
+
.map(block => (block.type === "text" && typeof block.text === "string" ? block.text : ""))
|
|
147
|
+
.join("\n");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A Messages hosted web-search tool (or a forced WebSearch choice) would engage the sidecar. */
|
|
151
|
+
function messagesDeclaresHostedWebSearch(body: Readonly<Rec>): boolean {
|
|
152
|
+
const tools = Array.isArray(body.tools)
|
|
153
|
+
&& body.tools.some(tool => isRec(tool) && typeof tool.type === "string" && tool.type.startsWith("web_search"));
|
|
154
|
+
const choice = body.tool_choice;
|
|
155
|
+
return tools || (isRec(choice) && choice.type === "tool" && typeof choice.name === "string"
|
|
156
|
+
&& isClaudeWebSearchToolName(choice.name));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** An image block in a user turn or inside a tool result. */
|
|
160
|
+
function messagesBodyCarriesImage(body: Readonly<Rec>): boolean {
|
|
161
|
+
if (!Array.isArray(body.messages)) return false;
|
|
162
|
+
return body.messages.some(message => isRec(message) && message.role === "user"
|
|
163
|
+
&& anthropicBlocks(message.content).some(block => block.type === "image"
|
|
164
|
+
|| (block.type === "tool_result" && anthropicBlocks(block.content).some(inner => inner.type === "image"))));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The Messages counterpart of `carriesBridgeRewrittenText`: system text and assistant history. */
|
|
168
|
+
function messagesCarryBridgeRewrittenText(body: Readonly<Rec>): boolean {
|
|
169
|
+
if (anthropicText(body.system).includes(SKILLS_CATALOG_MARKER)) return true;
|
|
170
|
+
if (!Array.isArray(body.messages)) return false;
|
|
171
|
+
return body.messages.some(message => {
|
|
172
|
+
if (!isRec(message)) return false;
|
|
173
|
+
if (message.role === "system") return anthropicText(message.content).includes(SKILLS_CATALOG_MARKER);
|
|
174
|
+
if (message.role === "assistant") return anthropicText(message.content).includes(GENERATED_IMAGE_LINK);
|
|
175
|
+
return false;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The first rule that keeps a Messages request off the Antigravity direct lane, or `undefined`
|
|
181
|
+
* when the route is eligible. `model` is the resolved model the bridge body would carry, which is
|
|
182
|
+
* what the shadow-call intercept matches.
|
|
183
|
+
*
|
|
184
|
+
* The route and config rules are the Chat ones. The body rules are the Messages spellings of the
|
|
185
|
+
* same hazards: a hosted web-search tool (`web_search_*`) or forced WebSearch choice, an image
|
|
186
|
+
* (in a user turn or a tool result) for a model the operator declared unable to read it, a
|
|
187
|
+
* shadow-call intercept, a skills catalog in the system text or a generated-image link in
|
|
188
|
+
* assistant history, or an observing cache diagnostic recorder. Blocked-skill elision, the
|
|
189
|
+
* `stabilizePromptCache` relocation and the ocxr1 envelopes are reproduced by the decoder and
|
|
190
|
+
* decline nothing.
|
|
191
|
+
*/
|
|
192
|
+
export function antigravityMessagesDeclineReason(
|
|
193
|
+
route: RouteShape,
|
|
194
|
+
body: Readonly<Rec>,
|
|
195
|
+
model: string,
|
|
196
|
+
config: OcxConfig,
|
|
197
|
+
runtime: AntigravityDirectRuntime = {},
|
|
198
|
+
): AntigravityDirectDeclineReason | undefined {
|
|
199
|
+
const routeDecline = antigravityRouteDeclineReason(route, config);
|
|
200
|
+
if (routeDecline) return routeDecline;
|
|
201
|
+
if (messagesDeclaresHostedWebSearch(body)) return "hosted-tool";
|
|
202
|
+
if (messagesBodyCarriesImage(body) && requiresVisionPreprocessing(config, route.provider, route.modelId, route.providerName)) {
|
|
203
|
+
return "vision-preprocessing";
|
|
204
|
+
}
|
|
205
|
+
if (shadowInterceptMayApply(model, config)) return "bridge-only-policy";
|
|
206
|
+
if (messagesCarryBridgeRewrittenText(body) || runtime.cacheDiagnostics === true) return "bridge-only-policy";
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -925,6 +925,8 @@ export function providerManagementConfigError(
|
|
|
925
925
|
if (raw.xaiResponsesXSearch !== undefined && typeof raw.xaiResponsesXSearch !== "boolean") {
|
|
926
926
|
return `provider ${name} xaiResponsesXSearch must be a boolean`;
|
|
927
927
|
}
|
|
928
|
+
const minMaxOutputError = positiveIntegerConfigError(raw.minMaxOutputTokens, "minMaxOutputTokens");
|
|
929
|
+
if (minMaxOutputError) return `provider ${name} ${minMaxOutputError}`;
|
|
928
930
|
const defaultMaxOutputError = positiveIntegerConfigError(raw.defaultMaxOutputTokens, "defaultMaxOutputTokens");
|
|
929
931
|
if (defaultMaxOutputError) return `provider ${name} ${defaultMaxOutputError}`;
|
|
930
932
|
const maxOutputError = positiveIntegerRecordConfigError(raw.modelMaxOutputTokens, "modelMaxOutputTokens");
|
|
@@ -1085,6 +1087,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1085
1087
|
newModelPolicy: "editor",
|
|
1086
1088
|
modelPreset: "editor",
|
|
1087
1089
|
contextWindow: "editor",
|
|
1090
|
+
inferContextWindow: "editor",
|
|
1088
1091
|
modelContextWindows: "editor",
|
|
1089
1092
|
modelContextTiers: "editor",
|
|
1090
1093
|
modelInputModalities: "editor",
|
|
@@ -1092,6 +1095,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1092
1095
|
modelMaxInputTokens: "runtime",
|
|
1093
1096
|
modelAutoCompactTokenLimits: "editor",
|
|
1094
1097
|
defaultMaxOutputTokens: "editor",
|
|
1098
|
+
minMaxOutputTokens: "editor",
|
|
1095
1099
|
modelMaxOutputTokens: "editor",
|
|
1096
1100
|
modelCosts: "editor",
|
|
1097
1101
|
headers: "redacted",
|