@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -10,8 +10,9 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
|
10
10
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
11
11
|
import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
12
12
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
13
|
-
import { AnthropicRequestError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
14
|
-
import { resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
13
|
+
import { AnthropicRequestError, DesktopModelMappingUnavailableError, anthropicToResponsesTranslation, extractOcxEffortDirective, extractOcxRouteDirective, resolveInboundModel, type ClaudeCacheKeySource } from "../claude/inbound";
|
|
14
|
+
import { isKnownDesktop3pModelId, resolveDesktop3pAlias } from "../claude/desktop-3p";
|
|
15
|
+
import { resolveAlias, claudeCodeNativeAlias } from "../claude/alias";
|
|
15
16
|
import { recordDesktopRequest } from "../claude/desktop-health";
|
|
16
17
|
import { stripOneMillionMarker } from "../claude/context-windows";
|
|
17
18
|
import { captureClaudeInbound } from "../claude/inbound-debug";
|
|
@@ -72,17 +73,37 @@ type Rec = Record<string, unknown>;
|
|
|
72
73
|
* resolve a synthetic one.
|
|
73
74
|
*/
|
|
74
75
|
function decodeClaudeFastSelector(raw: string, cc?: OcxConfig["claudeCode"]): string {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
const model = stripOneMillionMarker(raw);
|
|
77
|
+
const exact = resolveInboundModel(model, cc);
|
|
78
|
+
if (!model.endsWith("--fast")) return exact;
|
|
79
|
+
const fullMapping = cc?.modelMap?.[model];
|
|
80
|
+
if (resolveAlias(model) || isKnownDesktop3pModelId(model)
|
|
81
|
+
|| (typeof fullMapping === "string" && fullMapping.length > 0)) return exact;
|
|
82
|
+
const bare = model.slice(0, -"--fast".length);
|
|
83
|
+
// A classifier fallback is not an exact match for a registered Desktop base.
|
|
84
|
+
// Preserve established non-Desktop fallback behavior while decoding that base first.
|
|
85
|
+
if (exact !== model && !resolveDesktop3pAlias(bare)) return exact;
|
|
78
86
|
const decodedBase = resolveInboundModel(bare, cc);
|
|
79
87
|
return decodedBase === bare ? exact : `${decodedBase}--fast`;
|
|
80
88
|
}
|
|
81
89
|
|
|
90
|
+
/** Restore the reversible Fable picker alias before Anthropic passthrough checks. */
|
|
91
|
+
function decodeFablePickerAlias(raw: string, cc?: OcxConfig["claudeCode"]): string {
|
|
92
|
+
const decoded = resolveInboundModel(raw, cc);
|
|
93
|
+
if (!decoded.startsWith("claude-fable-")) return raw;
|
|
94
|
+
return claudeCodeNativeAlias(decoded) === raw ? decoded : raw;
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
function isRec(v: unknown): v is Rec {
|
|
83
98
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
84
99
|
}
|
|
85
100
|
|
|
101
|
+
function desktopMappingUnavailableResponse(error: DesktopModelMappingUnavailableError): Response {
|
|
102
|
+
const response = anthropicErrorResponse(503, error.message, "api_error", "desktop_model_mapping_unavailable");
|
|
103
|
+
response.headers.set("Retry-After", "1");
|
|
104
|
+
return response;
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
/** Resolve Claude-only sidecar overrides without mutating the shared server config. */
|
|
87
108
|
export function buildClaudeReplayConfig(config: OcxConfig): OcxConfig {
|
|
88
109
|
return {
|
|
@@ -649,6 +670,9 @@ async function handleClaudeMessagesWithBudget(
|
|
|
649
670
|
effortOverride = extractOcxEffortDirective(anthropicBody);
|
|
650
671
|
}
|
|
651
672
|
}
|
|
673
|
+
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
674
|
+
anthropicBody.model = decodeFablePickerAlias(anthropicBody.model, config.claudeCode);
|
|
675
|
+
}
|
|
652
676
|
if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
|
|
653
677
|
requestedModel = anthropicBody.model;
|
|
654
678
|
// Decode for Fast only. A Claude alias is `claude-ocx-<provider>--<model>`, so it
|
|
@@ -712,8 +736,10 @@ async function handleClaudeMessagesWithBudget(
|
|
|
712
736
|
cacheKeySource = translation.cacheKeySource;
|
|
713
737
|
} catch (err) {
|
|
714
738
|
const overflow = isTranslatorBudgetExceededError(err);
|
|
715
|
-
const
|
|
739
|
+
const unavailable = err instanceof DesktopModelMappingUnavailableError;
|
|
740
|
+
const status = overflow ? 413 : unavailable ? 503 : err instanceof AnthropicRequestError ? 400 : 500;
|
|
716
741
|
if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" });
|
|
742
|
+
if (unavailable) return desktopMappingUnavailableResponse(err);
|
|
717
743
|
return anthropicErrorResponse(
|
|
718
744
|
status,
|
|
719
745
|
overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err),
|
|
@@ -1056,6 +1082,7 @@ export async function handleClaudeCountTokens(
|
|
|
1056
1082
|
try {
|
|
1057
1083
|
body = await readAnthropicBody(req, translatorBudget);
|
|
1058
1084
|
} catch (err) {
|
|
1085
|
+
if (err instanceof DesktopModelMappingUnavailableError) return desktopMappingUnavailableResponse(err);
|
|
1059
1086
|
if (err instanceof AnthropicRequestError) return anthropicErrorResponse(400, err.message);
|
|
1060
1087
|
return anthropicErrorResponse(500, err instanceof Error ? err.message : String(err));
|
|
1061
1088
|
} finally { translatorBudget.dispose(); }
|
|
@@ -1066,36 +1093,44 @@ export async function handleClaudeCountTokens(
|
|
|
1066
1093
|
if (typeof raw.model !== "string" || raw.model.length === 0) {
|
|
1067
1094
|
return anthropicErrorResponse(400, "model is required");
|
|
1068
1095
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
model
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
// synthetic id reaches native passthrough as a model Anthropic has never heard of.
|
|
1085
|
-
const countFastRow = parseFastOnlyRowId(
|
|
1086
|
-
config, () => decodeClaudeFastSelector(model, config.claudeCode),
|
|
1087
|
-
);
|
|
1088
|
-
if (countFastRow) {
|
|
1089
|
-
model = countFastRow.baseId;
|
|
1096
|
+
try {
|
|
1097
|
+
let model = raw.model;
|
|
1098
|
+
// Case-insensitive [1m] strip (audit 021 #7 — the CLI matches /\[1m\]/i).
|
|
1099
|
+
const stripped = stripOneMillionMarker(model);
|
|
1100
|
+
if (stripped !== model) {
|
|
1101
|
+
model = stripped;
|
|
1102
|
+
raw.model = model;
|
|
1103
|
+
}
|
|
1104
|
+
// ocx-route override (devlog 072): keep count_tokens consistent with messages.
|
|
1105
|
+
const countRoute = extractOcxRouteDirective(raw);
|
|
1106
|
+
if (countRoute) {
|
|
1107
|
+
model = stripOneMillionMarker(countRoute);
|
|
1108
|
+
raw.model = model;
|
|
1109
|
+
}
|
|
1110
|
+
model = decodeFablePickerAlias(model, config.claudeCode);
|
|
1090
1111
|
raw.model = model;
|
|
1112
|
+
// Fast-only: count_tokens never parsed an effort row, so it must not start. It returns a
|
|
1113
|
+
// token estimate and sends no tier, so only the IDENTITY is corrected - without this the
|
|
1114
|
+
// synthetic id reaches native passthrough as a model Anthropic has never heard of.
|
|
1115
|
+
const countFastRow = parseFastOnlyRowId(
|
|
1116
|
+
config, () => decodeClaudeFastSelector(model, config.claudeCode),
|
|
1117
|
+
);
|
|
1118
|
+
if (countFastRow) {
|
|
1119
|
+
model = countFastRow.baseId;
|
|
1120
|
+
raw.model = model;
|
|
1121
|
+
}
|
|
1122
|
+
captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
|
|
1123
|
+
if (wantsNativePassthrough(req, config, requestPolicy, model)) {
|
|
1124
|
+
return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
|
|
1125
|
+
}
|
|
1126
|
+
const inputTokens = estimateClaudeRequestTokens(raw, model);
|
|
1127
|
+
return new Response(JSON.stringify({ input_tokens: inputTokens }), {
|
|
1128
|
+
status: 200,
|
|
1129
|
+
headers: { "Content-Type": "application/json" },
|
|
1130
|
+
});
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
if (error instanceof DesktopModelMappingUnavailableError) return desktopMappingUnavailableResponse(error);
|
|
1133
|
+
if (error instanceof AnthropicRequestError) return anthropicErrorResponse(400, error.message);
|
|
1134
|
+
throw error;
|
|
1091
1135
|
}
|
|
1092
|
-
captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
|
|
1093
|
-
if (wantsNativePassthrough(req, config, requestPolicy, model)) {
|
|
1094
|
-
return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
|
|
1095
|
-
}
|
|
1096
|
-
const inputTokens = estimateClaudeRequestTokens(raw, model);
|
|
1097
|
-
return new Response(JSON.stringify({ input_tokens: inputTokens }), {
|
|
1098
|
-
status: 200,
|
|
1099
|
-
headers: { "Content-Type": "application/json" },
|
|
1100
|
-
});
|
|
1101
1136
|
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/** Strict terminal reconstruction selected by the Grok compatibility marker. */
|
|
2
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
3
|
+
import { MAX_COMPLETED_OUTPUT_ITEMS, MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES } from "./relay";
|
|
4
|
+
import { sseDataPayload, type SseBlockRewrite } from "./sse-payload-rewrite";
|
|
5
|
+
import { isPlainObject, jsonBlock, type RetainedOutputItem } from "./responses-snapshot-codec";
|
|
6
|
+
|
|
7
|
+
type SparseTerminalOpenItem = {
|
|
8
|
+
type: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
sourceBytes: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type SparseTerminalCompletedItem = RetainedOutputItem & {
|
|
14
|
+
visibleToGrok: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MAX_GROK_OPEN_ITEM_IDENTITY_BYTES = MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES;
|
|
18
|
+
|
|
19
|
+
const GROK_TERMINAL_OUTPUT_ITEM_TYPES = new Set([
|
|
20
|
+
"message",
|
|
21
|
+
"reasoning",
|
|
22
|
+
"function_call",
|
|
23
|
+
"custom_tool_call",
|
|
24
|
+
"web_search_call",
|
|
25
|
+
"code_interpreter_call",
|
|
26
|
+
"mcp_call",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function hasValidOptionalId(item: Record<string, unknown>): boolean {
|
|
30
|
+
return !("id" in item)
|
|
31
|
+
|| (typeof item.id === "string" && item.id.trim().length > 0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasCompletedStatusWhenPresent(item: Record<string, unknown>): boolean {
|
|
35
|
+
return !("status" in item) || item.status === "completed";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isNullableString(value: unknown): boolean {
|
|
39
|
+
return value === null || typeof value === "string";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isValidOutputMessagePart(part: unknown): boolean {
|
|
43
|
+
if (!isPlainObject(part)) return false;
|
|
44
|
+
if (part.type === "output_text") {
|
|
45
|
+
return typeof part.text === "string"
|
|
46
|
+
&& (!("annotations" in part) || Array.isArray(part.annotations))
|
|
47
|
+
&& (!("logprobs" in part) || part.logprobs === null || Array.isArray(part.logprobs));
|
|
48
|
+
}
|
|
49
|
+
return part.type === "refusal" && typeof part.refusal === "string";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isValidReasoningPart(part: unknown, type: "summary_text" | "reasoning_text"): boolean {
|
|
53
|
+
return isPlainObject(part) && part.type === type && typeof part.text === "string";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isValidWebSearchAction(value: unknown): boolean {
|
|
57
|
+
if (!isPlainObject(value)) return false;
|
|
58
|
+
if (value.type === "search") {
|
|
59
|
+
return typeof value.query === "string"
|
|
60
|
+
&& (!("sources" in value) || value.sources === null || (Array.isArray(value.sources)
|
|
61
|
+
&& value.sources.every(source => isPlainObject(source)
|
|
62
|
+
&& typeof source.type === "string" && typeof source.url === "string")));
|
|
63
|
+
}
|
|
64
|
+
if (value.type === "open_page") {
|
|
65
|
+
return !("url" in value) || isNullableString(value.url);
|
|
66
|
+
}
|
|
67
|
+
if (value.type === "find" || value.type === "find_in_page") {
|
|
68
|
+
return typeof value.url === "string" && typeof value.pattern === "string";
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isValidCodeInterpreterOutput(value: unknown): boolean {
|
|
74
|
+
return isPlainObject(value)
|
|
75
|
+
&& ((value.type === "logs" && typeof value.logs === "string")
|
|
76
|
+
|| (value.type === "image" && typeof value.url === "string"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validate the pre-field-backfill item carried by a real output_item.done.
|
|
81
|
+
* Missing ids, message status, and output-text annotations are allowed because
|
|
82
|
+
* the always-on field backfill safely supplies only those schema defaults.
|
|
83
|
+
* Contradictory values and semantic content repairs are never accepted as
|
|
84
|
+
* proof that an empty terminal snapshot was sparse.
|
|
85
|
+
*/
|
|
86
|
+
function trustedGrokCompletedItem(
|
|
87
|
+
item: Record<string, unknown>,
|
|
88
|
+
): { visibleToGrok: boolean } | null {
|
|
89
|
+
if (!hasValidOptionalId(item) || !hasCompletedStatusWhenPresent(item)) return null;
|
|
90
|
+
|
|
91
|
+
if (item.type === "message") {
|
|
92
|
+
if (item.role !== "assistant" || !Array.isArray(item.content)) return null;
|
|
93
|
+
if (!(item.content as unknown[]).every(isValidOutputMessagePart)) return null;
|
|
94
|
+
if ("phase" in item && item.phase !== "commentary" && item.phase !== "final_answer") return null;
|
|
95
|
+
return {
|
|
96
|
+
// grok-build currently turns only output_text parts into final Assistant
|
|
97
|
+
// content; refusal parts do not satisfy its visible-content gate.
|
|
98
|
+
visibleToGrok: item.content.some(part => isPlainObject(part)
|
|
99
|
+
&& part.type === "output_text" && typeof part.text === "string" && part.text.length > 0),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (item.type === "reasoning") {
|
|
104
|
+
if (!Array.isArray(item.summary)
|
|
105
|
+
|| !item.summary.every(part => isValidReasoningPart(part, "summary_text"))) return null;
|
|
106
|
+
if ("content" in item && item.content !== null
|
|
107
|
+
&& (!Array.isArray(item.content)
|
|
108
|
+
|| !item.content.every(part => isValidReasoningPart(part, "reasoning_text")))) return null;
|
|
109
|
+
if ("encrypted_content" in item && !isNullableString(item.encrypted_content)) return null;
|
|
110
|
+
return { visibleToGrok: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (item.type === "function_call") {
|
|
114
|
+
if (typeof item.call_id !== "string" || item.call_id.trim().length === 0
|
|
115
|
+
|| typeof item.name !== "string" || item.name.trim().length === 0
|
|
116
|
+
|| typeof item.arguments !== "string") return null;
|
|
117
|
+
return { visibleToGrok: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (item.type === "custom_tool_call") {
|
|
121
|
+
if (typeof item.call_id !== "string" || item.call_id.trim().length === 0
|
|
122
|
+
|| typeof item.name !== "string" || item.name.trim().length === 0
|
|
123
|
+
|| typeof item.input !== "string") return null;
|
|
124
|
+
return { visibleToGrok: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (item.type === "web_search_call") {
|
|
128
|
+
if (item.status !== "completed" || !isValidWebSearchAction(item.action)) return null;
|
|
129
|
+
return { visibleToGrok: false };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (item.type === "code_interpreter_call") {
|
|
133
|
+
if (item.status !== "completed"
|
|
134
|
+
|| typeof item.container_id !== "string" || item.container_id.trim().length === 0
|
|
135
|
+
|| ("code" in item && !isNullableString(item.code))
|
|
136
|
+
|| ("outputs" in item && item.outputs !== null
|
|
137
|
+
&& (!Array.isArray(item.outputs) || !item.outputs.every(isValidCodeInterpreterOutput)))) return null;
|
|
138
|
+
return { visibleToGrok: false };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (item.type === "mcp_call") {
|
|
142
|
+
if (typeof item.arguments !== "string"
|
|
143
|
+
|| typeof item.name !== "string" || item.name.trim().length === 0
|
|
144
|
+
|| typeof item.server_label !== "string" || item.server_label.trim().length === 0
|
|
145
|
+
|| ("approval_request_id" in item && !isNullableString(item.approval_request_id))
|
|
146
|
+
|| ("error" in item && !isNullableString(item.error))
|
|
147
|
+
|| ("output" in item && !isNullableString(item.output))) return null;
|
|
148
|
+
return { visibleToGrok: false };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function plausibleGrokOpenItem(
|
|
155
|
+
item: Record<string, unknown>,
|
|
156
|
+
): Omit<SparseTerminalOpenItem, "sourceBytes"> | null {
|
|
157
|
+
const type = typeof item.type === "string" ? item.type : "";
|
|
158
|
+
if (!GROK_TERMINAL_OUTPUT_ITEM_TYPES.has(type) || !hasValidOptionalId(item)) return null;
|
|
159
|
+
if ("status" in item && item.status !== "in_progress") return null;
|
|
160
|
+
if (type === "message") {
|
|
161
|
+
if ("role" in item && item.role !== "assistant") return null;
|
|
162
|
+
if ("content" in item && !Array.isArray(item.content)) return null;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
type,
|
|
166
|
+
...(typeof item.id === "string" ? { id: item.id } : {}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Narrow client repair for grok-build's Responses consumer.
|
|
172
|
+
*
|
|
173
|
+
* grok-build streams text deltas but builds its durable Assistant item only
|
|
174
|
+
* from response.completed.response.output. Some native Responses streams put
|
|
175
|
+
* the durable items in output_item.done and finish with a missing or explicit
|
|
176
|
+
* empty output. Reconstruct only from real, unique, contiguous, bounded done
|
|
177
|
+
* events whose raw semantics are already valid. Any ambiguity stays byte-level
|
|
178
|
+
* fail-closed; the provider-opt-in snapshot repair above is unchanged.
|
|
179
|
+
*/
|
|
180
|
+
export function createGrokResponsesSparseTerminalBlockRewrite(
|
|
181
|
+
budget?: TranslatorBudget,
|
|
182
|
+
): SseBlockRewrite {
|
|
183
|
+
const openItems = new Map<number, SparseTerminalOpenItem>();
|
|
184
|
+
const completedItems = new Map<number, SparseTerminalCompletedItem>();
|
|
185
|
+
let aggregateItemBytes = 0;
|
|
186
|
+
let aggregateOpenItemBytes = 0;
|
|
187
|
+
let tainted = false;
|
|
188
|
+
let hasVisibleOutput = false;
|
|
189
|
+
|
|
190
|
+
const clearRetained = (): void => {
|
|
191
|
+
const retainedBytes = aggregateItemBytes + aggregateOpenItemBytes;
|
|
192
|
+
if (retainedBytes > 0) {
|
|
193
|
+
budget?.releaseRetained(retainedBytes, { kind: "retained_collectors" });
|
|
194
|
+
}
|
|
195
|
+
openItems.clear();
|
|
196
|
+
completedItems.clear();
|
|
197
|
+
aggregateItemBytes = 0;
|
|
198
|
+
aggregateOpenItemBytes = 0;
|
|
199
|
+
hasVisibleOutput = false;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const reset = (): void => {
|
|
203
|
+
clearRetained();
|
|
204
|
+
tainted = false;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const taintAndRelease = (): void => {
|
|
208
|
+
clearRetained();
|
|
209
|
+
tainted = true;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const retainCompletedItem = (
|
|
213
|
+
index: number,
|
|
214
|
+
item: Record<string, unknown>,
|
|
215
|
+
visibleToGrok: boolean,
|
|
216
|
+
): void => {
|
|
217
|
+
if (tainted) return;
|
|
218
|
+
const sourceBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
|
|
219
|
+
if (sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES
|
|
220
|
+
|| completedItems.size >= MAX_COMPLETED_OUTPUT_ITEMS
|
|
221
|
+
|| aggregateItemBytes + sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES) {
|
|
222
|
+
taintAndRelease();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
budget?.chargeRetained(sourceBytes, { kind: "retained_collectors" });
|
|
226
|
+
completedItems.set(index, { item, sourceBytes, visibleToGrok });
|
|
227
|
+
aggregateItemBytes += sourceBytes;
|
|
228
|
+
hasVisibleOutput = hasVisibleOutput || visibleToGrok;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const closeOpenItem = (index: number): void => {
|
|
232
|
+
const open = openItems.get(index);
|
|
233
|
+
if (!open) return;
|
|
234
|
+
openItems.delete(index);
|
|
235
|
+
aggregateOpenItemBytes -= open.sourceBytes;
|
|
236
|
+
budget?.releaseRetained(open.sourceBytes, { kind: "retained_collectors" });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const rewrite: SseBlockRewrite = (block: string): readonly string[] => {
|
|
240
|
+
const payload = sseDataPayload(block);
|
|
241
|
+
if (payload === null) return [block];
|
|
242
|
+
if (payload === "[DONE]") {
|
|
243
|
+
reset();
|
|
244
|
+
return [block];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let parsed: unknown;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(payload);
|
|
250
|
+
} catch {
|
|
251
|
+
taintAndRelease();
|
|
252
|
+
return [block];
|
|
253
|
+
}
|
|
254
|
+
if (!isPlainObject(parsed) || typeof parsed.type !== "string") {
|
|
255
|
+
taintAndRelease();
|
|
256
|
+
return [block];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const type = parsed.type;
|
|
260
|
+
const outputIndex = Number.isInteger(parsed.output_index) && (parsed.output_index as number) >= 0
|
|
261
|
+
? parsed.output_index as number
|
|
262
|
+
: undefined;
|
|
263
|
+
|
|
264
|
+
if (type === "response.output_item.added") {
|
|
265
|
+
const open = isPlainObject(parsed.item) ? plausibleGrokOpenItem(parsed.item) : null;
|
|
266
|
+
if (outputIndex === undefined || !open
|
|
267
|
+
|| openItems.has(outputIndex) || completedItems.has(outputIndex)
|
|
268
|
+
|| openItems.size >= MAX_COMPLETED_OUTPUT_ITEMS) {
|
|
269
|
+
taintAndRelease();
|
|
270
|
+
} else if (!tainted) {
|
|
271
|
+
const sourceBytes = Buffer.byteLength(JSON.stringify(open), "utf8");
|
|
272
|
+
if (sourceBytes > MAX_COMPLETED_OUTPUT_ITEM_SOURCE_BYTES
|
|
273
|
+
|| aggregateOpenItemBytes + sourceBytes > MAX_GROK_OPEN_ITEM_IDENTITY_BYTES) {
|
|
274
|
+
taintAndRelease();
|
|
275
|
+
} else {
|
|
276
|
+
budget?.chargeRetained(sourceBytes, { kind: "retained_collectors" });
|
|
277
|
+
openItems.set(outputIndex, { ...open, sourceBytes });
|
|
278
|
+
aggregateOpenItemBytes += sourceBytes;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return [block];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (type === "response.output_item.done") {
|
|
285
|
+
const item = isPlainObject(parsed.item) ? parsed.item : null;
|
|
286
|
+
const proof = item ? trustedGrokCompletedItem(item) : null;
|
|
287
|
+
if (outputIndex === undefined || !proof || completedItems.has(outputIndex)) {
|
|
288
|
+
taintAndRelease();
|
|
289
|
+
return [block];
|
|
290
|
+
}
|
|
291
|
+
const open = openItems.get(outputIndex);
|
|
292
|
+
const doneId = typeof item!.id === "string" ? item!.id : undefined;
|
|
293
|
+
if (open && (open.type !== item!.type || open.id !== doneId)) {
|
|
294
|
+
taintAndRelease();
|
|
295
|
+
return [block];
|
|
296
|
+
}
|
|
297
|
+
closeOpenItem(outputIndex);
|
|
298
|
+
retainCompletedItem(outputIndex, item!, proof.visibleToGrok);
|
|
299
|
+
return [block];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const isTerminal = type === "response.completed"
|
|
303
|
+
|| type === "response.failed"
|
|
304
|
+
|| type === "response.incomplete";
|
|
305
|
+
if (!isTerminal) return [block];
|
|
306
|
+
|
|
307
|
+
let out = block;
|
|
308
|
+
if (type === "response.completed" && !tainted && isPlainObject(parsed.response)) {
|
|
309
|
+
const response = parsed.response;
|
|
310
|
+
const output = response.output;
|
|
311
|
+
const terminalStatusConsistent = !("status" in response) || response.status === "completed";
|
|
312
|
+
const outputIsAuthoritative = Array.isArray(output) && output.length > 0;
|
|
313
|
+
const outputIsSparse = !("output" in response)
|
|
314
|
+
|| (Array.isArray(output) && output.length === 0);
|
|
315
|
+
if (!outputIsAuthoritative && outputIsSparse && terminalStatusConsistent
|
|
316
|
+
&& completedItems.size > 0 && openItems.size === 0 && hasVisibleOutput) {
|
|
317
|
+
const ordered = [...completedItems.entries()].sort(([left], [right]) => left - right);
|
|
318
|
+
if (ordered.every(([index], position) => index === position)) {
|
|
319
|
+
out = jsonBlock({
|
|
320
|
+
...parsed,
|
|
321
|
+
response: { ...response, output: ordered.map(([, retained]) => retained.item) },
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
reset();
|
|
327
|
+
return [out];
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
rewrite.dispose = reset;
|
|
331
|
+
return rewrite;
|
|
332
|
+
}
|
|
333
|
+
|
package/src/server/index.ts
CHANGED
|
@@ -183,7 +183,8 @@ export { disableResponsesRequestTimeout, linkAbortSignal } from "./responses";
|
|
|
183
183
|
import { handleClaudeCountTokens, handleClaudeMessages } from "./claude-messages";
|
|
184
184
|
import { handleChatCompletions } from "./chat-completions";
|
|
185
185
|
import { anthropicErrorResponse } from "../claude/outbound";
|
|
186
|
-
import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
186
|
+
import { buildDesktop3pRegistry, generateDesktop3pModels } from "../claude/desktop-3p";
|
|
187
|
+
import { buildDesktopDiscoveryInputs } from "../claude/desktop-discovery-inputs";
|
|
187
188
|
import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
188
189
|
import {
|
|
189
190
|
bindNativeMainStartupLifecycle,
|
|
@@ -1358,6 +1359,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1358
1359
|
if (!isAllowedRequestOrigin(req, policy)) {
|
|
1359
1360
|
return withCors(formatErrorResponse(403, "origin_rejected", "cross-origin data-plane request blocked"), req, policy);
|
|
1360
1361
|
}
|
|
1362
|
+
const wantsDesktopConfig = url.searchParams.get("format") === "desktop-config";
|
|
1363
|
+
if (wantsDesktopConfig && (url.searchParams.get("ids") === "cli" || url.searchParams.has("client_version"))) {
|
|
1364
|
+
return jsonResponse({ error: "Desktop config format cannot use CLI or client-version selectors" }, 400, req, policy);
|
|
1365
|
+
}
|
|
1361
1366
|
// The Integrations page reports whether a Cursor client has reached this proxy; the
|
|
1362
1367
|
// recorder keeps only a bounded User-Agent value and a timestamp, in memory.
|
|
1363
1368
|
recordCursorSeen(req.headers);
|
|
@@ -1380,7 +1385,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1380
1385
|
}
|
|
1381
1386
|
throw error;
|
|
1382
1387
|
}
|
|
1383
|
-
const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxOutputTokens, nativeOpenAiContextTier, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort,
|
|
1388
|
+
const { accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, buildCatalogEntries, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, exactComboCatalogSlugs, loadCatalogTemplate, NATIVE_OPENAI_MODELS, nativeContextLimits, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxOutputTokens, nativeOpenAiContextTier, nativeOpenAiSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, uniqueCatalogModelsForRawPublicList, visibleCodexAccountSelectors, visibleNativeSlugs, desktopVisibleNativeSlugs } = await import("../codex/catalog");
|
|
1384
1389
|
const { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } = await import("../codex/catalog/native-models");
|
|
1385
1390
|
const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
|
|
1386
1391
|
const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
|
|
@@ -1430,11 +1435,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1430
1435
|
const accountNativeSlugs = [...new Set(
|
|
1431
1436
|
[...accountNativeSlugsBySelector.values()].flatMap(slugs => [...slugs]),
|
|
1432
1437
|
)];
|
|
1433
|
-
const
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
const
|
|
1438
|
+
const desktopInputs = buildDesktopDiscoveryInputs({
|
|
1439
|
+
config, models: goModels, modelEntitlements,
|
|
1440
|
+
desktopNativeCandidates: desktopVisibleNativeSlugs(config),
|
|
1441
|
+
});
|
|
1442
|
+
const desktopNativeSlugs = desktopInputs.nativeSlugs;
|
|
1443
|
+
const goOrdered = desktopInputs.routedModels;
|
|
1438
1444
|
// Claude Code / Claude Desktop gateway model discovery (GET /v1/models with
|
|
1439
1445
|
// Anthropic-style headers; 003 G1-G8 + devlog 131). Entries use the official
|
|
1440
1446
|
// ModelInfo shape incl. capabilities (effort ladder / thinking) — Desktop 3P can
|
|
@@ -1443,7 +1449,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1443
1449
|
// aliases; legacy claude-ocx-* ids keep decoding via resolveAlias. Detection:
|
|
1444
1450
|
// anthropic-version header (Claude Code sends it) or explicit ?flavor=anthropic.
|
|
1445
1451
|
// Codex catalog (client_version) and the OpenAI list shape below stay byte-identical.
|
|
1446
|
-
const wantsAnthropicList = req.headers.get("anthropic-version") !== null
|
|
1452
|
+
const wantsAnthropicList = wantsDesktopConfig || req.headers.get("anthropic-version") !== null
|
|
1447
1453
|
|| url.searchParams.get("flavor") === "anthropic";
|
|
1448
1454
|
/**
|
|
1449
1455
|
* Whether a NATIVE slug may carry a Fast sibling.
|
|
@@ -1477,12 +1483,22 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1477
1483
|
catalogFastRowEligible(config, m);
|
|
1478
1484
|
|
|
1479
1485
|
if (wantsAnthropicList && !url.searchParams.has("client_version")) {
|
|
1486
|
+
if (wantsDesktopConfig) {
|
|
1487
|
+
const models = config.claudeCode?.enabled === false ? [] : generateDesktop3pModels(
|
|
1488
|
+
desktopInputs.nativeSlugs, desktopInputs.routedModels,
|
|
1489
|
+
config.claudeCode?.desktopProfile, desktopInputs.nativeContextCap,
|
|
1490
|
+
);
|
|
1491
|
+
const response = jsonResponse({ version: 1, models }, 200, req, policy);
|
|
1492
|
+
response.headers.set("Cache-Control", "no-store");
|
|
1493
|
+
return response;
|
|
1494
|
+
}
|
|
1480
1495
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
1481
1496
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
1482
1497
|
buildDesktop3pRegistry(
|
|
1483
1498
|
desktopNativeSlugs,
|
|
1484
|
-
|
|
1499
|
+
desktopInputs.routedModels,
|
|
1485
1500
|
config.claudeCode?.desktopProfile,
|
|
1501
|
+
desktopInputs.nativeContextCap,
|
|
1486
1502
|
);
|
|
1487
1503
|
const { buildAnthropicModelInfos } = await import("../claude/model-info");
|
|
1488
1504
|
const { resolveAutoContext } = await import("../claude/context-windows");
|
|
@@ -1503,7 +1519,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1503
1519
|
resolveAutoContext(config.claudeCode),
|
|
1504
1520
|
idStyle,
|
|
1505
1521
|
activeDesktop3pAlias,
|
|
1506
|
-
|
|
1522
|
+
desktopInputs.nativeContextCap,
|
|
1507
1523
|
config.fastMode,
|
|
1508
1524
|
// Explicit opt-out omits the Fast predicate.
|
|
1509
1525
|
config.fastRows !== false
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { currentAccountSelectionRevision, subscribeAccountSelections } from "../../lib/account-selection-events";
|
|
2
|
+
import { registerOptionalShutdownHook } from "../../lib/optional-shutdown-hooks";
|
|
3
|
+
|
|
4
|
+
const MAX_SELECTION_STREAMS = 64;
|
|
5
|
+
const HEARTBEAT_MS = 15_000;
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const connections = new Set<() => void>();
|
|
8
|
+
|
|
9
|
+
/** The management boundary admits the request; every frame revalidates current authority. */
|
|
10
|
+
export function accountSelectionStream(request: Request, validate: () => boolean): Response {
|
|
11
|
+
const authorized = () => {
|
|
12
|
+
try { return validate() === true; } catch { return false; }
|
|
13
|
+
};
|
|
14
|
+
if (!authorized()) return Response.json({ error: "Management session is no longer authorized" }, { status: 401 });
|
|
15
|
+
if (connections.size >= MAX_SELECTION_STREAMS) {
|
|
16
|
+
return Response.json({ error: "Too many account selection streams" }, {
|
|
17
|
+
status: 429, headers: { "Retry-After": "15" },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
let cleanup = () => {};
|
|
21
|
+
const body = new ReadableStream<Uint8Array>({
|
|
22
|
+
start(controller) {
|
|
23
|
+
let closed = false;
|
|
24
|
+
let unsubscribe = () => {};
|
|
25
|
+
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
26
|
+
const close = () => {
|
|
27
|
+
if (closed) return;
|
|
28
|
+
closed = true;
|
|
29
|
+
unsubscribe();
|
|
30
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
31
|
+
request.signal.removeEventListener("abort", close);
|
|
32
|
+
connections.delete(close);
|
|
33
|
+
try { controller.close(); } catch { /* The consumer may already have cancelled. */ }
|
|
34
|
+
};
|
|
35
|
+
cleanup = close;
|
|
36
|
+
const send = (frame: string) => {
|
|
37
|
+
if (closed) return;
|
|
38
|
+
if (!authorized()) {
|
|
39
|
+
// Error clears queued frames as well, so a revoked consumer cannot drain them.
|
|
40
|
+
try { controller.error(new DOMException("Management session is no longer authorized", "NotAllowedError")); }
|
|
41
|
+
finally { close(); }
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
// Reconnection sends a ready event, so a slow reader can reconcile without an
|
|
45
|
+
// unbounded queue or silently dropping a provider's latest invalidation.
|
|
46
|
+
if (controller.desiredSize !== null && controller.desiredSize <= 0) { close(); return; }
|
|
47
|
+
try { controller.enqueue(encoder.encode(frame)); } catch { close(); }
|
|
48
|
+
};
|
|
49
|
+
connections.add(close);
|
|
50
|
+
registerOptionalShutdownHook("account-selection-streams", () => {
|
|
51
|
+
for (const finish of [...connections]) finish();
|
|
52
|
+
});
|
|
53
|
+
if (request.signal.aborted) { close(); return; }
|
|
54
|
+
request.signal.addEventListener("abort", close, { once: true });
|
|
55
|
+
unsubscribe = subscribeAccountSelections(event => {
|
|
56
|
+
send(`event: account-selection\ndata: ${JSON.stringify(event)}\n\n`);
|
|
57
|
+
});
|
|
58
|
+
send(`event: ready\ndata: ${JSON.stringify({ revision: currentAccountSelectionRevision() })}\n\n`);
|
|
59
|
+
if (closed) return;
|
|
60
|
+
heartbeat = setInterval(() => send(": heartbeat\n\n"), HEARTBEAT_MS);
|
|
61
|
+
heartbeat.unref?.();
|
|
62
|
+
},
|
|
63
|
+
cancel() { cleanup(); },
|
|
64
|
+
}, { highWaterMark: 16 });
|
|
65
|
+
return new Response(body, { headers: {
|
|
66
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
67
|
+
"Cache-Control": "no-cache, no-transform",
|
|
68
|
+
"X-Accel-Buffering": "no",
|
|
69
|
+
} });
|
|
70
|
+
}
|