@bitkyc08/opencodex 2.67.0 → 2.68.0-preview.20260927
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/bin/ocx.mjs +3 -0
- package/gui/dist/assets/App-B5-cAEd2.js +51 -0
- package/gui/dist/assets/App-DCBismRi.css +1 -0
- package/gui/dist/assets/Tray-DFnMiyD0.js +1 -0
- package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
- package/gui/dist/assets/index-BcXblvem.js +86 -0
- package/gui/dist/assets/quota-summary-i84eOU3o.js +1 -0
- package/gui/dist/index.html +21 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-guard.ts +13 -1
- package/src/adapters/anthropic-image-normalize.ts +3 -2
- package/src/adapters/anthropic-output-schema.ts +36 -0
- package/src/adapters/anthropic.ts +10 -2
- package/src/adapters/base.ts +7 -0
- package/src/adapters/codebuddy/live-models.ts +180 -0
- package/src/adapters/coding-agent/protocol.ts +91 -12
- package/src/adapters/coding-agent/turn.ts +32 -27
- package/src/adapters/devin/cloud-direct/index.ts +1 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
- package/src/adapters/devin.ts +9 -6
- package/src/adapters/google-http.ts +4 -4
- package/src/adapters/google.ts +93 -3
- package/src/adapters/kiro/adapter.ts +6 -2
- package/src/adapters/kiro/stream.ts +12 -2
- package/src/adapters/kiro/usage.ts +3 -1
- package/src/adapters/kiro-errors.ts +11 -1
- package/src/adapters/kiro-events.ts +27 -1
- package/src/adapters/kiro-refusal.ts +30 -0
- package/src/adapters/kiro-retry.ts +71 -31
- package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
- package/src/adapters/openai-chat/messages.ts +31 -4
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
- package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
- package/src/adapters/openai-chat/tool-schema.ts +6 -2
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
- package/src/adapters/openai-responses/passthrough.ts +9 -3
- package/src/adapters/opencode-go-additional-tools.ts +19 -10
- package/src/adapters/physical-send.ts +10 -3
- package/src/adapters/registry.ts +2 -1
- package/src/adapters/responses-tool-schema.ts +31 -3
- package/src/adapters/run-turn-queue.ts +63 -23
- package/src/adapters/unique-tool-call-ids.ts +63 -0
- package/src/adapters/xai-web-search.ts +32 -2
- package/src/bridge/sse.ts +14 -2
- package/src/chat/inbound.ts +20 -6
- package/src/claude/agents-inject.ts +4 -0
- package/src/claude/desktop-3p.ts +53 -13
- package/src/claude/desktop-profile.ts +41 -4
- package/src/claude/inbound-model-options.ts +14 -2
- package/src/claude/inbound.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +17 -1
- package/src/claude/intercept/local-ca.ts +7 -1
- package/src/claude/message-threads.ts +28 -0
- package/src/cli/account-api.ts +13 -0
- package/src/cli/account-auth.ts +49 -2
- package/src/cli/account-extended.ts +4 -2
- package/src/cli/account.ts +4 -2
- package/src/cli/capabilities.ts +17 -0
- package/src/cli/claude.ts +3 -1
- package/src/cli/dispatch.ts +62 -5
- package/src/cli/index.ts +84 -79
- package/src/cli/minimax.ts +4 -2
- package/src/cli/opencode.ts +6 -1
- package/src/cli/restart-handoff.ts +110 -0
- package/src/cli/status.ts +51 -0
- package/src/client/connect.ts +34 -15
- package/src/client/link-ingress.ts +102 -0
- package/src/client/link-join.ts +37 -16
- package/src/client/link-relay.ts +228 -45
- package/src/client/link-state.ts +54 -0
- package/src/client/link-status.ts +39 -0
- package/src/client/link-teardown.ts +2 -2
- package/src/client/link-tunnel.ts +615 -86
- package/src/client/machine-api.ts +10 -4
- package/src/client/machine-listener.ts +57 -10
- package/src/client/runtime.ts +210 -35
- package/src/clients/config-export/constants.ts +1 -12
- package/src/clients/config-export/contracts.ts +2 -0
- package/src/clients/config-export/model-metadata.ts +9 -3
- package/src/clients/config-export/omp.ts +1 -1
- package/src/clients/config-export/zcode-store.ts +2 -2
- package/src/clients/config-export.ts +36 -3
- package/src/codex/app-server-processes.ts +32 -0
- package/src/codex/app-server-restart-service.ts +20 -3
- package/src/codex/catalog/access-programs.ts +50 -0
- package/src/codex/catalog/build-entries.ts +13 -0
- package/src/codex/catalog/derive-entry.ts +1 -0
- package/src/codex/catalog/model-hints.ts +11 -0
- package/src/codex/catalog/parsing.ts +6 -2
- package/src/codex/catalog/provider-models.ts +81 -0
- package/src/codex/catalog/retained-sync.ts +3 -0
- package/src/codex/codex-write-lock.ts +2 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desired-state.ts +14 -4
- package/src/codex/home.ts +21 -3
- package/src/codex/inject/restore.ts +17 -0
- package/src/codex/inject/routing-classify.ts +3 -2
- package/src/codex/inject-coordination.ts +4 -1
- package/src/codex/inject.ts +8 -1
- package/src/codex/journal.ts +6 -2
- package/src/codex/management-convergence.ts +9 -0
- package/src/codex/model-entitlements.ts +28 -5
- package/src/codex/routing/idle-window.ts +58 -0
- package/src/codex/routing-drift.ts +134 -0
- package/src/codex/routing-healer.ts +419 -0
- package/src/codex/routing.ts +10 -0
- package/src/codex/runtime.ts +188 -57
- package/src/codex/sibling-handoff.ts +71 -0
- package/src/codex/sibling-start.ts +139 -0
- package/src/codex/sync.ts +5 -3
- package/src/combos/failover.ts +32 -5
- package/src/combos/request.ts +11 -3
- package/src/combos/reset-window.ts +10 -5
- package/src/combos/resolve.ts +36 -6
- package/src/config/diagnostics.ts +2 -1
- package/src/config/load-degrade.ts +2 -0
- package/src/config/paths.ts +7 -1
- package/src/config/process-state.ts +10 -1
- package/src/config/schema/compaction-recovery.ts +15 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +9 -3
- package/src/generated/compatibility-version.json +360 -204
- package/src/images/loop.ts +47 -32
- package/src/integrations/catalog-refresh.ts +4 -0
- package/src/integrations/omp-yaml-source.ts +1 -0
- package/src/lib/claude-request-projection.ts +101 -0
- package/src/lib/local-account-switch-capability.ts +48 -0
- package/src/lib/local-upstream.ts +146 -0
- package/src/lib/package-tree-integrity.ts +25 -3
- package/src/lib/package-tree-retarget.ts +130 -0
- package/src/lib/process-control.ts +4 -1
- package/src/lib/system-restart-contract.ts +106 -0
- package/src/lib/upstream-retry.ts +57 -1
- package/src/link/ports.ts +9 -0
- package/src/link/ssh-argv.ts +16 -0
- package/src/link/ssh-runner.ts +70 -2
- package/src/link/tunnel-state.ts +58 -9
- package/src/oauth/account-quota-rank.ts +29 -18
- package/src/oauth/generic-account-failover.ts +179 -22
- package/src/oauth/index.ts +17 -4
- package/src/oauth/kiro-account-load.ts +107 -0
- package/src/oauth/kiro-device-login.ts +312 -0
- package/src/oauth/kiro-terminal-failover.ts +28 -0
- package/src/oauth/login-flow-state.ts +7 -1
- package/src/oauth/pool-settings-capability.ts +23 -14
- package/src/oauth/store.ts +56 -3
- package/src/oauth/types.ts +4 -0
- package/src/plugins/loader.ts +350 -0
- package/src/plugins/upstream-hooks.ts +119 -0
- package/src/protocols/encoders/adapter-events.ts +9 -2
- package/src/providers/account-quota-disk.ts +42 -4
- package/src/providers/codebuddy-models.ts +6 -2
- package/src/providers/command-code-efforts.ts +33 -10
- package/src/providers/kiro-account-state-disk.ts +44 -0
- package/src/providers/kiro-model-catalog.ts +162 -0
- package/src/providers/kiro-models.ts +5 -4
- package/src/providers/kiro-quota-metrics.ts +35 -0
- package/src/providers/kiro-usage.ts +118 -14
- package/src/providers/quota/account-cache.ts +43 -7
- package/src/providers/quota/antigravity.ts +8 -3
- package/src/providers/quota/kiro-account-probe.ts +12 -0
- package/src/providers/quota/vendor-probes-key.ts +31 -26
- package/src/providers/quota/vendor-probes-oauth.ts +16 -7
- package/src/providers/quota-types.ts +14 -0
- package/src/providers/quota.ts +21 -21
- package/src/providers/registry/entries-extended.ts +5 -3
- package/src/providers/request-pacing.ts +410 -12
- package/src/remote-control/workspace-codex-runtime.ts +4 -3
- package/src/responses/citation-markers.ts +132 -65
- package/src/responses/hosted-tool-policy.ts +14 -3
- package/src/responses/parser-content.ts +3 -2
- package/src/responses/parser.ts +4 -1
- package/src/responses/schema.ts +4 -0
- package/src/responses/visualization-directives.ts +182 -0
- package/src/server/chat-native-sse.ts +24 -12
- package/src/server/chat-native.ts +137 -68
- package/src/server/claude-messages.ts +147 -12
- package/src/server/index/link-listener.ts +7 -0
- package/src/server/index/optional-listeners.ts +4 -1
- package/src/server/index/package-tree-guard.ts +35 -16
- package/src/server/index/serve-options.ts +4 -2
- package/src/server/index/startup-warnings.ts +6 -0
- package/src/server/index.ts +7 -7
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/inference/context.ts +30 -2
- package/src/server/local-account-switch-auth.ts +79 -0
- package/src/server/management/agent-settings-routes.ts +11 -3
- package/src/server/management/config-routes.ts +28 -11
- package/src/server/management/context.ts +2 -0
- package/src/server/management/link-routes.ts +123 -39
- package/src/server/management/logs-usage-routes.ts +9 -42
- package/src/server/management/model-rows.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +67 -12
- package/src/server/management/route-registry.ts +5 -5
- package/src/server/management/sibling-guard.ts +60 -0
- package/src/server/management/storage-log-guard-routes.ts +72 -6
- package/src/server/management/system-restart.ts +92 -135
- package/src/server/management/system-routes.ts +1 -1
- package/src/server/management-api.ts +31 -3
- package/src/server/management-auth.ts +3 -0
- package/src/server/port-reclaim.ts +111 -1
- package/src/server/proxy-liveness.ts +16 -0
- package/src/server/relay.ts +16 -15
- package/src/server/request-log-filter.ts +28 -27
- package/src/server/request-log.ts +7 -3
- package/src/server/request-metrics.ts +28 -0
- package/src/server/responses/adapter-continuation.ts +151 -21
- package/src/server/responses/adapter-delivery.ts +21 -7
- package/src/server/responses/adapter-dispatch.ts +239 -51
- package/src/server/responses/codex-ws-pool.ts +5 -4
- package/src/server/responses/codex-ws-request.ts +4 -1
- package/src/server/responses/compact.ts +8 -4
- package/src/server/responses/compaction-recovery-policy.ts +116 -0
- package/src/server/responses/compaction-recovery.ts +330 -0
- package/src/server/responses/core-combo-failure.ts +1 -0
- package/src/server/responses/core-combo.ts +5 -1
- package/src/server/responses/core-lifetime.ts +22 -0
- package/src/server/responses/core-options.ts +16 -1
- package/src/server/responses/core.ts +14 -4
- package/src/server/responses/empty-completion-guard.ts +2 -0
- package/src/server/responses/fetch-helpers.ts +82 -31
- package/src/server/responses/passthrough-delivery.ts +3 -2
- package/src/server/responses/passthrough-dispatch.ts +12 -2
- package/src/server/responses/passthrough-execution.ts +2 -2
- package/src/server/responses/request-send-budget.ts +21 -5
- package/src/server/responses/request-transport.ts +116 -18
- package/src/server/responses/reset-replay.ts +32 -0
- package/src/server/responses/run-turn-execution.ts +136 -19
- package/src/server/responses/sidecar-execution.ts +41 -8
- package/src/server/responses/terminal-guard.ts +2 -0
- package/src/server/responses/ws-upstream.ts +44 -4
- package/src/server/restart-replacement.ts +382 -0
- package/src/server/startup-health-cache.ts +23 -4
- package/src/server/stop-teardown.ts +35 -1
- package/src/server/system-env.ts +3 -0
- package/src/service/windows-taskxml.ts +11 -7
- package/src/service/windows-wrapper-exit.ts +8 -0
- package/src/stall-timeout.ts +35 -10
- package/src/storage/policy-job.ts +4 -0
- package/src/storage/scanner.ts +177 -29
- package/src/storage/storage-mutation-coordinator.ts +17 -0
- package/src/tray/windows-tray.ps1 +50 -23
- package/src/types/config.ts +4 -0
- package/src/types/provider.ts +5 -1
- package/src/types/request.ts +13 -2
- package/src/update/index.ts +3 -1
- package/src/update/job.ts +6 -2
- package/src/update/mise-launcher-target.ts +100 -0
- package/src/update/worker-launch.ts +46 -0
- package/src/usage/log.ts +2 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/App-BJsT8Icc.css +0 -1
- package/gui/dist/assets/App-D3pNGiN4.js +0 -51
- package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
- package/gui/dist/assets/index-CouvvtMV.js +0 -86
- package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
|
@@ -3,6 +3,7 @@ import { diagnoseCodexShim, installCodexShim, uninstallCodexShim } from "../code
|
|
|
3
3
|
import { readManagementJsonBody } from "../server/management/body";
|
|
4
4
|
import type { OcxClientConnectionConfig } from "../types";
|
|
5
5
|
import { disconnectClient, syncConnectedClient } from "./connect";
|
|
6
|
+
import { isLinkConnection } from "./state";
|
|
6
7
|
|
|
7
8
|
export type HubReachability = "unknown" | "online" | "offline" | "unauthorized";
|
|
8
9
|
|
|
@@ -50,14 +51,19 @@ async function jsonBody(req: Request): Promise<unknown | Response> {
|
|
|
50
51
|
|
|
51
52
|
function statusPayload(req: Request, state: OcxClientConnectionConfig, deps: MachineApiDeps): MachineStatusV1 {
|
|
52
53
|
const machineBase = new URL(req.url).origin;
|
|
54
|
+
// A link Child's management URL is the tunnel, whose hub-link ingress serves no /api/* and no
|
|
55
|
+
// session bootstrap. Its dashboard reads the Child's own listener instead (`/api/link/status`).
|
|
56
|
+
const linkMode = isLinkConnection(state);
|
|
53
57
|
return {
|
|
54
58
|
mode: "client",
|
|
55
59
|
connected: true,
|
|
56
60
|
machineBase,
|
|
57
|
-
sharedBase:
|
|
58
|
-
?
|
|
59
|
-
: state.
|
|
60
|
-
|
|
61
|
+
sharedBase: linkMode
|
|
62
|
+
? machineBase
|
|
63
|
+
: state.managementTransport === "relay"
|
|
64
|
+
? `${machineBase}/api/machine/hub-relay`
|
|
65
|
+
: state.managementUrl,
|
|
66
|
+
sharedServerOrigin: linkMode ? machineBase : state.managementUrl,
|
|
61
67
|
managementTransport: state.managementTransport,
|
|
62
68
|
apiKeyId: state.apiKeyId,
|
|
63
69
|
protocolVersion: state.protocolVersion,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import { loadConfig } from "../config";
|
|
3
|
-
import { browserSecurityHeaders } from "../server/auth-cors";
|
|
3
|
+
import { browserSecurityHeaders, requestPolicyView } from "../server/auth-cors";
|
|
4
4
|
import { serveGuiFile, serveSessionBootstrap } from "../server/gui-static";
|
|
5
5
|
import {
|
|
6
6
|
initializeManagementAuthState,
|
|
@@ -9,13 +9,18 @@ import {
|
|
|
9
9
|
requireManagementAuth,
|
|
10
10
|
type ManagementAuthState,
|
|
11
11
|
} from "../server/management-auth";
|
|
12
|
+
import { resolveInboundBodyLimitBytes } from "../server/request-decompress";
|
|
12
13
|
import type { OcxClientConnectionConfig, OcxConfig } from "../types";
|
|
13
14
|
import { disconnectClient, syncConnectedClient } from "./connect";
|
|
14
15
|
import { isLinkConnection, readClientConnectionState } from "./state";
|
|
15
16
|
import { handleMachineApi, type HubReachability, type MachineApiDeps } from "./machine-api";
|
|
16
17
|
import { MACHINE_GUI_ORIGIN_HEADER, requireMachineAuth } from "./machine-auth";
|
|
17
18
|
import { HUB_RELAY_REQUEST_BODY_MAX_BYTES, relayHubManagementRequest } from "./hub-relay";
|
|
18
|
-
import {
|
|
19
|
+
import { createLinkKeySource, handleLinkIngress, type LinkIngress, type LinkKeySourceDeps } from "./link-ingress";
|
|
20
|
+
import type { LinkTunnelGate } from "./link-relay";
|
|
21
|
+
import { readClientLinkState, type ClientLinkState } from "./link-state";
|
|
22
|
+
import { projectClientLinkChild, type ClientLinkSidecarRead } from "./link-status";
|
|
23
|
+
import type { ClientLinkSupervisorStatus } from "./link-tunnel";
|
|
19
24
|
import { packageVersion } from "../lib/package-version";
|
|
20
25
|
import { linkRouteAllowed } from "../link/routes";
|
|
21
26
|
|
|
@@ -24,12 +29,27 @@ const GUI_SPA_PATHS = new Set([
|
|
|
24
29
|
"/dashboard", "/startup", "/providers", "/models", "/subagents",
|
|
25
30
|
"/logs", "/usage", "/storage", "/codex-set", "/integrations",
|
|
26
31
|
]);
|
|
32
|
+
/** The standalone listener's idle limit: long generations and held turns are never cut. */
|
|
33
|
+
const LINK_LISTENER_IDLE_TIMEOUT_SECONDS = 255;
|
|
27
34
|
|
|
28
35
|
export interface MachineListenerDeps {
|
|
29
36
|
state?: OcxClientConnectionConfig;
|
|
30
37
|
managementAuthState?: ManagementAuthState;
|
|
31
38
|
fetchImpl?: typeof fetch;
|
|
32
39
|
machineApi?: Partial<MachineApiDeps>;
|
|
40
|
+
/** Link mode: the client tunnel supervisor state for `GET /api/link/status`. */
|
|
41
|
+
linkStatus?: () => ClientLinkSupervisorStatus;
|
|
42
|
+
/** Link mode: the sidecar read for `GET /api/link/status`. */
|
|
43
|
+
readSidecar?: () => ClientLinkState | null;
|
|
44
|
+
/** Link mode: how the link key is read; the listener reads it once and caches it. */
|
|
45
|
+
linkKey?: LinkKeySourceDeps;
|
|
46
|
+
/** Link mode: a key source the runtime already holds (shared with the tunnel supervisor). */
|
|
47
|
+
linkKeySource?: () => string | null;
|
|
48
|
+
/** Link mode: the tunnel supervisor; relayed requests wait on it while it reconnects. */
|
|
49
|
+
linkTunnel?: LinkTunnelGate;
|
|
50
|
+
/** Link mode: relay seams (deadline clock and byte cap). */
|
|
51
|
+
linkRelay?: LinkIngress["relay"];
|
|
52
|
+
serve?: (options: Parameters<typeof Bun.serve>[0]) => Server<unknown>;
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
function json404(req: Request): Response {
|
|
@@ -47,6 +67,8 @@ export function machineRouteAllowed(url: URL, req: Request, relayEnabled: boolea
|
|
|
47
67
|
const path = url.pathname;
|
|
48
68
|
if (req.method === "GET" && (path === "/healthz" || path === "/readyz" || path === "/" || path === "/opencodex-session")) return true;
|
|
49
69
|
if ((req.method === "GET" || req.method === "HEAD") && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
|
|
70
|
+
// The one link route a connected Child serves: its own read-only link status.
|
|
71
|
+
if (linkMode && (req.method === "GET" || req.method === "HEAD") && path === "/api/link/status") return true;
|
|
50
72
|
if (req.method === "POST" && (path === "/api/machine/sync" || path === "/api/machine/shim" || path === "/api/machine/disconnect")) return true;
|
|
51
73
|
if (relayEnabled && path.startsWith("/api/machine/hub-relay/")) return true;
|
|
52
74
|
// Known machine endpoints are admitted for every method so an unsupported
|
|
@@ -60,6 +82,10 @@ export function machineRouteAllowed(url: URL, req: Request, relayEnabled: boolea
|
|
|
60
82
|
|| /\.(?:css|gif|ico|jpe?g|js|json|map|png|svg|webp|woff2?)$/i.test(path);
|
|
61
83
|
}
|
|
62
84
|
|
|
85
|
+
function readSidecarSafely(read: () => ClientLinkState | null): ClientLinkSidecarRead {
|
|
86
|
+
try { return read(); } catch { return "invalid"; }
|
|
87
|
+
}
|
|
88
|
+
|
|
63
89
|
export function startMachineListener(
|
|
64
90
|
port?: number,
|
|
65
91
|
deps: MachineListenerDeps = {},
|
|
@@ -84,17 +110,33 @@ export function startMachineListener(
|
|
|
84
110
|
setHubReachability: deps.machineApi?.setHubReachability ?? (value => { hubReachability = value; }),
|
|
85
111
|
};
|
|
86
112
|
const relayEnabled = !linkMode && connection.managementTransport === "relay";
|
|
113
|
+
// Link mode only. Everything is resolved once here, so a relayed request reads no file and
|
|
114
|
+
// builds no policy: the key is cached, the loopback policy is fixed at bind like the listener.
|
|
115
|
+
const inboundBodyLimit = resolveInboundBodyLimitBytes(config.maxInboundBodyBytes);
|
|
116
|
+
const linkIngress: LinkIngress | null = linkMode
|
|
117
|
+
? {
|
|
118
|
+
tunnelPort: connection.link!.tunnelPort,
|
|
119
|
+
policy: requestPolicyView(config, "127.0.0.1"),
|
|
120
|
+
linkKey: deps.linkKeySource ?? createLinkKeySource(connection.tokenFingerprint, deps.linkKey),
|
|
121
|
+
relay: { fetchImpl: deps.fetchImpl, bodyLimitBytes: inboundBodyLimit, tunnel: deps.linkTunnel, ...deps.linkRelay },
|
|
122
|
+
}
|
|
123
|
+
: null;
|
|
124
|
+
const readSidecar = deps.readSidecar ?? (() => readClientLinkState());
|
|
87
125
|
|
|
88
|
-
return Bun.serve({
|
|
126
|
+
return (deps.serve ?? (options => Bun.serve(options)))({
|
|
89
127
|
port: port ?? config.port ?? 10100,
|
|
90
128
|
hostname: "127.0.0.1",
|
|
91
|
-
|
|
92
|
-
|
|
129
|
+
// A hub client relays only bounded management calls. A link Child carries the Codex data
|
|
130
|
+
// plane, so it admits what a standalone listener admits and keeps its idle limit.
|
|
131
|
+
maxRequestBodySize: linkMode ? inboundBodyLimit : HUB_RELAY_REQUEST_BODY_MAX_BYTES,
|
|
132
|
+
...(linkMode ? { idleTimeout: LINK_LISTENER_IDLE_TIMEOUT_SECONDS } : {}),
|
|
133
|
+
async fetch(req: Request, server: Server<unknown>) {
|
|
93
134
|
const url = new URL(req.url);
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
|
|
135
|
+
if (linkIngress) {
|
|
136
|
+
const handled = handleLinkIngress(req, url, linkIngress, server);
|
|
137
|
+
if (handled) return handled;
|
|
97
138
|
}
|
|
139
|
+
if (!machineRouteAllowed(url, req, relayEnabled, linkMode)) return json404(req);
|
|
98
140
|
if (url.pathname === "/healthz" && req.method === "GET") {
|
|
99
141
|
return Response.json({ service: "opencodex", version: VERSION, role: "client", uptime: process.uptime(), pid: process.pid, port: server.port });
|
|
100
142
|
}
|
|
@@ -116,7 +158,7 @@ export function startMachineListener(
|
|
|
116
158
|
else hubReachability = "online";
|
|
117
159
|
return response;
|
|
118
160
|
}
|
|
119
|
-
if (url.pathname.startsWith("/api/machine/")) {
|
|
161
|
+
if (url.pathname.startsWith("/api/machine/") || (linkMode && url.pathname === "/api/link/status")) {
|
|
120
162
|
const authError = requireManagementAuth(req, managementAuth, config);
|
|
121
163
|
if (authError) return authError;
|
|
122
164
|
if (managementPrincipal(req, managementAuth, config) !== "gui-session") {
|
|
@@ -130,6 +172,11 @@ export function startMachineListener(
|
|
|
130
172
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
131
173
|
return Response.json({ error: "opencodex machine changes require the local CLI" }, { status: 403 });
|
|
132
174
|
}
|
|
175
|
+
if (url.pathname === "/api/link/status") {
|
|
176
|
+
// A Child never joins again from here, so the dashboard reads `joinAvailable: false`.
|
|
177
|
+
const status = projectClientLinkChild(readSidecarSafely(readSidecar), deps.linkStatus?.() ?? { kind: "stopped" }, Date.now());
|
|
178
|
+
return Response.json({ ...status, joinAvailable: false }, { headers: { "Cache-Control": "no-store" } });
|
|
179
|
+
}
|
|
133
180
|
return await handleMachineApi(req, url, connection, machineApiDeps) ?? json404(req);
|
|
134
181
|
}
|
|
135
182
|
|
|
@@ -158,5 +205,5 @@ export function startMachineListener(
|
|
|
158
205
|
}
|
|
159
206
|
return json404(req);
|
|
160
207
|
},
|
|
161
|
-
});
|
|
208
|
+
} as Parameters<typeof Bun.serve>[0]);
|
|
162
209
|
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
2
|
import type { Server } from "bun";
|
|
3
|
+
import { siblingRuntimeField, withSiblingMarker } from "../codex/sibling-start";
|
|
4
|
+
import { issueSiblingHandoff } from "../codex/sibling-handoff";
|
|
4
5
|
import { loadConfig } from "../config";
|
|
5
|
-
import { removePid, removeRuntimePort, writePid, writeRuntimePort } from "../config/process-state";
|
|
6
|
+
import { removePid, removeRuntimePort, writePid, writeRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
6
7
|
import { installCrashGuards } from "../lib/crash-guard";
|
|
7
|
-
import {
|
|
8
|
+
import { createLocalAttestationSecret } from "../lib/local-management-attestation";
|
|
8
9
|
import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DESKTOP_RESTART_EXIT_CODE,
|
|
12
|
+
DESKTOP_SUPERVISED_ENV,
|
|
13
|
+
DESKTOP_SUPERVISED_PORT_WAIT_MS,
|
|
14
|
+
isDesktopSupervised,
|
|
15
|
+
} from "../lib/system-restart-contract";
|
|
16
|
+
import { findAvailablePort, isAddrInUse, PortUnavailableError, waitForPortAvailable } from "../server/ports";
|
|
17
|
+
import type { ReplacementStartRequest } from "../server/restart-replacement";
|
|
18
|
+
import type { OcxClientConnectionConfig } from "../types";
|
|
19
|
+
import { createLinkKeySource } from "./link-ingress";
|
|
20
|
+
import { HOME_INITIATED_LINK_TUNNEL } from "./link-relay";
|
|
10
21
|
import { createClientLinkSupervisor, type ClientLinkSupervisor } from "./link-tunnel";
|
|
11
|
-
import { clientLinkStatePath } from "./link-state";
|
|
12
|
-
import { startMachineListener } from "./machine-listener";
|
|
22
|
+
import { clientLinkStatePath, isChildInitiatedLink, recordChildInitiatedLink } from "./link-state";
|
|
23
|
+
import { startMachineListener, type MachineListenerDeps } from "./machine-listener";
|
|
13
24
|
import { isLinkConnection, readClientConnectionState } from "./state";
|
|
14
25
|
|
|
15
26
|
let activeServer: Server<unknown> | null = null;
|
|
@@ -17,16 +28,64 @@ let activePort: number | null = null;
|
|
|
17
28
|
let activeSupervisor: ClientLinkSupervisor | null = null;
|
|
18
29
|
let recycleScheduled = false;
|
|
19
30
|
|
|
31
|
+
/**
|
|
32
|
+
* How long link mode waits for its configured port: the budget a hard-pinned `ocx start --port`
|
|
33
|
+
* gives `reclaimListenPort` (`src/cli/index.ts`).
|
|
34
|
+
*/
|
|
35
|
+
export const LINK_PORT_WAIT_MS = 60_000;
|
|
36
|
+
|
|
37
|
+
/** How long a pinned port is re-probed after the reclaim wait before the start gives up on it. */
|
|
38
|
+
export const PINNED_PREFER_RETRY_MS = 5_000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Link mode's port-reclaim budget. Under the desktop app the whole wait (this plus
|
|
42
|
+
* {@link PINNED_PREFER_RETRY_MS}) ends inside the app's 30-second startup deadline, so the app sees
|
|
43
|
+
* this start either serve or exit instead of giving up on it first.
|
|
44
|
+
*/
|
|
45
|
+
export function linkPortWaitMs(desktopSupervised: boolean = isDesktopSupervised()): number {
|
|
46
|
+
return desktopSupervised ? DESKTOP_SUPERVISED_PORT_WAIT_MS : LINK_PORT_WAIT_MS;
|
|
47
|
+
}
|
|
48
|
+
/** Bind attempts when the port is taken between the free-port probe and `Bun.serve`. */
|
|
49
|
+
const LINK_BIND_ATTEMPTS = 3;
|
|
50
|
+
|
|
51
|
+
export interface ClientRuntimeIo {
|
|
52
|
+
/** Link-mode port budget; defaults to {@link LINK_PORT_WAIT_MS}. */
|
|
53
|
+
portWaitMs?: number;
|
|
54
|
+
startListener?: typeof startMachineListener;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface StandaloneRecycleIo {
|
|
58
|
+
spawnReplacement?: (request: ReplacementStartRequest) => Promise<void>;
|
|
59
|
+
exitProcess?: (code: number) => void;
|
|
60
|
+
configuredPort?: () => number | undefined;
|
|
61
|
+
isDesktopSupervised?: () => boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
20
64
|
function cleanup(): void {
|
|
21
65
|
removePid(process.pid);
|
|
22
66
|
removeRuntimePort(process.pid);
|
|
23
67
|
}
|
|
24
68
|
|
|
69
|
+
/**
|
|
70
|
+
* What this runtime publishes in `runtime-port.json`. The attestation secret is what the desktop app
|
|
71
|
+
* reads to authenticate the runtime it started (`desktop/src-tauri/src/auth.rs`); a record without one
|
|
72
|
+
* reads as unusable there, the same as a standalone start's record would.
|
|
73
|
+
*/
|
|
74
|
+
export function clientRuntimeRecord(
|
|
75
|
+
pid: number,
|
|
76
|
+
port: number,
|
|
77
|
+
attestationSecret: string = createLocalAttestationSecret(),
|
|
78
|
+
): RuntimePortState {
|
|
79
|
+
return { pid, port, hostname: "127.0.0.1", attestationSecret, ...siblingRuntimeField() };
|
|
80
|
+
}
|
|
81
|
+
|
|
25
82
|
export function standaloneRecycleEnv(
|
|
26
83
|
env: NodeJS.ProcessEnv,
|
|
27
84
|
disconnectedTokenFingerprint: string,
|
|
28
85
|
): NodeJS.ProcessEnv {
|
|
29
86
|
const childEnv = { ...env };
|
|
87
|
+
// A detached replacement is not the desktop app's child.
|
|
88
|
+
delete childEnv[DESKTOP_SUPERVISED_ENV];
|
|
30
89
|
const admissionToken = childEnv.OPENCODEX_API_AUTH_TOKEN?.trim();
|
|
31
90
|
if (admissionToken && serviceApiTokenFingerprint(admissionToken) !== disconnectedTokenFingerprint) {
|
|
32
91
|
// A surviving env token shadows OCX_API_TOKEN_FILE entirely, so nothing below can
|
|
@@ -54,8 +113,31 @@ export function scheduleStandaloneRecycle(disconnectedTokenFingerprint: string):
|
|
|
54
113
|
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
55
114
|
}
|
|
56
115
|
|
|
57
|
-
|
|
58
|
-
|
|
116
|
+
function configuredTcpPort(): number | undefined {
|
|
117
|
+
try {
|
|
118
|
+
const port = loadConfig().port;
|
|
119
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : undefined;
|
|
120
|
+
} catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function errorCode(error: unknown): string {
|
|
126
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
127
|
+
return typeof code === "string" && /^[A-Za-z0-9_]{1,64}$/.test(code) ? code : "failed";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function recycleStandalone(
|
|
131
|
+
disconnectedTokenFingerprint: string,
|
|
132
|
+
io: StandaloneRecycleIo = {},
|
|
133
|
+
): Promise<void> {
|
|
134
|
+
// The configured port when the listener never recorded one: the recycle still owes a proxy.
|
|
135
|
+
const port = activePort ?? (io.configuredPort ?? configuredTcpPort)();
|
|
136
|
+
const exit = io.exitProcess ?? ((code: number) => { process.exit(code); });
|
|
137
|
+
// Capture the one-use sibling handoff before this process removes its runtime record.
|
|
138
|
+
const replacementEnv = port && process.env.OCX_SERVICE !== "1"
|
|
139
|
+
? withSiblingMarker(standaloneRecycleEnv(process.env, disconnectedTokenFingerprint), issueSiblingHandoff)
|
|
140
|
+
: null;
|
|
59
141
|
try {
|
|
60
142
|
await activeSupervisor?.stop();
|
|
61
143
|
} catch (error) {
|
|
@@ -69,8 +151,14 @@ async function recycleStandalone(disconnectedTokenFingerprint: string): Promise<
|
|
|
69
151
|
}
|
|
70
152
|
cleanup();
|
|
71
153
|
// Recycling back to standalone after `ocx disconnect` must actually bring a standalone
|
|
72
|
-
// proxy back, under
|
|
154
|
+
// proxy back, under every launch shape.
|
|
73
155
|
//
|
|
156
|
+
// Under the desktop app that spawned us: exit 75 and let the app start the standalone
|
|
157
|
+
// runtime, so it keeps owning it (tray Stop, Quit) instead of losing it to a detached child.
|
|
158
|
+
if ((io.isDesktopSupervised ?? isDesktopSupervised)()) {
|
|
159
|
+
exit(DESKTOP_RESTART_EXIT_CODE);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
74
162
|
// Unsupervised: spawn the replacement ourselves and exit 0.
|
|
75
163
|
//
|
|
76
164
|
// Supervised (`OCX_SERVICE=1`): do NOT spawn — the supervisor owns the process, and a
|
|
@@ -83,22 +171,103 @@ async function recycleStandalone(disconnectedTokenFingerprint: string): Promise<
|
|
|
83
171
|
//
|
|
84
172
|
// launchd's KeepAlive restarts on any exit, so it is correct under both branches.
|
|
85
173
|
if (process.env.OCX_SERVICE === "1") {
|
|
86
|
-
|
|
174
|
+
exit(1);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (port === undefined) {
|
|
178
|
+
console.warn("[client] no valid port to restart the standalone proxy on; run 'ocx start'");
|
|
179
|
+
exit(1);
|
|
180
|
+
return;
|
|
87
181
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
182
|
+
if (!replacementEnv) throw new Error("Standalone recycle has no replacement environment.");
|
|
183
|
+
// Wait until the replacement answers (it retries an early exit) instead of exiting the moment
|
|
184
|
+
// it spawned: a replacement that died unseen left no proxy and nothing to report it.
|
|
185
|
+
try {
|
|
186
|
+
const spawnReplacement = io.spawnReplacement
|
|
187
|
+
?? (async (request: ReplacementStartRequest) => (await import("../server/restart-replacement")).spawnReplacementStart(request));
|
|
188
|
+
await spawnReplacement({
|
|
189
|
+
port,
|
|
190
|
+
waitForHealth: true,
|
|
191
|
+
// A sibling's replacement stays a sibling even if the owner is down while it probes.
|
|
192
|
+
env: replacementEnv,
|
|
94
193
|
});
|
|
95
|
-
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn(`[client] the standalone replacement did not start (${errorCode(error)}); run 'ocx start'`);
|
|
196
|
+
exit(1);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Bind the client listener. Link mode is pinned to the configured port because Codex routes to it:
|
|
204
|
+
* like a hard-pinned `ocx start --port`, it waits for a port that a restarting parent is still
|
|
205
|
+
* releasing, never kills the holder and never hops. The waits run only while the port is busy; a
|
|
206
|
+
* free port binds on the first probe.
|
|
207
|
+
*/
|
|
208
|
+
export async function bindClientListener(
|
|
209
|
+
request: {
|
|
210
|
+
state: OcxClientConnectionConfig;
|
|
211
|
+
linkMode: boolean;
|
|
212
|
+
preferred: number;
|
|
213
|
+
explicitPort: boolean;
|
|
214
|
+
configuredPort: number;
|
|
215
|
+
} & Pick<MachineListenerDeps, "linkStatus" | "linkKeySource" | "linkTunnel">,
|
|
216
|
+
io: ClientRuntimeIo = {},
|
|
217
|
+
): Promise<{ server: Server<unknown>; port: number }> {
|
|
218
|
+
const { linkMode } = request;
|
|
219
|
+
const portWaitMs = io.portWaitMs ?? (linkMode ? linkPortWaitMs() : LINK_PORT_WAIT_MS);
|
|
220
|
+
const deadline = Date.now() + portWaitMs;
|
|
221
|
+
const busy = (cause: unknown) => new Error(
|
|
222
|
+
`link mode needs port ${request.configuredPort}; free it or change port`,
|
|
223
|
+
{ cause },
|
|
224
|
+
);
|
|
225
|
+
let port: number;
|
|
226
|
+
try {
|
|
227
|
+
if (linkMode) {
|
|
228
|
+
const { reclaimListenPort } = await import("../server/port-reclaim");
|
|
229
|
+
await reclaimListenPort(request.configuredPort, "127.0.0.1", {
|
|
230
|
+
timeoutMs: portWaitMs,
|
|
231
|
+
intervalMs: 100,
|
|
232
|
+
scanIntervalMs: 500,
|
|
233
|
+
killOcxHolders: false,
|
|
234
|
+
dropTcpRows: true,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
port = await findAvailablePort(request.preferred, "127.0.0.1", {
|
|
238
|
+
preferRetryMs: request.explicitPort ? PINNED_PREFER_RETRY_MS : 750,
|
|
239
|
+
preferRetryIntervalMs: 50,
|
|
240
|
+
allowEphemeralFallback: linkMode ? false : !request.explicitPort,
|
|
241
|
+
});
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (linkMode && error instanceof PortUnavailableError) throw busy(error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
const startListener = io.startListener ?? startMachineListener;
|
|
247
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
248
|
+
try {
|
|
249
|
+
const server = startListener(port, {
|
|
250
|
+
state: request.state,
|
|
251
|
+
...(request.linkStatus ? { linkStatus: request.linkStatus } : {}),
|
|
252
|
+
...(request.linkKeySource ? { linkKeySource: request.linkKeySource } : {}),
|
|
253
|
+
...(request.linkTunnel ? { linkTunnel: request.linkTunnel } : {}),
|
|
254
|
+
});
|
|
255
|
+
return { server, port: server.port ?? port };
|
|
256
|
+
} catch (error) {
|
|
257
|
+
// Check-then-bind: the port can be taken between the probe and Bun.serve.
|
|
258
|
+
if (!linkMode || !isAddrInUse(error)) throw error;
|
|
259
|
+
const waitMs = Math.max(1_000, deadline - Date.now());
|
|
260
|
+
if (attempt >= LINK_BIND_ATTEMPTS
|
|
261
|
+
|| !(await waitForPortAvailable(port, "127.0.0.1", { timeoutMs: waitMs, intervalMs: 100 }))) {
|
|
262
|
+
throw busy(error);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
96
265
|
}
|
|
97
|
-
process.exit(0);
|
|
98
266
|
}
|
|
99
267
|
|
|
100
268
|
export async function startClientRuntime(
|
|
101
269
|
options: { port?: number; block?: boolean } = {},
|
|
270
|
+
io: ClientRuntimeIo = {},
|
|
102
271
|
): Promise<void> {
|
|
103
272
|
const state = readClientConnectionState();
|
|
104
273
|
if (state.kind !== "connected") throw new Error(`client runtime refused: client state is ${state.kind}`);
|
|
@@ -108,33 +277,39 @@ export async function startClientRuntime(
|
|
|
108
277
|
throw new Error("link mode requires a valid local config port");
|
|
109
278
|
}
|
|
110
279
|
const preferred = linkMode ? config.port : options.port ?? config.port ?? 10100;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
allowEphemeralFallback: linkMode ? false : options.port === undefined,
|
|
117
|
-
});
|
|
118
|
-
} catch (error) {
|
|
119
|
-
if (linkMode && error instanceof PortUnavailableError) {
|
|
120
|
-
throw new Error(`link mode needs port ${config.port}; free it or change port`, { cause: error });
|
|
121
|
-
}
|
|
122
|
-
throw error;
|
|
280
|
+
// Share one cached key source between the listener and its tunnel supervisor.
|
|
281
|
+
const linkKey = linkMode ? createLinkKeySource(state.value.tokenFingerprint) : undefined;
|
|
282
|
+
// Joins made before the marker existed get one now, while their sidecar is still here.
|
|
283
|
+
if (linkMode && state.value.link) {
|
|
284
|
+
try { recordChildInitiatedLink(state.value.link.linkId); } catch { /* the sidecar check below still applies */ }
|
|
123
285
|
}
|
|
124
|
-
const server = startMachineListener(port, { state: state.value });
|
|
125
|
-
const boundPort = server.port ?? port;
|
|
126
|
-
activeServer = server;
|
|
127
|
-
activePort = boundPort;
|
|
128
286
|
const supervisor = linkMode && existsSync(clientLinkStatePath())
|
|
129
287
|
? createClientLinkSupervisor({
|
|
130
288
|
onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
|
|
289
|
+
linkKey,
|
|
131
290
|
})
|
|
132
291
|
: null;
|
|
292
|
+
// A Child with no sidecar and no record of joining itself was connected by its Home over
|
|
293
|
+
// `ssh -R`; that link keeps 2.67.0's unproven forward. A Child-initiated link that lost its
|
|
294
|
+
// sidecar gets no gate at all, so the relay refuses it.
|
|
295
|
+
const homeInitiated = linkMode && !supervisor && !!state.value.link
|
|
296
|
+
&& !isChildInitiatedLink(state.value.link.linkId);
|
|
297
|
+
const { server, port: boundPort } = await bindClientListener({
|
|
298
|
+
state: state.value,
|
|
299
|
+
linkMode,
|
|
300
|
+
preferred,
|
|
301
|
+
explicitPort: options.port !== undefined,
|
|
302
|
+
configuredPort: config.port,
|
|
303
|
+
...(linkMode ? { linkStatus: () => supervisor?.status() ?? { kind: "stopped" as const }, linkKeySource: linkKey } : {}),
|
|
304
|
+
...(supervisor ? { linkTunnel: supervisor } : homeInitiated ? { linkTunnel: HOME_INITIATED_LINK_TUNNEL } : {}),
|
|
305
|
+
}, io);
|
|
306
|
+
activeServer = server;
|
|
307
|
+
activePort = boundPort;
|
|
133
308
|
activeSupervisor = supervisor;
|
|
134
309
|
supervisor?.start();
|
|
135
310
|
installCrashGuards();
|
|
136
311
|
writePid(process.pid);
|
|
137
|
-
writeRuntimePort(
|
|
312
|
+
writeRuntimePort(clientRuntimeRecord(process.pid, boundPort));
|
|
138
313
|
|
|
139
314
|
let shuttingDown = false;
|
|
140
315
|
const shutdown = () => {
|
|
@@ -45,18 +45,7 @@ export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
|
|
|
45
45
|
/** Pi's wire-dialect selector for an OpenAI-compatible endpoint. */
|
|
46
46
|
export const PI_API_DIALECT = "openai-completions";
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* opencode's config schema rejects a `limit` block that carries `context` without
|
|
50
|
-
* `output`, but CatalogModel has no authoritative per-model output field. Dropping
|
|
51
|
-
* `limit` entirely would also throw away the authoritative context window we DO have,
|
|
52
|
-
* so the block is emitted with this budget standing in for the missing half.
|
|
53
|
-
*
|
|
54
|
-
* The value matches REASONING_MAX_TOKENS_CEILING in src/adapters/anthropic.ts — the
|
|
55
|
-
* project's existing "safe ceiling across current models" figure. It is a ceiling for
|
|
56
|
-
* schema validity, NOT a claim about any specific model's true maximum, and it is
|
|
57
|
-
* clamped to the context window so a small-context model can never be emitted with
|
|
58
|
-
* output > context. Pi's `maxTokens` uses the same stand-in and the same clamp.
|
|
59
|
-
*/
|
|
48
|
+
/** Fallback only when the model has no known output limit; always clamped to context. */
|
|
60
49
|
export const SCHEMA_REQUIRED_OUTPUT_BUDGET = 32_000;
|
|
61
50
|
|
|
62
51
|
/** Deterministic loopback default for exported provider-block helpers in tests. */
|
|
@@ -37,6 +37,7 @@ export interface OpencodeCatalogModel {
|
|
|
37
37
|
provider?: string;
|
|
38
38
|
id?: string;
|
|
39
39
|
contextWindow?: number;
|
|
40
|
+
maxTokens?: number;
|
|
40
41
|
displayName?: string;
|
|
41
42
|
/**
|
|
42
43
|
* Declared input modalities, carried verbatim from `/api/models`. Serialized as opencode's
|
|
@@ -71,6 +72,7 @@ export interface ExportModel {
|
|
|
71
72
|
native?: boolean;
|
|
72
73
|
displayName?: string;
|
|
73
74
|
contextWindow?: number;
|
|
75
|
+
maxTokens?: number;
|
|
74
76
|
inputModalities?: string[];
|
|
75
77
|
/** Optional effort ladder exported only to clients that support it. */
|
|
76
78
|
reasoningEfforts?: string[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Shared client export model metadata.
|
|
2
|
+
import { getModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
2
3
|
import { SCHEMA_REQUIRED_OUTPUT_BUDGET } from "./constants";
|
|
3
4
|
import { expandFastExportModels } from "./fast-models";
|
|
4
5
|
import type { OpencodeCatalogModel, ExportModel, ExportClientId, ManagedContribution } from "./contracts";
|
|
@@ -18,9 +19,14 @@ export function authoritativeContextWindow(contextWindow: number | undefined): n
|
|
|
18
19
|
return undefined;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
export function outputBudgetFor(context: number): number {
|
|
23
|
-
|
|
22
|
+
/** Known model output limit, with a schema-required fallback, clamped to context. */
|
|
23
|
+
export function outputBudgetFor(context: number, model: OpencodeCatalogModel): number {
|
|
24
|
+
const provider = model.provider ?? "";
|
|
25
|
+
const metadata = getModelMetadata(resolveMetadataProvider(provider) ?? provider, model.id ?? model.namespaced);
|
|
26
|
+
const limit = authoritativeContextWindow(model.maxTokens)
|
|
27
|
+
?? authoritativeContextWindow(metadata?.maxTokens)
|
|
28
|
+
?? SCHEMA_REQUIRED_OUTPUT_BUDGET;
|
|
29
|
+
return Math.min(limit, context);
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -67,7 +67,7 @@ export function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig {
|
|
|
67
67
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
68
68
|
if (context !== undefined) {
|
|
69
69
|
entry.contextWindow = context;
|
|
70
|
-
entry.maxTokens = outputBudgetFor(context);
|
|
70
|
+
entry.maxTokens = outputBudgetFor(context, model);
|
|
71
71
|
}
|
|
72
72
|
const efforts = ompEfforts(model);
|
|
73
73
|
if (efforts.length > 0) {
|
|
@@ -42,7 +42,7 @@ export interface ZcodeStoreProviderRule {
|
|
|
42
42
|
providerName: string;
|
|
43
43
|
config: {
|
|
44
44
|
group: string;
|
|
45
|
-
access: { apiKey: string };
|
|
45
|
+
access: { type: "api-key"; apiKey: string };
|
|
46
46
|
api: { type: string; baseUrl: string };
|
|
47
47
|
personalModelIds: string[];
|
|
48
48
|
modelOrder: string[];
|
|
@@ -113,7 +113,7 @@ export function buildZcodeStoreProviderRule(ctx: ExportContext): ZcodeStoreProvi
|
|
|
113
113
|
providerName: ZCODE_STORE_PROVIDER_NAME,
|
|
114
114
|
config: {
|
|
115
115
|
group: ZCODE_STORE_PROVIDER_GROUP,
|
|
116
|
-
access: { apiKey: LOOPBACK_API_KEY_PLACEHOLDER },
|
|
116
|
+
access: { type: "api-key", apiKey: LOOPBACK_API_KEY_PLACEHOLDER },
|
|
117
117
|
api: { type: ZCODE_STORE_API_TYPE, baseUrl: `${ctx.baseUrl.replace(/\/v1\/?$/, "")}/v1` },
|
|
118
118
|
personalModelIds: ids,
|
|
119
119
|
/*
|
|
@@ -722,7 +722,7 @@ export function opencodeProviderBlocks(
|
|
|
722
722
|
const entry: OpencodeModelEntry = { name: exportModelLabel(model) };
|
|
723
723
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
724
724
|
if (context !== undefined) {
|
|
725
|
-
entry.limit = { context, output: outputBudgetFor(context) };
|
|
725
|
+
entry.limit = { context, output: outputBudgetFor(context, model) };
|
|
726
726
|
}
|
|
727
727
|
// `attachment` / `modalities` are fields of opencode's V1 model schema — the shape its
|
|
728
728
|
// published config.json defines and the one its loader reads (verified against opencode
|
|
@@ -934,6 +934,17 @@ export interface GajaeModelEntry {
|
|
|
934
934
|
input: string[];
|
|
935
935
|
contextWindow?: number;
|
|
936
936
|
maxTokens?: number;
|
|
937
|
+
/** Advertised only when the catalog declares at least one wire-selectable effort. */
|
|
938
|
+
reasoning?: true;
|
|
939
|
+
/** GJC's per-model reasoning-effort capability declaration. */
|
|
940
|
+
thinking?: {
|
|
941
|
+
mode: "effort";
|
|
942
|
+
minLevel: string;
|
|
943
|
+
maxLevel: string;
|
|
944
|
+
levels: string[];
|
|
945
|
+
};
|
|
946
|
+
/** Tells GJC to pass the selected level as OpenAI-compatible reasoning_effort. */
|
|
947
|
+
compat?: { supportsReasoningEffort: true };
|
|
937
948
|
}
|
|
938
949
|
|
|
939
950
|
/** Gajae validates strictly: an unknown field fails the whole config. */
|
|
@@ -1004,7 +1015,7 @@ function buildPiClientConfig(ctx: ExportContext, options: PiExportOptions = {}):
|
|
|
1004
1015
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
1005
1016
|
if (context !== undefined) {
|
|
1006
1017
|
entry.contextWindow = context;
|
|
1007
|
-
entry.maxTokens = outputBudgetFor(context);
|
|
1018
|
+
entry.maxTokens = outputBudgetFor(context, model);
|
|
1008
1019
|
}
|
|
1009
1020
|
models.push(entry);
|
|
1010
1021
|
}
|
|
@@ -1140,7 +1151,29 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
|
|
|
1140
1151
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
1141
1152
|
if (context !== undefined) {
|
|
1142
1153
|
entry.contextWindow = context;
|
|
1143
|
-
entry.maxTokens = outputBudgetFor(context);
|
|
1154
|
+
entry.maxTokens = outputBudgetFor(context, model);
|
|
1155
|
+
}
|
|
1156
|
+
// GJC accepts the OpenAI-compatible effort ladder in model metadata. `none` means
|
|
1157
|
+
// no parameter and `ultra` is an OCX orchestration level that folds to `max` on the
|
|
1158
|
+
// wire, so neither can be offered as a GJC model-level effort.
|
|
1159
|
+
const declaredEfforts = model.reasoningEfforts
|
|
1160
|
+
// Native Codex rows do not repeat their built-in ladder in the catalog. They still
|
|
1161
|
+
// accept the standard effort field, so omitting this fallback hides GJC's thinking
|
|
1162
|
+
// control for the models most likely to need it.
|
|
1163
|
+
?? (model.native && model.provider === "openai"
|
|
1164
|
+
? ["low", "medium", "high", "xhigh", "max"]
|
|
1165
|
+
: []);
|
|
1166
|
+
const efforts = canonicalizeReasoningEfforts(declaredEfforts)
|
|
1167
|
+
.filter(effort => effort !== "none" && effort !== "ultra");
|
|
1168
|
+
if (efforts.length > 0) {
|
|
1169
|
+
entry.reasoning = true;
|
|
1170
|
+
entry.thinking = {
|
|
1171
|
+
mode: "effort",
|
|
1172
|
+
minLevel: efforts[0]!,
|
|
1173
|
+
maxLevel: efforts.at(-1)!,
|
|
1174
|
+
levels: efforts,
|
|
1175
|
+
};
|
|
1176
|
+
entry.compat = { supportsReasoningEffort: true };
|
|
1144
1177
|
}
|
|
1145
1178
|
models.push(entry);
|
|
1146
1179
|
}
|