@bitkyc08/opencodex 2.90.0 → 2.91.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/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-nZ34cR5G.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BuLy7hL1.js} +1 -1
- package/gui/dist/assets/{index-TmaHa54u.js → index-DGXS51sp.js} +12 -12
- package/gui/dist/assets/{quota-summary-B16x5DZZ.js → quota-summary-DO1Sx5TM.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +32 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +36 -4
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +50 -0
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-shim-autorestore.ts +5 -0
- 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/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 +84 -9
- package/src/cli/provider.ts +44 -2
- 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/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-parity-reporter.ts +107 -0
- package/src/client/link-relay-transport.ts +8 -1
- package/src/client/link-relay.ts +27 -3
- package/src/client/machine-listener.ts +83 -1
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +137 -25
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- 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/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/generated/compatibility-version.json +205 -97
- package/src/lib/authorization-scheme.ts +95 -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/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +24 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +2 -1
- 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/mistral-transport.ts +37 -4
- package/src/providers/quota/antigravity.ts +2 -8
- 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/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 +6 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +8 -1
- 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 +11 -2
- package/src/server/index/optional-listeners.ts +7 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- 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 +12 -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/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/compact.ts +2 -2
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +72 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +9 -1
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +7 -0
- package/src/types/fallback.ts +38 -0
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Remote Link Child tells its Home about itself, and what the Home remembers of it.
|
|
3
|
+
*
|
|
4
|
+
* A Child sends its release version and its peer feature ids (`src/remote/features.ts`) as two
|
|
5
|
+
* headers on the credential-free relay-auth challenge it already sends before every relayed
|
|
6
|
+
* request (`src/client/link-relay-transport.ts`). The Home records them only after the caller
|
|
7
|
+
* proof verifies, so an observation is bound to the link key; it is still the Child's own claim
|
|
8
|
+
* and is used for display and compatibility verdicts, never for admission.
|
|
9
|
+
*
|
|
10
|
+
* A Home older than this report ignores the headers, and a Child older than it sends none: the
|
|
11
|
+
* Home then knows only the relay-auth version the challenge itself names. Observations live in
|
|
12
|
+
* the Home's memory and start empty after a restart; the next relayed request refills them.
|
|
13
|
+
*/
|
|
14
|
+
export const LINK_PEER_REPORT_VERSION = "1";
|
|
15
|
+
export const LINK_PEER_VERSION_HEADER = "x-opencodex-link-peer-version";
|
|
16
|
+
export const LINK_PEER_FEATURES_HEADER = "x-opencodex-link-peer-features";
|
|
17
|
+
|
|
18
|
+
const RELEASE_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]{1,128})?$/;
|
|
19
|
+
const FEATURE_ID = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,79}$/;
|
|
20
|
+
const MAX_FEATURES = 64;
|
|
21
|
+
const MAX_LINKS = 256;
|
|
22
|
+
|
|
23
|
+
export interface LinkPeerReport {
|
|
24
|
+
/** True when the challenge carried either report header, valid or not; false for a pre-report Child. */
|
|
25
|
+
reported: boolean;
|
|
26
|
+
version: string | null;
|
|
27
|
+
features: string[] | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Header values a Child sends. Invalid input yields no header rather than a malformed one. */
|
|
31
|
+
export function linkPeerReportHeaders(version: string, features: readonly string[]): Record<string, string> {
|
|
32
|
+
const headers: Record<string, string> = {};
|
|
33
|
+
if (RELEASE_VERSION.test(version)) headers[LINK_PEER_VERSION_HEADER] = version;
|
|
34
|
+
const ids = parseFeatureList(features.join(","));
|
|
35
|
+
if (ids) headers[LINK_PEER_FEATURES_HEADER] = ids.join(",");
|
|
36
|
+
return headers;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseFeatureList(value: string | null): string[] | null {
|
|
40
|
+
if (value === null || value === "") return null;
|
|
41
|
+
const ids = value.split(",");
|
|
42
|
+
if (ids.length > MAX_FEATURES || new Set(ids).size !== ids.length || ids.some(id => !FEATURE_ID.test(id))) return null;
|
|
43
|
+
return ids;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Home side: read a report from challenge headers. Each field is dropped on its own when invalid. */
|
|
47
|
+
export function parseLinkPeerReport(headers: Headers): LinkPeerReport {
|
|
48
|
+
const version = headers.get(LINK_PEER_VERSION_HEADER);
|
|
49
|
+
const features = headers.get(LINK_PEER_FEATURES_HEADER);
|
|
50
|
+
return {
|
|
51
|
+
reported: version !== null || features !== null,
|
|
52
|
+
version: version !== null && RELEASE_VERSION.test(version) ? version : null,
|
|
53
|
+
features: parseFeatureList(features),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LinkPeerObservation extends LinkPeerReport {
|
|
58
|
+
/** The relay-auth version of the verified challenge. */
|
|
59
|
+
relayAuth: string;
|
|
60
|
+
observedAt: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A challenge for a known link and key that named a relay-auth version this Home does not speak. */
|
|
64
|
+
export interface LinkPeerRefusal {
|
|
65
|
+
relayAuth: string;
|
|
66
|
+
refusedAt: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LinkPeerEntry {
|
|
70
|
+
observed: LinkPeerObservation | null;
|
|
71
|
+
refused: LinkPeerRefusal | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface LinkPeerObservations {
|
|
75
|
+
observe(linkId: string, relayAuth: string, report: LinkPeerReport): void;
|
|
76
|
+
refuse(linkId: string, relayAuth: string): void;
|
|
77
|
+
get(linkId: string): LinkPeerEntry;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function createLinkPeerObservations(now: () => Date = () => new Date()): LinkPeerObservations {
|
|
81
|
+
const entries = new Map<string, LinkPeerEntry>();
|
|
82
|
+
const update = (linkId: string, change: Partial<LinkPeerEntry>) => {
|
|
83
|
+
const current = entries.get(linkId) ?? { observed: null, refused: null };
|
|
84
|
+
entries.delete(linkId);
|
|
85
|
+
entries.set(linkId, { ...current, ...change });
|
|
86
|
+
// Only known link ids reach here; the cap bounds memory if the store churns for a long time.
|
|
87
|
+
if (entries.size > MAX_LINKS) entries.delete(entries.keys().next().value!);
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
observe(linkId, relayAuth, report) {
|
|
91
|
+
update(linkId, { observed: { relayAuth, ...report, observedAt: now().toISOString() }, refused: null });
|
|
92
|
+
},
|
|
93
|
+
refuse(linkId, relayAuth) {
|
|
94
|
+
update(linkId, { refused: { relayAuth, refusedAt: now().toISOString() } });
|
|
95
|
+
},
|
|
96
|
+
get(linkId) {
|
|
97
|
+
return entries.get(linkId) ?? { observed: null, refused: null };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows recognition for Remote Link Children.
|
|
3
|
+
*
|
|
4
|
+
* Remote Link drives its Child through a POSIX `sh` prelude, so a Windows Child is refused
|
|
5
|
+
* before any link key is issued or tunnel started. A Windows OpenSSH server shows up in one of
|
|
6
|
+
* three ways, depending on its default shell:
|
|
7
|
+
* - Git Bash, MSYS2 or Cygwin: `uname -s` reports MINGW*, MSYS*, CYGWIN* (or Windows*), and the
|
|
8
|
+
* ocx prelude exits with REMOTE_PLATFORM_UNSUPPORTED.
|
|
9
|
+
* - cmd.exe: a POSIX command such as `true` or `sh` is "not recognized as an internal or
|
|
10
|
+
* external command".
|
|
11
|
+
* - PowerShell: the same command "is not recognized as the name of a cmdlet".
|
|
12
|
+
*
|
|
13
|
+
* Hub mode (`ocx hub invite` on a Hub, `ocx connect` on the client) supports Windows clients, so
|
|
14
|
+
* every refusal points there.
|
|
15
|
+
*/
|
|
16
|
+
import { buildWindowsProbeArgv, REMOTE_COMMAND_NOT_FOUND, REMOTE_PLATFORM_UNSUPPORTED } from "./ssh-argv";
|
|
17
|
+
import type { SshRunner, SshRunResult } from "./ssh-runner";
|
|
18
|
+
|
|
19
|
+
export type RemotePlatform = "windows" | "posix" | "unknown";
|
|
20
|
+
|
|
21
|
+
const WINDOWS_UNAME = /^(?:Windows|MSYS|MINGW|CYGWIN)/i;
|
|
22
|
+
const POSIX_UNAME = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/;
|
|
23
|
+
// Windows PowerShell says "the name of a cmdlet", PowerShell 7 "a name of a cmdlet". Localized
|
|
24
|
+
// installs say neither, which is what confirmWindowsShell is for.
|
|
25
|
+
const WINDOWS_SHELL_REFUSAL = /is not recognized as (?:an internal or external command|(?:the|a) name of a cmdlet)/i;
|
|
26
|
+
/** ssh's own failure status: the remote shell never ran, so there is nothing to ask. */
|
|
27
|
+
const SSH_TRANSPORT_FAILURE = 255;
|
|
28
|
+
|
|
29
|
+
/** Remote argv that prints the kernel name; under cmd or PowerShell, `sh` itself is refused. */
|
|
30
|
+
export const REMOTE_UNAME_ARGV: readonly string[] = ["sh", "-c", "uname -s 2>/dev/null", "ocx-platform"];
|
|
31
|
+
|
|
32
|
+
/** Server-side refusal text, shared by the dashboard and the CLI's JSON consumers. */
|
|
33
|
+
export const WINDOWS_CHILD_REFUSAL = "Remote Link Children require macOS or Linux; Windows is unsupported. Connect a Windows computer in Hub mode: run ocx hub invite on a Hub, then the printed ocx connect command on the Windows computer.";
|
|
34
|
+
|
|
35
|
+
/** Does this output come from cmd.exe or PowerShell refusing a POSIX command? */
|
|
36
|
+
export function isWindowsShellRefusal(output: string): boolean {
|
|
37
|
+
return WINDOWS_SHELL_REFUSAL.test(output);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Classify one remote command result. Transport failures and odd output stay unknown. */
|
|
41
|
+
export function classifyRemotePlatform(result: SshRunResult): RemotePlatform {
|
|
42
|
+
if (result.code === REMOTE_PLATFORM_UNSUPPORTED) return "windows";
|
|
43
|
+
if (isWindowsShellRefusal(result.stderr) || isWindowsShellRefusal(result.stdout)) return "windows";
|
|
44
|
+
if (result.code !== 0) return "unknown";
|
|
45
|
+
const name = result.stdout.trim().split(/\r?\n/)[0] ?? "";
|
|
46
|
+
if (WINDOWS_UNAME.test(name)) return "windows";
|
|
47
|
+
return POSIX_UNAME.test(name) ? "posix" : "unknown";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Could this failure be a Windows shell whose refusal text is localized? Success, the prelude's own
|
|
52
|
+
* 125/127 answers and ssh transport failures are already explained, so they never cost another call.
|
|
53
|
+
*/
|
|
54
|
+
export function mayBeWindowsShell(result: SshRunResult): boolean {
|
|
55
|
+
return result.code !== 0 && result.code !== REMOTE_PLATFORM_UNSUPPORTED
|
|
56
|
+
&& result.code !== REMOTE_COMMAND_NOT_FOUND && result.code !== SSH_TRANSPORT_FAILURE;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Ask the host the fixed, locale-independent WINDOWS_OS_PROBE. Any failure answers false. */
|
|
60
|
+
export async function confirmWindowsShell(runner: SshRunner, alias: string, knownHostsFile: string, timeoutMs = 30_000): Promise<boolean> {
|
|
61
|
+
try {
|
|
62
|
+
const result = await runner.run(buildWindowsProbeArgv({ alias, knownHostsFile }), { timeoutMs });
|
|
63
|
+
return result.code === 0 && result.stdout.trim().split(/\r?\n/)[0]?.trim() === "Windows_NT";
|
|
64
|
+
} catch { return false; }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** classifyRemotePlatform, plus the locale-independent follow-up when the answer could be Windows. */
|
|
68
|
+
export async function detectRemotePlatform(runner: SshRunner, alias: string, knownHostsFile: string, result: SshRunResult): Promise<RemotePlatform> {
|
|
69
|
+
const platform = classifyRemotePlatform(result);
|
|
70
|
+
if (platform !== "unknown" || !mayBeWindowsShell(result)) return platform;
|
|
71
|
+
return await confirmWindowsShell(runner, alias, knownHostsFile) ? "windows" : "unknown";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The CLI sentence for a refused Windows Child. */
|
|
75
|
+
export function windowsChildRefusal(alias: string): string {
|
|
76
|
+
return `${alias} is a Windows computer. Remote Link Children require macOS or Linux, so no link key was issued and no tunnel was started. `
|
|
77
|
+
+ `Connect it in Hub mode instead: run 'ocx hub invite' on a Hub, then run the printed 'ocx connect' command on ${alias}.`;
|
|
78
|
+
}
|
package/src/link/ssh-argv.ts
CHANGED
|
@@ -191,6 +191,25 @@ export function buildProbeArgv(options: ProbeArgvOptions): string[] {
|
|
|
191
191
|
];
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Locale-independent Windows check for a host whose SSH shell is cmd.exe or PowerShell. Both run
|
|
196
|
+
* `cmd /c echo %OS%`, and cmd expands it to `Windows_NT` regardless of display language. The
|
|
197
|
+
* command is a fixed literal; a POSIX shell answers "command not found" and nothing else.
|
|
198
|
+
*/
|
|
199
|
+
export const WINDOWS_OS_PROBE = "cmd /c echo %OS%";
|
|
200
|
+
|
|
201
|
+
/** Run WINDOWS_OS_PROBE against an already trusted (pinned or just-probed) host key only. */
|
|
202
|
+
export function buildWindowsProbeArgv(options: { alias: string; knownHostsFile: string }): string[] {
|
|
203
|
+
const alias = assertSshAlias(options.alias);
|
|
204
|
+
return [
|
|
205
|
+
"ssh", "-T",
|
|
206
|
+
...commonOptions(alias, options.knownHostsFile, "yes"),
|
|
207
|
+
"-o", "ClearAllForwardings=yes",
|
|
208
|
+
"--", alias,
|
|
209
|
+
WINDOWS_OS_PROBE,
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
194
213
|
/** Read the fingerprint of the host key recorded by a probe. */
|
|
195
214
|
export function buildFingerprintArgv(tempKnownHostsFile: string): string[] {
|
|
196
215
|
return ["ssh-keygen", "-l", "-f", assertKnownHostsPath(tempKnownHostsFile)];
|
|
@@ -2,6 +2,9 @@ import type { LinkStore } from "./store";
|
|
|
2
2
|
import type { LinkTunnelStatus } from "./supervisor";
|
|
3
3
|
import type { TunnelState } from "./tunnel-state";
|
|
4
4
|
import { readCompensation, type CompensationStore } from "./compensation";
|
|
5
|
+
import { EMPTY_LINK_CHILD_PARITY, type LinkChildParity } from "./child-parity";
|
|
6
|
+
import { linkPeerStatus, type LinkLocalPeer, type LinkPeerStatus } from "./compatibility";
|
|
7
|
+
import type { LinkPeerEntry } from "./peer-report";
|
|
5
8
|
import type { OcxClientConnectionConfig } from "../types";
|
|
6
9
|
|
|
7
10
|
export type LinkWireState = "connecting" | "connected" | "reconnecting" | "failed" | "idle";
|
|
@@ -9,7 +12,7 @@ export type LinkWireState = "connecting" | "connected" | "reconnecting" | "faile
|
|
|
9
12
|
export interface LinkStatusDto {
|
|
10
13
|
role: "standalone" | "home" | "child";
|
|
11
14
|
listener: { state: "off" | "listening" | "failed"; port: number | null };
|
|
12
|
-
links: Array<{
|
|
15
|
+
links: Array<LinkChildParity & {
|
|
13
16
|
id: string;
|
|
14
17
|
alias: string;
|
|
15
18
|
direction: "hub-initiated" | "client-initiated";
|
|
@@ -17,6 +20,8 @@ export interface LinkStatusDto {
|
|
|
17
20
|
since: string;
|
|
18
21
|
reason: string | null;
|
|
19
22
|
tunnelPort: number;
|
|
23
|
+
/** Home only: what the link carried from this Child, and the compatibility verdict. */
|
|
24
|
+
peer?: LinkPeerStatus;
|
|
20
25
|
}>;
|
|
21
26
|
child: null | {
|
|
22
27
|
alias: string;
|
|
@@ -24,6 +29,17 @@ export interface LinkStatusDto {
|
|
|
24
29
|
since: string;
|
|
25
30
|
reason: string | null;
|
|
26
31
|
};
|
|
32
|
+
/** This build's version and link protocol, the side every `peer` verdict compares against. */
|
|
33
|
+
local?: LinkLocalPeer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Add this build's identity and, on a Home, each Child's observed peer status. */
|
|
37
|
+
export function attachLinkPeers(dto: LinkStatusDto, local: LinkLocalPeer, observation: (linkId: string) => LinkPeerEntry): LinkStatusDto {
|
|
38
|
+
if (dto.role === "home") {
|
|
39
|
+
for (const link of dto.links) link.peer = linkPeerStatus(local, observation(link.id));
|
|
40
|
+
}
|
|
41
|
+
dto.local = local;
|
|
42
|
+
return dto;
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
export interface ClientLinkIdentity { direction: "hub-initiated" | "client-initiated"; alias?: string }
|
|
@@ -75,6 +91,8 @@ export function projectLinkStatus(
|
|
|
75
91
|
compensation: CompensationStore = readCompensation(),
|
|
76
92
|
clientChild: LinkStatusDto["child"] = null,
|
|
77
93
|
identity: ClientLinkIdentity | null = null,
|
|
94
|
+
/** The Home's view of each Child's last apply (OCX-6); every field is null without a report. */
|
|
95
|
+
parity: (linkId: string) => LinkChildParity = () => EMPTY_LINK_CHILD_PARITY,
|
|
78
96
|
): LinkStatusDto {
|
|
79
97
|
const byId = new Map(supervisorStates.map(status => [status.linkId, status]));
|
|
80
98
|
const links = store.links.map(record => ({
|
|
@@ -83,6 +101,7 @@ export function projectLinkStatus(
|
|
|
83
101
|
direction: record.direction,
|
|
84
102
|
...stateDetails(record, byId.get(record.id), compensation),
|
|
85
103
|
tunnelPort: record.tunnelPort,
|
|
104
|
+
...parity(record.id),
|
|
86
105
|
}));
|
|
87
106
|
const childRecord = config.runtimeRole === "client"
|
|
88
107
|
? (config.client?.transport === "link" ? store.links.find(record => record.id === config.client?.link?.linkId) : store.links[0])
|
|
@@ -101,7 +120,10 @@ export function projectLinkStatus(
|
|
|
101
120
|
: client?.link ? { alias: identity?.alias ?? client.serverUrl, state: "idle" as const, since: client.connectedAt, reason: null } : null);
|
|
102
121
|
if (client?.link && child && identity) {
|
|
103
122
|
const active = links.find(link => link.id === client.link!.linkId);
|
|
104
|
-
const projected = {
|
|
123
|
+
const projected = {
|
|
124
|
+
id: client.link.linkId, direction: identity.direction, tunnelPort: client.link.tunnelPort, ...child, alias: identity.alias ?? child.alias,
|
|
125
|
+
...parity(client.link.linkId),
|
|
126
|
+
};
|
|
105
127
|
if (active) Object.assign(active, projected, compensation.entries[active.id] && childRecord ? stateDetails(childRecord, byId.get(active.id), compensation) : {});
|
|
106
128
|
else links.push(projected);
|
|
107
129
|
}
|
package/src/oauth/index.ts
CHANGED
|
@@ -1672,7 +1672,7 @@ export async function runLogin(
|
|
|
1672
1672
|
: existing.accountId && cred.accountId
|
|
1673
1673
|
? existing.accountId === cred.accountId
|
|
1674
1674
|
: existing.email && cred.email
|
|
1675
|
-
? existing.email.toLowerCase() === cred.email.toLowerCase()
|
|
1675
|
+
? existing.email.trim().toLowerCase() === cred.email.trim().toLowerCase()
|
|
1676
1676
|
: false;
|
|
1677
1677
|
if (!identityMatches) {
|
|
1678
1678
|
throw new OAuthReauthIdentityMismatchError();
|
package/src/oauth/kimi.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
7
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
7
8
|
import { getConfigDir } from "../config";
|
|
8
9
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
9
10
|
|
|
@@ -107,6 +108,7 @@ function getDeviceId(): string {
|
|
|
107
108
|
if ((e as { code?: string })?.code !== "ENOENT") throw e;
|
|
108
109
|
}
|
|
109
110
|
const id = randomUUID().replace(/-/g, "");
|
|
111
|
+
assertNotRealHomeUnderTest(getConfigDir());
|
|
110
112
|
recordOwnedConfigPath(getConfigDir(), p);
|
|
111
113
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
112
114
|
writeFileSync(p, id + "\n", { mode: 0o600 });
|
package/src/oauth/nous.ts
CHANGED
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { createHash } from "node:crypto";
|
|
37
37
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
38
|
-
import { join } from "node:path";
|
|
38
|
+
import { dirname, join } from "node:path";
|
|
39
39
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
40
40
|
import { getAuthStorePath } from "./store";
|
|
41
41
|
import { atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
|
|
42
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
42
43
|
import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBytes } from "../lib/bounded-body";
|
|
43
44
|
|
|
44
45
|
export const NOUS_PORTAL_BASE_URL = "https://portal.nousresearch.com";
|
|
@@ -220,6 +221,8 @@ function readRefreshIntent(refreshToken: string): RefreshIntent | undefined {
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
function writeRefreshIntent(refreshToken: string, status: RefreshIntentStatus): void {
|
|
224
|
+
// Before the mkdir/chmod below, which would otherwise touch the real home first.
|
|
225
|
+
assertNotRealHomeUnderTest(dirname(getAuthStorePath()));
|
|
223
226
|
const dir = refreshIntentDir();
|
|
224
227
|
// Hardened, owner-only directory + atomic (temp+rename) write. Throws on
|
|
225
228
|
// failure so the caller can fail closed instead of refreshing blind.
|
package/src/oauth/store.ts
CHANGED
|
@@ -203,6 +203,7 @@ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAu
|
|
|
203
203
|
}
|
|
204
204
|
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): OAuthRefreshIntent {
|
|
205
205
|
const dir = getConfigDir();
|
|
206
|
+
assertNotRealHomeUnderTest(dir);
|
|
206
207
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
207
208
|
hardenConfigDir();
|
|
208
209
|
const intent: OAuthRefreshIntent = {
|
|
@@ -1152,7 +1153,7 @@ export async function upsertCredentialByIdentity(
|
|
|
1152
1153
|
return Boolean(
|
|
1153
1154
|
safe.email
|
|
1154
1155
|
&& account.credential.email
|
|
1155
|
-
&& account.credential.email.toLowerCase() === safe.email.toLowerCase(),
|
|
1156
|
+
&& account.credential.email.trim().toLowerCase() === safe.email.trim().toLowerCase(),
|
|
1156
1157
|
);
|
|
1157
1158
|
};
|
|
1158
1159
|
const existing = safe.accountId
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* (`src/chat/inbound.ts`), including its validation and the fields it drops, which
|
|
7
7
|
* `src/protocols/features.ts` declares.
|
|
8
8
|
*/
|
|
9
|
-
import { chatCompletionsToResponsesBody, chatSettingsToResponsesBody } from "../../chat/inbound";
|
|
9
|
+
import { chatCompletionsToResponsesBody, chatSettingsToResponsesBody, type ChatResponsesProjectionOptions } from "../../chat/inbound";
|
|
10
10
|
import { featuresFromChatBody } from "../features";
|
|
11
11
|
|
|
12
12
|
/** Project a Chat Completions body onto the internal Responses bridge body. */
|
|
13
|
-
export function chatToResponsesBody(body: unknown): Record<string, unknown> {
|
|
14
|
-
return chatCompletionsToResponsesBody(body);
|
|
13
|
+
export function chatToResponsesBody(body: unknown, options?: ChatResponsesProjectionOptions): Record<string, unknown> {
|
|
14
|
+
return chatCompletionsToResponsesBody(body, options);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -109,7 +109,13 @@ export interface ClientWireWriter {
|
|
|
109
109
|
text(delta: string): void;
|
|
110
110
|
messageDone(): void;
|
|
111
111
|
reasoning(delta: string, itemId: string, channel: "summary" | "raw"): void;
|
|
112
|
-
|
|
112
|
+
/** `hidden`: the signature covers thinking text the client was not shown (the bridge's `txt`). */
|
|
113
|
+
reasoningDone(item: { itemId: string; signature?: string; redacted?: string[]; hidden?: true }): void;
|
|
114
|
+
/**
|
|
115
|
+
* An Anthropic progress update: `text` is shown as content and is also the signed thinking
|
|
116
|
+
* block's text. Optional — a writer without it renders the update as ordinary text.
|
|
117
|
+
*/
|
|
118
|
+
progressThinking?(text: string, signature: string): void;
|
|
113
119
|
toolStart(call: ClientToolCall): void;
|
|
114
120
|
toolArgsDelta(itemId: string, delta: string): void;
|
|
115
121
|
toolArgsDone(itemId: string, args: string): void;
|
|
@@ -331,6 +337,8 @@ export function encodeAdapterEventStream(
|
|
|
331
337
|
let currentRawReasoning: { itemId: string } | null = null;
|
|
332
338
|
let pendingSignature: string | undefined;
|
|
333
339
|
let pendingRedacted: string[] = [];
|
|
340
|
+
// The bridge's hiddenThinking: non-empty thinking text suppressed by hideThinkingSummary.
|
|
341
|
+
let hiddenThinkingSeen = false;
|
|
334
342
|
let hiddenRawBytes = 0;
|
|
335
343
|
let pendingKiroRedacted: string | undefined;
|
|
336
344
|
let currentToolCall: OpenToolCall | null = null;
|
|
@@ -367,9 +375,11 @@ export function encodeAdapterEventStream(
|
|
|
367
375
|
};
|
|
368
376
|
// hideThinkingSummary: a signed or redacted block still round-trips as an envelope-only item.
|
|
369
377
|
const flushHiddenReasoningEnvelope = () => {
|
|
378
|
+
const hidden = hiddenThinkingSeen;
|
|
379
|
+
hiddenThinkingSeen = false;
|
|
370
380
|
const envelope = takeReasoningEnvelope();
|
|
371
381
|
if (!envelope) return;
|
|
372
|
-
writer.reasoningDone({ itemId: `rs_${uuid()}`, ...envelope });
|
|
382
|
+
writer.reasoningDone({ itemId: `rs_${uuid()}`, ...envelope, ...(hidden ? { hidden: true as const } : {}) });
|
|
373
383
|
};
|
|
374
384
|
const flushHiddenRawReasoning = () => {
|
|
375
385
|
if (hiddenRawBytes === 0) return;
|
|
@@ -570,6 +580,19 @@ export function encodeAdapterEventStream(
|
|
|
570
580
|
break;
|
|
571
581
|
}
|
|
572
582
|
case "text_delta": {
|
|
583
|
+
if (event.anthropicThinkingSignature && writer.progressThinking) {
|
|
584
|
+
// The bridge's progress items: close everything, the carrier, then the visible copy.
|
|
585
|
+
if (currentMsg) closeCurrentMessage();
|
|
586
|
+
if (currentReasoning) closeCurrentReasoning();
|
|
587
|
+
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
588
|
+
flushHiddenRawReasoning();
|
|
589
|
+
flushHiddenReasoningEnvelope();
|
|
590
|
+
if (currentToolCall) closeCurrentToolCall();
|
|
591
|
+
writer.progressThinking(event.text, event.anthropicThinkingSignature);
|
|
592
|
+
writer.text(event.text);
|
|
593
|
+
writer.messageDone();
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
573
596
|
if (currentReasoning) closeCurrentReasoning();
|
|
574
597
|
if (currentRawReasoning) closeCurrentRawReasoning();
|
|
575
598
|
flushHiddenRawReasoning();
|
|
@@ -585,6 +608,7 @@ export function encodeAdapterEventStream(
|
|
|
585
608
|
}
|
|
586
609
|
case "thinking_delta": {
|
|
587
610
|
if (options.hideThinkingSummary) {
|
|
611
|
+
if (event.thinking.length > 0) hiddenThinkingSeen = true;
|
|
588
612
|
flushHiddenRawReasoning();
|
|
589
613
|
break;
|
|
590
614
|
}
|
|
@@ -13,6 +13,7 @@ import type { AdapterEvent } from "../../types";
|
|
|
13
13
|
import type { TranslatorBudget } from "../../lib/translator-budget";
|
|
14
14
|
import { responsesUsage } from "../../bridge/internal";
|
|
15
15
|
import {
|
|
16
|
+
appendedUtf8Bytes,
|
|
16
17
|
chatCompletionsErrorResponse,
|
|
17
18
|
chatCompletionsFailedResponse,
|
|
18
19
|
chatCompletionsIncompleteOutcome,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
type ClientWireWriter,
|
|
33
34
|
} from "./adapter-events";
|
|
34
35
|
import type { RelayedEventObservation } from "../../usage/attempt-delivery";
|
|
36
|
+
import { reasoningDetailEntries } from "../../chat/reasoning-details";
|
|
35
37
|
|
|
36
38
|
type Rec = Record<string, unknown>;
|
|
37
39
|
|
|
@@ -54,6 +56,41 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien
|
|
|
54
56
|
const emittedToolIndexes = new Set<number>();
|
|
55
57
|
let nextToolIndex = 0;
|
|
56
58
|
let staged: { text: string; observation?: RelayedEventObservation }[] | undefined;
|
|
59
|
+
// #7429: the shown thinking text of the open reasoning item, which its signature pairs with.
|
|
60
|
+
const budget = sink.budget;
|
|
61
|
+
let reasoningItemId: string | undefined;
|
|
62
|
+
let reasoningText = "";
|
|
63
|
+
let reasoningTextBytes = 0;
|
|
64
|
+
let nextReasoningDetailIndex = 0;
|
|
65
|
+
const releaseReasoningText = () => {
|
|
66
|
+
budget.releaseRetained(reasoningTextBytes, { kind: "reasoning" });
|
|
67
|
+
reasoningItemId = undefined;
|
|
68
|
+
reasoningText = "";
|
|
69
|
+
reasoningTextBytes = 0;
|
|
70
|
+
};
|
|
71
|
+
const retainReasoningText = (itemId: string, delta: string) => {
|
|
72
|
+
if (reasoningItemId !== itemId) releaseReasoningText();
|
|
73
|
+
reasoningItemId = itemId;
|
|
74
|
+
const nextBytes = appendedUtf8Bytes(reasoningText, reasoningTextBytes, delta);
|
|
75
|
+
const reservation = budget.reserveTransient(nextBytes, { kind: "reasoning" });
|
|
76
|
+
try {
|
|
77
|
+
reasoningText += delta;
|
|
78
|
+
reservation.commitRetained();
|
|
79
|
+
budget.releaseRetained(reasoningTextBytes, { kind: "reasoning" });
|
|
80
|
+
reasoningTextBytes = nextBytes;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
reservation.release();
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const emitReasoningDetails = (entries: Rec[]) => {
|
|
87
|
+
if (entries.length === 0) return;
|
|
88
|
+
nextReasoningDetailIndex += entries.length;
|
|
89
|
+
ensureRole();
|
|
90
|
+
const frame = chunkBase(id, model, created);
|
|
91
|
+
frame.choices = [{ index: 0, delta: { reasoning_details: entries }, finish_reason: null }];
|
|
92
|
+
emit(frame);
|
|
93
|
+
};
|
|
57
94
|
|
|
58
95
|
const emit = (payload: Rec | "[DONE]", observation?: RelayedEventObservation) => {
|
|
59
96
|
if (failed) return;
|
|
@@ -138,15 +175,30 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien
|
|
|
138
175
|
emit(frame, { semanticBytes: Buffer.byteLength(delta) });
|
|
139
176
|
},
|
|
140
177
|
messageDone() { /* the converter reads nothing from a finished message */ },
|
|
141
|
-
reasoning(delta) {
|
|
178
|
+
reasoning(delta, itemId, channel) {
|
|
142
179
|
if (!delta) return;
|
|
143
180
|
ensureRole();
|
|
144
181
|
// Many OpenAI-compatible clients accept reasoning_content; harmless if ignored.
|
|
145
182
|
const frame = chunkBase(id, model, created);
|
|
146
183
|
frame.choices = [{ index: 0, delta: { reasoning_content: delta }, finish_reason: null }];
|
|
147
184
|
emit(frame, { semanticBytes: Buffer.byteLength(delta) });
|
|
185
|
+
// Only summary-channel thinking can carry a signature; raw reasoning never does.
|
|
186
|
+
if (channel === "summary") retainReasoningText(itemId, delta);
|
|
187
|
+
},
|
|
188
|
+
reasoningDone({ itemId, signature, redacted, hidden }) {
|
|
189
|
+
// The bridge converter's output_item.done for a reasoning item (#7429), entry for entry.
|
|
190
|
+
const entries = reasoningDetailEntries({
|
|
191
|
+
text: reasoningItemId === itemId ? reasoningText : "",
|
|
192
|
+
...(signature ? { signature } : {}),
|
|
193
|
+
...(redacted ? { redacted } : {}),
|
|
194
|
+
...(hidden ? { hidden } : {}),
|
|
195
|
+
}, nextReasoningDetailIndex);
|
|
196
|
+
releaseReasoningText();
|
|
197
|
+
emitReasoningDetails(entries);
|
|
198
|
+
},
|
|
199
|
+
progressThinking(text, signature) {
|
|
200
|
+
emitReasoningDetails(reasoningDetailEntries({ text, signature }, nextReasoningDetailIndex));
|
|
148
201
|
},
|
|
149
|
-
reasoningDone() { /* reasoning items carry nothing a Chat client reads */ },
|
|
150
202
|
toolStart(call) {
|
|
151
203
|
// Custom and tool-search calls have no Chat representation; the converter skips them.
|
|
152
204
|
if (call.kind !== "function") return;
|
|
@@ -198,7 +250,7 @@ function createChatCompletionWriter(sink: ClientFrameSink, model: string): Clien
|
|
|
198
250
|
type: "upstream_error",
|
|
199
251
|
});
|
|
200
252
|
},
|
|
201
|
-
dispose() {
|
|
253
|
+
dispose() { releaseReasoningText(); },
|
|
202
254
|
};
|
|
203
255
|
}
|
|
204
256
|
|
|
@@ -8,6 +8,14 @@ import { RequestPacingQueueOverloadError } from "./request-pacing";
|
|
|
8
8
|
import type { OcxProviderTransport } from "./xai-transport";
|
|
9
9
|
|
|
10
10
|
const MAX_IDENTITIES = 128;
|
|
11
|
+
/** Queued waiters per credential+model identity; one hot identity never refuses another. */
|
|
12
|
+
const MAX_WAITERS_PER_IDENTITY = 128;
|
|
13
|
+
/**
|
|
14
|
+
* Process-wide waiter bound. Model strings come from clients, so identities are not bounded by
|
|
15
|
+
* configuration; this keeps memory and pending requests finite. It is a multiple of the
|
|
16
|
+
* per-identity cap, so no single identity can fill it alone.
|
|
17
|
+
*/
|
|
18
|
+
const MAX_WAITERS_TOTAL = 4 * MAX_WAITERS_PER_IDENTITY;
|
|
11
19
|
const DEFAULT_MAX_WAIT_MS = 60_000; // Existing retry wait budget, not an inferred rate window.
|
|
12
20
|
const MINUTE_MS = 60 * 1000; // Unit named by the observed headers, never an account quota.
|
|
13
21
|
type RecoveryState = { generation: number; probe?: { done: Promise<void>; finish(): void } };
|
|
@@ -61,7 +69,14 @@ export class MistralBudgetPacer {
|
|
|
61
69
|
private readonly recoveries = new Map<string, RecoveryState>();
|
|
62
70
|
/** Latest live deadline evicted at capacity; unknown identities wait it out instead of sending early. */
|
|
63
71
|
private evictedUntil = 0;
|
|
64
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Parked waiters keyed by identity. A request counts only once it is about to wait, so a held
|
|
74
|
+
* credential or model fills only its own queue and a request that can send now is never answered
|
|
75
|
+
* queue_full (HTTP 429). The map holds identities with live waiters only; the total is capped by
|
|
76
|
+
* MAX_WAITERS_TOTAL.
|
|
77
|
+
*/
|
|
78
|
+
private readonly waiting = new Map<string, number>();
|
|
79
|
+
private waitingTotal = 0;
|
|
65
80
|
|
|
66
81
|
constructor(
|
|
67
82
|
private readonly now: () => number = Date.now,
|
|
@@ -103,9 +118,18 @@ export class MistralBudgetPacer {
|
|
|
103
118
|
/** Wait within the admission budget and return a release callback for one recovery probe. */
|
|
104
119
|
async acquire(identity: string, signal?: AbortSignal, maxWaitMs = DEFAULT_MAX_WAIT_MS): Promise<(success?: boolean) => void> {
|
|
105
120
|
signal?.throwIfAborted();
|
|
106
|
-
if (this.waiting >= MAX_IDENTITIES) throw new RequestPacingQueueOverloadError("mistral", "queue_full", 60);
|
|
107
|
-
this.waiting++;
|
|
108
121
|
const deadline = this.now() + maxWaitMs;
|
|
122
|
+
let parked = false;
|
|
123
|
+
const park = (): void => {
|
|
124
|
+
if (parked) return;
|
|
125
|
+
const queued = this.waiting.get(identity) ?? 0;
|
|
126
|
+
if (queued >= MAX_WAITERS_PER_IDENTITY || this.waitingTotal >= MAX_WAITERS_TOTAL) {
|
|
127
|
+
throw new RequestPacingQueueOverloadError("mistral", "queue_full", 60);
|
|
128
|
+
}
|
|
129
|
+
this.waiting.set(identity, queued + 1);
|
|
130
|
+
this.waitingTotal++;
|
|
131
|
+
parked = true;
|
|
132
|
+
};
|
|
109
133
|
try {
|
|
110
134
|
for (;;) {
|
|
111
135
|
signal?.throwIfAborted();
|
|
@@ -118,6 +142,7 @@ export class MistralBudgetPacer {
|
|
|
118
142
|
throw new RequestPacingQueueOverloadError("mistral", "queue_expired", Math.ceil(delay / 1000));
|
|
119
143
|
}
|
|
120
144
|
if (readyAt !== undefined && readyAt > this.now()) {
|
|
145
|
+
park();
|
|
121
146
|
await this.sleep(readyAt - this.now(), signal);
|
|
122
147
|
continue;
|
|
123
148
|
}
|
|
@@ -126,6 +151,7 @@ export class MistralBudgetPacer {
|
|
|
126
151
|
if (state.probe) {
|
|
127
152
|
const remaining = deadline - this.now();
|
|
128
153
|
if (remaining <= 0) throw new RequestPacingQueueOverloadError("mistral", "queue_expired", 1);
|
|
154
|
+
park();
|
|
129
155
|
const local = new AbortController();
|
|
130
156
|
try {
|
|
131
157
|
await Promise.race([state.probe.done, this.sleep(remaining, AbortSignal.any([local.signal, ...(signal ? [signal] : [])]))]);
|
|
@@ -150,7 +176,14 @@ export class MistralBudgetPacer {
|
|
|
150
176
|
probe.finish();
|
|
151
177
|
};
|
|
152
178
|
}
|
|
153
|
-
} finally {
|
|
179
|
+
} finally {
|
|
180
|
+
if (parked) {
|
|
181
|
+
this.waitingTotal--;
|
|
182
|
+
const left = (this.waiting.get(identity) ?? 1) - 1;
|
|
183
|
+
if (left > 0) this.waiting.set(identity, left);
|
|
184
|
+
else this.waiting.delete(identity);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
154
187
|
}
|
|
155
188
|
}
|
|
156
189
|
|
|
@@ -8,6 +8,7 @@ import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TI
|
|
|
8
8
|
import { report, type ProviderQuotaReport } from "./report-cache";
|
|
9
9
|
import { quotaCredentialIdentity } from "./account-cache";
|
|
10
10
|
import type { ProviderQuota, ProviderQuotaWindow, QuotaFailureCode } from "../quota-types";
|
|
11
|
+
import { httpQuotaFailure } from "./failure-codes";
|
|
11
12
|
|
|
12
13
|
export function antigravityQuotaDiagnosticIdentity(accountId: string, credential = getAccountCredential("google-antigravity", accountId)): string | undefined {
|
|
13
14
|
return credential ? quotaCredentialIdentity("google-antigravity", accountId, credential, {
|
|
@@ -275,13 +276,6 @@ function quotaTransportFailure(error: unknown): QuotaFailureCode {
|
|
|
275
276
|
return "transport_error";
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
function quotaHttpFailure(status: number): QuotaFailureCode {
|
|
279
|
-
if (status >= 300 && status < 400) return "redirect_blocked";
|
|
280
|
-
if (status === 401 || status === 403) return "access_denied";
|
|
281
|
-
if (status === 429) return "rate_limited";
|
|
282
|
-
return "upstream_error";
|
|
283
|
-
}
|
|
284
|
-
|
|
285
279
|
function unavailableAntigravityQuota(failure: QuotaFailureCode): AntigravityQuotaProbeResult {
|
|
286
280
|
return { kind: "unavailable", failure, legacy: { kind: "null" } };
|
|
287
281
|
}
|
|
@@ -342,7 +336,7 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId:
|
|
|
342
336
|
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked"));
|
|
343
337
|
}
|
|
344
338
|
if (!response.ok) {
|
|
345
|
-
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure,
|
|
339
|
+
return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, httpQuotaFailure(response.status)));
|
|
346
340
|
}
|
|
347
341
|
const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
|
|
348
342
|
if (!customWindows.length) {
|