@bitkyc08/opencodex 2.90.0 → 2.92.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/bin/ocx.mjs +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire shape of `GET /api/quota-pool`: the GOAT contract `ocx.quota-pool/1`.
|
|
3
|
+
*
|
|
4
|
+
* GOAT owns the decoder (`packages/contracts/src/quotaPool.ts` in goat-si/goat); OpenCodex is
|
|
5
|
+
* the producer. Every optional field is OMITTED when OpenCodex does not know it. A missing
|
|
6
|
+
* window means unknown, never 0%, and no field is ever `null`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const QUOTA_POOL_SCHEMA = "ocx.quota-pool/1" as const;
|
|
10
|
+
|
|
11
|
+
export type QuotaPoolWindowKind = "burst_5h" | "daily" | "weekly" | "monthly" | "credits" | "custom";
|
|
12
|
+
/** measured: a usage probe body; header: response rate-limit headers; estimated: derived by OpenCodex. */
|
|
13
|
+
export type QuotaPoolSource = "measured" | "header" | "estimated";
|
|
14
|
+
export type QuotaPoolUnit = "requests" | "tokens" | "credits" | "usd";
|
|
15
|
+
export type QuotaPoolAccountState = "available" | "exhausted" | "paused" | "reauth" | "unknown";
|
|
16
|
+
export type QuotaPoolRole = "standalone" | "hub" | "client";
|
|
17
|
+
|
|
18
|
+
export interface QuotaPoolWindow {
|
|
19
|
+
id: string;
|
|
20
|
+
kind: QuotaPoolWindowKind;
|
|
21
|
+
label: string;
|
|
22
|
+
usedPercent?: number;
|
|
23
|
+
used?: number;
|
|
24
|
+
limit?: number;
|
|
25
|
+
unit?: QuotaPoolUnit;
|
|
26
|
+
resetsAt?: string;
|
|
27
|
+
windowSeconds?: number;
|
|
28
|
+
scopeModel?: string;
|
|
29
|
+
source: QuotaPoolSource;
|
|
30
|
+
observedAt: string;
|
|
31
|
+
exhausted?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface QuotaPoolAccount {
|
|
35
|
+
provider: string;
|
|
36
|
+
driver?: string;
|
|
37
|
+
/** Opaque, stable for this install, never an email or a raw account id (see identity.ts). */
|
|
38
|
+
accountRef: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
plan?: string;
|
|
41
|
+
state: QuotaPoolAccountState;
|
|
42
|
+
windows: QuotaPoolWindow[];
|
|
43
|
+
/** A closed `QuotaFailureCode`; never upstream text. */
|
|
44
|
+
failure?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface QuotaPoolProvider {
|
|
48
|
+
provider: string;
|
|
49
|
+
accounts: QuotaPoolAccount[];
|
|
50
|
+
pooled: { windows: QuotaPoolWindow[]; headroom?: number; nextResetAt?: string };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface QuotaPool {
|
|
54
|
+
schema: typeof QUOTA_POOL_SCHEMA;
|
|
55
|
+
producer: { version: string; role: QuotaPoolRole; homeId: string };
|
|
56
|
+
generatedAt: string;
|
|
57
|
+
providers: QuotaPoolProvider[];
|
|
58
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install identity behind `ocx.quota-pool/1`: the Home's `homeId` and the secret that keys
|
|
3
|
+
* every `accountRef`.
|
|
4
|
+
*
|
|
5
|
+
* Both live in `<OPENCODEX_HOME>/quota-pool-identity.json` (owner-only). The server creates the
|
|
6
|
+
* file once, in the background after it starts listening (`ensureQuotaPoolIdentity`); the
|
|
7
|
+
* `/api/quota-pool` read only reads it, so the read path never writes. A read that finds no
|
|
8
|
+
* valid file reports the identity as unavailable instead of minting one.
|
|
9
|
+
*
|
|
10
|
+
* `accountRef` is `acct_` + base64url(HMAC-SHA256(secret, "ocx.quota-pool/1" NUL provider NUL
|
|
11
|
+
* accountId)) truncated to 128 bits. It is stable across restarts because the secret is
|
|
12
|
+
* persisted, distinct per provider, and not reversible: without the per-install secret a
|
|
13
|
+
* recipient cannot confirm a guessed email or account id against it, and it never contains
|
|
14
|
+
* either. Deleting the file rotates `homeId` and every `accountRef` together.
|
|
15
|
+
*/
|
|
16
|
+
import { createHmac, randomBytes, randomUUID } from "node:crypto";
|
|
17
|
+
import { closeSync, constants as fsConstants, fsyncSync, linkSync, lstatSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { getConfigDir } from "../config/paths";
|
|
20
|
+
|
|
21
|
+
export const QUOTA_POOL_IDENTITY_FILE = "quota-pool-identity.json";
|
|
22
|
+
|
|
23
|
+
export interface QuotaPoolIdentity {
|
|
24
|
+
homeId: string;
|
|
25
|
+
secret: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HOME_ID = /^home_[0-9a-f]{32}$/;
|
|
29
|
+
const SECRET = /^[0-9a-f]{64}$/;
|
|
30
|
+
let memo: { path: string; identity: QuotaPoolIdentity } | null = null;
|
|
31
|
+
|
|
32
|
+
export function quotaPoolIdentityPath(configDir = getConfigDir()): string {
|
|
33
|
+
return join(configDir, QUOTA_POOL_IDENTITY_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseIdentity(text: string): QuotaPoolIdentity | null {
|
|
37
|
+
try {
|
|
38
|
+
const raw = JSON.parse(text) as Record<string, unknown>;
|
|
39
|
+
if (raw?.version !== 1 || typeof raw.homeId !== "string" || typeof raw.secret !== "string") return null;
|
|
40
|
+
if (!HOME_ID.test(raw.homeId) || !SECRET.test(raw.secret)) return null;
|
|
41
|
+
return { homeId: raw.homeId, secret: raw.secret };
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Read-only: the persisted identity, or null when it is missing or malformed. Never writes. */
|
|
48
|
+
export function readQuotaPoolIdentity(configDir = getConfigDir()): QuotaPoolIdentity | null {
|
|
49
|
+
const path = quotaPoolIdentityPath(configDir);
|
|
50
|
+
if (memo?.path === path) return memo.identity;
|
|
51
|
+
try {
|
|
52
|
+
const stat = lstatSync(path);
|
|
53
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024) return null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
let identity: QuotaPoolIdentity | null;
|
|
58
|
+
try { identity = parseIdentity(readFileSync(path, "utf8")); } catch { return null; }
|
|
59
|
+
if (identity) memo = { path, identity };
|
|
60
|
+
return identity;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const NOFOLLOW = process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW;
|
|
64
|
+
/** How long a loser waits for a concurrent winner's file to become readable before giving up. */
|
|
65
|
+
const PUBLISHED_READ_ATTEMPTS = 50;
|
|
66
|
+
const PUBLISHED_READ_INTERVAL_MS = 10;
|
|
67
|
+
|
|
68
|
+
/** Test seams for the publication windows; production never sets them. */
|
|
69
|
+
export interface QuotaPoolIdentityTestHooks {
|
|
70
|
+
/** After the first read found no identity, before the existence check. */
|
|
71
|
+
afterAbsentRead?: (path: string) => void;
|
|
72
|
+
/** After a file was exclusively created, before its bytes are written. */
|
|
73
|
+
afterCreate?: (path: string) => void;
|
|
74
|
+
/** Replace `linkSync` (to force the in-place fallback). */
|
|
75
|
+
link?: (existing: string, target: string) => void;
|
|
76
|
+
/** Override the bounded wait for a concurrent winner. */
|
|
77
|
+
publishedReadAttempts?: number;
|
|
78
|
+
}
|
|
79
|
+
let hooks: QuotaPoolIdentityTestHooks = {};
|
|
80
|
+
|
|
81
|
+
export function setQuotaPoolIdentityHooksForTests(next: QuotaPoolIdentityTestHooks = {}): void {
|
|
82
|
+
hooks = next;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Write a whole new owner-only file, flushed to disk; fails if anything (a symlink included) already exists. */
|
|
86
|
+
function createExclusive(path: string, content: string): void {
|
|
87
|
+
const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | NOFOLLOW, 0o600);
|
|
88
|
+
try {
|
|
89
|
+
hooks.afterCreate?.(path);
|
|
90
|
+
writeSync(fd, content);
|
|
91
|
+
fsyncSync(fd);
|
|
92
|
+
} finally { closeSync(fd); }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function errorCode(error: unknown): string | undefined {
|
|
96
|
+
return (error as NodeJS.ErrnoException | null)?.code;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Make the new directory entry durable too: fsyncing the file alone can lose the entry on power
|
|
101
|
+
* loss, and a lost entry would mint a new identity and rotate every accountRef on the next start.
|
|
102
|
+
* Best effort; Windows cannot open a directory for fsync.
|
|
103
|
+
*/
|
|
104
|
+
function syncParentDirectory(path: string): void {
|
|
105
|
+
if (process.platform === "win32") return;
|
|
106
|
+
try {
|
|
107
|
+
const fd = openSync(dirname(path), fsConstants.O_RDONLY);
|
|
108
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
109
|
+
} catch { /* the file itself is flushed; a directory fsync is an extra guarantee */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function pause(ms: number): void {
|
|
113
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Something already sits at the path: adopt it once it reads as a valid identity.
|
|
118
|
+
*
|
|
119
|
+
* A concurrent winner's file is complete the moment it appears when it was hard-linked, but the
|
|
120
|
+
* in-place fallback creates the file before writing it, so a loser can briefly see it empty. The
|
|
121
|
+
* wait is bounded; an invalid result is never returned or memoized. A symlink, or a file that stays
|
|
122
|
+
* invalid past the bound, is refused and left alone.
|
|
123
|
+
*/
|
|
124
|
+
function adoptPublishedIdentity(configDir: string, path: string): QuotaPoolIdentity {
|
|
125
|
+
const attempts = hooks.publishedReadAttempts ?? PUBLISHED_READ_ATTEMPTS;
|
|
126
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
127
|
+
let stat;
|
|
128
|
+
try { stat = lstatSync(path); } catch (error) {
|
|
129
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
130
|
+
stat = null;
|
|
131
|
+
}
|
|
132
|
+
if (stat && (stat.isSymbolicLink() || !stat.isFile())) break;
|
|
133
|
+
const identity = readQuotaPoolIdentity(configDir);
|
|
134
|
+
if (identity) return identity;
|
|
135
|
+
if (attempt + 1 < attempts) pause(PUBLISHED_READ_INTERVAL_MS);
|
|
136
|
+
}
|
|
137
|
+
throw new Error("quota-pool identity file is not a valid regular file; remove it to rotate the identity");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create the identity once. Called off the request path (server startup); idempotent.
|
|
142
|
+
*
|
|
143
|
+
* Publication is exclusive and never follows a link: the secret is written and flushed to a
|
|
144
|
+
* private temp file, then hard-linked into place, which fails if anything already sits at the path.
|
|
145
|
+
* Where hard links are unavailable it is created in place exclusively. Two servers starting on one
|
|
146
|
+
* fresh home therefore cannot each keep a different identity: a loser, including one that finds
|
|
147
|
+
* the winner's file appear between its own checks, re-reads within a bound and adopts the winner's.
|
|
148
|
+
* A symlink or a persistently malformed file at the path is refused and left alone (the read keeps
|
|
149
|
+
* reporting the identity as unavailable until an operator removes it); it is never overwritten.
|
|
150
|
+
*/
|
|
151
|
+
export function ensureQuotaPoolIdentity(configDir = getConfigDir()): QuotaPoolIdentity {
|
|
152
|
+
const existing = readQuotaPoolIdentity(configDir);
|
|
153
|
+
if (existing) return existing;
|
|
154
|
+
const path = quotaPoolIdentityPath(configDir);
|
|
155
|
+
hooks.afterAbsentRead?.(path);
|
|
156
|
+
let present = true;
|
|
157
|
+
try { lstatSync(path); } catch (error) { if (errorCode(error) !== "ENOENT") throw error; present = false; }
|
|
158
|
+
if (present) return adoptPublishedIdentity(configDir, path);
|
|
159
|
+
const identity: QuotaPoolIdentity = {
|
|
160
|
+
homeId: `home_${randomUUID().replaceAll("-", "")}`,
|
|
161
|
+
secret: randomBytes(32).toString("hex"),
|
|
162
|
+
};
|
|
163
|
+
const content = `${JSON.stringify({ version: 1, ...identity })}\n`;
|
|
164
|
+
const temp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
165
|
+
let published = false;
|
|
166
|
+
try {
|
|
167
|
+
createExclusive(temp, content);
|
|
168
|
+
try {
|
|
169
|
+
(hooks.link ?? linkSync)(temp, path);
|
|
170
|
+
published = true;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
const code = errorCode(error);
|
|
173
|
+
if (code === "EEXIST") return adoptPublishedIdentity(configDir, path);
|
|
174
|
+
// No hard links on this filesystem: exclusive create in place is still never a replacement.
|
|
175
|
+
if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV" && code !== "ENOSYS") throw error;
|
|
176
|
+
}
|
|
177
|
+
} finally {
|
|
178
|
+
try { unlinkSync(temp); } catch { /* best effort */ }
|
|
179
|
+
}
|
|
180
|
+
if (!published) {
|
|
181
|
+
try {
|
|
182
|
+
createExclusive(path, content);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
185
|
+
return adoptPublishedIdentity(configDir, path);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
syncParentDirectory(path);
|
|
189
|
+
memo = { path, identity };
|
|
190
|
+
return identity;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The opaque account reference: keyed by the install secret, never the raw id or an email. */
|
|
194
|
+
export function quotaPoolAccountRef(identity: QuotaPoolIdentity, provider: string, accountId: string): string {
|
|
195
|
+
const digest = createHmac("sha256", Buffer.from(identity.secret, "hex"))
|
|
196
|
+
.update(`ocx.quota-pool/1\0${provider}\0${accountId}`)
|
|
197
|
+
.digest();
|
|
198
|
+
return `acct_${digest.subarray(0, 16).toString("base64url")}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function resetQuotaPoolIdentityMemoForTests(): void {
|
|
202
|
+
memo = null;
|
|
203
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one registry of peer-visible features this build serves, advertised in `GET /readyz`
|
|
3
|
+
* `features` so a Hub, a Child or GOAT decides compatibility by feature instead of parsing versions.
|
|
4
|
+
*
|
|
5
|
+
* Every entry reads the module that owns the capability; nothing here restates a fact by hand:
|
|
6
|
+
*
|
|
7
|
+
* - `quota-pool/1` when the management route registry declares `GET /api/quota-pool`. The registry
|
|
8
|
+
* is reconciled against dispatch source (`tests/server/management-route-registry.test.ts`), so the
|
|
9
|
+
* feature appears exactly when the route is reachable.
|
|
10
|
+
* - `fallback-policy/1` when the registry declares `GET /api/fallback` (OCX-11). GOAT reads a Home
|
|
11
|
+
* without it as "fallback state unknown".
|
|
12
|
+
* - `link-management-relay` when the Remote Link management read table is non-empty.
|
|
13
|
+
* - `served-by-header` when the served-by emitter says responses carry the header.
|
|
14
|
+
* - `relay-auth/<n>` with the connection-bound link relay auth version.
|
|
15
|
+
* - `link-peer-report/<n>` with the version of the report a Child sends on the relay challenge and a
|
|
16
|
+
* Home records for `ocx link status` (`src/link/peer-report.ts`).
|
|
17
|
+
*
|
|
18
|
+
* The ids are static strings. They carry no account, credential or configuration data, which is
|
|
19
|
+
* what keeps the unauthenticated `/readyz` body safe. `parseRemoteReadyMetadata` accepts at most
|
|
20
|
+
* 64 unique ids of at most 80 characters, and `tests/server/readyz-features.test.ts` holds the
|
|
21
|
+
* full registry to that.
|
|
22
|
+
*/
|
|
23
|
+
import { LINK_MANAGEMENT_READ_PATHS } from "../link/management-routes";
|
|
24
|
+
import { LINK_PEER_REPORT_VERSION } from "../link/peer-report";
|
|
25
|
+
import { LINK_RELAY_AUTH_VERSION } from "../link/relay-auth";
|
|
26
|
+
import { MANAGEMENT_ROUTES, type ManagementRoute } from "../server/management/route-registry";
|
|
27
|
+
import { SERVED_BY_HEADER_EMITTED } from "./served-by";
|
|
28
|
+
|
|
29
|
+
export const QUOTA_POOL_PATH = "/api/quota-pool";
|
|
30
|
+
export const FALLBACK_STATE_PATH = "/api/fallback";
|
|
31
|
+
|
|
32
|
+
/** The owning sources the registry reads. Tests pass their own to drive presence and absence. */
|
|
33
|
+
export interface PeerFeatureSources {
|
|
34
|
+
readonly managementRoutes: readonly Pick<ManagementRoute, "method" | "path">[];
|
|
35
|
+
readonly linkManagementReadPaths: readonly string[];
|
|
36
|
+
readonly servedByHeaderEmitted: boolean;
|
|
37
|
+
readonly relayAuthVersion: string;
|
|
38
|
+
readonly linkPeerReportVersion: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One feature: its id when the sources say it is served, otherwise null. */
|
|
42
|
+
export type PeerFeature = (sources: PeerFeatureSources) => string | null;
|
|
43
|
+
|
|
44
|
+
/** Ordered; `/readyz` lists the served ids in this order. */
|
|
45
|
+
export const PEER_FEATURES: readonly PeerFeature[] = [
|
|
46
|
+
s => s.managementRoutes.some(route => route.method === "GET" && route.path === QUOTA_POOL_PATH) ? "quota-pool/1" : null,
|
|
47
|
+
s => s.managementRoutes.some(route => route.method === "GET" && route.path === FALLBACK_STATE_PATH) ? "fallback-policy/1" : null,
|
|
48
|
+
s => s.linkManagementReadPaths.length > 0 ? "link-management-relay" : null,
|
|
49
|
+
s => s.servedByHeaderEmitted ? "served-by-header" : null,
|
|
50
|
+
s => `relay-auth/${s.relayAuthVersion}`,
|
|
51
|
+
s => `link-peer-report/${s.linkPeerReportVersion}`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
export function currentPeerFeatureSources(): PeerFeatureSources {
|
|
55
|
+
return {
|
|
56
|
+
managementRoutes: MANAGEMENT_ROUTES,
|
|
57
|
+
linkManagementReadPaths: LINK_MANAGEMENT_READ_PATHS,
|
|
58
|
+
servedByHeaderEmitted: SERVED_BY_HEADER_EMITTED,
|
|
59
|
+
relayAuthVersion: LINK_RELAY_AUTH_VERSION,
|
|
60
|
+
linkPeerReportVersion: LINK_PEER_REPORT_VERSION,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function servedPeerFeatures(sources: PeerFeatureSources = currentPeerFeatureSources()): string[] {
|
|
65
|
+
return PEER_FEATURES.map(feature => feature(sources)).filter((id): id is string => id !== null);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let buildFeatures: readonly string[] | undefined;
|
|
69
|
+
|
|
70
|
+
/** This build's served features. The sources are build constants, so the list is computed once. */
|
|
71
|
+
export function buildPeerFeatures(): string[] {
|
|
72
|
+
buildFeatures ??= Object.freeze(servedPeerFeatures());
|
|
73
|
+
return [...buildFeatures];
|
|
74
|
+
}
|
package/src/remote/protocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types/config";
|
|
2
|
+
import { buildPeerFeatures } from "./features";
|
|
2
3
|
|
|
3
4
|
export const REMOTE_HUB_PROTOCOL = 1;
|
|
4
5
|
export const MINIMUM_REMOTE_CLIENT_PROTOCOL = 1;
|
|
@@ -53,6 +54,7 @@ export function readyProtocolMetadata(config: OcxConfig, req: Request): RemoteRe
|
|
|
53
54
|
protocol: REMOTE_HUB_PROTOCOL,
|
|
54
55
|
minimumClientProtocol: MINIMUM_REMOTE_CLIENT_PROTOCOL,
|
|
55
56
|
managementUrl,
|
|
57
|
+
features: buildPeerFeatures(),
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The served-by response header, the Home instance id it carries, and whether this build sends it.
|
|
3
|
+
*
|
|
4
|
+
* Every `GET /v1/models` response the server answers itself names the serving Home in
|
|
5
|
+
* `x-opencodex-served-by`, and `GET /healthz` publishes the same value as `instanceId`, so a peer
|
|
6
|
+
* (GOAT's parity check P4) can prove which Home answered without reading logs. A Remote Link Child
|
|
7
|
+
* never answers `/v1/models` itself: its relay copies the Home's response headers, so the value a
|
|
8
|
+
* Child returns is the Home's. A Hub-mode client's Codex calls the Hub's data listener directly, so
|
|
9
|
+
* the Hub stamps its own response.
|
|
10
|
+
*
|
|
11
|
+
* The instance id is the install's `homeId` from `src/quota-pool/identity.ts`: a random
|
|
12
|
+
* `home_<32 hex>` minted once per install, persisted owner-only, and stable across restarts. It is
|
|
13
|
+
* never a hostname and is not derived from one. Until the identity exists (it is created in the
|
|
14
|
+
* background after the listener starts) or when its file is unreadable, the header and the
|
|
15
|
+
* `/healthz` field are omitted; an empty or malformed value is never sent.
|
|
16
|
+
*
|
|
17
|
+
* `SERVED_BY_HEADER_EMITTED` tells `/readyz` to advertise `served-by-header`
|
|
18
|
+
* (`src/remote/features.ts`); `tests/server/readyz-features.test.ts` fails if the advertisement
|
|
19
|
+
* and live responses disagree.
|
|
20
|
+
*/
|
|
21
|
+
import { readQuotaPoolIdentity } from "../quota-pool/identity";
|
|
22
|
+
|
|
23
|
+
export const SERVED_BY_HEADER = "x-opencodex-served-by";
|
|
24
|
+
|
|
25
|
+
/** Whether responses from this build carry {@link SERVED_BY_HEADER}. */
|
|
26
|
+
export const SERVED_BY_HEADER_EMITTED: boolean = true;
|
|
27
|
+
|
|
28
|
+
const INSTANCE_ID = /^home_[0-9a-f]{32}$/;
|
|
29
|
+
|
|
30
|
+
/** This Home's opaque instance id, or undefined while the install identity is unavailable. */
|
|
31
|
+
export function homeInstanceId(): string | undefined {
|
|
32
|
+
let homeId: unknown;
|
|
33
|
+
try { homeId = readQuotaPoolIdentity()?.homeId; } catch { return undefined; }
|
|
34
|
+
return typeof homeId === "string" && INSTANCE_ID.test(homeId) ? homeId : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Stamp {@link SERVED_BY_HEADER} with this Home's instance id; leaves the response alone without one. */
|
|
38
|
+
export function withServedBy(response: Response, instanceId = homeInstanceId()): Response {
|
|
39
|
+
if (!instanceId) return response;
|
|
40
|
+
try {
|
|
41
|
+
response.headers.set(SERVED_BY_HEADER, instanceId);
|
|
42
|
+
return response;
|
|
43
|
+
} catch {
|
|
44
|
+
const headers = new Headers(response.headers);
|
|
45
|
+
headers.set(SERVED_BY_HEADER, instanceId);
|
|
46
|
+
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The server's last step for every answered request: `GET /v1/models` names the serving Home,
|
|
52
|
+
* whichever branch produced the response. `pathname` is the dispatch path the router matched.
|
|
53
|
+
*/
|
|
54
|
+
export function stampServedBy(method: string, pathname: string, response: Response): Response {
|
|
55
|
+
if (!response || method !== "GET" || pathname !== "/v1/models") return response;
|
|
56
|
+
return withServedBy(response);
|
|
57
|
+
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -329,6 +329,13 @@ export function parseRequest(
|
|
|
329
329
|
continue;
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
if (effectiveType === "configuration_update") {
|
|
333
|
+
// Codex 0.164 positional reasoning-effort control. It is not a turn, so a translated route
|
|
334
|
+
// drops it without touching pending reasoning; the native passthrough keeps it verbatim in
|
|
335
|
+
// _rawBody. Mapping its effort onto the routed request is deliberately not done (#6895).
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
332
339
|
if (isCompactionItemType(effectiveType)) {
|
|
333
340
|
// A stored summary from a previous compaction. Decode our ocx1/ocx2 envelopes into plain text so
|
|
334
341
|
// the routed model keeps the compacted context; real OpenAI-encrypted blobs degrade to a note.
|
|
@@ -121,6 +121,18 @@ export function isProviderIssuedThinkingSignature(
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Whether a value has the spelling of a signature Anthropic issued. An allow-list on top of
|
|
126
|
+
* `isProviderIssuedThinkingSignature`: Anthropic's signature is base64, so item ids and the
|
|
127
|
+
* parser's serialized placeholder never pass. Shared by the adapter, which forwards only such a
|
|
128
|
+
* signature, and the Chat `reasoning_details` carrier, which accepts and emits only such a one.
|
|
129
|
+
*/
|
|
130
|
+
export function isLikelyRealAnthropicThinkingSignature(signature: string | undefined): signature is string {
|
|
131
|
+
if (typeof signature !== "string" || signature.length < 16) return false;
|
|
132
|
+
if (/^(fc|call|msg|rs|resp|reasoning|item|ws|tool|func|function)[-_]/i.test(signature)) return false;
|
|
133
|
+
return /^[A-Za-z0-9+/_=-]+$/.test(signature);
|
|
134
|
+
}
|
|
135
|
+
|
|
124
136
|
/** Decode an ocxr1 envelope; returns null for native (OpenAI-encrypted) blobs or garbage. */
|
|
125
137
|
export function decodeReasoningEnvelope(encryptedContent: string, budget?: TranslatorBudget): ReasoningEnvelope | null {
|
|
126
138
|
if (!encryptedContent.startsWith(OCX_REASONING_PREFIX)) return null;
|
|
@@ -143,8 +143,9 @@ function tupleForServingIdentity(
|
|
|
143
143
|
|
|
144
144
|
function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined): string | undefined {
|
|
145
145
|
const identity = tupleForIdentity(scope?.current);
|
|
146
|
-
if (!nonEmpty(callId) || !nonEmpty(scope?.clientThreadId) || !identity) return undefined;
|
|
146
|
+
if (!nonEmpty(callId) || !nonEmpty(scope?.clientPrincipalId) || !nonEmpty(scope.clientThreadId) || !identity) return undefined;
|
|
147
147
|
return JSON.stringify([
|
|
148
|
+
scope.clientPrincipalId,
|
|
148
149
|
scope.clientThreadId,
|
|
149
150
|
...identity,
|
|
150
151
|
callId,
|
|
@@ -166,18 +167,21 @@ function sweepExpiredServingIdentities(at: number): void {
|
|
|
166
167
|
|
|
167
168
|
function servingIdentityFor(
|
|
168
169
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
169
|
-
): {
|
|
170
|
+
): { key: string; identity: string } | undefined {
|
|
170
171
|
const threadId = scope?.clientThreadId;
|
|
171
172
|
const identityTuple = tupleForServingIdentity(scope?.current);
|
|
172
173
|
if (!nonEmpty(threadId) || !identityTuple) return undefined;
|
|
173
|
-
|
|
174
|
+
// Preserve physical safeguards for keyless callers in a separate namespace. A caller's
|
|
175
|
+
// thread name cannot select an authenticated principal's serving or rejection history.
|
|
176
|
+
const key = JSON.stringify([nonEmpty(scope?.clientPrincipalId) ? scope.clientPrincipalId : null, threadId]);
|
|
177
|
+
return { key, identity: JSON.stringify(identityTuple) };
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
function opaqueBlobRejectionKeyFor(
|
|
177
181
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
178
182
|
): string | undefined {
|
|
179
183
|
const current = servingIdentityFor(scope);
|
|
180
|
-
return current ? JSON.stringify([current.
|
|
184
|
+
return current ? JSON.stringify([current.key, current.identity]) : undefined;
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
function deleteOpaqueBlobRejection(key: string): void {
|
|
@@ -224,7 +228,7 @@ export function reasoningReplayServingIdentityChange(
|
|
|
224
228
|
if (!current) return undefined;
|
|
225
229
|
const at = now();
|
|
226
230
|
sweepExpiredServingIdentities(at);
|
|
227
|
-
const previous = servingIdentities.get(current.
|
|
231
|
+
const previous = servingIdentities.get(current.key);
|
|
228
232
|
if (previous === undefined || previous.identity === current.identity) return undefined;
|
|
229
233
|
const before = JSON.parse(previous.identity) as ReasoningReplayIdentityTuple;
|
|
230
234
|
const after = JSON.parse(current.identity) as ReasoningReplayIdentityTuple;
|
|
@@ -254,7 +258,7 @@ export function reasoningReplayItemStoreChanged(
|
|
|
254
258
|
if (!current) return false;
|
|
255
259
|
const at = now();
|
|
256
260
|
sweepExpiredServingIdentities(at);
|
|
257
|
-
const previous = servingIdentities.get(current.
|
|
261
|
+
const previous = servingIdentities.get(current.key);
|
|
258
262
|
if (previous === undefined || previous.identity === current.identity) return false;
|
|
259
263
|
const storeOwner = (identity: string): string => {
|
|
260
264
|
const [, destination, , , credential] = JSON.parse(identity) as ReasoningReplayIdentityTuple;
|
|
@@ -271,16 +275,16 @@ export function commitReasoningReplayServingIdentity(
|
|
|
271
275
|
if (!current) return;
|
|
272
276
|
const at = now();
|
|
273
277
|
sweepExpiredServingIdentities(at);
|
|
274
|
-
const previous = servingIdentities.get(current.
|
|
275
|
-
const {
|
|
276
|
-
const bytes = Buffer.byteLength(JSON.stringify([
|
|
278
|
+
const previous = servingIdentities.get(current.key);
|
|
279
|
+
const { key, identity } = current;
|
|
280
|
+
const bytes = Buffer.byteLength(JSON.stringify([key, identity]), "utf8");
|
|
277
281
|
if (bytes > MAX_TOTAL_BYTES) {
|
|
278
|
-
deleteServingIdentity(
|
|
282
|
+
deleteServingIdentity(key);
|
|
279
283
|
return;
|
|
280
284
|
}
|
|
281
285
|
|
|
282
|
-
if (previous) deleteServingIdentity(
|
|
283
|
-
servingIdentities.set(
|
|
286
|
+
if (previous) deleteServingIdentity(key);
|
|
287
|
+
servingIdentities.set(key, { identity, bytes, at });
|
|
284
288
|
servingIdentityTotalBytes += bytes;
|
|
285
289
|
while (
|
|
286
290
|
(servingIdentityTotalBytes > MAX_TOTAL_BYTES || servingIdentities.size > MAX_ENTRIES)
|
|
@@ -517,13 +521,13 @@ export function reasoningReplayKeyCredentialIdentity(
|
|
|
517
521
|
return reasoningReplayCredentialIdentity("key", apiKey, provider.headers);
|
|
518
522
|
}
|
|
519
523
|
|
|
520
|
-
/**
|
|
524
|
+
/** Bind physical identity independently of the principal required for plaintext cache access. */
|
|
521
525
|
export function bindReasoningReplayScope(
|
|
522
526
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
523
527
|
identity: OcxReasoningReplayIdentity | undefined,
|
|
524
528
|
): void {
|
|
525
529
|
if (!scope) return;
|
|
526
|
-
if (!identity || !
|
|
530
|
+
if (!identity || !nonEmpty(scope.clientThreadId) || !tupleForIdentity(identity)) {
|
|
527
531
|
delete scope.current;
|
|
528
532
|
return;
|
|
529
533
|
}
|
package/src/router.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
|
|
34
34
|
import { effectiveProviderAliasDecision, resolveModelAlias } from "./providers/default-aliases";
|
|
35
35
|
import { resolveBlockedModelRedirect } from "./lib/shadow-call";
|
|
36
|
+
import { workBuddyCatalogIdentity } from "./providers/workbuddy";
|
|
36
37
|
import { getRoutingCached } from "./codex/model-cache";
|
|
37
38
|
import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
|
|
38
39
|
import {
|
|
@@ -177,6 +178,7 @@ export function knownModelIdsForProvider(
|
|
|
177
178
|
if (!account || account.needsReauth || !Number.isFinite(account.credential.expires)
|
|
178
179
|
|| account.credential.expires <= Date.now()) return undefined;
|
|
179
180
|
key = account.credential.access;
|
|
181
|
+
if (provName === "workbuddy-ai") return workBuddyCatalogIdentity(key, account.credential.workbuddy);
|
|
180
182
|
if (routed.adapter === "devin") destination = validateDevinApiBaseUrl(account.credential.apiBaseUrl) ?? routed.baseUrl;
|
|
181
183
|
}
|
|
182
184
|
if (!key) return undefined;
|
package/src/routing/analytics.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* sample for the full history.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import type
|
|
14
|
+
import { normalizePersistedUsageRow, type PersistedUsageEntry } from "../usage/log";
|
|
15
15
|
import { computeEntryCost } from "../usage/summary";
|
|
16
16
|
import { openRequestHistoryIndex, requestHistoryDb } from "./history/indexer";
|
|
17
17
|
|
|
@@ -141,8 +141,7 @@ function classifyRow(row: ScannedRow): "success" | "failure" | "cancelled" {
|
|
|
141
141
|
|
|
142
142
|
function parseEntry(rowJson: string): PersistedUsageEntry | null {
|
|
143
143
|
try {
|
|
144
|
-
|
|
145
|
-
return parsed && typeof parsed === "object" && typeof parsed.requestId === "string" ? parsed : null;
|
|
144
|
+
return normalizePersistedUsageRow(JSON.parse(rowJson)) ?? null;
|
|
146
145
|
} catch {
|
|
147
146
|
return null;
|
|
148
147
|
}
|