@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913
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/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-D_t6sCWs.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { formatErrorResponse } from "../bridge";
|
|
2
|
+
import {
|
|
3
|
+
CodexAccountCooldownError,
|
|
4
|
+
CodexMainProfileDrainingError,
|
|
5
|
+
codexMainProfileDrainingResponse,
|
|
6
|
+
cooldownErrorResponse,
|
|
7
|
+
isCodexAuthContextUsable,
|
|
8
|
+
materializeCodexUpstreamAuth,
|
|
9
|
+
releaseCodexAuthContextProbeLease,
|
|
10
|
+
resolveCodexAuthContext,
|
|
11
|
+
type CodexAuthContext,
|
|
12
|
+
} from "../codex/auth-context";
|
|
13
|
+
import { formatCodexProviderForLog } from "../codex/routing";
|
|
14
|
+
import type { AdmissionLease } from "../lib/admission";
|
|
15
|
+
import { captureExplicitOpenAiCallerAuth, resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../providers/openai-sidecar";
|
|
16
|
+
import type { OcxConfig } from "../types";
|
|
17
|
+
import {
|
|
18
|
+
isProxyAdmissionSecret,
|
|
19
|
+
resolveDataPlaneAdmissionSecret,
|
|
20
|
+
validateForwardAdmissionCredential,
|
|
21
|
+
type DataPlaneAdmission,
|
|
22
|
+
} from "./auth-cors";
|
|
23
|
+
import { codexAccountSelectionForTurn } from "./lifecycle";
|
|
24
|
+
import type { RequestLogContext } from "./request-log";
|
|
25
|
+
|
|
26
|
+
export const TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
|
|
27
|
+
export const LIVE_AUDIO_MODEL = "gpt-live-1-codex";
|
|
28
|
+
|
|
29
|
+
/** Audio keys remain identifiable even on the otherwise unauthenticated local listener. */
|
|
30
|
+
export function resolveAudioAdmission(headers: Headers, config: OcxConfig): DataPlaneAdmission | null {
|
|
31
|
+
const dedicated = headers.get("x-opencodex-api-key")?.trim();
|
|
32
|
+
if (dedicated) return resolveDataPlaneAdmissionSecret(dedicated, config, "dedicated");
|
|
33
|
+
const authorization = headers.get("authorization")?.trim();
|
|
34
|
+
if (authorization) {
|
|
35
|
+
const token = /^Bearer\s+([^\s,]+)$/i.exec(authorization)?.[1];
|
|
36
|
+
return token ? resolveDataPlaneAdmissionSecret(token, config, "bearer") : null;
|
|
37
|
+
}
|
|
38
|
+
const key = headers.get("x-api-key")?.trim();
|
|
39
|
+
return key ? resolveDataPlaneAdmissionSecret(key, config, "x-api-key") : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface AudioUpstream {
|
|
43
|
+
providerName: string;
|
|
44
|
+
providerBaseUrl: string;
|
|
45
|
+
headers: Record<string, string>;
|
|
46
|
+
keyed: boolean;
|
|
47
|
+
authContext?: CodexAuthContext;
|
|
48
|
+
recordOutcome?: (status: number | "timeout" | "connect_error") => void;
|
|
49
|
+
release: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AudioUpstreamOptions {
|
|
53
|
+
admission: DataPlaneAdmission;
|
|
54
|
+
model: string;
|
|
55
|
+
lease?: AdmissionLease;
|
|
56
|
+
exactAccountId?: string;
|
|
57
|
+
providerName?: string;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Keep client admission, account selection, and outbound credentials as separate values. */
|
|
62
|
+
export async function resolveAudioUpstream(
|
|
63
|
+
incoming: Headers,
|
|
64
|
+
config: OcxConfig,
|
|
65
|
+
log: RequestLogContext,
|
|
66
|
+
options: AudioUpstreamOptions,
|
|
67
|
+
): Promise<AudioUpstream | Response> {
|
|
68
|
+
const candidates = selectOpenAiImagesProvider(config);
|
|
69
|
+
if (options.providerName) {
|
|
70
|
+
candidates.forwardCandidates = candidates.forwardCandidates.filter(candidate => candidate.providerName === options.providerName);
|
|
71
|
+
if (candidates.keyed?.providerName !== options.providerName) delete candidates.keyed;
|
|
72
|
+
}
|
|
73
|
+
const headers = new Headers(incoming);
|
|
74
|
+
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
75
|
+
if (bearer && isProxyAdmissionSecret(bearer, config)) {
|
|
76
|
+
headers.delete("authorization");
|
|
77
|
+
headers.delete("chatgpt-account-id");
|
|
78
|
+
}
|
|
79
|
+
headers.delete("x-opencodex-api-key");
|
|
80
|
+
headers.delete("x-api-key");
|
|
81
|
+
headers.delete("sec-websocket-protocol");
|
|
82
|
+
let context: CodexAuthContext | undefined;
|
|
83
|
+
try {
|
|
84
|
+
options.signal?.throwIfAborted();
|
|
85
|
+
const candidate = candidates.forwardCandidates[0];
|
|
86
|
+
if (candidate) {
|
|
87
|
+
let selected: Headers;
|
|
88
|
+
let recordOutcome: AudioUpstream["recordOutcome"];
|
|
89
|
+
const beginCodexAccountSelection = codexAccountSelectionForTurn(options.lease);
|
|
90
|
+
if (candidate.accountMode === "direct" && options.exactAccountId === undefined
|
|
91
|
+
&& !captureExplicitOpenAiCallerAuth(incoming, config)) {
|
|
92
|
+
context = await resolveCodexAuthContext(headers, config, "direct", {
|
|
93
|
+
admission: options.admission,
|
|
94
|
+
substituteMainCredentialForDirect: true,
|
|
95
|
+
beginCodexAccountSelection,
|
|
96
|
+
signal: options.signal,
|
|
97
|
+
});
|
|
98
|
+
options.signal?.throwIfAborted();
|
|
99
|
+
selected = materializeCodexUpstreamAuth(headers, context, {
|
|
100
|
+
config,
|
|
101
|
+
admission: options.admission,
|
|
102
|
+
substituteMainCredential: true,
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
const resolved = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, headers, config, {
|
|
106
|
+
admission: options.admission,
|
|
107
|
+
beginCodexAccountSelection,
|
|
108
|
+
signal: options.signal,
|
|
109
|
+
...(options.exactAccountId ? { exactAccount: { accountId: options.exactAccountId, modelId: options.model } } : {}),
|
|
110
|
+
});
|
|
111
|
+
if (!resolved) return formatErrorResponse(401, "authentication_error", "Connect a ChatGPT account to use audio");
|
|
112
|
+
context = resolved.authContext;
|
|
113
|
+
selected = resolved.headers;
|
|
114
|
+
recordOutcome = resolved.recordOutcome;
|
|
115
|
+
}
|
|
116
|
+
options.signal?.throwIfAborted();
|
|
117
|
+
if (!isCodexAuthContextUsable(context, config)) {
|
|
118
|
+
releaseCodexAuthContextProbeLease(context);
|
|
119
|
+
return formatErrorResponse(401, "authentication_error", "Selected audio account is unavailable");
|
|
120
|
+
}
|
|
121
|
+
validateForwardAdmissionCredential(selected, config);
|
|
122
|
+
log.provider = formatCodexProviderForLog(candidate.providerName, context.accountId, config);
|
|
123
|
+
log.model = options.model;
|
|
124
|
+
return {
|
|
125
|
+
providerName: candidate.providerName,
|
|
126
|
+
providerBaseUrl: candidate.provider.baseUrl,
|
|
127
|
+
headers: Object.fromEntries(selected),
|
|
128
|
+
keyed: false,
|
|
129
|
+
authContext: context,
|
|
130
|
+
recordOutcome,
|
|
131
|
+
release: () => releaseCodexAuthContextProbeLease(context),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
if (candidates.keyed && options.exactAccountId === undefined) {
|
|
135
|
+
const { providerName, provider, apiKey } = candidates.keyed;
|
|
136
|
+
const selected = new Headers(provider.headers);
|
|
137
|
+
selected.set("authorization", `Bearer ${apiKey}`);
|
|
138
|
+
validateForwardAdmissionCredential(selected, config);
|
|
139
|
+
log.provider = providerName;
|
|
140
|
+
log.model = options.model;
|
|
141
|
+
return {
|
|
142
|
+
providerName, providerBaseUrl: provider.baseUrl, headers: Object.fromEntries(selected),
|
|
143
|
+
keyed: true, release: () => {},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return formatErrorResponse(400, "invalid_request_error", "Audio requires a connected ChatGPT account or OpenAI API provider");
|
|
147
|
+
} catch (error) {
|
|
148
|
+
releaseCodexAuthContextProbeLease(context);
|
|
149
|
+
if (error instanceof CodexAccountCooldownError) return cooldownErrorResponse(error);
|
|
150
|
+
if (error instanceof CodexMainProfileDrainingError) return codexMainProfileDrainingResponse();
|
|
151
|
+
return formatErrorResponse(401, "authentication_error", "Audio account authentication unavailable; reconnect the selected OpenAI account");
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
1
2
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
3
|
import { initialModelSelection } from "../providers/initial-model-selection";
|
|
3
4
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
} from "../config";
|
|
14
15
|
import {
|
|
15
16
|
apiKeyTransportConfigError,
|
|
17
|
+
autoReviewModelOverridesConfigError,
|
|
18
|
+
autoReviewModelTargetConfigError,
|
|
16
19
|
booleanRecordConfigError,
|
|
17
20
|
providerReasoningPinsConfigError,
|
|
18
21
|
modelAdapterRecordConfigError,
|
|
@@ -482,6 +485,9 @@ export interface ApiAuthMatrixRow {
|
|
|
482
485
|
* against every cell rather than reading the table back to itself.
|
|
483
486
|
*/
|
|
484
487
|
export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
|
|
488
|
+
{ endpoint: "/v1/audio/transcriptions", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
489
|
+
{ endpoint: "/v1/live", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
490
|
+
{ endpoint: "/v1/realtime/calls", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
485
491
|
// #1686: a bearer that is one of OUR admission secrets is now accepted here. It is safe
|
|
486
492
|
// because materializeCodexUpstreamAuth substitutes the stored main credential rather than
|
|
487
493
|
// forwarding it; a bearer that is NOT our secret stays unadmitted and remains Codex Direct
|
|
@@ -499,6 +505,8 @@ export const AUTH_MATRIX: readonly ApiAuthMatrixRow[] = [
|
|
|
499
505
|
// it forwards no caller credential upstream and its body is booleans plus model ids — and it
|
|
500
506
|
// 404s on any host whose runtimeRole is not "hub", so no standalone install gains a surface.
|
|
501
507
|
{ endpoint: "/v1/hub-state", bearer: "accepted", dedicated: "accepted", xApiKey: "accepted" },
|
|
508
|
+
// Usage additionally requires a configured key identity; unscoped environment keys are refused.
|
|
509
|
+
{ endpoint: "/v1/usage", bearer: "rejected", dedicated: "accepted", xApiKey: "rejected" },
|
|
502
510
|
];
|
|
503
511
|
|
|
504
512
|
/** Whether `token` is the environment-provided management secret. */
|
|
@@ -602,6 +610,23 @@ function sameCanonicalProviderSeed(actual: Record<string, unknown>, expected: Oc
|
|
|
602
610
|
return actualKeys.every(key => JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]));
|
|
603
611
|
}
|
|
604
612
|
|
|
613
|
+
/**
|
|
614
|
+
* Operator-overlay tolerant variant of the canonical seed check: every key the registry
|
|
615
|
+
* seed defines must still match the submitted provider verbatim, but keys the seed never
|
|
616
|
+
* defines are ignored instead of failing the comparison. Field-masked writes (PATCH,
|
|
617
|
+
* the provider editor, reload) merge onto the persisted row, so the submitted candidate
|
|
618
|
+
* legitimately carries stored operator overlays like `selectedModels` or `disabled`.
|
|
619
|
+
* Those fields are validated by their own write boundaries and cannot widen what the
|
|
620
|
+
* forward proxy claims. Full-object writes (POST) keep the strict exact-key comparison
|
|
621
|
+
* so a forged overlay cannot ride in on a canonical transport seed.
|
|
622
|
+
*/
|
|
623
|
+
function matchesCanonicalProviderSeed(actual: Record<string, unknown>, expected: OcxProviderConfig): boolean {
|
|
624
|
+
return Object.keys(expected).every(
|
|
625
|
+
key => Object.hasOwn(actual, key)
|
|
626
|
+
&& JSON.stringify(actual[key]) === JSON.stringify((expected as unknown as Record<string, unknown>)[key]),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
|
|
605
630
|
function positiveWindowValue(value: unknown): boolean {
|
|
606
631
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
607
632
|
}
|
|
@@ -638,13 +663,41 @@ function nativeContextOverlayError(raw: Record<string, unknown>): string | null
|
|
|
638
663
|
* string, or null when the provider may be persisted. Caller-controlled names/fields are
|
|
639
664
|
* redacted and JSON-escaped so secrets never reach the response.
|
|
640
665
|
*/
|
|
641
|
-
export function providerManagementConfigError(
|
|
666
|
+
export function providerManagementConfigError(
|
|
667
|
+
name: unknown,
|
|
668
|
+
provider: unknown,
|
|
669
|
+
options?: { allowOperatorOverlays?: boolean },
|
|
670
|
+
): string | null {
|
|
642
671
|
if (typeof name !== "string" || !provider || typeof provider !== "object" || Array.isArray(provider)) {
|
|
643
672
|
return "provider must be a plain object";
|
|
644
673
|
}
|
|
645
674
|
const raw = provider as Record<string, unknown>;
|
|
675
|
+
const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities);
|
|
676
|
+
if (capabilitiesError) return capabilitiesError;
|
|
646
677
|
const pinsError = providerReasoningPinsConfigError(raw);
|
|
647
678
|
if (pinsError) return pinsError;
|
|
679
|
+
if (name === "openai" && (Object.hasOwn(raw, "autoReviewModel") || Object.hasOwn(raw, "autoReviewModelOverrides"))) {
|
|
680
|
+
return "provider openai must not include autoReviewModel or autoReviewModelOverrides";
|
|
681
|
+
}
|
|
682
|
+
// Canonical OpenAI is the ChatGPT forward seed. allowPrivateNetwork is the explicit
|
|
683
|
+
// opt-in that skips destination DNS classification (loopback, RFC1918, metadata).
|
|
684
|
+
// Overlay-tolerant comparison would otherwise treat it as an extra key and persist it.
|
|
685
|
+
if (name === "openai" && Object.hasOwn(raw, "allowPrivateNetwork")) {
|
|
686
|
+
return "provider openai must not include allowPrivateNetwork";
|
|
687
|
+
}
|
|
688
|
+
// The same reasoning applies to `headers`, and it is not hypothetical. Canonical OpenAI
|
|
689
|
+
// has no registry `staticHeaders`, so any header block on this row is operator-authored,
|
|
690
|
+
// and the forward adapter copies it onto the ChatGPT request BEFORE the incoming forward
|
|
691
|
+
// headers — a persisted value therefore wins whenever the caller omits that header. The
|
|
692
|
+
// exact-key comparison rejected it as an extra key; overlay tolerance would silently admit
|
|
693
|
+
// it on every merge-based write path while POST still refused it.
|
|
694
|
+
if (name === "openai" && Object.hasOwn(raw, "headers")) {
|
|
695
|
+
return "provider openai must not include headers";
|
|
696
|
+
}
|
|
697
|
+
const autoReviewTargetError = autoReviewModelTargetConfigError(raw.autoReviewModel, "autoReviewModel", true);
|
|
698
|
+
if (autoReviewTargetError) return autoReviewTargetError;
|
|
699
|
+
const autoReviewMapError = autoReviewModelOverridesConfigError(raw.autoReviewModelOverrides, "autoReviewModelOverrides", true);
|
|
700
|
+
if (autoReviewMapError) return autoReviewMapError;
|
|
648
701
|
for (const field of FORBIDDEN_PROVIDER_RUNTIME_FIELDS) {
|
|
649
702
|
if (Object.hasOwn(raw, field)) return `provider ${name} must not include runtime field "${field}"`;
|
|
650
703
|
}
|
|
@@ -683,7 +736,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
683
736
|
// validation and then rejected by the seed comparison, so canonical OpenAI could never
|
|
684
737
|
// set OR clear it — the value was admitted and then refused in the same request.
|
|
685
738
|
delete canonicalCandidate.annotateEmptyToolOutputs;
|
|
686
|
-
const canonical = seed &&
|
|
739
|
+
const canonical = seed && (options?.allowOperatorOverlays
|
|
740
|
+
? matchesCanonicalProviderSeed(canonicalCandidate, seed)
|
|
741
|
+
: sameCanonicalProviderSeed(canonicalCandidate, seed));
|
|
687
742
|
if (!canonical) {
|
|
688
743
|
return `provider ${name} must equal the canonical built-in provider seed`;
|
|
689
744
|
}
|
|
@@ -888,6 +943,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
888
943
|
contextWindow: "editor",
|
|
889
944
|
modelContextWindows: "editor",
|
|
890
945
|
modelInputModalities: "editor",
|
|
946
|
+
modelCapabilities: "editor",
|
|
891
947
|
modelMaxInputTokens: "runtime",
|
|
892
948
|
modelAutoCompactTokenLimits: "editor",
|
|
893
949
|
defaultMaxOutputTokens: "editor",
|
|
@@ -910,6 +966,8 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
910
966
|
modelDefaultReasoningEfforts: "editor",
|
|
911
967
|
pinnedReasoningEffort: "editor",
|
|
912
968
|
modelPinnedReasoningEfforts: "editor",
|
|
969
|
+
autoReviewModel: "editor",
|
|
970
|
+
autoReviewModelOverrides: "editor",
|
|
913
971
|
modelSupportsReasoningSummaries: "editor",
|
|
914
972
|
modelSupportsVerbosity: "editor",
|
|
915
973
|
supportsVerbosity: "editor",
|
|
@@ -942,6 +1000,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
942
1000
|
autoToolChoiceOnlyModels: "editor",
|
|
943
1001
|
preserveReasoningContentModels: "editor",
|
|
944
1002
|
requiresReasoningPlaceholderModels: "editor",
|
|
1003
|
+
showThinkingSummary: "editor",
|
|
945
1004
|
retryOn429: "editor",
|
|
946
1005
|
transientRetryOn5xx: "editor",
|
|
947
1006
|
reasoningSplitModels: "editor",
|
|
@@ -288,7 +288,7 @@ async function handleChatCompletionsWithBudget(
|
|
|
288
288
|
try {
|
|
289
289
|
internalBodyJson = JSON.stringify(internalBody);
|
|
290
290
|
translatorBudget.chargeRetained(
|
|
291
|
-
|
|
291
|
+
Buffer.byteLength(internalBodyJson, "utf8"),
|
|
292
292
|
{ kind: "request_copies" },
|
|
293
293
|
);
|
|
294
294
|
} catch (err) {
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
type TranslatorBudget,
|
|
8
8
|
} from "../lib/translator-budget";
|
|
9
9
|
import type { OcxUsage } from "../types";
|
|
10
|
-
import {
|
|
10
|
+
import { resolveStallTimeoutSec } from "../stall-timeout";
|
|
11
|
+
import { createSseBlockBuffer, replaceSseDataPayload, sseDataPayload } from "./sse-payload-rewrite";
|
|
11
12
|
|
|
12
13
|
type Rec = Record<string, unknown>;
|
|
13
14
|
|
|
@@ -65,6 +66,10 @@ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: T
|
|
|
65
66
|
const id = typeof value.id === "string" ? value.id : `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
|
|
66
67
|
const created = typeof value.created === "number" ? value.created : Math.floor(Date.now() / 1000);
|
|
67
68
|
const model = requestedModel;
|
|
69
|
+
// Relay the upstream service-tier echo on every chunk, matching how OpenAI/xAI
|
|
70
|
+
// annotate streamed chat chunks, so streaming callers see the same confirmation
|
|
71
|
+
// the non-streaming body carries.
|
|
72
|
+
const serviceTier = typeof value.service_tier === "string" ? { service_tier: value.service_tier } : {};
|
|
68
73
|
const choices = Array.isArray(value.choices) ? value.choices : [];
|
|
69
74
|
const choice = isRec(choices[0]) ? choices[0] : {};
|
|
70
75
|
const message = isRec(choice.message) ? choice.message : {};
|
|
@@ -73,6 +78,7 @@ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: T
|
|
|
73
78
|
object: "chat.completion.chunk",
|
|
74
79
|
created,
|
|
75
80
|
model,
|
|
81
|
+
...serviceTier,
|
|
76
82
|
choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }],
|
|
77
83
|
}];
|
|
78
84
|
const delta: Rec = {};
|
|
@@ -85,13 +91,14 @@ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: T
|
|
|
85
91
|
delta.tool_calls = message.tool_calls.filter(isRec).map((tool, index) => ({ ...tool, index }));
|
|
86
92
|
}
|
|
87
93
|
if (Object.keys(delta).length > 0) {
|
|
88
|
-
frames.push({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] });
|
|
94
|
+
frames.push({ id, object: "chat.completion.chunk", created, model, ...serviceTier, choices: [{ index: 0, delta, finish_reason: null }] });
|
|
89
95
|
}
|
|
90
96
|
frames.push({
|
|
91
97
|
id,
|
|
92
98
|
object: "chat.completion.chunk",
|
|
93
99
|
created,
|
|
94
100
|
model,
|
|
101
|
+
...serviceTier,
|
|
95
102
|
choices: [{ index: 0, delta: {}, finish_reason: typeof choice.finish_reason === "string" ? choice.finish_reason : "stop" }],
|
|
96
103
|
...(value.usage !== undefined ? { usage: value.usage } : {}),
|
|
97
104
|
});
|
|
@@ -121,6 +128,7 @@ interface NativeChatSseOptions {
|
|
|
121
128
|
requestedModel: string;
|
|
122
129
|
translatorBudget: TranslatorBudget;
|
|
123
130
|
signal: AbortSignal;
|
|
131
|
+
stallTimeoutSec?: number;
|
|
124
132
|
onFirstOutput?: () => void;
|
|
125
133
|
onUsage: (usage: OcxUsage) => void;
|
|
126
134
|
onTerminal?: (status: number, message?: string) => void;
|
|
@@ -135,8 +143,11 @@ export function nativeChatSse(
|
|
|
135
143
|
const decoder = new TextDecoder();
|
|
136
144
|
const encoder = new TextEncoder();
|
|
137
145
|
const scope = { kind: "live_transient" as const };
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
const buffer = createSseBlockBuffer(options.translatorBudget, nextBytes => {
|
|
147
|
+
if (nextBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
|
|
148
|
+
throw new Error("upstream SSE event exceeded the safe limit", { cause: { code: "translation_buffer_limit" } });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
140
151
|
let queuedBytes = 0;
|
|
141
152
|
let sawFinish = false;
|
|
142
153
|
let sawDone = false;
|
|
@@ -144,50 +155,31 @@ export function nativeChatSse(
|
|
|
144
155
|
let settled = false;
|
|
145
156
|
let cancelled = false;
|
|
146
157
|
let cancelledBySignal = false;
|
|
158
|
+
const stallMs = resolveStallTimeoutSec(options.stallTimeoutSec) * 1_000;
|
|
159
|
+
// Count only active upstream consumption; downstream backpressure must not
|
|
160
|
+
// consume the provider's inactivity allowance. Comments and empty deltas do.
|
|
161
|
+
let remainingStallMs = stallMs;
|
|
162
|
+
let pullDeadline = 0;
|
|
163
|
+
const stalled = new Error("upstream stream stalled without meaningful progress");
|
|
147
164
|
|
|
148
165
|
const releaseQueued = () => {
|
|
149
166
|
if (queuedBytes === 0) return;
|
|
150
167
|
options.translatorBudget.releaseRetained(queuedBytes, scope);
|
|
151
168
|
queuedBytes = 0;
|
|
152
169
|
};
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
const reservation = options.translatorBudget.reserveTransient(
|
|
156
|
-
reservation.commitRetained();
|
|
157
|
-
options.translatorBudget.releaseRetained(bufferBytes, scope);
|
|
158
|
-
buffer = next;
|
|
159
|
-
bufferBytes = nextBytes;
|
|
160
|
-
};
|
|
161
|
-
const appendBuffer = (fragment: string) => {
|
|
162
|
-
if (!fragment) return;
|
|
163
|
-
const fragmentBytes = encoder.encode(fragment).byteLength;
|
|
164
|
-
const nextBytes = bufferBytes + fragmentBytes;
|
|
165
|
-
if (nextBytes > TRANSLATOR_MAX_SSE_EVENT_BYTES) {
|
|
166
|
-
throw new Error("upstream SSE event exceeded the safe limit", { cause: { code: "translation_buffer_limit" } });
|
|
167
|
-
}
|
|
168
|
-
const reservation = options.translatorBudget.reserveTransient(nextBytes, scope);
|
|
170
|
+
const enqueue = (controller: ReadableStreamDefaultController<Uint8Array>, text: string) => {
|
|
171
|
+
const byteLength = Buffer.byteLength(text);
|
|
172
|
+
const reservation = options.translatorBudget.reserveTransient(byteLength, scope);
|
|
169
173
|
try {
|
|
170
|
-
|
|
174
|
+
controller.enqueue(encoder.encode(text));
|
|
171
175
|
reservation.commitRetained();
|
|
172
|
-
|
|
173
|
-
bufferBytes = nextBytes;
|
|
176
|
+
queuedBytes += byteLength;
|
|
174
177
|
} catch (error) {
|
|
175
178
|
reservation.release();
|
|
176
179
|
throw error;
|
|
177
180
|
}
|
|
178
181
|
};
|
|
179
|
-
const
|
|
180
|
-
const bytes = encoder.encode(text);
|
|
181
|
-
const reservation = options.translatorBudget.reserveTransient(bytes.byteLength, scope);
|
|
182
|
-
controller.enqueue(bytes);
|
|
183
|
-
reservation.commitRetained();
|
|
184
|
-
queuedBytes += bytes.byteLength;
|
|
185
|
-
};
|
|
186
|
-
const releaseBuffer = () => {
|
|
187
|
-
options.translatorBudget.releaseRetained(bufferBytes, scope);
|
|
188
|
-
buffer = "";
|
|
189
|
-
bufferBytes = 0;
|
|
190
|
-
};
|
|
182
|
+
const releaseBuffer = () => buffer.clear();
|
|
191
183
|
const settle = (status: number, message?: string) => {
|
|
192
184
|
if (settled) return;
|
|
193
185
|
settled = true;
|
|
@@ -219,13 +211,20 @@ export function nativeChatSse(
|
|
|
219
211
|
): void => {
|
|
220
212
|
const payload = sseDataPayload(block);
|
|
221
213
|
if (payload === null) {
|
|
214
|
+
if (performance.now() >= pullDeadline) throw stalled;
|
|
222
215
|
enqueue(controller, block + delimiter);
|
|
223
216
|
return;
|
|
224
217
|
}
|
|
225
218
|
const trimmed = payload.trim();
|
|
219
|
+
if (trimmed === "") {
|
|
220
|
+
if (performance.now() >= pullDeadline) throw stalled;
|
|
221
|
+
enqueue(controller, block + delimiter);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
226
224
|
if (trimmed === "[DONE]") {
|
|
227
225
|
sawDone = true;
|
|
228
226
|
enqueue(controller, replaceSseDataPayload(block, "[DONE]") + delimiter);
|
|
227
|
+
releaseBuffer();
|
|
229
228
|
settle(200);
|
|
230
229
|
try { void reader.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
231
230
|
controller.close();
|
|
@@ -254,6 +253,7 @@ export function nativeChatSse(
|
|
|
254
253
|
}
|
|
255
254
|
const safe = chatCompletionsErrorBody(status, classified.message, classified.type, classified.code);
|
|
256
255
|
enqueue(controller, replaceSseDataPayload(block, JSON.stringify(safe)) + delimiter);
|
|
256
|
+
releaseBuffer();
|
|
257
257
|
settle(isCyberPolicyCode(classified.code) ? 400 : status, classified.message);
|
|
258
258
|
try { void reader.cancel(new Error(classified.message)).catch(() => {}); } catch { /* already closed */ }
|
|
259
259
|
controller.close();
|
|
@@ -262,16 +262,28 @@ export function nativeChatSse(
|
|
|
262
262
|
const usage = usageFromChat(parsed.usage);
|
|
263
263
|
if (usage) options.onUsage(usage);
|
|
264
264
|
const choices = Array.isArray(parsed.choices) ? parsed.choices : [];
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (!firstOutput && choices.some(choice => {
|
|
265
|
+
const finished = choices.some(choice => isRec(choice) && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0);
|
|
266
|
+
if (finished) sawFinish = true;
|
|
267
|
+
const hasProgress = choices.some(choice => {
|
|
269
268
|
if (!isRec(choice) || !isRec(choice.delta)) return false;
|
|
270
269
|
const delta = choice.delta;
|
|
271
270
|
return (typeof delta.content === "string" && delta.content.length > 0)
|
|
272
271
|
|| (typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0)
|
|
273
|
-
|| (
|
|
274
|
-
|
|
272
|
+
|| (typeof delta.reasoning === "string" && delta.reasoning.length > 0)
|
|
273
|
+
|| (Array.isArray(delta.reasoning_details) && delta.reasoning_details.some(detail =>
|
|
274
|
+
isRec(detail) && typeof detail.text === "string" && detail.text.length > 0))
|
|
275
|
+
|| (typeof delta.refusal === "string" && delta.refusal.length > 0)
|
|
276
|
+
|| (Array.isArray(delta.tool_calls) && delta.tool_calls.some(tool => {
|
|
277
|
+
if (!isRec(tool)) return false;
|
|
278
|
+
const fn = isRec(tool.function) ? tool.function : {};
|
|
279
|
+
return (typeof tool.id === "string" && tool.id.length > 0)
|
|
280
|
+
|| (typeof fn.name === "string" && fn.name.length > 0)
|
|
281
|
+
|| (typeof fn.arguments === "string" && fn.arguments.length > 0);
|
|
282
|
+
}));
|
|
283
|
+
});
|
|
284
|
+
if (hasProgress || finished) pullDeadline = performance.now() + stallMs;
|
|
285
|
+
else if (performance.now() >= pullDeadline) throw stalled;
|
|
286
|
+
if (!firstOutput && hasProgress) {
|
|
275
287
|
firstOutput = true;
|
|
276
288
|
options.onFirstOutput?.();
|
|
277
289
|
}
|
|
@@ -280,6 +292,7 @@ export function nativeChatSse(
|
|
|
280
292
|
|
|
281
293
|
function onAbort() {
|
|
282
294
|
cancelledBySignal = true;
|
|
295
|
+
releaseBuffer();
|
|
283
296
|
if (!settled) {
|
|
284
297
|
settled = true;
|
|
285
298
|
options.onCancel?.();
|
|
@@ -292,15 +305,39 @@ export function nativeChatSse(
|
|
|
292
305
|
return new ReadableStream<Uint8Array>({
|
|
293
306
|
async pull(controller) {
|
|
294
307
|
releaseQueued();
|
|
308
|
+
pullDeadline = performance.now() + remainingStallMs;
|
|
295
309
|
try {
|
|
296
310
|
for (;;) {
|
|
297
|
-
|
|
311
|
+
if (cancelledBySignal) {
|
|
312
|
+
releaseBuffer();
|
|
313
|
+
controller.close();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const next = buffer.next();
|
|
298
317
|
if (next) {
|
|
299
|
-
|
|
318
|
+
buffer.compact();
|
|
300
319
|
processBlock(controller, next.block, next.delimiter);
|
|
301
320
|
return;
|
|
302
321
|
}
|
|
303
|
-
|
|
322
|
+
if (performance.now() >= pullDeadline) throw stalled;
|
|
323
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
324
|
+
let read: Awaited<ReturnType<typeof reader.read>>;
|
|
325
|
+
try {
|
|
326
|
+
read = await Promise.race([
|
|
327
|
+
reader.read(),
|
|
328
|
+
new Promise<never>((_, reject) => {
|
|
329
|
+
const check = () => {
|
|
330
|
+
const remaining = pullDeadline - performance.now();
|
|
331
|
+
if (remaining <= 0) reject(stalled);
|
|
332
|
+
else timeout = setTimeout(check, Math.min(remaining, 2_147_483_647));
|
|
333
|
+
};
|
|
334
|
+
check();
|
|
335
|
+
}),
|
|
336
|
+
]);
|
|
337
|
+
} finally {
|
|
338
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
339
|
+
}
|
|
340
|
+
const { done, value } = read;
|
|
304
341
|
if (cancelled) return;
|
|
305
342
|
if (cancelledBySignal) {
|
|
306
343
|
releaseBuffer();
|
|
@@ -308,11 +345,11 @@ export function nativeChatSse(
|
|
|
308
345
|
return;
|
|
309
346
|
}
|
|
310
347
|
if (!done) {
|
|
311
|
-
|
|
348
|
+
buffer.append(decoder.decode(value, { stream: true }));
|
|
312
349
|
continue;
|
|
313
350
|
}
|
|
314
|
-
|
|
315
|
-
if (buffer.trim().length > 0) {
|
|
351
|
+
buffer.append(decoder.decode());
|
|
352
|
+
if (buffer.tail().trim().length > 0) {
|
|
316
353
|
fail(controller, "upstream SSE ended with an unterminated event", "upstream_sse_unterminated");
|
|
317
354
|
return;
|
|
318
355
|
}
|
|
@@ -327,13 +364,20 @@ export function nativeChatSse(
|
|
|
327
364
|
return;
|
|
328
365
|
}
|
|
329
366
|
} catch (error) {
|
|
367
|
+
if (cancelled || cancelledBySignal) {
|
|
368
|
+
releaseBuffer();
|
|
369
|
+
try { controller.close(); } catch { /* already closed */ }
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
330
372
|
const overflow = isTranslatorBudgetExceededError(error)
|
|
331
373
|
|| (error instanceof Error && (error.cause as { code?: unknown } | undefined)?.code === "translation_buffer_limit");
|
|
332
374
|
fail(
|
|
333
375
|
controller,
|
|
334
376
|
overflow ? "upstream SSE event exceeded the safe limit" : error instanceof Error ? error.message : String(error),
|
|
335
|
-
overflow ? "translation_buffer_limit" : "upstream_sse_error",
|
|
377
|
+
overflow ? "translation_buffer_limit" : error === stalled ? "upstream_stall_timeout" : "upstream_sse_error",
|
|
336
378
|
);
|
|
379
|
+
} finally {
|
|
380
|
+
remainingStallMs = Math.max(0, pullDeadline - performance.now());
|
|
337
381
|
}
|
|
338
382
|
},
|
|
339
383
|
cancel(reason) {
|