@bitkyc08/opencodex 2.18.2 → 2.20.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/gui/dist/assets/index-DF_UFrGS.css +1 -0
- package/gui/dist/assets/index-DSK3S5HY.js +76 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/client-fingerprint.ts +14 -10
- package/src/adapters/google-antigravity-wire.ts +4 -3
- package/src/adapters/google.ts +1 -1
- package/src/adapters/mimo-free.ts +17 -0
- package/src/adapters/openai-chat.ts +367 -32
- package/src/adapters/registry.ts +144 -0
- package/src/chat/inbound.ts +37 -8
- package/src/cli/claude.ts +2 -1
- package/src/cli/combo.ts +3 -0
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/export-command.ts +2 -2
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +3 -2
- package/src/cli/lab.ts +135 -1
- package/src/cli/minimax.ts +491 -0
- package/src/cli/models-runtime.ts +22 -1
- package/src/cli/models.ts +67 -2
- package/src/cli/opencode.ts +2 -1
- package/src/cli/registry.ts +22 -2
- package/src/clients/config-export.ts +125 -7
- package/src/codex/app-server-processes.ts +57 -2
- package/src/codex/app-server-restart-service.ts +232 -0
- package/src/codex/catalog/aggregation.ts +10 -1
- package/src/codex/catalog/effort.ts +15 -3
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +45 -5
- package/src/codex/catalog/sync.ts +74 -4
- package/src/codex/convergence.ts +2 -0
- package/src/codex/shim.ts +100 -5
- package/src/combos/index.ts +1 -0
- package/src/combos/request.ts +30 -0
- package/src/combos/types.ts +6 -0
- package/src/config.ts +52 -0
- package/src/generated/compatibility-version.json +239 -87
- package/src/images/loop.ts +11 -1
- package/src/integrations/registry.ts +7 -0
- package/src/lab/conformance/jcs.ts +42 -2
- package/src/lab/conformance/negative-controls.ts +6 -2
- package/src/lab/conformance/runner.ts +16 -5
- package/src/lab/fabric/observe.ts +49 -14
- package/src/lab/index.ts +16 -0
- package/src/lab/ledger/purge.ts +152 -83
- package/src/lab/ledger/store.ts +168 -54
- package/src/lab/observe/from-conformance.ts +8 -6
- package/src/lab/observe/from-live.ts +8 -2
- package/src/lab/paths.ts +23 -0
- package/src/lab/public/bundle.ts +217 -0
- package/src/lab/public/community-authority.ts +175 -0
- package/src/lab/public/community-files.ts +29 -0
- package/src/lab/public/community.ts +479 -0
- package/src/lab/public/file-safety.ts +155 -0
- package/src/lab/public/ids.ts +26 -0
- package/src/lab/public/index.ts +16 -0
- package/src/lab/public/mutation-lock.ts +424 -0
- package/src/lab/public/operator.ts +353 -0
- package/src/lab/public/origin-purge.ts +79 -0
- package/src/lab/public/origin.ts +203 -0
- package/src/lab/public/privacy.ts +143 -0
- package/src/lab/public/private-file.ts +261 -0
- package/src/lab/public/project.ts +124 -0
- package/src/lab/public/purge-test-fault.ts +21 -0
- package/src/lab/public/purge.ts +223 -0
- package/src/lab/public/registry.ts +44 -0
- package/src/lab/public/revocation.ts +252 -0
- package/src/lab/public/signature.ts +219 -0
- package/src/lab/public/storage.ts +105 -0
- package/src/lab/public/strict-json.ts +206 -0
- package/src/lab/public/time.ts +26 -0
- package/src/lab/public/types.ts +172 -0
- package/src/lab/public/validate.ts +391 -0
- package/src/lib/codex-restart-contract.ts +120 -0
- package/src/lib/errors.ts +27 -0
- package/src/lib/lab-activation.ts +109 -47
- package/src/lib/lab-live-pinned-sender.ts +16 -5
- package/src/lib/pinned-http.ts +70 -16
- package/src/lib/self-launch-argv.ts +15 -0
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/upstream-reachability.ts +4 -0
- package/src/lib/windows-elevation.ts +10 -1
- package/src/providers/derive.ts +24 -4
- package/src/providers/registry.ts +9 -3
- package/src/providers/request-pacing.ts +310 -0
- package/src/providers/service-tier.ts +143 -0
- package/src/providers/static-model-discovery.ts +86 -0
- package/src/reasoning-effort.ts +27 -1
- package/src/responses/spill-store.ts +20 -1
- package/src/responses/state.ts +159 -3
- package/src/router.ts +23 -6
- package/src/routing/capability.ts +4 -2
- package/src/routing/compatibility/behavior.ts +5 -1
- package/src/server/adapter-resolve.ts +2 -32
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +74 -34
- package/src/server/chat-native-sse.ts +331 -0
- package/src/server/chat-native.ts +371 -0
- package/src/server/effort-policy.ts +18 -0
- package/src/server/management/combo-routes.ts +16 -2
- package/src/server/management/config-routes.ts +6 -4
- package/src/server/management/context.ts +17 -0
- package/src/server/management/lab-routes.ts +181 -19
- package/src/server/management/model-routes.ts +76 -2
- package/src/server/management/model-rows.ts +8 -0
- package/src/server/management/provider-capability-config.ts +48 -0
- package/src/server/management/provider-routes.ts +76 -4
- package/src/server/management/system-restart.ts +4 -2
- package/src/server/management/system-routes.ts +38 -0
- package/src/server/relay.ts +17 -3
- package/src/server/responses/compact.ts +4 -1
- package/src/server/responses/core.ts +278 -48
- package/src/server/responses/empty-completion-guard.ts +276 -0
- package/src/server/responses/fetch-helpers.ts +35 -4
- package/src/server/responses/input-admission.ts +169 -0
- package/src/server/responses/pacing-overload.ts +13 -0
- package/src/server/responses/policy-fallback.ts +16 -2
- package/src/server/responses/terminal-guard.ts +1 -1
- package/src/server/responses/upstream-error.ts +5 -0
- package/src/server/responses.ts +17 -2
- package/src/service-manager-probe.ts +2 -3
- package/src/types.ts +76 -6
- package/src/update/index.ts +6 -5
- package/src/update/job.ts +5 -6
- package/src/update/notify.ts +5 -3
- package/src/usage/log.ts +11 -1
- package/src/vision/index.ts +21 -4
- package/src/web-search/index.ts +2 -1
- package/gui/dist/assets/index-CXI1262_.js +0 -76
- package/gui/dist/assets/index-DUCH59lJ.css +0 -1
package/src/lib/errors.ts
CHANGED
|
@@ -43,6 +43,25 @@ function isSubscriptionGateMessage(text: string): boolean {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function isLocalAclHardeningMessage(text: string): boolean {
|
|
47
|
+
const secretPathHardening = text.includes("secret path") && (
|
|
48
|
+
text.includes("acl") ||
|
|
49
|
+
text.includes("harden") ||
|
|
50
|
+
text.includes("permission") ||
|
|
51
|
+
text.includes("access denied") ||
|
|
52
|
+
text.includes("inheritance")
|
|
53
|
+
);
|
|
54
|
+
return (
|
|
55
|
+
text.includes("icacls") ||
|
|
56
|
+
text.includes("acl hardening") ||
|
|
57
|
+
text.includes("ntfs") ||
|
|
58
|
+
secretPathHardening ||
|
|
59
|
+
text.includes("inheritance:r") ||
|
|
60
|
+
/\bwindows\b.*\bacl\b/.test(text) ||
|
|
61
|
+
text.includes("/grant:r")
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
function isAuthenticationMessage(text: string): boolean {
|
|
47
66
|
const accessDeniedWithCredentialCue = (
|
|
48
67
|
text.includes("access denied") ||
|
|
@@ -170,6 +189,11 @@ export function classifyError(status: number, type: string, message: string): Oc
|
|
|
170
189
|
if (type === "origin_rejected") {
|
|
171
190
|
return { message, type: "invalid_request_error", code: "origin_rejected" };
|
|
172
191
|
}
|
|
192
|
+
// Local ACL setup failures can contain provider-like auth wording (for example
|
|
193
|
+
// "access denied" or "authentication") but represent unavailable infrastructure.
|
|
194
|
+
if (status === 503 && isLocalAclHardeningMessage(text)) {
|
|
195
|
+
return { message, type: "server_error", code: "upstream_server_error" };
|
|
196
|
+
}
|
|
173
197
|
// HTTP 401 and explicit auth failures are authoritative even when provider text
|
|
174
198
|
// also advertises an upgrade or subscription.
|
|
175
199
|
if (
|
|
@@ -280,6 +304,9 @@ export function inferHttpStatusFromAdapterMessage(message: string): number {
|
|
|
280
304
|
lower.includes("too many requests") ||
|
|
281
305
|
lower.includes("throttling")
|
|
282
306
|
) return 429;
|
|
307
|
+
// Local Windows filesystem hardening is infrastructure, not provider authentication.
|
|
308
|
+
// Keep this ahead of auth/permission and timeout keyword inference.
|
|
309
|
+
if (isLocalAclHardeningMessage(lower)) return 503;
|
|
283
310
|
// Strong authentication signals win when a message contains mixed auth and
|
|
284
311
|
// subscription/permission wording.
|
|
285
312
|
if (isAuthenticationMessage(lower)) return 401;
|
|
@@ -29,6 +29,7 @@ import { labAutomationPolicyPath } from "../lab/paths";
|
|
|
29
29
|
import type { OcxConfig } from "../types";
|
|
30
30
|
import { LabAutomationError } from "../lab/automation/types";
|
|
31
31
|
import { registerLabPassiveRouteLinker } from "./lab-passive-linker-registration";
|
|
32
|
+
import { registerCurrentServerResourceCleanup } from "./server-resource-ownership";
|
|
32
33
|
import { setCompatibilityEvidenceProvider } from "../routing/compatibility/provider-slot";
|
|
33
34
|
import { labCompatibilityEvidenceProvider } from "../routing/compatibility/lab-evidence-provider";
|
|
34
35
|
import {
|
|
@@ -37,8 +38,18 @@ import {
|
|
|
37
38
|
} from "../lab/automation/orchestrator";
|
|
38
39
|
import { createProductionLabRouteExecutor } from "./lab-live-route-production";
|
|
39
40
|
|
|
41
|
+
interface LabRuntimeBinding {
|
|
42
|
+
release(): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface LabActivationRecord {
|
|
46
|
+
staticDetach: Array<() => void>;
|
|
47
|
+
runtime: LabRuntimeBinding | null;
|
|
48
|
+
seenRuntimeConfigs: WeakSet<OcxConfig>;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
/** Activation records keyed by configDir, so one process can own several configs. */
|
|
41
|
-
const activated = new Map<string,
|
|
52
|
+
const activated = new Map<string, LabActivationRecord>();
|
|
42
53
|
|
|
43
54
|
const activationKey = (configDir?: string): string => configDir ?? "";
|
|
44
55
|
|
|
@@ -83,60 +94,110 @@ export function labActivationRequired(config: OcxConfig, configDir?: string): bo
|
|
|
83
94
|
return labAutomationEnabledOnDisk(configDir);
|
|
84
95
|
}
|
|
85
96
|
|
|
97
|
+
function startAutomationIfEnabled(configDir?: string): void {
|
|
98
|
+
if (!labAutomationEnabledOnDisk(configDir)) return;
|
|
99
|
+
try {
|
|
100
|
+
startLabAutomationScheduler(configDir);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Neither a malformed automation file nor a busy state lock may take the proxy down
|
|
103
|
+
// at startup. Lab automation stays off for this run; routing, evidence, and every
|
|
104
|
+
// other subsystem keep working.
|
|
105
|
+
//
|
|
106
|
+
// The two causes get different messages because they need different actions, and a
|
|
107
|
+
// lock-contention failure reported as "invalid config" sends the operator to fix a
|
|
108
|
+
// file that is fine. Contention can also stall startup by up to the 5s lock wait.
|
|
109
|
+
const code = err instanceof LabAutomationError ? err.code : null;
|
|
110
|
+
if (code === "state_lock_busy" || code === "state_lock_failed") {
|
|
111
|
+
console.warn(
|
|
112
|
+
"[lab] Lab automation did not start: another process holds the automation state lock."
|
|
113
|
+
+ " Automation stays off for this run and will be retried on the next start.",
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
console.warn(
|
|
117
|
+
"[lab] Lab automation is disabled for this run because its configuration could not be"
|
|
118
|
+
+ " loaded:",
|
|
119
|
+
err instanceof Error ? err.message : err,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function installLabAutomationRuntime(
|
|
126
|
+
record: LabActivationRecord,
|
|
127
|
+
config: OcxConfig,
|
|
128
|
+
configDir?: string,
|
|
129
|
+
): void {
|
|
130
|
+
const previous = record.runtime;
|
|
131
|
+
const routeExecutor = createProductionLabRouteExecutor({ configDir, loadConfig: () => config });
|
|
132
|
+
const releaseDispatchDeps = setLabAutomationDispatchDeps({
|
|
133
|
+
configDir,
|
|
134
|
+
loadConfig: () => config,
|
|
135
|
+
routeExecutor,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
let released = false;
|
|
139
|
+
let detachOwnerCleanup = () => {};
|
|
140
|
+
const binding: LabRuntimeBinding = {
|
|
141
|
+
release() {
|
|
142
|
+
if (released) return;
|
|
143
|
+
released = true;
|
|
144
|
+
detachOwnerCleanup();
|
|
145
|
+
releaseDispatchDeps();
|
|
146
|
+
if (record.runtime === binding) record.runtime = null;
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
// `setLabAutomationDispatchDeps` already registers its own owner cleanup. This second
|
|
150
|
+
// receipt only keeps the activation record in sync with that owner-scoped lifetime, so a
|
|
151
|
+
// later same-process server can see that the static Lab slots survived but CL-08 authority
|
|
152
|
+
// did not. The release is idempotent, so cleanup order does not matter.
|
|
153
|
+
detachOwnerCleanup = registerCurrentServerResourceCleanup(binding.release);
|
|
154
|
+
|
|
155
|
+
// Install the successor before releasing the predecessor. The dispatcher token check then
|
|
156
|
+
// makes the predecessor release a no-op for the successor scheduler/authority.
|
|
157
|
+
record.runtime = binding;
|
|
158
|
+
record.seenRuntimeConfigs.add(config);
|
|
159
|
+
previous?.release();
|
|
160
|
+
}
|
|
161
|
+
|
|
86
162
|
/**
|
|
87
|
-
* Register Lab into the core slots.
|
|
88
|
-
*
|
|
163
|
+
* Register Lab into the core slots. Static activation is idempotent per configDir. The
|
|
164
|
+
* server-owned CL-08 runtime binding is refreshed when its prior owner ended or when a new
|
|
165
|
+
* server instance arrives with a config object that has not owned this activation before.
|
|
89
166
|
*/
|
|
90
167
|
export function activateLab(config: OcxConfig, configDir?: string): void {
|
|
91
168
|
const key = activationKey(configDir);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
169
|
+
const existing = activated.get(key);
|
|
170
|
+
if (existing) {
|
|
171
|
+
// A released predecessor leaves the static Lab slots resident but removes dispatch
|
|
172
|
+
// authority and its scheduler. A live successor uses a fresh config object. Rebind in
|
|
173
|
+
// either case, but never let an older already-seen server steal authority back from a
|
|
174
|
+
// newer successor merely because it receives another management request.
|
|
175
|
+
if (existing.runtime === null || !existing.seenRuntimeConfigs.has(config)) {
|
|
176
|
+
installLabAutomationRuntime(existing, config, configDir);
|
|
177
|
+
startAutomationIfEnabled(configDir);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
103
181
|
|
|
104
|
-
|
|
105
|
-
|
|
182
|
+
// INVARIANT: static activation is all-or-nothing and reason-independent. Every static
|
|
183
|
+
// slot is registered here regardless of WHY activation was required, so automation-only
|
|
184
|
+
// activation still installs the compatibility provider a later profile needs.
|
|
185
|
+
const record: LabActivationRecord = {
|
|
186
|
+
staticDetach: [],
|
|
187
|
+
runtime: null,
|
|
188
|
+
seenRuntimeConfigs: new WeakSet<OcxConfig>(),
|
|
189
|
+
};
|
|
190
|
+
record.staticDetach.push(registerLabPassiveRouteLinker(configDir));
|
|
191
|
+
record.staticDetach.push(setCompatibilityEvidenceProvider(labCompatibilityEvidenceProvider));
|
|
192
|
+
installLabAutomationRuntime(record, config, configDir);
|
|
106
193
|
|
|
107
194
|
// Record the activation BEFORE the scheduler start. startLabAutomationScheduler runs the
|
|
108
195
|
// full automation normalizer, which throws on any field violation, and this call sits on
|
|
109
196
|
// the startup path of every install that has a routing profile. Storing the record first
|
|
110
197
|
// means a throw cannot orphan the detach receipts and leave slots registered with no
|
|
111
|
-
// activation record
|
|
112
|
-
activated.set(key,
|
|
113
|
-
|
|
114
|
-
if (labAutomationEnabledOnDisk(configDir)) {
|
|
115
|
-
try {
|
|
116
|
-
startLabAutomationScheduler(configDir);
|
|
117
|
-
} catch (err) {
|
|
118
|
-
// Neither a malformed automation file nor a busy state lock may take the proxy down
|
|
119
|
-
// at startup. Lab automation stays off for this run; routing, evidence, and every
|
|
120
|
-
// other subsystem keep working.
|
|
121
|
-
//
|
|
122
|
-
// The two causes get different messages because they need different actions, and a
|
|
123
|
-
// lock-contention failure reported as "invalid config" sends the operator to fix a
|
|
124
|
-
// file that is fine. Contention can also stall startup by up to the 5s lock wait.
|
|
125
|
-
const code = err instanceof LabAutomationError ? err.code : null;
|
|
126
|
-
if (code === "state_lock_busy" || code === "state_lock_failed") {
|
|
127
|
-
console.warn(
|
|
128
|
-
"[lab] Lab automation did not start: another process holds the automation state lock."
|
|
129
|
-
+ " Automation stays off for this run and will be retried on the next start.",
|
|
130
|
-
);
|
|
131
|
-
} else {
|
|
132
|
-
console.warn(
|
|
133
|
-
"[lab] Lab automation is disabled for this run because its configuration could not be"
|
|
134
|
-
+ " loaded:",
|
|
135
|
-
err instanceof Error ? err.message : err,
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
198
|
+
// activation record, which would let a later activateLab register them a second time.
|
|
199
|
+
activated.set(key, record);
|
|
200
|
+
startAutomationIfEnabled(configDir);
|
|
140
201
|
}
|
|
141
202
|
|
|
142
203
|
/** True when this configDir has been activated. */
|
|
@@ -152,9 +213,10 @@ export function isLabActivated(configDir?: string): boolean {
|
|
|
152
213
|
* users who never opted in.
|
|
153
214
|
*/
|
|
154
215
|
export function resetLabActivationForTests(): void {
|
|
155
|
-
for (const [key,
|
|
216
|
+
for (const [key, record] of [...activated]) {
|
|
156
217
|
activated.delete(key);
|
|
157
|
-
|
|
218
|
+
try { record.runtime?.release(); } catch { /* teardown is best-effort */ }
|
|
219
|
+
for (const release of [...record.staticDetach].reverse()) {
|
|
158
220
|
try { release(); } catch { /* teardown is best-effort */ }
|
|
159
221
|
}
|
|
160
222
|
}
|
|
@@ -19,22 +19,33 @@ export function createLabAuthorizedPinnedSender(
|
|
|
19
19
|
headers,
|
|
20
20
|
maxBytes: limits.maxOutputBytes,
|
|
21
21
|
connectTimeoutMs: limits.connectTimeoutMs,
|
|
22
|
-
|
|
22
|
+
firstByteTimeoutMs: limits.firstByteTimeoutMs,
|
|
23
|
+
inactivityTimeoutMs: limits.inactivityTimeoutMs,
|
|
23
24
|
rejectUnauthorized: true,
|
|
24
25
|
context: "Lab provider response",
|
|
25
26
|
};
|
|
26
27
|
let response: Response;
|
|
28
|
+
let body: string;
|
|
27
29
|
try {
|
|
28
30
|
response = request.method === "POST"
|
|
29
31
|
? await pinnedHttpPost(url, pinned, request.body ?? "", signal, options)
|
|
30
32
|
: await pinnedHttpGet(url, pinned, signal, options);
|
|
33
|
+
body = await response.text();
|
|
31
34
|
} catch (error) {
|
|
32
|
-
if (error instanceof PinnedHttpError
|
|
33
|
-
|
|
35
|
+
if (error instanceof PinnedHttpError) {
|
|
36
|
+
switch (error.code) {
|
|
37
|
+
case "connect_timeout":
|
|
38
|
+
throw new TransportError("connect_timeout", "pinned provider connection timed out");
|
|
39
|
+
case "first_byte_timeout":
|
|
40
|
+
throw new TransportError("first_byte_timeout", "pinned provider first byte timed out");
|
|
41
|
+
case "inactivity_timeout":
|
|
42
|
+
throw new TransportError("inactivity_timeout", "pinned provider response stalled");
|
|
43
|
+
case "output_byte_limit":
|
|
44
|
+
throw new TransportError("output_byte_limit", "pinned provider response exceeded byte budget");
|
|
45
|
+
}
|
|
34
46
|
}
|
|
35
47
|
throw error;
|
|
36
48
|
}
|
|
37
|
-
const body = await response.text();
|
|
38
49
|
const responseHeaders: Record<string, string> = {};
|
|
39
50
|
for (const headerName of LAB_RESPONSE_HEADER_ALLOWLIST) {
|
|
40
51
|
const value = response.headers.get(headerName);
|
|
@@ -42,4 +53,4 @@ export function createLabAuthorizedPinnedSender(
|
|
|
42
53
|
}
|
|
43
54
|
return { status: response.status, headers: responseHeaders, body };
|
|
44
55
|
};
|
|
45
|
-
}
|
|
56
|
+
}
|
package/src/lib/pinned-http.ts
CHANGED
|
@@ -3,7 +3,11 @@ import https from "node:https";
|
|
|
3
3
|
|
|
4
4
|
export type PinnedAddress = { address: string; family: number };
|
|
5
5
|
|
|
6
|
-
export type PinnedHttpErrorCode =
|
|
6
|
+
export type PinnedHttpErrorCode =
|
|
7
|
+
| "connect_timeout"
|
|
8
|
+
| "first_byte_timeout"
|
|
9
|
+
| "inactivity_timeout"
|
|
10
|
+
| "output_byte_limit";
|
|
7
11
|
|
|
8
12
|
export class PinnedHttpError extends Error {
|
|
9
13
|
override readonly name = "PinnedHttpError";
|
|
@@ -15,6 +19,11 @@ export interface PinnedHttpRequestOptions {
|
|
|
15
19
|
maxBytes?: number;
|
|
16
20
|
/** Optional deadline for establishing the TCP connection and, for HTTPS, completing TLS. */
|
|
17
21
|
connectTimeoutMs?: number;
|
|
22
|
+
/** Optional deadline from connection establishment until response headers arrive. */
|
|
23
|
+
firstByteTimeoutMs?: number;
|
|
24
|
+
/** Optional maximum idle interval between response-body chunks. */
|
|
25
|
+
inactivityTimeoutMs?: number;
|
|
26
|
+
/** @deprecated Use firstByteTimeoutMs and inactivityTimeoutMs. */
|
|
18
27
|
idleTimeoutMs?: number;
|
|
19
28
|
rejectUnauthorized?: boolean;
|
|
20
29
|
context?: string;
|
|
@@ -37,7 +46,11 @@ function pinnedHttpRequest(
|
|
|
37
46
|
}
|
|
38
47
|
const context = options?.context ?? "request";
|
|
39
48
|
const connectTimeoutMs = options?.connectTimeoutMs;
|
|
40
|
-
const
|
|
49
|
+
const legacyIdleTimeoutMs = options?.idleTimeoutMs ?? 60_000;
|
|
50
|
+
const usesLegacyIdleTimeout = options?.firstByteTimeoutMs === undefined
|
|
51
|
+
&& options?.inactivityTimeoutMs === undefined;
|
|
52
|
+
const firstByteTimeoutMs = options?.firstByteTimeoutMs ?? legacyIdleTimeoutMs;
|
|
53
|
+
const inactivityTimeoutMs = options?.inactivityTimeoutMs ?? legacyIdleTimeoutMs;
|
|
41
54
|
const maxBytes = options?.maxBytes;
|
|
42
55
|
const headers = new Headers(options?.headers);
|
|
43
56
|
headers.set("host", parsed.host);
|
|
@@ -56,17 +69,30 @@ function pinnedHttpRequest(
|
|
|
56
69
|
let settled = false;
|
|
57
70
|
let req: ClientRequest | undefined;
|
|
58
71
|
let connectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
72
|
+
let firstByteTimer: ReturnType<typeof setTimeout> | undefined;
|
|
59
73
|
const clearConnectTimer = () => {
|
|
60
74
|
if (connectTimer !== undefined) clearTimeout(connectTimer);
|
|
61
75
|
connectTimer = undefined;
|
|
62
76
|
};
|
|
77
|
+
const clearFirstByteTimer = () => {
|
|
78
|
+
if (firstByteTimer !== undefined) clearTimeout(firstByteTimer);
|
|
79
|
+
firstByteTimer = undefined;
|
|
80
|
+
};
|
|
63
81
|
const fail = (error: unknown) => {
|
|
64
82
|
clearConnectTimer();
|
|
83
|
+
clearFirstByteTimer();
|
|
65
84
|
try { req?.destroy(); } catch { /* ignore */ }
|
|
66
85
|
if (settled) return;
|
|
67
86
|
settled = true;
|
|
68
87
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
69
88
|
};
|
|
89
|
+
const startFirstByteTimer = () => {
|
|
90
|
+
clearFirstByteTimer();
|
|
91
|
+
firstByteTimer = setTimeout(
|
|
92
|
+
() => fail(new PinnedHttpError("first_byte_timeout", `${context} first byte timed out`)),
|
|
93
|
+
firstByteTimeoutMs,
|
|
94
|
+
);
|
|
95
|
+
};
|
|
70
96
|
const requestOptions: RequestOptions & { servername?: string } = {
|
|
71
97
|
protocol: parsed.protocol,
|
|
72
98
|
hostname: parsed.hostname,
|
|
@@ -101,6 +127,7 @@ function pinnedHttpRequest(
|
|
|
101
127
|
|
|
102
128
|
const onResponse = (response: IncomingMessage) => {
|
|
103
129
|
clearConnectTimer();
|
|
130
|
+
clearFirstByteTimer();
|
|
104
131
|
const status = response.statusCode ?? 0;
|
|
105
132
|
const responseHeaders = new Headers();
|
|
106
133
|
for (const [key, value] of Object.entries(response.headers)) {
|
|
@@ -124,28 +151,35 @@ function pinnedHttpRequest(
|
|
|
124
151
|
let received = 0;
|
|
125
152
|
const stream = new ReadableStream<Uint8Array>({
|
|
126
153
|
start(controller) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
154
|
+
let bodySettled = false;
|
|
155
|
+
const failBody = (error: Error) => {
|
|
156
|
+
if (bodySettled) return;
|
|
157
|
+
bodySettled = true;
|
|
130
158
|
try { controller.error(error); } catch { /* closed */ }
|
|
159
|
+
try { response.destroy(); } catch { /* ignore */ }
|
|
160
|
+
try { req?.destroy(); } catch { /* ignore */ }
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
response.setTimeout(inactivityTimeoutMs, () => {
|
|
164
|
+
failBody(new PinnedHttpError("inactivity_timeout", `${context} stalled`));
|
|
131
165
|
});
|
|
132
166
|
response.on("data", (chunk: Buffer | string) => {
|
|
167
|
+
if (bodySettled) return;
|
|
133
168
|
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
134
169
|
received += buffer.byteLength;
|
|
135
170
|
if (maxBytes !== undefined && received > maxBytes) {
|
|
136
|
-
|
|
137
|
-
fail(error);
|
|
138
|
-
try { controller.error(error); } catch { /* closed */ }
|
|
171
|
+
failBody(new PinnedHttpError("output_byte_limit", `${context} exceeds ${maxBytes} byte cap`));
|
|
139
172
|
return;
|
|
140
173
|
}
|
|
141
174
|
try { controller.enqueue(buffer); } catch { /* closed */ }
|
|
142
175
|
});
|
|
143
176
|
response.on("end", () => {
|
|
177
|
+
if (bodySettled) return;
|
|
178
|
+
bodySettled = true;
|
|
144
179
|
try { controller.close(); } catch { /* closed */ }
|
|
145
180
|
});
|
|
146
181
|
response.on("error", (error: Error) => {
|
|
147
|
-
|
|
148
|
-
try { controller.error(error); } catch { /* closed */ }
|
|
182
|
+
failBody(error);
|
|
149
183
|
});
|
|
150
184
|
},
|
|
151
185
|
cancel() {
|
|
@@ -163,20 +197,40 @@ function pinnedHttpRequest(
|
|
|
163
197
|
const onAbort = () => fail(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
|
|
164
198
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
165
199
|
req.on("socket", (socket) => {
|
|
166
|
-
if (!socket.connecting || connectTimeoutMs === undefined) return;
|
|
167
200
|
const connectedEvent = parsed.protocol === "https:" ? "secureConnect" : "connect";
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
201
|
+
if (!socket.connecting) {
|
|
202
|
+
if (!usesLegacyIdleTimeout) startFirstByteTimer();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (connectTimeoutMs !== undefined) {
|
|
206
|
+
connectTimer = setTimeout(
|
|
207
|
+
() => fail(new PinnedHttpError("connect_timeout", `${context} connect timed out`)),
|
|
208
|
+
connectTimeoutMs,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
socket.once(connectedEvent, () => {
|
|
212
|
+
clearConnectTimer();
|
|
213
|
+
if (!usesLegacyIdleTimeout) startFirstByteTimer();
|
|
214
|
+
});
|
|
215
|
+
socket.once("error", () => {
|
|
216
|
+
clearConnectTimer();
|
|
217
|
+
clearFirstByteTimer();
|
|
218
|
+
});
|
|
219
|
+
socket.once("close", () => {
|
|
220
|
+
clearConnectTimer();
|
|
221
|
+
clearFirstByteTimer();
|
|
222
|
+
});
|
|
172
223
|
});
|
|
173
|
-
|
|
224
|
+
if (usesLegacyIdleTimeout) {
|
|
225
|
+
req.setTimeout(legacyIdleTimeoutMs, () => fail(new Error(`${context} timed out`)));
|
|
226
|
+
}
|
|
174
227
|
req.on("error", error => {
|
|
175
228
|
signal?.removeEventListener("abort", onAbort);
|
|
176
229
|
fail(error);
|
|
177
230
|
});
|
|
178
231
|
req.on("close", () => {
|
|
179
232
|
clearConnectTimer();
|
|
233
|
+
clearFirstByteTimer();
|
|
180
234
|
signal?.removeEventListener("abort", onAbort);
|
|
181
235
|
});
|
|
182
236
|
req.end(body);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface SelfLaunchArgvOptions {
|
|
2
|
+
isStandaloneExecutable?: boolean;
|
|
3
|
+
sourceEntrypoint?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** Build argv for re-entering the current CLI in compiled or source mode. */
|
|
7
|
+
export function selfLaunchArgv(
|
|
8
|
+
args: readonly string[],
|
|
9
|
+
options: SelfLaunchArgvOptions = {},
|
|
10
|
+
): string[] {
|
|
11
|
+
const bunStandalone = (Bun as unknown as { isStandaloneExecutable?: boolean }).isStandaloneExecutable;
|
|
12
|
+
const isStandaloneExecutable = options.isStandaloneExecutable ?? Boolean(bunStandalone);
|
|
13
|
+
if (isStandaloneExecutable) return [...args];
|
|
14
|
+
return [options.sourceEntrypoint ?? process.argv[1], ...args];
|
|
15
|
+
}
|
|
@@ -34,6 +34,7 @@ import { sweepExpiredAnthropicRoutingHealth } from "../oauth/anthropic-routing";
|
|
|
34
34
|
import { listLiveOAuthAccountKeys, reconcileOAuthReauthState } from "../oauth/store";
|
|
35
35
|
import { reconcileGuardianBackoff } from "../oauth/token-guardian";
|
|
36
36
|
import { sweepExpiredApiKeyCooldowns } from "../providers/key-failover";
|
|
37
|
+
import { reconcileProviderRequestPacing } from "../providers/request-pacing";
|
|
37
38
|
import { sweepExpiredResponseStates } from "../responses/state";
|
|
38
39
|
import { sweepExpiredAntigravityReplay } from "../adapters/google-antigravity-replay";
|
|
39
40
|
import { reconcileProviderAccountQuotaRows } from "../providers/quota";
|
|
@@ -75,6 +76,7 @@ export function buildGenerationContext(): GenerationContext {
|
|
|
75
76
|
export const STATE_STORE_REGISTRATIONS = [
|
|
76
77
|
{ name: "subagent-model-health", sweepExpired: sweepExpiredSubagentModelHealth },
|
|
77
78
|
{ name: "api-key-cooldowns", sweepExpired: sweepExpiredApiKeyCooldowns },
|
|
79
|
+
{ name: "provider-request-pacing", reconcileGeneration: reconcileProviderRequestPacing },
|
|
78
80
|
{
|
|
79
81
|
name: "combo-target-cooldowns",
|
|
80
82
|
sweepExpired: sweepExpiredComboTargetCooldowns,
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* MUST stay a leaf module: imports nothing from server.ts or adapters.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { RequestPacingQueueOverloadError } from "../providers/request-pacing";
|
|
26
27
|
import { UpstreamRetryEvidenceError } from "./upstream-retry";
|
|
27
28
|
|
|
28
29
|
export const PRE_CONNECT_REACHABILITY_CODES = new Set([
|
|
@@ -68,6 +69,9 @@ export type TransportFailureKind = "timeout" | "connect_neutral" | "connect_erro
|
|
|
68
69
|
* account-attributed behavior.
|
|
69
70
|
*/
|
|
70
71
|
export function classifyTransportFailureKind(err: unknown): TransportFailureKind {
|
|
72
|
+
// A local pacing admission failure happened before transport classification and must
|
|
73
|
+
// remain a client-visible overload, not account or host health evidence.
|
|
74
|
+
if (err instanceof RequestPacingQueueOverloadError) throw err;
|
|
71
75
|
const evidence = err instanceof UpstreamRetryEvidenceError ? err : undefined;
|
|
72
76
|
const rejection = evidence ? evidence.cause : err;
|
|
73
77
|
if (rejection instanceof Error && rejection.name === "TimeoutError") return "timeout";
|
|
@@ -175,7 +175,7 @@ export function assertTrustedSystemExecutableForTests(candidate: string, label:
|
|
|
175
175
|
return assertTrustedSystemExecutable(candidate, label);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
-
type ElevationExeOverrides = { powershell?: string; schtasks?: string };
|
|
178
|
+
type ElevationExeOverrides = { powershell?: string; schtasks?: string; taskkill?: string };
|
|
179
179
|
let elevationExeOverridesForTests: ElevationExeOverrides | null = null;
|
|
180
180
|
|
|
181
181
|
/**
|
|
@@ -211,6 +211,15 @@ export function resolveTrustedWindowsSchtasksExe(): string {
|
|
|
211
211
|
return assertTrustedSystemExecutable(candidate, "schtasks.exe");
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
+
/** Absolute path to System32\\taskkill.exe from a trusted system directory. */
|
|
215
|
+
export function resolveTrustedWindowsTaskkillExe(): string {
|
|
216
|
+
if (elevationExeOverridesForTests?.taskkill) {
|
|
217
|
+
return elevationExeOverridesForTests.taskkill;
|
|
218
|
+
}
|
|
219
|
+
const candidate = join(resolveTrustedWindowsSystemDirectory(), "taskkill.exe");
|
|
220
|
+
return assertTrustedSystemExecutable(candidate, "taskkill.exe");
|
|
221
|
+
}
|
|
222
|
+
|
|
214
223
|
/** Stable machine-readable marker for a denied `schtasks /create`. Crosses the CLI→proxy boundary. */
|
|
215
224
|
export const WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER =
|
|
216
225
|
"OCX_ERROR_CODE=WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED";
|
package/src/providers/derive.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
2
|
import {
|
|
3
3
|
PROVIDER_REGISTRY,
|
|
4
|
-
providerMatchesRegistryTransport,
|
|
5
4
|
registryEntryForProviderDestination,
|
|
6
5
|
type ProviderRegistryEntry,
|
|
7
6
|
} from "./registry";
|
|
7
|
+
import {
|
|
8
|
+
providerMatchesRegistryTransportWithStaticGuards,
|
|
9
|
+
registryEntrySupportsLiveModelDiscovery,
|
|
10
|
+
repairStaticModelCatalogProvider,
|
|
11
|
+
} from "./static-model-discovery";
|
|
8
12
|
|
|
9
13
|
export interface DerivedKeyLoginProvider {
|
|
10
14
|
label: string;
|
|
@@ -204,6 +208,7 @@ export function applyDirectReasoningEffortContracts(
|
|
|
204
208
|
* keep distinguishing local runtimes from API-key providers after the seed round-trip.
|
|
205
209
|
*/
|
|
206
210
|
export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderConfig {
|
|
211
|
+
const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
|
|
207
212
|
return {
|
|
208
213
|
adapter: entry.adapter,
|
|
209
214
|
baseUrl: entry.baseUrl,
|
|
@@ -219,7 +224,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
219
224
|
...(entry.staticHeaders ? { headers: { ...entry.staticHeaders } } : {}),
|
|
220
225
|
...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
|
|
221
226
|
...(entry.models ? { models: [...entry.models] } : {}),
|
|
222
|
-
...(
|
|
227
|
+
...(liveModels !== undefined ? { liveModels } : {}),
|
|
223
228
|
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
|
224
229
|
...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
|
|
225
230
|
...(entry.modelInputModalities ? { modelInputModalities: cloneRecordOfArrays(entry.modelInputModalities) } : {}),
|
|
@@ -263,6 +268,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
263
268
|
for (const entry of PROVIDER_REGISTRY) {
|
|
264
269
|
if (entry.authKind !== "key") continue;
|
|
265
270
|
if (!entry.dashboardUrl) throw new Error(`Registry key provider missing dashboardUrl: ${entry.id}`);
|
|
271
|
+
const liveModels = registryEntrySupportsLiveModelDiscovery(entry) ? entry.liveModels : false;
|
|
266
272
|
out[entry.id] = {
|
|
267
273
|
label: entry.label,
|
|
268
274
|
baseUrl: entry.baseUrl,
|
|
@@ -272,7 +278,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
272
278
|
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
|
|
273
279
|
dashboardUrl: entry.dashboardUrl,
|
|
274
280
|
...(entry.models ? { models: [...entry.models] } : {}),
|
|
275
|
-
...(
|
|
281
|
+
...(liveModels !== undefined ? { liveModels } : {}),
|
|
276
282
|
...(entry.defaultModel ? { defaultModel: entry.defaultModel } : {}),
|
|
277
283
|
...(entry.contextWindow !== undefined ? { contextWindow: entry.contextWindow } : {}),
|
|
278
284
|
...(entry.modelContextWindows ? { modelContextWindows: { ...entry.modelContextWindows } } : {}),
|
|
@@ -359,6 +365,17 @@ function applyReasoningSummaryDefaults(
|
|
|
359
365
|
};
|
|
360
366
|
}
|
|
361
367
|
|
|
368
|
+
function applyServiceTierModelDefaults(
|
|
369
|
+
prov: OcxProviderConfig,
|
|
370
|
+
defaults: Readonly<Record<string, boolean>> | undefined,
|
|
371
|
+
): void {
|
|
372
|
+
if (!defaults) return;
|
|
373
|
+
prov.modelSupportsServiceTier = {
|
|
374
|
+
...defaults,
|
|
375
|
+
...(prov.modelSupportsServiceTier ?? {}),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
362
379
|
/**
|
|
363
380
|
* Last-resort enrichment for a provider whose NAME matches no registry id.
|
|
364
381
|
*
|
|
@@ -378,7 +395,7 @@ function enrichReasoningSummariesByDestination(prov: OcxProviderConfig): void {
|
|
|
378
395
|
|
|
379
396
|
export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig): void {
|
|
380
397
|
const entry = PROVIDER_REGISTRY.find(row => row.id === name);
|
|
381
|
-
if (!entry || !
|
|
398
|
+
if (!entry || !providerMatchesRegistryTransportWithStaticGuards(name, prov)) {
|
|
382
399
|
// Name lookup failed, but the row may still point at a vendor route we know. #1100 was
|
|
383
400
|
// reported against a hand-added provider literally named "GLM": routing worked, yet every
|
|
384
401
|
// piece of registry metadata was skipped because no registry id is called "GLM".
|
|
@@ -386,6 +403,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
386
403
|
// which vendor endpoint is this row talking to — and is already restricted to fixed key
|
|
387
404
|
// destinations, so a templated or overridable base URL cannot be claimed by it.
|
|
388
405
|
enrichReasoningSummariesByDestination(prov);
|
|
406
|
+
applyServiceTierModelDefaults(prov, registryEntryForProviderDestination(prov)?.modelSupportsServiceTier);
|
|
389
407
|
return;
|
|
390
408
|
}
|
|
391
409
|
const explicitDirectReasoning: DirectReasoningEffortOverrides = {
|
|
@@ -395,6 +413,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
395
413
|
modelReasoningEffortMap: prov.modelReasoningEffortMap,
|
|
396
414
|
};
|
|
397
415
|
const seed = providerConfigSeed(entry);
|
|
416
|
+
repairStaticModelCatalogProvider(name, prov);
|
|
398
417
|
if (prov.apiKeyTransport === undefined && seed.apiKeyTransport !== undefined) prov.apiKeyTransport = seed.apiKeyTransport;
|
|
399
418
|
if (!prov.defaultModel && seed.defaultModel) prov.defaultModel = seed.defaultModel;
|
|
400
419
|
if (prov.responsesPath === undefined && seed.responsesPath !== undefined) prov.responsesPath = seed.responsesPath;
|
|
@@ -433,6 +452,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
433
452
|
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
|
|
434
453
|
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
|
|
435
454
|
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
|
|
455
|
+
applyServiceTierModelDefaults(prov, entry.modelSupportsServiceTier);
|
|
436
456
|
// Registry-only repair policy (#938): fill only when the runtime provider has
|
|
437
457
|
// no explicit policy, and deep-clone so saved/user values never alias the
|
|
438
458
|
// registry constant.
|