@bitkyc08/opencodex 2.75.0 → 2.76.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 +6 -1
- package/bin/ocx.mjs +12 -1
- package/gui/dist/assets/App-CddorHdA.js +51 -0
- package/gui/dist/assets/App-DMudtISW.css +1 -0
- package/gui/dist/assets/{Tray-CGAD2AGk.js → Tray-DjNwwrWS.js} +1 -1
- package/gui/dist/assets/index-Btze1I3T.css +1 -0
- package/gui/dist/assets/index-CvnY7E6i.js +86 -0
- package/gui/dist/assets/quota-summary-B_AWpHfP.js +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/opengateway.svg +12 -0
- package/package.json +1 -1
- package/src/adapters/anthropic/account-metadata.ts +17 -0
- package/src/adapters/anthropic/client-identity.ts +69 -0
- package/src/adapters/anthropic/passthrough.ts +23 -4
- package/src/adapters/anthropic.ts +18 -1
- package/src/adapters/command-code-project-context.ts +7 -11
- package/src/adapters/devin/cloud-direct/tool-schema.ts +12 -48
- package/src/adapters/kiro-retry.ts +23 -8
- package/src/adapters/openai-chat/messages.ts +1 -1
- package/src/adapters/registry.ts +8 -1
- package/src/adapters/zed.ts +377 -0
- package/src/bridge/sse.ts +4 -2
- package/src/chatgpt/app-server-shim/app-server-rewrite.ts +45 -0
- package/src/chatgpt/app-server-shim/bundle-trust.ts +77 -0
- package/src/chatgpt/app-server-shim/filter.ts +157 -0
- package/src/chatgpt/app-server-shim/gate-rewrite.ts +99 -0
- package/src/chatgpt/app-server-shim/launcher.ts +91 -0
- package/src/claude/desktop-3p-startup.ts +62 -0
- package/src/claude/desktop-3p.ts +5 -0
- package/src/claude/first-party-settings.ts +14 -0
- package/src/claude/intercept/cli-catalog.ts +150 -0
- package/src/claude/intercept/cli-picker.ts +82 -0
- package/src/claude/intercept/listener.ts +21 -6
- package/src/claude/intercept/picker-bootstrap.ts +23 -5
- package/src/claude/intercept/picker-models.ts +57 -13
- package/src/claude/intercept/runtime.ts +30 -2
- package/src/claude/intercept/settings.ts +3 -0
- package/src/cli/agent.ts +172 -0
- package/src/cli/alias.ts +2 -1
- package/src/cli/capabilities-command.ts +3 -2
- package/src/cli/capabilities.ts +87 -0
- package/src/cli/chatgpt-command.ts +140 -0
- package/src/cli/combo.ts +144 -19
- package/src/cli/config-command.ts +3 -1
- package/src/cli/dispatch.ts +10 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +5 -24
- package/src/cli/init.ts +16 -2
- package/src/cli/integrations.ts +13 -0
- package/src/cli/internal-command.ts +12 -5
- package/src/cli/observe.ts +6 -1
- package/src/cli/registry.ts +6 -0
- package/src/cli/route-policy.ts +3 -1
- package/src/cli/runtime-api.ts +5 -2
- package/src/cli/status-oauth.ts +5 -0
- package/src/cli/status.ts +8 -1
- package/src/cli/stop-approval.ts +55 -16
- package/src/cli/stop-report.ts +5 -0
- package/src/clients/lazycodex.ts +48 -0
- package/src/clients/omo-role-models.ts +111 -0
- package/src/codex/account-credit-use.ts +99 -0
- package/src/codex/account-lifecycle.ts +2 -0
- package/src/codex/agent-role-models.ts +171 -0
- package/src/codex/auth-api/account-list.ts +10 -3
- package/src/codex/auth-api/routes.ts +34 -0
- package/src/codex/auth-context.ts +52 -11
- package/src/codex/catalog/discovered-natives.ts +11 -1
- package/src/codex/catalog/gather-capture.ts +6 -1
- package/src/codex/catalog/model-hints.ts +9 -1
- package/src/codex/catalog/provider-models.ts +60 -0
- package/src/codex/catalog/routed-gather.ts +30 -5
- package/src/codex/convergence-types.ts +2 -1
- package/src/codex/main-account-external-usage.ts +62 -0
- package/src/codex/main-account-hard-lock.ts +24 -13
- package/src/codex/native-profile-processes.ts +53 -16
- package/src/codex/prompt-layers/toml-edit.ts +1 -1
- package/src/codex/quota-types.ts +3 -7
- package/src/codex/quota.ts +19 -4
- package/src/codex/role-auto-assign.ts +159 -0
- package/src/codex/role-sizing-limits.ts +2 -0
- package/src/codex/role-sizing.ts +216 -0
- package/src/codex/routing/selection.ts +36 -0
- package/src/codex/routing/thread-affinity.ts +2 -0
- package/src/codex/routing.ts +4 -4
- package/src/codex/subagent-model-fallback.ts +82 -26
- package/src/codex/subagent-selectable-models.ts +32 -0
- package/src/combos/index.ts +18 -0
- package/src/combos/jev-decision-contract.ts +20 -0
- package/src/combos/jev-dispatch.ts +18 -0
- package/src/combos/jev-model-backend.ts +127 -0
- package/src/combos/jev.ts +249 -33
- package/src/combos/types.ts +125 -2
- package/src/config/diagnostics.ts +18 -0
- package/src/config/macos-system-proxy.ts +72 -15
- package/src/config/owner-registry.ts +13 -4
- package/src/config/process-state.ts +17 -8
- package/src/config/proxy-env.ts +35 -5
- package/src/config/schema/config-schema.ts +30 -1
- package/src/config/schema/leaf-validators.ts +4 -0
- package/src/config/serving-runtimes.ts +95 -1
- package/src/generated/compatibility-version.json +336 -156
- package/src/lib/errors.ts +7 -7
- package/src/lib/jsonc.ts +13 -0
- package/src/lib/local-chat-completion.ts +144 -0
- package/src/lib/provider-outbound.ts +87 -1
- package/src/lib/provider-runtime-fetch.ts +23 -0
- package/src/lib/provider-tls-profile.ts +221 -0
- package/src/lib/request-execution-budget.ts +16 -6
- package/src/lib/spend-reservation-ledger.ts +54 -20
- package/src/lib/sse-decoder.ts +20 -17
- package/src/lib/synced-state-location.ts +96 -0
- package/src/lib/winsw.ts +14 -5
- package/src/oauth/anthropic-account-refusal.ts +53 -4
- package/src/oauth/anthropic-model-quota.ts +83 -0
- package/src/oauth/anthropic-rate-limit-policy.ts +37 -0
- package/src/oauth/anthropic-routing.ts +112 -58
- package/src/oauth/health.ts +39 -3
- package/src/oauth/index.ts +14 -0
- package/src/oauth/kiro-account-load.ts +10 -0
- package/src/oauth/zed.ts +134 -0
- package/src/providers/model-discovery.ts +19 -8
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/provider-id-rewrite.ts +9 -2
- package/src/providers/quota/account-cache.ts +29 -7
- package/src/providers/quota/anthropic-cooldown-recovery.ts +18 -0
- package/src/providers/quota/anthropic-family-headers.ts +25 -0
- package/src/providers/quota/vendor-probes-oauth.ts +5 -1
- package/src/providers/quota-types.ts +4 -0
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +55 -0
- package/src/providers/registry/types.ts +3 -0
- package/src/providers/xai-fast-model.ts +12 -4
- package/src/providers/zed.ts +598 -0
- package/src/responses/state/snapshot-select.ts +22 -9
- package/src/responses/state.ts +2 -3
- package/src/router.ts +9 -0
- package/src/server/anthropic-rate-limit-headers.ts +7 -0
- package/src/server/auth-cors.ts +7 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/claude-messages.ts +59 -21
- package/src/server/index/claude-intercept-lifecycle.ts +71 -25
- package/src/server/index/optional-listeners.ts +4 -0
- package/src/server/index/spend-ledger-lifecycle.ts +4 -0
- package/src/server/management/agent-settings-routes.ts +74 -37
- package/src/server/management/claude-desktop-picker-routes.ts +7 -0
- package/src/server/management/claude-intercept-routes.ts +35 -0
- package/src/server/management/codex-agent-role-routes.ts +122 -0
- package/src/server/management/codex-role-auto-assign.ts +208 -0
- package/src/server/management/combo-routes.ts +62 -5
- package/src/server/management/config-routes.ts +35 -3
- package/src/server/management/context.ts +4 -1
- package/src/server/management/decision-discovery.ts +74 -0
- package/src/server/management/decision-model-validation.ts +80 -0
- package/src/server/management/decision-routes.ts +160 -0
- package/src/server/management/native-integration-routes.ts +16 -0
- package/src/server/management/provider-routes.ts +30 -34
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management/zed-provider-probe.ts +38 -0
- package/src/server/management-api.ts +12 -1
- package/src/server/messages-native-oauth.ts +7 -2
- package/src/server/messages-native.ts +7 -3
- package/src/server/relay.ts +3 -1
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/adapter-continuation.ts +3 -3
- package/src/server/responses/adapter-dispatch.ts +18 -3
- package/src/server/responses/compact.ts +14 -1
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-combo.ts +67 -11
- package/src/server/responses/core-normalize.ts +21 -1
- package/src/server/responses/core-options.ts +11 -1
- package/src/server/responses/core.ts +5 -3
- package/src/server/responses/fetch-helpers.ts +6 -3
- package/src/server/responses/jev-model-invoke.ts +190 -0
- package/src/server/responses/passthrough-delivery.ts +16 -0
- package/src/server/responses/policy-fallback.ts +55 -3
- package/src/server/responses/policy-request-scope.ts +80 -0
- package/src/server/responses/request-prepare.ts +70 -18
- package/src/server/responses/request-transport.ts +54 -23
- package/src/server/responses/run-turn-execution.ts +1 -0
- package/src/server/responses/sidecar-execution.ts +9 -3
- package/src/server/responses-hosted-image-display.ts +225 -0
- package/src/service/managing-cli.ts +34 -4
- package/src/service/systemd-env.ts +75 -0
- package/src/service/windows-guarded-manager.ts +60 -19
- package/src/service/windows-scheduler.ts +22 -3
- package/src/service/windows-taskxml.ts +32 -10
- package/src/service-manager-probe.ts +72 -19
- package/src/types/config.ts +31 -1
- package/src/types/provider.ts +2 -0
- package/src/types/request.ts +2 -0
- package/src/update/index.ts +39 -6
- package/src/update/job.ts +63 -63
- package/src/update/npm-cache-preflight.d.mts +33 -1
- package/src/update/npm-cache-preflight.mjs +131 -18
- package/src/update/ownership-transaction.ts +19 -1
- package/src/update/restart-ownership.ts +80 -6
- package/src/update/transactional-install.d.mts +2 -0
- package/src/update/transactional-install.mjs +6 -1
- package/src/usage/jev-stats.ts +28 -1
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +5 -3
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +2 -1
- package/src/vision/routed-describe.ts +29 -121
- package/src/web-search/alpha-search.ts +3 -3
- package/src/web-search/anthropic-executor.ts +6 -4
- package/src/web-search/index.ts +1 -1
- package/src/web-search/loop.ts +3 -3
- package/src/web-search/passthrough-bridge.ts +2 -1
- package/src/web-search/run-turn-loop.ts +1 -1
- package/src/web-search/sidecar-providers.ts +2 -1
- package/gui/dist/assets/App-ia4U0ufY.css +0 -1
- package/gui/dist/assets/App-xm0zGNOe.js +0 -51
- package/gui/dist/assets/index-DBT3Q31Y.css +0 -1
- package/gui/dist/assets/index-D_RGAT6Q.js +0 -86
- package/gui/dist/assets/quota-summary-CUFq48t-.js +0 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "./base";
|
|
3
|
+
import { createAnthropicAdapter } from "./anthropic";
|
|
4
|
+
import { createGoogleAdapter } from "./google";
|
|
5
|
+
import { createOpenAIChatAdapter } from "./openai-chat";
|
|
6
|
+
import { createResponsesPassthroughAdapter } from "./openai-responses";
|
|
7
|
+
import type { AdapterEvent, OcxParsedRequest, OcxProviderConfig } from "../types";
|
|
8
|
+
import { createTranslatorBudget, type TranslatorBudget } from "../lib/translator-budget";
|
|
9
|
+
import { redactSecretString } from "../lib/redact";
|
|
10
|
+
import {
|
|
11
|
+
normalizeZedProvider,
|
|
12
|
+
resolveZedModels,
|
|
13
|
+
scrubZedCredentials,
|
|
14
|
+
zedLlmFetch,
|
|
15
|
+
ZED_HEADERS,
|
|
16
|
+
type ZedCredentials,
|
|
17
|
+
} from "../providers/zed";
|
|
18
|
+
|
|
19
|
+
export type ZedProvider = "anthropic" | "open_ai" | "google" | "x_ai";
|
|
20
|
+
|
|
21
|
+
interface ZedDelegate {
|
|
22
|
+
provider: ZedProvider;
|
|
23
|
+
adapter: ProviderAdapter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function zedCredentials(provider: OcxProviderConfig, parsed: OcxParsedRequest): ZedCredentials {
|
|
31
|
+
const accessToken = provider.apiKey?.trim();
|
|
32
|
+
const userId = parsed._zedAuthContext?.userId?.trim();
|
|
33
|
+
if (!accessToken) throw new Error("Zed access token missing — run ocx login zed");
|
|
34
|
+
if (!userId) throw new Error("Zed account identity missing — run ocx login zed again");
|
|
35
|
+
return { userId, accessToken };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function delegateProvider(provider: OcxProviderConfig, zedProvider: ZedProvider): OcxProviderConfig {
|
|
39
|
+
const common: OcxProviderConfig = {
|
|
40
|
+
...provider,
|
|
41
|
+
authMode: "key",
|
|
42
|
+
apiKey: "zed-delegate-placeholder",
|
|
43
|
+
models: undefined,
|
|
44
|
+
liveModels: false,
|
|
45
|
+
};
|
|
46
|
+
if (zedProvider === "anthropic") {
|
|
47
|
+
return { ...common, adapter: "anthropic", baseUrl: "https://api.anthropic.com" };
|
|
48
|
+
}
|
|
49
|
+
if (zedProvider === "google") {
|
|
50
|
+
return { ...common, adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", googleMode: "ai-studio" };
|
|
51
|
+
}
|
|
52
|
+
if (zedProvider === "open_ai") {
|
|
53
|
+
return { ...common, adapter: "openai-responses", baseUrl: "https://api.openai.com/v1" };
|
|
54
|
+
}
|
|
55
|
+
return { ...common, adapter: "openai-chat", baseUrl: "https://api.x.ai/v1" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function forceStreaming(parsed: OcxParsedRequest): OcxParsedRequest {
|
|
59
|
+
const rawBody = isRecord(parsed._rawBody) ? { ...parsed._rawBody, stream: true } : parsed._rawBody;
|
|
60
|
+
return { ...parsed, stream: true, _rawBody: rawBody };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function providerFromCatalog(catalog: Awaited<ReturnType<typeof resolveZedModels>> | undefined, model: string): ZedProvider {
|
|
64
|
+
const raw = catalog?.rawById.get(model);
|
|
65
|
+
return normalizeZedProvider(raw?.provider, model);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nativeErrorPayload(provider: ZedProvider, message: string): Record<string, unknown> {
|
|
69
|
+
if (provider === "anthropic") {
|
|
70
|
+
return { type: "error", error: { type: "api_error", message } };
|
|
71
|
+
}
|
|
72
|
+
if (provider === "open_ai") {
|
|
73
|
+
return { type: "error", error: { message } };
|
|
74
|
+
}
|
|
75
|
+
return { error: { message } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function nativeTerminalPayload(provider: ZedProvider): Record<string, unknown> {
|
|
79
|
+
if (provider === "anthropic") return { type: "message_stop" };
|
|
80
|
+
if (provider === "google") return { candidates: [{ finishReason: "STOP" }] };
|
|
81
|
+
if (provider === "open_ai") return { type: "response.completed", response: { output: [] } };
|
|
82
|
+
return { choices: [{ delta: {}, finish_reason: "stop" }] };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function normalizedStatus(value: unknown): { type: string; message?: string } | undefined {
|
|
86
|
+
if (typeof value === "string") return { type: value };
|
|
87
|
+
if (!isRecord(value)) return undefined;
|
|
88
|
+
if (typeof value.type === "string") {
|
|
89
|
+
return {
|
|
90
|
+
type: value.type,
|
|
91
|
+
...(typeof value.message === "string" ? { message: value.message } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const first = Object.entries(value)[0];
|
|
95
|
+
if (!first) return undefined;
|
|
96
|
+
const [type, body] = first;
|
|
97
|
+
if (isRecord(body)) {
|
|
98
|
+
return { type, ...(typeof body.message === "string" ? { message: body.message } : {}) };
|
|
99
|
+
}
|
|
100
|
+
return { type };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** One Zed frame larger than this without a newline is treated as a broken stream, not buffered forever. */
|
|
104
|
+
const MAX_ZED_FRAME_CHARS = 1024 * 1024;
|
|
105
|
+
|
|
106
|
+
/** Whether a delegated native event is itself that protocol's terminal event. */
|
|
107
|
+
function isNativeTerminalEvent(event: Record<string, unknown>): boolean {
|
|
108
|
+
if (event.type === "message_stop") return true;
|
|
109
|
+
if (event.type === "response.completed" || event.type === "response.incomplete" || event.type === "response.failed") return true;
|
|
110
|
+
if (Array.isArray(event.choices) && event.choices.some(choice => isRecord(choice) && choice.finish_reason != null)) return true;
|
|
111
|
+
return Array.isArray(event.candidates) && event.candidates.some(candidate => isRecord(candidate) && candidate.finishReason != null);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Translate Zed's NDJSON/SSE completion frames into the delegated provider's native SSE.
|
|
116
|
+
*
|
|
117
|
+
* The request declares support for the stream-ended status, so a well-formed response ends with
|
|
118
|
+
* an explicit terminal status (or `[DONE]`). The stream fails closed instead of synthesizing a
|
|
119
|
+
* success when that guarantee is broken: a malformed or non-object frame, a frame over
|
|
120
|
+
* `MAX_ZED_FRAME_CHARS`, a partial trailing frame, or an EOF with neither a terminal status nor a
|
|
121
|
+
* native terminal event becomes the provider-native error. Nothing is forwarded after a terminal
|
|
122
|
+
* status or `[DONE]`; frames after a native terminal event still pass, because some wires (Chat
|
|
123
|
+
* with usage) send a usage chunk after `finish_reason`. Upstream failure text goes through
|
|
124
|
+
* `scrub`, which removes this account's token and user id.
|
|
125
|
+
*/
|
|
126
|
+
export function zedEventStream(
|
|
127
|
+
body: ReadableStream<Uint8Array>,
|
|
128
|
+
provider: ZedProvider,
|
|
129
|
+
scrub: (text: string) => string = redactSecretString,
|
|
130
|
+
): ReadableStream<Uint8Array> {
|
|
131
|
+
const decoder = new TextDecoder();
|
|
132
|
+
const encoder = new TextEncoder();
|
|
133
|
+
let buffer = "";
|
|
134
|
+
let finished = false;
|
|
135
|
+
let sawNativeTerminal = false;
|
|
136
|
+
const output = new TransformStream<Uint8Array, Uint8Array>({
|
|
137
|
+
transform(chunk, controller) {
|
|
138
|
+
if (finished) return;
|
|
139
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
140
|
+
let start = 0;
|
|
141
|
+
let newline = buffer.indexOf("\n", start);
|
|
142
|
+
while (newline >= 0 && !finished) {
|
|
143
|
+
processLine(buffer.slice(start, newline), controller);
|
|
144
|
+
start = newline + 1;
|
|
145
|
+
newline = buffer.indexOf("\n", start);
|
|
146
|
+
}
|
|
147
|
+
buffer = finished ? "" : buffer.slice(start);
|
|
148
|
+
if (!finished && buffer.length > MAX_ZED_FRAME_CHARS) {
|
|
149
|
+
buffer = "";
|
|
150
|
+
fail(controller, "Zed stream frame exceeded the size limit");
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
flush(controller) {
|
|
154
|
+
if (finished) return;
|
|
155
|
+
buffer += decoder.decode();
|
|
156
|
+
if (buffer.trim()) {
|
|
157
|
+
buffer = "";
|
|
158
|
+
fail(controller, "Zed stream ended inside a partial frame");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (sawNativeTerminal) emit(controller, nativeTerminalPayload(provider));
|
|
162
|
+
else fail(controller, "Zed stream ended before completion");
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
function emit(controller: TransformStreamDefaultController<Uint8Array>, payload: Record<string, unknown>): void {
|
|
167
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function fail(controller: TransformStreamDefaultController<Uint8Array>, message: string): void {
|
|
171
|
+
emit(controller, nativeErrorPayload(provider, message));
|
|
172
|
+
finished = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function processLine(line: string, controller: TransformStreamDefaultController<Uint8Array>): void {
|
|
176
|
+
let text = line.replace(/\r$/, "").trim();
|
|
177
|
+
if (!text) return;
|
|
178
|
+
if (text.startsWith("data:")) text = text.slice(5).trimStart();
|
|
179
|
+
if (!text || text.startsWith(":") || text.startsWith("event:")) return;
|
|
180
|
+
if (text === "[DONE]") {
|
|
181
|
+
emit(controller, nativeTerminalPayload(provider));
|
|
182
|
+
finished = true;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
let parsed: unknown;
|
|
186
|
+
try { parsed = JSON.parse(text) as unknown; } catch {
|
|
187
|
+
fail(controller, "Zed stream sent a malformed frame");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!isRecord(parsed)) {
|
|
191
|
+
fail(controller, "Zed stream sent a malformed frame");
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (Object.hasOwn(parsed, "status")) {
|
|
195
|
+
const status = normalizedStatus(parsed.status);
|
|
196
|
+
if (status?.type === "failed" || status?.type === "error") {
|
|
197
|
+
fail(controller, status.message ? scrub(status.message) : "Zed request failed");
|
|
198
|
+
} else if (status?.type === "stream_ended" || status?.type === "completed") {
|
|
199
|
+
emit(controller, nativeTerminalPayload(provider));
|
|
200
|
+
finished = true;
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const event = Object.hasOwn(parsed, "event") ? parsed.event : parsed;
|
|
205
|
+
if (!isRecord(event)) {
|
|
206
|
+
fail(controller, "Zed stream sent a malformed frame");
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (isNativeTerminalEvent(event)) sawNativeTerminal = true;
|
|
210
|
+
emit(controller, event);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return body.pipeThrough(output);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function createDelegate(provider: OcxProviderConfig, zedProvider: ZedProvider): ProviderAdapter {
|
|
217
|
+
const config = delegateProvider(provider, zedProvider);
|
|
218
|
+
if (zedProvider === "anthropic") return createAnthropicAdapter(config);
|
|
219
|
+
if (zedProvider === "google") return createGoogleAdapter(config);
|
|
220
|
+
if (zedProvider === "open_ai") return createResponsesPassthroughAdapter(config);
|
|
221
|
+
return createOpenAIChatAdapter(config);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function createZedAdapter(provider: OcxProviderConfig): ProviderAdapter {
|
|
225
|
+
let delegate: ZedDelegate | undefined;
|
|
226
|
+
let credentials: ZedCredentials | undefined;
|
|
227
|
+
|
|
228
|
+
const buildRequest = async (parsed: OcxParsedRequest, incoming?: IncomingMeta): Promise<AdapterRequest> => {
|
|
229
|
+
credentials = zedCredentials(provider, parsed);
|
|
230
|
+
const threadId = parsed._clientThreadId ?? parsed._codexOwnThreadId ?? parsed.previousResponseId ?? randomUUID();
|
|
231
|
+
const promptId = randomUUID();
|
|
232
|
+
let catalog: Awaited<ReturnType<typeof resolveZedModels>> | undefined;
|
|
233
|
+
try {
|
|
234
|
+
const catalogTimeout = AbortSignal.timeout(8_000);
|
|
235
|
+
const catalogSignal = incoming?.abortSignal ? AbortSignal.any([incoming.abortSignal, catalogTimeout]) : catalogTimeout;
|
|
236
|
+
catalog = await resolveZedModels(
|
|
237
|
+
credentials,
|
|
238
|
+
{ signal: catalogSignal, ...(incoming?.providerFetch ? { fetchFn: incoming.providerFetch } : {}) },
|
|
239
|
+
);
|
|
240
|
+
} catch {
|
|
241
|
+
/* Model inference fallback below; a transient catalog outage must not block passthrough. */
|
|
242
|
+
}
|
|
243
|
+
const zedProvider = providerFromCatalog(catalog, parsed.modelId);
|
|
244
|
+
const selected = createDelegate(provider, zedProvider);
|
|
245
|
+
const safeIncoming: IncomingMeta = {
|
|
246
|
+
headers: incoming?.headers ?? new Headers(),
|
|
247
|
+
translatorBudget: incoming?.translatorBudget ?? createTranslatorBudget(),
|
|
248
|
+
...(incoming?.providerFetch ? { providerFetch: incoming.providerFetch } : {}),
|
|
249
|
+
...(incoming?.abortSignal ? { abortSignal: incoming.abortSignal } : {}),
|
|
250
|
+
};
|
|
251
|
+
const built = await selected.buildRequest(forceStreaming(parsed), safeIncoming);
|
|
252
|
+
let providerRequest: unknown;
|
|
253
|
+
try { providerRequest = JSON.parse(built.body) as unknown; } catch { throw new Error("Zed delegate produced an invalid request body"); }
|
|
254
|
+
if (zedProvider === "google" && isRecord(providerRequest)) delete providerRequest.safetySettings;
|
|
255
|
+
if (zedProvider === "anthropic" && isRecord(providerRequest) && Array.isArray(providerRequest.messages)) {
|
|
256
|
+
providerRequest.messages = providerRequest.messages.map((m: unknown) => {
|
|
257
|
+
if (isRecord(m) && typeof m.content === "string") {
|
|
258
|
+
return { ...m, content: [{ type: "text", text: m.content }] };
|
|
259
|
+
}
|
|
260
|
+
return m;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (zedProvider === "open_ai" && isRecord(providerRequest)) {
|
|
264
|
+
if (typeof providerRequest.input === "string") {
|
|
265
|
+
providerRequest.input = [
|
|
266
|
+
{
|
|
267
|
+
type: "message",
|
|
268
|
+
role: "user",
|
|
269
|
+
content: [{ type: "input_text", text: providerRequest.input }],
|
|
270
|
+
},
|
|
271
|
+
];
|
|
272
|
+
} else if (Array.isArray(providerRequest.input)) {
|
|
273
|
+
providerRequest.input = providerRequest.input.map((item: unknown) => {
|
|
274
|
+
if (isRecord(item)) {
|
|
275
|
+
const role = typeof item.role === "string" ? item.role : "user";
|
|
276
|
+
const type = typeof item.type === "string" ? item.type : "message";
|
|
277
|
+
if (typeof item.content === "string") {
|
|
278
|
+
return { ...item, type, role, content: [{ type: "input_text", text: item.content }] };
|
|
279
|
+
}
|
|
280
|
+
if (Array.isArray(item.content)) {
|
|
281
|
+
return {
|
|
282
|
+
...item,
|
|
283
|
+
type,
|
|
284
|
+
role,
|
|
285
|
+
content: item.content.map((c: unknown) => {
|
|
286
|
+
if (isRecord(c) && c.type === "text") {
|
|
287
|
+
return { ...c, type: "input_text" };
|
|
288
|
+
}
|
|
289
|
+
return c;
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return { ...item, type, role };
|
|
294
|
+
}
|
|
295
|
+
return item;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!isRecord(providerRequest)) throw new Error("Zed delegate produced a non-object request body");
|
|
300
|
+
delegate = { provider: zedProvider, adapter: selected };
|
|
301
|
+
const requestUrl = `${provider.baseUrl.replace(/\/+$/, "")}/completions`;
|
|
302
|
+
return {
|
|
303
|
+
url: requestUrl,
|
|
304
|
+
method: "POST",
|
|
305
|
+
headers: {
|
|
306
|
+
"Content-Type": "application/json",
|
|
307
|
+
Accept: "application/x-ndjson, text/event-stream, */*",
|
|
308
|
+
"User-Agent": "OpenCodex/zed",
|
|
309
|
+
"x-zed-version": "0.200.0",
|
|
310
|
+
[ZED_HEADERS.clientSupportsStatus]: "true",
|
|
311
|
+
[ZED_HEADERS.clientSupportsStreamEnded]: "true",
|
|
312
|
+
},
|
|
313
|
+
body: JSON.stringify({
|
|
314
|
+
thread_id: threadId,
|
|
315
|
+
prompt_id: promptId,
|
|
316
|
+
provider: zedProvider,
|
|
317
|
+
model: parsed.modelId,
|
|
318
|
+
provider_request: providerRequest,
|
|
319
|
+
}),
|
|
320
|
+
...(built.tierLog ? { tierLog: built.tierLog } : {}),
|
|
321
|
+
};
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const adapter: ProviderAdapter = {
|
|
325
|
+
name: "zed",
|
|
326
|
+
formatErrorBody(status, _headers, payloadText) {
|
|
327
|
+
let payload: unknown;
|
|
328
|
+
try { payload = JSON.parse(payloadText) as unknown; } catch { payload = undefined; }
|
|
329
|
+
const record = isRecord(payload) ? payload : undefined;
|
|
330
|
+
const error = isRecord(record?.error) ? record.error : undefined;
|
|
331
|
+
const code = typeof record?.code === "string" ? record.code : typeof error?.code === "string" ? error.code : "";
|
|
332
|
+
const message = typeof record?.message === "string" ? record.message
|
|
333
|
+
: typeof error?.message === "string" ? error.message
|
|
334
|
+
: "Zed upstream request failed";
|
|
335
|
+
const scrub = (text: string) => credentials ? scrubZedCredentials(text, credentials) : redactSecretString(text);
|
|
336
|
+
return `Zed${code ? ` ${scrub(code)}` : ""}: ${scrub(message)} (HTTP ${status})`;
|
|
337
|
+
},
|
|
338
|
+
buildRequest,
|
|
339
|
+
async fetchResponse(request, ctx) {
|
|
340
|
+
if (!credentials) throw new Error("Zed request credentials were not initialized");
|
|
341
|
+
const fetchFn = ctx?.executor ?? globalThis.fetch;
|
|
342
|
+
return zedLlmFetch(credentials, "/completions", {
|
|
343
|
+
fetchFn,
|
|
344
|
+
signal: ctx?.abortSignal,
|
|
345
|
+
baseUrl: provider.baseUrl,
|
|
346
|
+
fetchInit: {
|
|
347
|
+
method: request.method,
|
|
348
|
+
headers: request.headers,
|
|
349
|
+
body: request.body,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
|
|
354
|
+
if (!response.body) {
|
|
355
|
+
yield { type: "error", message: "Zed response had no body" };
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (!delegate) {
|
|
359
|
+
yield { type: "error", message: "Zed response arrived before request translation" };
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
const requestCredentials = credentials;
|
|
363
|
+
const scrub = (text: string) => requestCredentials ? scrubZedCredentials(text, requestCredentials) : redactSecretString(text);
|
|
364
|
+
const translated = new Response(zedEventStream(response.body, delegate.provider, scrub), {
|
|
365
|
+
status: response.status,
|
|
366
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
367
|
+
});
|
|
368
|
+
yield* delegate.adapter.parseStream(translated, budget);
|
|
369
|
+
},
|
|
370
|
+
async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
|
|
371
|
+
const events: AdapterEvent[] = [];
|
|
372
|
+
for await (const event of adapter.parseStream(response, budget)) events.push(event);
|
|
373
|
+
return events;
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
return adapter;
|
|
377
|
+
}
|
package/src/bridge/sse.ts
CHANGED
|
@@ -88,7 +88,7 @@ export function bridgeToResponsesSSE(
|
|
|
88
88
|
* response.completed — codex-rs collect_compaction_output requires exactly one.
|
|
89
89
|
*/
|
|
90
90
|
compaction?: boolean;
|
|
91
|
-
/** One-shot: first non-empty text/thinking/raw-reasoning delta observed
|
|
91
|
+
/** One-shot: first non-empty text/thinking/raw-reasoning/tool-input delta observed. */
|
|
92
92
|
onFirstOutput?: () => void;
|
|
93
93
|
onTerminal?: (status: ResponsesTerminalStatus) => void;
|
|
94
94
|
onCompletedResponse?: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
|
|
@@ -707,7 +707,9 @@ export function bridgeToResponsesSSE(
|
|
|
707
707
|
? event.thinking.length > 0
|
|
708
708
|
: event.type === "reasoning_raw_delta"
|
|
709
709
|
? event.text.length > 0
|
|
710
|
-
:
|
|
710
|
+
: event.type === "tool_call_delta"
|
|
711
|
+
? event.arguments.length > 0
|
|
712
|
+
: false;
|
|
711
713
|
if (!nonEmpty) return;
|
|
712
714
|
firstOutputReported = true;
|
|
713
715
|
try { options?.onFirstOutput?.(); } catch { /* metrics must not break the stream */ }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { unlockRateLimitGate } from "./gate-rewrite";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Line-level rewrite for the app-server's JSON-RPC output.
|
|
5
|
+
*
|
|
6
|
+
* Only the two messages that carry the composer's gate are rewritten: the
|
|
7
|
+
* `account/rateLimits/updated` notification and a result whose top level holds the rate-limit
|
|
8
|
+
* snapshot (the `account/rateLimits/read` response). Every other message is left alone, including
|
|
9
|
+
* thread items and tool results that happen to nest a `rate_limit` object. Lines that do not
|
|
10
|
+
* mention the snapshot fields are not even parsed, so the hot path costs two substring checks and
|
|
11
|
+
* the bytes stay identical.
|
|
12
|
+
*/
|
|
13
|
+
const RATE_LIMITS_UPDATED = "account/rateLimits/updated";
|
|
14
|
+
const RATE_LIMIT_RESULT_FIELDS = ["rateLimits", "rateLimitsByLimitId", "ordinaryUsageAllowed"];
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The part of a message the gate rewrite may touch, or null when the message carries no gate. */
|
|
21
|
+
function gatePayload(message: unknown): Record<string, unknown> | null {
|
|
22
|
+
if (!isRecord(message)) return null;
|
|
23
|
+
if (message.method === RATE_LIMITS_UPDATED) return isRecord(message.params) ? message.params : null;
|
|
24
|
+
if ("method" in message) return null;
|
|
25
|
+
const result = message.result;
|
|
26
|
+
return isRecord(result) && RATE_LIMIT_RESULT_FIELDS.some(field => field in result) ? result : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Returns the rewritten line, or null when the line is not a gate-bearing JSON-RPC message or the
|
|
31
|
+
* rewrite has nothing to change. Parse failures also return null: the rewrite owns removal of
|
|
32
|
+
* known-shaped locks, not validation.
|
|
33
|
+
*/
|
|
34
|
+
export function rewriteAppServerLine(line: string): string | null {
|
|
35
|
+
if (!line.includes("rateLimit") && !line.includes("ordinaryUsageAllowed")) return null;
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(line);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const payload = gatePayload(parsed);
|
|
43
|
+
if (payload === null) return null;
|
|
44
|
+
return unlockRateLimitGate(payload) ? JSON.stringify(parsed) : null;
|
|
45
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { dirname, relative, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The launcher persistently execs a binary taken from a discovered bundle, so the bundle must be
|
|
7
|
+
* one this user can trust: OpenAI-signed, and not replaceable by another local account. Bundle
|
|
8
|
+
* identifier alone (what discovery checks) is plain text in Info.plist that anyone can copy.
|
|
9
|
+
*/
|
|
10
|
+
export const OPENAI_TEAM_ID = "2DC432GLL2";
|
|
11
|
+
|
|
12
|
+
const CODESIGN = "/usr/bin/codesign";
|
|
13
|
+
const CODESIGN_TIMEOUT_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
export interface BundleTrustDeps {
|
|
16
|
+
readonly uid: number;
|
|
17
|
+
stat(path: string): { uid: number; mode: number; isFile: boolean; isDirectory: boolean; isSymbolicLink: boolean } | null;
|
|
18
|
+
codesign(args: readonly string[]): { status: number | null; output: string };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defaultBundleTrustDeps(): BundleTrustDeps {
|
|
22
|
+
return {
|
|
23
|
+
uid: typeof process.getuid === "function" ? process.getuid() : -1,
|
|
24
|
+
stat(path) {
|
|
25
|
+
try {
|
|
26
|
+
const s = lstatSync(path);
|
|
27
|
+
return { uid: s.uid, mode: s.mode, isFile: s.isFile(), isDirectory: s.isDirectory(), isSymbolicLink: s.isSymbolicLink() };
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
codesign(args) {
|
|
33
|
+
const result = spawnSync(CODESIGN, [...args], { encoding: "utf8", timeout: CODESIGN_TIMEOUT_MS });
|
|
34
|
+
return { status: result.status, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ownedSafely(deps: BundleTrustDeps, path: string, kind: "file" | "directory"): string | null {
|
|
40
|
+
const s = deps.stat(path);
|
|
41
|
+
if (!s) return "is missing";
|
|
42
|
+
if (s.isSymbolicLink) return "is a symbolic link";
|
|
43
|
+
if (kind === "file" ? !s.isFile : !s.isDirectory) return `is not a ${kind}`;
|
|
44
|
+
if (s.uid !== 0 && s.uid !== deps.uid) return "is owned by another user";
|
|
45
|
+
if ((s.mode & 0o022) !== 0) return "is writable by group or others";
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Null when the bundle root and the app-server binary inside it may be exec'd by this user;
|
|
51
|
+
* otherwise a short reason. Every directory from the binary up to the bundle root, and the
|
|
52
|
+
* binary itself, must be owned by root or this user and not group/other-writable, and both the
|
|
53
|
+
* bundle and the binary must pass strict code-signature verification under OpenAI's team ID.
|
|
54
|
+
*/
|
|
55
|
+
export function untrustedChatgptBundleReason(
|
|
56
|
+
bundleRoot: string,
|
|
57
|
+
binary: string,
|
|
58
|
+
deps: BundleTrustDeps = defaultBundleTrustDeps(),
|
|
59
|
+
): string | null {
|
|
60
|
+
const inside = relative(bundleRoot, binary);
|
|
61
|
+
if (!inside || inside.startsWith("..") || inside.startsWith(sep)) return "the app-server binary is outside the bundle";
|
|
62
|
+
const fileProblem = ownedSafely(deps, binary, "file");
|
|
63
|
+
if (fileProblem) return `the app-server binary ${fileProblem}`;
|
|
64
|
+
for (let dir = dirname(binary); ; dir = dirname(dir)) {
|
|
65
|
+
const dirProblem = ownedSafely(deps, dir, "directory");
|
|
66
|
+
if (dirProblem) return `${dir} ${dirProblem}`;
|
|
67
|
+
if (dir === bundleRoot || dirname(dir) === dir) break;
|
|
68
|
+
}
|
|
69
|
+
for (const target of [bundleRoot, binary]) {
|
|
70
|
+
if (deps.codesign(["--verify", "--strict", target]).status !== 0) return `${target} failed code-signature verification`;
|
|
71
|
+
const details = deps.codesign(["-dv", "--verbose=2", target]);
|
|
72
|
+
if (details.status !== 0 || !new RegExp(`^TeamIdentifier=${OPENAI_TEAM_ID}$`, "m").test(details.output)) {
|
|
73
|
+
return `${target} is not signed by OpenAI (team ${OPENAI_TEAM_ID})`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { rewriteAppServerLine } from "./app-server-rewrite";
|
|
2
|
+
|
|
3
|
+
/** Byte-preserving stdout filter; unexpected rewrite machinery failures disable filtering. */
|
|
4
|
+
const NEWLINE = 0x0a;
|
|
5
|
+
/**
|
|
6
|
+
* Gate messages are a few kilobytes. A line longer than this is passed through raw instead of
|
|
7
|
+
* being buffered, so a runaway line without a newline cannot grow the filter's memory unbounded.
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_FILTERED_LINE_BYTES = 8 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
function concatParts(parts: readonly Uint8Array[], length: number): Uint8Array {
|
|
12
|
+
if (parts.length === 1) return parts[0]!;
|
|
13
|
+
const out = new Uint8Array(length);
|
|
14
|
+
let offset = 0;
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
out.set(part, offset);
|
|
17
|
+
offset += part.length;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Splits a byte stream into lines and rewrites the ones that need it. Returns the bytes to write
|
|
24
|
+
* for each chunk; a partial trailing line is held back until its newline arrives (or `flush`).
|
|
25
|
+
* The held-back pieces are kept as a list and joined once when the line ends, so a long line
|
|
26
|
+
* spread over many pipe chunks costs linear copying rather than recopying its prefix per chunk.
|
|
27
|
+
* A line over `maxLineBytes` is emitted raw as it arrives and never parsed.
|
|
28
|
+
*/
|
|
29
|
+
export function createRpcLineFilter(
|
|
30
|
+
rewrite: (line: string) => string | null = rewriteAppServerLine,
|
|
31
|
+
maxLineBytes = MAX_FILTERED_LINE_BYTES,
|
|
32
|
+
): { push(chunk: Uint8Array): Uint8Array[]; flush(): Uint8Array[] } {
|
|
33
|
+
const decoder = new TextDecoder();
|
|
34
|
+
const encoder = new TextEncoder();
|
|
35
|
+
let pending: Uint8Array[] = [];
|
|
36
|
+
let pendingLength = 0;
|
|
37
|
+
let passthrough = false;
|
|
38
|
+
// Inside an oversized line: bytes pass through raw until its newline.
|
|
39
|
+
let skippingLine = false;
|
|
40
|
+
|
|
41
|
+
// `line` excludes the newline, `whole` includes it when there is one. An untouched line is
|
|
42
|
+
// returned as the very bytes that arrived; only a rewritten one is re-encoded.
|
|
43
|
+
const emit = (line: Uint8Array, whole: Uint8Array, terminated: boolean): Uint8Array => {
|
|
44
|
+
let rewritten: string | null;
|
|
45
|
+
try {
|
|
46
|
+
rewritten = rewrite(decoder.decode(line));
|
|
47
|
+
} catch {
|
|
48
|
+
return whole;
|
|
49
|
+
}
|
|
50
|
+
if (rewritten === null) return whole;
|
|
51
|
+
const body = encoder.encode(rewritten);
|
|
52
|
+
if (!terminated) return body;
|
|
53
|
+
const out = new Uint8Array(body.length + 1);
|
|
54
|
+
out.set(body, 0);
|
|
55
|
+
out[body.length] = NEWLINE;
|
|
56
|
+
return out;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
push(chunk) {
|
|
61
|
+
if (passthrough) return [chunk];
|
|
62
|
+
const previous = pending;
|
|
63
|
+
const previousLength = pendingLength;
|
|
64
|
+
try {
|
|
65
|
+
const out: Uint8Array[] = [];
|
|
66
|
+
let start = 0;
|
|
67
|
+
if (skippingLine) {
|
|
68
|
+
const end = chunk.indexOf(NEWLINE);
|
|
69
|
+
if (end === -1) return [chunk];
|
|
70
|
+
out.push(chunk.subarray(0, end + 1));
|
|
71
|
+
skippingLine = false;
|
|
72
|
+
start = end + 1;
|
|
73
|
+
}
|
|
74
|
+
let held = previous;
|
|
75
|
+
let heldLength = previousLength;
|
|
76
|
+
for (let i = chunk.indexOf(NEWLINE, start); i !== -1; i = chunk.indexOf(NEWLINE, start)) {
|
|
77
|
+
const tail = chunk.subarray(start, i + 1);
|
|
78
|
+
if (heldLength + tail.length - 1 > maxLineBytes) {
|
|
79
|
+
// A complete line over the cap is passed through as it arrived, never joined or parsed.
|
|
80
|
+
out.push(...held, tail);
|
|
81
|
+
} else {
|
|
82
|
+
const whole = heldLength === 0 ? tail : concatParts([...held, tail], heldLength + tail.length);
|
|
83
|
+
out.push(emit(whole.subarray(0, whole.length - 1), whole, true));
|
|
84
|
+
}
|
|
85
|
+
held = [];
|
|
86
|
+
heldLength = 0;
|
|
87
|
+
start = i + 1;
|
|
88
|
+
}
|
|
89
|
+
if (start < chunk.length) {
|
|
90
|
+
const rest = chunk.slice(start);
|
|
91
|
+
if (heldLength + rest.length > maxLineBytes) {
|
|
92
|
+
// Too long to be a gate message: release what is held and stream the rest raw.
|
|
93
|
+
out.push(...held, rest);
|
|
94
|
+
held = [];
|
|
95
|
+
heldLength = 0;
|
|
96
|
+
skippingLine = true;
|
|
97
|
+
} else {
|
|
98
|
+
// Appending in place is safe: nothing after this point can throw back to `previous`.
|
|
99
|
+
held.push(rest);
|
|
100
|
+
heldLength += rest.length;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
pending = held;
|
|
104
|
+
pendingLength = heldLength;
|
|
105
|
+
return out;
|
|
106
|
+
} catch {
|
|
107
|
+
pending = [];
|
|
108
|
+
pendingLength = 0;
|
|
109
|
+
passthrough = true;
|
|
110
|
+
return [...previous, chunk];
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
flush() {
|
|
114
|
+
if (pendingLength === 0) return [];
|
|
115
|
+
const parts = pending;
|
|
116
|
+
const last = concatParts(parts, pendingLength);
|
|
117
|
+
pending = [];
|
|
118
|
+
pendingLength = 0;
|
|
119
|
+
if (passthrough) return parts;
|
|
120
|
+
try { return [emit(last, last, false)]; } catch { return [last]; }
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Copy `input` to `write`, rewriting gate lines on the way. Resolves when `input` ends. */
|
|
126
|
+
export async function runStdoutFilter(
|
|
127
|
+
input: AsyncIterable<Uint8Array>,
|
|
128
|
+
write: (bytes: Uint8Array) => Promise<unknown> | unknown,
|
|
129
|
+
rewrite?: (line: string) => string | null,
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
let filter: ReturnType<typeof createRpcLineFilter> | undefined;
|
|
132
|
+
try { filter = createRpcLineFilter(rewrite); } catch { /* Raw passthrough if setup fails. */ }
|
|
133
|
+
for await (const chunk of input) {
|
|
134
|
+
for (const out of filter ? filter.push(chunk) : [chunk]) await write(out);
|
|
135
|
+
}
|
|
136
|
+
for (const out of filter?.flush() ?? []) await write(out);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Hidden CLI entry; the self-test emits no stdout so the launcher can probe it safely. */
|
|
140
|
+
export async function runChatgptAppServerFilter({ selfTest = false }: { selfTest?: boolean } = {}): Promise<number> {
|
|
141
|
+
if (selfTest) {
|
|
142
|
+
try {
|
|
143
|
+
const fixture = JSON.stringify({ id: 1, result: {
|
|
144
|
+
ordinaryUsageAllowed: false,
|
|
145
|
+
rateLimits: { rateLimitReachedType: "rate_limit_reached", primary: { usedPercent: 100 } },
|
|
146
|
+
} });
|
|
147
|
+
const rewritten = rewriteAppServerLine(fixture);
|
|
148
|
+
if (!rewritten) return 1;
|
|
149
|
+
const result = JSON.parse(rewritten).result;
|
|
150
|
+
return result.ordinaryUsageAllowed === true
|
|
151
|
+
&& result.rateLimits.rateLimitReachedType === null
|
|
152
|
+
&& result.rateLimits.primary.usedPercent === 100 ? 0 : 1;
|
|
153
|
+
} catch { return 1; }
|
|
154
|
+
}
|
|
155
|
+
await runStdoutFilter(Bun.stdin.stream(), bytes => Bun.write(Bun.stdout, bytes));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|